- Refactor of workflow and steps types to differentiate between editView and normalView.

- Added side panel with steps details
This commit is contained in:
Joao Correia
2025-01-25 03:44:50 +00:00
parent 1f7639a30e
commit f71a7182dd
10 changed files with 225 additions and 100 deletions

View File

@@ -28,10 +28,9 @@ export default function StartedOn({ date }: Props) {
<div> <div>
<p className="pb-1 text-sm font-medium text-gray-800">Started on</p> <p className="pb-1 text-sm font-medium text-gray-800">Started on</p>
<div className="flex items-center"> <div className="flex items-center">
{/* Display the formatted date and add a title attribute for hover */}
<p <p
className="text-xs font-medium text-gray-800" className="text-xs font-medium text-gray-800"
title={fullDateTime} // Shows full date and time on hover title={fullDateTime}
> >
{yearMonthDay} {yearMonthDay}
</p> </p>

View File

@@ -1,15 +1,15 @@
import { WorkflowStep } from "@/interfaces/approval.workflow"; import { EditableWorkflowStep } from "@/interfaces/approval.workflow";
import Option from "@/interfaces/option"; import Option from "@/interfaces/option";
import { CorporateUser, TeacherUser } from "@/interfaces/user"; import { CorporateUser, TeacherUser } from "@/interfaces/user";
import Image from "next/image";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { AiOutlineUserAdd } from "react-icons/ai"; import { AiOutlineUserAdd } from "react-icons/ai";
import { BsTrash } from "react-icons/bs"; import { BsTrash } from "react-icons/bs";
import { LuGripHorizontal } from "react-icons/lu"; import { LuGripHorizontal } from "react-icons/lu";
import WorkflowStepNumber from "./WorkflowStepNumber"; import WorkflowStepNumber from "./WorkflowStepNumber";
import WorkflowStepSelects from "./WorkflowStepSelects"; import WorkflowStepSelects from "./WorkflowStepSelects";
import Image from "next/image";
interface Props extends WorkflowStep { interface Props extends Pick<EditableWorkflowStep, 'stepNumber' | 'assignees' | 'finalStep' | 'onDelete'> {
entityTeachers: TeacherUser[]; entityTeachers: TeacherUser[];
entityCorporates: CorporateUser[]; entityCorporates: CorporateUser[];
onSelectChange: (numberOfSelects: number, index: number, value: Option | null) => void; onSelectChange: (numberOfSelects: number, index: number, value: Option | null) => void;
@@ -102,7 +102,7 @@ export default function WorkflowEditableStepComponent({
return ( return (
<div className="flex w-full"> <div className="flex w-full">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<WorkflowStepNumber stepNumber={stepNumber} /> <WorkflowStepNumber stepNumber={stepNumber} completed={false} selected={false} />
{/* Vertical Bar connecting steps */} {/* Vertical Bar connecting steps */}
{!finalStep && ( {!finalStep && (

View File

@@ -1,17 +1,17 @@
import { ApprovalWorkflow, WorkflowStep } from "@/interfaces/approval.workflow"; import { EditableApprovalWorkflow, EditableWorkflowStep } from "@/interfaces/approval.workflow";
import Option from "@/interfaces/option"; import Option from "@/interfaces/option";
import { CorporateUser, TeacherUser } from "@/interfaces/user"; 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 { useState } from "react";
import { FaRegCheckCircle } from "react-icons/fa"; import { FaRegCheckCircle } from "react-icons/fa";
import { IoIosAddCircleOutline } from "react-icons/io"; import { IoIosAddCircleOutline } from "react-icons/io";
import Button from "../Low/Button"; import Button from "../Low/Button";
import WorkflowEditableStepComponent from "./WorkflowEditableStepComponent";
import Tip from "./Tip"; import Tip from "./Tip";
import WorkflowEditableStepComponent from "./WorkflowEditableStepComponent";
interface Props { interface Props {
workflow: ApprovalWorkflow; workflow: EditableApprovalWorkflow;
onWorkflowChange: (workflow: ApprovalWorkflow) => void; onWorkflowChange: (workflow: EditableApprovalWorkflow) => void;
entityTeachers: TeacherUser[]; entityTeachers: TeacherUser[];
entityCorporates: CorporateUser[]; entityCorporates: CorporateUser[];
} }
@@ -20,7 +20,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityTeacher
const [stepCounter, setStepCounter] = useState<number>(3); // to guarantee unique keys used for animations const [stepCounter, setStepCounter] = useState<number>(3); // to guarantee unique keys used for animations
const lastStep = workflow.steps[workflow.steps.length - 1]; const lastStep = workflow.steps[workflow.steps.length - 1];
const renumberSteps = (steps: WorkflowStep[]): WorkflowStep[] => { const renumberSteps = (steps: EditableWorkflowStep[]): EditableWorkflowStep[] => {
return steps.map((step, index) => ({ return steps.map((step, index) => ({
...step, ...step,
stepNumber: index + 1, stepNumber: index + 1,
@@ -28,12 +28,13 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityTeacher
}; };
const addStep = () => { const addStep = () => {
const newStep: WorkflowStep = { const newStep: EditableWorkflowStep = {
key: stepCounter, key: stepCounter,
stepType: "approval-by", stepType: "approval-by",
stepNumber: workflow.steps.length, stepNumber: workflow.steps.length,
completed: false,
assignees: [null], assignees: [null],
firstStep: false,
finalStep: false,
}; };
setStepCounter((count) => count + 1); setStepCounter((count) => count + 1);
@@ -73,7 +74,7 @@ export default function WorkflowForm({ workflow, onWorkflowChange, entityTeacher
onWorkflowChange({ ...workflow, steps: updatedSteps }); onWorkflowChange({ ...workflow, steps: updatedSteps });
}; };
const handleReorder = (newOrder: WorkflowStep[]) => { const handleReorder = (newOrder: EditableWorkflowStep[]) => {
const firstIndex = newOrder.findIndex((s) => s.firstStep); const firstIndex = newOrder.findIndex((s) => s.firstStep);
if (firstIndex !== -1 && firstIndex !== 0) { if (firstIndex !== -1 && firstIndex !== 0) {
const [first] = newOrder.splice(firstIndex, 1); const [first] = newOrder.splice(firstIndex, 1);

View File

@@ -16,21 +16,20 @@ export default function WorkflowStepComponent({
stepNumber, stepNumber,
completed, completed,
completedBy, completedBy,
assignees,
finalStep, finalStep,
currentStep, currentStep,
selected = false, selected = false,
assignees,
assigneesType,
onClick, onClick,
}: Props) { }: Props) {
const completedByUser = workflowAssignees.find((assignee) => assignee.id === completedBy); 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 ( return (
<div <div
onClick={onClick} onClick={onClick}
className={clsx("flex flex-row gap-5 w-[700px] p-6 my-4 rounded-2xl transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer", { className={clsx("flex flex-row gap-5 w-[600px] p-6 my-4 rounded-2xl transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer", {
"bg-mti-purple-ultralight": selected, "bg-mti-purple-ultralight": selected,
})} })}
> >
@@ -85,9 +84,9 @@ export default function WorkflowStepComponent({
) : ( ) : (
stepType === "approval-by" && ( stepType === "approval-by" && (
<> <>
<p className="text-sm font-medium text-gray-800">Approval: {getUserTypeLabel(assigneesType)}</p>
{completed && completedBy ? ( {completed && completedBy ? (
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}> <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
<p className="text-sm font-medium text-gray-800">Approval: {getUserTypeLabel(completedByUser!.type)} Approval</p>
<UserWithProfilePic <UserWithProfilePic
prefix={`Approved by: ${getUserTypeLabelShort(completedByUser!.type)}`} prefix={`Approved by: ${getUserTypeLabelShort(completedByUser!.type)}`}
name={completedByUser!.name} name={completedByUser!.name}
@@ -96,6 +95,7 @@ export default function WorkflowStepComponent({
</div> </div>
) : !completed && currentStep ? ( ) : !completed && currentStep ? (
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}> <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
<p className="text-sm font-medium text-gray-800">Approval: </p>
In Progress... Assignees: In Progress... Assignees:
<div className="flex flex-row flex-wrap gap-3 items-center"> <div className="flex flex-row flex-wrap gap-3 items-center">
{assigneesUsers.map(user => ( {assigneesUsers.map(user => (
@@ -111,6 +111,7 @@ export default function WorkflowStepComponent({
</div> </div>
) : ( ) : (
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}> <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
<p className="text-sm font-medium text-gray-800">Approval: </p>
Waiting for previous steps... Waiting for previous steps...
</div> </div>
)} )}

View File

@@ -2,7 +2,9 @@ import { WorkflowStep } from "@/interfaces/approval.workflow";
import clsx from "clsx"; import clsx from "clsx";
import { IoCheckmarkDoneSharp, IoCheckmarkSharp } from "react-icons/io5"; import { IoCheckmarkDoneSharp, IoCheckmarkSharp } from "react-icons/io5";
export default function WorkflowStepNumber({ stepNumber, selected = false, completed, finalStep }: WorkflowStep) { type Props = Pick<WorkflowStep, 'stepNumber' | 'completed' | 'finalStep' | 'selected'>
export default function WorkflowStepNumber({ stepNumber, selected = false, completed, finalStep }: Props) {
return ( return (
<div <div
className={clsx( className={clsx(

View File

@@ -16,22 +16,26 @@
"stepNumber": 1, "stepNumber": 1,
"completed": true, "completed": true,
"completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0",
"completedDate": 1737712243906,
"assignees": [ "assignees": [
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment\nThis is a random comment\nThis is a random comment\nThis is a random comment\nThis is a random comment\n"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
"stepNumber": 2, "stepNumber": 2,
"completed": true, "completed": true,
"completedBy": "c5fc1514-1a94-4f8c-a046-a62099097a50", "completedBy": "c5fc1514-1a94-4f8c-a046-a62099097a50",
"completedDate": 1737712243906,
"assignees": [ "assignees": [
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
@@ -41,7 +45,8 @@
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
@@ -51,7 +56,8 @@
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
@@ -61,7 +67,8 @@
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
} }
] ]
}, },
@@ -85,55 +92,65 @@
"stepNumber": 1, "stepNumber": 1,
"completed": true, "completed": true,
"completedBy": "fd5fce42-4bcc-4150-a143-b484e750b265", "completedBy": "fd5fce42-4bcc-4150-a143-b484e750b265",
"completedDate": 1737712243906,
"assignees": [ "assignees": [
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
"stepNumber": 2, "stepNumber": 2,
"completed": true, "completed": true,
"completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0",
"completedDate": 1737712243906,
"assignees": [ "assignees": [
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
"stepNumber": 3, "stepNumber": 3,
"completed": true, "completed": true,
"completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0",
"completedDate": 1737712243906,
"assignees": [ "assignees": [
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
"stepNumber": 4, "stepNumber": 4,
"completed": true, "completed": true,
"completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0",
"completedDate": 1737712243906,
"assignees": [ "assignees": [
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
}, },
{ {
"stepType": "approval-by", "stepType": "approval-by",
"stepNumber": 5, "stepNumber": 5,
"completed": true, "completed": true,
"completedBy": "c5fc1514-1a94-4f8c-a046-a62099097a50", "completedBy": "c5fc1514-1a94-4f8c-a046-a62099097a50",
"completedDate": 1737712243906,
"assignees": [ "assignees": [
"fd5fce42-4bcc-4150-a143-b484e750b265", "fd5fce42-4bcc-4150-a143-b484e750b265",
"231c84b2-a65a-49a9-803c-c664d84b13e0", "231c84b2-a65a-49a9-803c-c664d84b13e0",
"c5fc1514-1a94-4f8c-a046-a62099097a50" "c5fc1514-1a94-4f8c-a046-a62099097a50"
] ],
"comments": "This is a random comment"
} }
] ]
} }

View File

@@ -12,6 +12,17 @@ export interface ApprovalWorkflow {
steps: WorkflowStep[], steps: WorkflowStep[],
} }
export interface EditableApprovalWorkflow {
id: string,
name: string,
entityId: string,
requester: User["id"],
startDate: number,
modules: Module[],
status: ApprovalWorkflowStatus,
steps: EditableWorkflowStep[],
}
export type StepType = "form-intake" | "approval-by"; export type StepType = "form-intake" | "approval-by";
export const StepTypeLabel: Record<StepType, string> = { export const StepTypeLabel: Record<StepType, string> = {
"form-intake": "Form Intake", "form-intake": "Form Intake",
@@ -19,19 +30,28 @@ export const StepTypeLabel: Record<StepType, string> = {
}; };
export interface WorkflowStep { export interface WorkflowStep {
key?: number, stepType: StepType,
stepType?: StepType,
stepNumber: number, stepNumber: number,
completed?: boolean, completed: boolean,
completedBy?: User["id"], 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 completedDate?: number,
assigneesType?: Type, assignees: (User["id"])[];
firstStep?: boolean, firstStep?: boolean,
currentStep?: boolean, currentStep?: boolean,
finalStep?: boolean, finalStep?: boolean,
selected?: boolean, selected: boolean,
comments?: string,
onClick: React.MouseEventHandler<HTMLDivElement>
}
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; onDelete?: () => void;
onClick?: React.MouseEventHandler<HTMLDivElement>
} }
export function getUserTypeLabel(type: Type | undefined): string { export function getUserTypeLabel(type: Type | undefined): string {

View File

@@ -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 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 { sessionOptions } from "@/lib/session";
import { redirect, serialize } from "@/utils"; import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api"; import { requestUser } from "@/utils/api";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import { withIronSessionSsr } from "iron-session/next"; import { withIronSessionSsr } from "iron-session/next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
import { BsChevronLeft } from "react-icons/bs"; 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 { ToastContainer } from "react-toastify";
import approvalWorkflowsData from '../../demo/approval_workflows.json'; // to test locally 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 }) => { 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 (!user) return redirect("/login")
@@ -29,7 +32,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
const { id } = params as { id: string }; 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 approvalWorkflowsDataAsWorkflows = approvalWorkflowsData as ApprovalWorkflow[];
const workflow: ApprovalWorkflow | undefined = approvalWorkflowsDataAsWorkflows.find(workflow => workflow.id === id); 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[] = [ const allAssigneeIds: string[] = [
...new Set( ...new Set(
workflow.steps workflow.steps
.map(step => step.assignees) .map(step => step.assignees)
.flat() as string[] // we are sure assignees coming from a db workflow are all valid strings. .flat()
) )
]; ];
@@ -65,13 +68,23 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
const steps = workflow.steps; const steps = workflow.steps;
let currentStep = steps.findIndex(step => !step.completed); let currentStep = steps.findIndex(step => !step.completed);
if (currentStep === -1) if (currentStep === -1)
currentStep = steps.length - 1; currentStep = steps.length - 1;
const [selectedIndex, setSelectedIndex] = useState(currentStep); const [selectedStepIndex, setSelectedStepIndex] = useState<number>(currentStep);
const [selectedStep, setSelectedStep] = useState<WorkflowStep>(steps[selectedStepIndex]);
const [isPanelOpen, setIsPanelOpen] = useState(true);
const [comments, setComments] = useState<string>(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 ( return (
@@ -88,16 +101,16 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
<ToastContainer /> <ToastContainer />
{user && ( {user && (
<Layout user={user} className="gap-6"> <Layout user={user} className="gap-6">
<section className="flex flex-col gap-0">
<div className="flex items-center gap-2"> <section className="flex items-center gap-2">
<Link <Link
href="/approval-workflows" href="/approval-workflows"
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"> className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
<BsChevronLeft /> <BsChevronLeft />
</Link> </Link>
<h1 className="text-2xl font-semibold">{workflow.name}</h1> <h1 className="text-2xl font-semibold">{workflow.name}</h1>
</div>
</section> </section>
<section className="flex flex-col gap-6"> <section className="flex flex-col gap-6">
<div className="flex flex-row gap-6"> <div className="flex flex-row gap-6">
<RequestedBy <RequestedBy
@@ -113,6 +126,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
/> />
</div> </div>
</section> </section>
<section className="flex flex-col gap-0"> <section className="flex flex-col gap-0">
{steps.map((step, index) => ( {steps.map((step, index) => (
<WorkflowStepComponent <WorkflowStepComponent
@@ -123,14 +137,95 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
stepNumber={step.stepNumber} stepNumber={step.stepNumber}
stepType={step.stepType} stepType={step.stepType}
assignees={step.assignees} assignees={step.assignees}
assigneesType={step.assigneesType}
finalStep={index === steps.length - 1} finalStep={index === steps.length - 1}
currentStep={currentStep === index} currentStep={index === currentStep}
selected={index === selectedIndex} selected={index === selectedStepIndex}
onClick={() => handleStepClick(index)} onClick={() => handleStepClick(index, step)}
/> />
))} ))}
</section> </section>
<section className={`absolute inset-y-0 right-0 h-full bg-mti-purple-ultralight bg-opacity-50 shadow-xl shadow-mti-purple transition-all duration-300 overflow-hidden ${isPanelOpen ? 'w-2/5' : 'w-0'}`}>
{isPanelOpen && (
<div className="relative inset-y-0 right-0 h-full p-6">
<div className="flex flex-row gap-2">
<p className="text-2xl font-medium text-left align-middle">Step {selectedStepIndex + 1}</p>
<div className="ml-auto flex flex-row">
<button
className="min-w-fit max-h-fit text-lg font-medium flex items-center gap-2 text-left"
onClick={() => setIsPanelOpen(false)}
>
Collapse
<MdOutlineDoubleArrow size={20} />
</button>
</div>
</div>
<hr className="my-4 h-[4px] bg-mti-purple-ultralight rounded-full w-full" />
<div>
<div className="my-8 flex flex-row gap-4 items-center text-lg font-medium">
{selectedStep.stepType === "approval-by" ? (
<>
<RiThumbUpLine size={30} />
Approval Step
</>
) : (
<>
<FaWpforms size={30} />
Form Intake Step
</>
)
}
</div>
{selectedStep.completed ? (
<div className={"text-base font-medium text-gray-500 my-7"}>
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 ")}
<p className="my-4 text-sm">No additional actions are required.</p>
</div>
) : (
<div className={"text-base font-medium text-gray-500"}>
One assignee is required to sign off to complete this step:
<div className="flex flex-col gap-2 mt-3">
{workflowAssignees.map(user => (
<span key={user.id}>
<UserWithProfilePic
prefix={getUserTypeLabelShort(user.type)}
name={user.name}
profileImage={user.profilePicture}
/>
</span>
))}
</div>
</div>
)}
<hr className="my-4 h-[4px] bg-mti-purple-ultralight rounded-full w-full" />
<textarea
value={comments}
onChange={(e) => setComments(e.target.value)}
placeholder="Input comments here"
className="w-full h-80 p-2 border-2 rounded-xl focus:border-mti-purple focus:outline-none mt-4"
/>
<button onClick={saveComments} className="mt-4 px-6 py-2 bg-mti-purple-dark text-white rounded-full">
Save Comments
</button>
</div>
</div>
)}
</section>
</Layout> </Layout>
)} )}
</> </>

View File

@@ -1,28 +1,27 @@
import Layout from "@/components/High/Layout";
import { sessionOptions } from "@/lib/session";
import { redirect, serialize } 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 Tip from "@/components/ApprovalWorkflows/Tip"; import Tip from "@/components/ApprovalWorkflows/Tip";
import WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm"; import WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm";
import Layout from "@/components/High/Layout";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import Input from "@/components/Low/Input"; import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; import { EditableApprovalWorkflow } from "@/interfaces/approval.workflow";
import { Entity } from "@/interfaces/entity"; import { Entity } from "@/interfaces/entity";
import { CorporateUser, TeacherUser, User } from "@/interfaces/user"; import { CorporateUser, TeacherUser, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { getEntities } from "@/utils/entities.be"; import { getEntities } from "@/utils/entities.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getEntitiesUsers } from "@/utils/users.be"; import { getEntitiesUsers } from "@/utils/users.be";
import { LayoutGroup, motion } from "framer-motion"; import { LayoutGroup, motion } from "framer-motion";
import { withIronSessionSsr } from "iron-session/next";
import Head from "next/head";
import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { BsChevronLeft, BsTrash } from "react-icons/bs";
import { MdFormatListBulletedAdd } from "react-icons/md"; import { MdFormatListBulletedAdd } from "react-icons/md";
import { ToastContainer } from "react-toastify";
import { v4 as uuidv4 } from 'uuid';
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res) const user = await requestUser(req, res)
@@ -51,7 +50,7 @@ interface Props {
} }
export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers, userEntitiesCorporates }: Props) { export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers, userEntitiesCorporates }: Props) {
const [workflows, setWorkflows] = useState<ApprovalWorkflow[]>([]); const [workflows, setWorkflows] = useState<EditableApprovalWorkflow[]>([]);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | undefined>(undefined); const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | undefined>(undefined);
const [entityId, setEntityId] = useState<string | null | undefined>(null); const [entityId, setEntityId] = useState<string | null | undefined>(null);
const [entityTeachers, setEntityTeachers] = useState<TeacherUser[]>([]); const [entityTeachers, setEntityTeachers] = useState<TeacherUser[]>([]);
@@ -80,7 +79,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
const ENTITY_OPTIONS = userEntitiesWithLabel.map(entity => ({ const ENTITY_OPTIONS = userEntitiesWithLabel.map(entity => ({
label: entity.label, label: entity.label,
value: entity.id, value: entity.id,
filter: (x: ApprovalWorkflow) => x.entityId === entity.id, filter: (x: EditableApprovalWorkflow) => x.entityId === entity.id,
})); }));
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
@@ -90,7 +89,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
const handleAddNewWorkflow = () => { const handleAddNewWorkflow = () => {
const newId = uuidv4(); const newId = uuidv4();
const newWorkflow: ApprovalWorkflow = { const newWorkflow: EditableApprovalWorkflow = {
id: newId, id: newId,
name: "", name: "",
entityId: "", entityId: "",
@@ -99,15 +98,15 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
startDate: Date.now(), startDate: Date.now(),
status: "pending", status: "pending",
steps: [ steps: [
{ key: Date.now(), completed: false, stepType: "form-intake", stepNumber: 1, firstStep: true, assignees: [null] }, { key: Date.now(), stepType: "form-intake", stepNumber: 1, firstStep: true, finalStep: false, assignees: [null] },
{ key: Date.now() + 1, completed: false, stepType: "approval-by", stepNumber: 2, finalStep: true, assignees: [null] }, { key: Date.now() + 1, stepType: "approval-by", stepNumber: 2, firstStep: false, finalStep: true, assignees: [null] },
], ],
}; };
setWorkflows((prev) => [...prev, newWorkflow]); setWorkflows((prev) => [...prev, newWorkflow]);
handleSelectWorkflow(newId); handleSelectWorkflow(newId);
}; };
const onWorkflowChange = (updatedWorkflow: ApprovalWorkflow) => { const onWorkflowChange = (updatedWorkflow: EditableApprovalWorkflow) => {
setWorkflows(prev => setWorkflows(prev =>
prev.map(wf => (wf.id === updatedWorkflow.id ? updatedWorkflow : wf)) prev.map(wf => (wf.id === updatedWorkflow.id ? updatedWorkflow : wf))
); );

View File

@@ -34,12 +34,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type))
return redirect("/") return redirect("/")
/* const entityIDS = mapBy(user.entities, "id"); // replace later with useApprovalWorkflows()
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
const allowedEntities = findAllowedEntities(user, entities, "view_approval_workflows"); */
// replace later with await getApprovalWorkflow(id). Don't think a hook is needed here;
const workflows = approvalWorkflowsData as ApprovalWorkflow[]; const workflows = approvalWorkflowsData as ApprovalWorkflow[];
const allAssigneeIds: string[] = [ const allAssigneeIds: string[] = [
@@ -47,8 +42,8 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
workflows workflows
.map(workflow => workflow.steps .map(workflow => workflow.steps
.map(step => step.assignees) .map(step => step.assignees)
.flat() as string[] // we are sure assignees coming from a db workflow are all valid strings. .flat()
).flat() as string[] ).flat()
) )
]; ];
@@ -166,10 +161,6 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
}; };
const columns = [ const columns = [
/* columnHelper.accessor("id", {
header: "ID",
cell: (info) => info.getValue(),
}), */
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: "NAME", header: "NAME",
cell: (info) => ( cell: (info) => (
@@ -216,7 +207,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
const steps = info.row.original.steps; const steps = info.row.original.steps;
const currentStep = steps.find((step) => !step.completed); const currentStep = steps.find((step) => !step.completed);
const assignees = currentStep?.assignees!.map((assigneeId) => { const assignees = currentStep?.assignees.map((assigneeId) => {
const assignee = workflowsAssignees.find((user) => user.id === assigneeId); const assignee = workflowsAssignees.find((user) => user.id === assigneeId);
return assignee?.name || "Unknown Assignee"; return assignee?.name || "Unknown Assignee";
}); });
@@ -245,7 +236,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
return ( return (
<span className="font-medium"> <span className="font-medium">
{currentStep {currentStep
? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType!]}` ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}`
: "Completed"} : "Completed"}
</span> </span>
); );