From f71a7182dde9639c53cc13d1c17ba6549ce2db3f Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Sat, 25 Jan 2025 03:44:50 +0000 Subject: [PATCH] - Refactor of workflow and steps types to differentiate between editView and normalView. - Added side panel with steps details --- .../ApprovalWorkflows/StartedOn.tsx | 3 +- .../WorkflowEditableStepComponent.tsx | 8 +- .../ApprovalWorkflows/WorkflowForm.tsx | 19 +-- .../WorkflowStepComponent.tsx | 11 +- .../ApprovalWorkflows/WorkflowStepNumber.tsx | 4 +- src/demo/approval_workflows.json | 37 +++-- src/interfaces/approval.workflow.ts | 34 +++- src/pages/approval-workflows/[id].tsx | 153 ++++++++++++++---- src/pages/approval-workflows/create.tsx | 37 +++-- src/pages/approval-workflows/index.tsx | 19 +-- 10 files changed, 225 insertions(+), 100 deletions(-) diff --git a/src/components/ApprovalWorkflows/StartedOn.tsx b/src/components/ApprovalWorkflows/StartedOn.tsx index 7babf998..2b1fde6d 100644 --- a/src/components/ApprovalWorkflows/StartedOn.tsx +++ b/src/components/ApprovalWorkflows/StartedOn.tsx @@ -28,10 +28,9 @@ export default function StartedOn({ date }: Props) {

Started on

- {/* Display the formatted date and add a title attribute for hover */}

{yearMonthDay}

diff --git a/src/components/ApprovalWorkflows/WorkflowEditableStepComponent.tsx b/src/components/ApprovalWorkflows/WorkflowEditableStepComponent.tsx index 2a44303b..ec53d821 100644 --- a/src/components/ApprovalWorkflows/WorkflowEditableStepComponent.tsx +++ b/src/components/ApprovalWorkflows/WorkflowEditableStepComponent.tsx @@ -1,15 +1,15 @@ -import { WorkflowStep } from "@/interfaces/approval.workflow"; +import { EditableWorkflowStep } from "@/interfaces/approval.workflow"; import Option from "@/interfaces/option"; import { CorporateUser, TeacherUser } from "@/interfaces/user"; +import Image from "next/image"; import { useEffect, useMemo, useState } from "react"; import { AiOutlineUserAdd } from "react-icons/ai"; import { BsTrash } from "react-icons/bs"; import { LuGripHorizontal } from "react-icons/lu"; import WorkflowStepNumber from "./WorkflowStepNumber"; import WorkflowStepSelects from "./WorkflowStepSelects"; -import Image from "next/image"; -interface Props extends WorkflowStep { +interface Props extends Pick { entityTeachers: TeacherUser[]; entityCorporates: CorporateUser[]; onSelectChange: (numberOfSelects: number, index: number, value: Option | null) => void; @@ -102,7 +102,7 @@ export default function WorkflowEditableStepComponent({ return (
- + {/* Vertical Bar connecting steps */} {!finalStep && ( diff --git a/src/components/ApprovalWorkflows/WorkflowForm.tsx b/src/components/ApprovalWorkflows/WorkflowForm.tsx index dbaa05f6..eb393d4d 100644 --- a/src/components/ApprovalWorkflows/WorkflowForm.tsx +++ b/src/components/ApprovalWorkflows/WorkflowForm.tsx @@ -1,17 +1,17 @@ -import { ApprovalWorkflow, WorkflowStep } from "@/interfaces/approval.workflow"; +import { EditableApprovalWorkflow, EditableWorkflowStep } from "@/interfaces/approval.workflow"; import Option from "@/interfaces/option"; import { CorporateUser, TeacherUser } from "@/interfaces/user"; -import { AnimatePresence, AnimateSharedLayout, Reorder, motion } from "framer-motion"; +import { AnimatePresence, Reorder, motion } from "framer-motion"; import { useState } from "react"; import { FaRegCheckCircle } from "react-icons/fa"; import { IoIosAddCircleOutline } from "react-icons/io"; import Button from "../Low/Button"; -import WorkflowEditableStepComponent from "./WorkflowEditableStepComponent"; import Tip from "./Tip"; +import WorkflowEditableStepComponent from "./WorkflowEditableStepComponent"; interface Props { - workflow: ApprovalWorkflow; - onWorkflowChange: (workflow: ApprovalWorkflow) => void; + workflow: EditableApprovalWorkflow; + onWorkflowChange: (workflow: EditableApprovalWorkflow) => void; entityTeachers: TeacherUser[]; entityCorporates: CorporateUser[]; } @@ -20,7 +20,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityTeacher const [stepCounter, setStepCounter] = useState(3); // to guarantee unique keys used for animations const lastStep = workflow.steps[workflow.steps.length - 1]; - const renumberSteps = (steps: WorkflowStep[]): WorkflowStep[] => { + const renumberSteps = (steps: EditableWorkflowStep[]): EditableWorkflowStep[] => { return steps.map((step, index) => ({ ...step, stepNumber: index + 1, @@ -28,12 +28,13 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityTeacher }; const addStep = () => { - const newStep: WorkflowStep = { + const newStep: EditableWorkflowStep = { key: stepCounter, stepType: "approval-by", stepNumber: workflow.steps.length, - completed: false, assignees: [null], + firstStep: false, + finalStep: false, }; setStepCounter((count) => count + 1); @@ -73,7 +74,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityTeacher onWorkflowChange({ ...workflow, steps: updatedSteps }); }; - const handleReorder = (newOrder: WorkflowStep[]) => { + const handleReorder = (newOrder: EditableWorkflowStep[]) => { const firstIndex = newOrder.findIndex((s) => s.firstStep); if (firstIndex !== -1 && firstIndex !== 0) { const [first] = newOrder.splice(firstIndex, 1); diff --git a/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx b/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx index 54a74a3d..a3387201 100644 --- a/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx +++ b/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx @@ -16,21 +16,20 @@ export default function WorkflowStepComponent({ stepNumber, completed, completedBy, + assignees, finalStep, currentStep, selected = false, - assignees, - assigneesType, onClick, }: Props) { const completedByUser = workflowAssignees.find((assignee) => assignee.id === completedBy); - const assigneesUsers = workflowAssignees.filter(user => assignees!.includes(user.id)); + const assigneesUsers = workflowAssignees.filter(user => assignees.includes(user.id)); return (
@@ -85,9 +84,9 @@ export default function WorkflowStepComponent({ ) : ( stepType === "approval-by" && ( <> -

Approval: {getUserTypeLabel(assigneesType)}

{completed && completedBy ? (
+

Approval: {getUserTypeLabel(completedByUser!.type)} Approval

) : !completed && currentStep ? (
+

Approval:

In Progress... Assignees:
{assigneesUsers.map(user => ( @@ -111,6 +111,7 @@ export default function WorkflowStepComponent({
) : (
+

Approval:

Waiting for previous steps...
)} diff --git a/src/components/ApprovalWorkflows/WorkflowStepNumber.tsx b/src/components/ApprovalWorkflows/WorkflowStepNumber.tsx index 86b0d46d..a1d6a900 100644 --- a/src/components/ApprovalWorkflows/WorkflowStepNumber.tsx +++ b/src/components/ApprovalWorkflows/WorkflowStepNumber.tsx @@ -2,7 +2,9 @@ import { WorkflowStep } from "@/interfaces/approval.workflow"; import clsx from "clsx"; import { IoCheckmarkDoneSharp, IoCheckmarkSharp } from "react-icons/io5"; -export default function WorkflowStepNumber({ stepNumber, selected = false, completed, finalStep }: WorkflowStep) { +type Props = Pick + +export default function WorkflowStepNumber({ stepNumber, selected = false, completed, finalStep }: Props) { return (
= { "form-intake": "Form Intake", @@ -19,19 +30,28 @@ export const StepTypeLabel: Record = { }; export interface WorkflowStep { - key?: number, - stepType?: StepType, + stepType: StepType, stepNumber: number, - completed?: boolean, + completed: boolean, completedBy?: User["id"], - assignees?: (User["id"] | null | undefined)[]; // bit of an hack, but allowing null or undefined values allows us to match one to one the select input components with the assignees array. And since select inputs allow undefined or null values, it is allowed here too, but must validate required input before form submission - assigneesType?: Type, + completedDate?: number, + assignees: (User["id"])[]; firstStep?: boolean, currentStep?: boolean, finalStep?: boolean, - selected?: boolean, + selected: boolean, + comments?: string, + onClick: React.MouseEventHandler +} + +export interface EditableWorkflowStep { + key: number, + stepType: StepType, + stepNumber: number, + assignees: (User["id"] | null | undefined)[]; // bit of an hack, but allowing null or undefined values allows us to match one to one the select input components with the assignees array. And since select inputs allow undefined or null values, it is allowed here too, but must validate required input before form submission + firstStep: boolean, + finalStep?: boolean, onDelete?: () => void; - onClick?: React.MouseEventHandler } export function getUserTypeLabel(type: Type | undefined): string { diff --git a/src/pages/approval-workflows/[id].tsx b/src/pages/approval-workflows/[id].tsx index 32117735..7b8fc7e7 100644 --- a/src/pages/approval-workflows/[id].tsx +++ b/src/pages/approval-workflows/[id].tsx @@ -1,25 +1,28 @@ +import RequestedBy from "@/components/ApprovalWorkflows/RequestedBy"; +import StartedOn from "@/components/ApprovalWorkflows/StartedOn"; +import Status from "@/components/ApprovalWorkflows/Status"; +import UserWithProfilePic from "@/components/ApprovalWorkflows/UserWithProfilePic"; +import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent"; import Layout from "@/components/High/Layout"; -import { ApprovalWorkflow, getUserTypeLabelShort } from "@/interfaces/approval.workflow"; +import { ApprovalWorkflow, getUserTypeLabelShort, WorkflowStep } from "@/interfaces/approval.workflow"; +import { User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { getSpecificUsers, getUser } from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; import Head from "next/head"; import Link from "next/link"; +import { useState } from "react"; import { BsChevronLeft } from "react-icons/bs"; +import { FaWpforms } from "react-icons/fa6"; +import { MdOutlineDoubleArrow } from "react-icons/md"; +import { RiThumbUpLine } from "react-icons/ri"; import { ToastContainer } from "react-toastify"; import approvalWorkflowsData from '../../demo/approval_workflows.json'; // to test locally -import RequestedBy from "@/components/ApprovalWorkflows/RequestedBy"; -import StartedOn from "@/components/ApprovalWorkflows/StartedOn"; -import Status from "@/components/ApprovalWorkflows/Status"; -import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent"; -import { User } from "@/interfaces/user"; -import { getSpecificUsers, getUser } from "@/utils/users.be"; -import { useState } from "react"; - export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => { const user = await requestUser(req, res); if (!user) return redirect("/login") @@ -29,7 +32,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params } const { id } = params as { id: string }; - // replace later with await getApprovalWorkflow(id). Don't think a hook is needed here; + // replace later with await getApprovalWorkflow(id). const approvalWorkflowsDataAsWorkflows = approvalWorkflowsData as ApprovalWorkflow[]; const workflow: ApprovalWorkflow | undefined = approvalWorkflowsDataAsWorkflows.find(workflow => workflow.id === id); @@ -39,8 +42,8 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params } const allAssigneeIds: string[] = [ ...new Set( workflow.steps - .map(step => step.assignees) - .flat() as string[] // we are sure assignees coming from a db workflow are all valid strings. + .map(step => step.assignees) + .flat() ) ]; @@ -65,13 +68,23 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques const steps = workflow.steps; let currentStep = steps.findIndex(step => !step.completed); - if (currentStep === -1) + if (currentStep === -1) currentStep = steps.length - 1; - const [selectedIndex, setSelectedIndex] = useState(currentStep); + const [selectedStepIndex, setSelectedStepIndex] = useState(currentStep); + const [selectedStep, setSelectedStep] = useState(steps[selectedStepIndex]); + const [isPanelOpen, setIsPanelOpen] = useState(true); + const [comments, setComments] = useState(selectedStep.comments || ""); + + const handleStepClick = (index: number, stepInfo: WorkflowStep) => { + setSelectedStep(stepInfo); + setSelectedStepIndex(index); + setComments(stepInfo.comments || ""); + setIsPanelOpen(true); + }; + + const saveComments = () => { - const handleStepClick = (index: number) => { - setSelectedIndex(index); }; return ( @@ -88,16 +101,16 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques {user && ( -
-
- - - -

{workflow.name}

-
+ +
+ + + +

{workflow.name}

+
+
{steps.map((step, index) => ( handleStepClick(index)} + currentStep={index === currentStep} + selected={index === selectedStepIndex} + onClick={() => handleStepClick(index, step)} /> ))}
+ +
+ {isPanelOpen && ( +
+
+

Step {selectedStepIndex + 1}

+
+ +
+
+ +
+ +
+
+ {selectedStep.stepType === "approval-by" ? ( + <> + + Approval Step + + ) : ( + <> + + Form Intake Step + + ) + } +
+ + {selectedStep.completed ? ( +
+ 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 ")} +

No additional actions are required.

+
+ + ) : ( +
+ One assignee is required to sign off to complete this step: +
+ {workflowAssignees.map(user => ( + + + + ))} +
+
+ )} + +
+ +