-
+
{/* 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}
-
+
+
+
+
{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 => (
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ )}
+
)}
>
diff --git a/src/pages/approval-workflows/create.tsx b/src/pages/approval-workflows/create.tsx
index 6c355b51..d42bdfab 100644
--- a/src/pages/approval-workflows/create.tsx
+++ b/src/pages/approval-workflows/create.tsx
@@ -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 WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm";
+import Layout from "@/components/High/Layout";
import Button from "@/components/Low/Button";
import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select";
-import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
+import { EditableApprovalWorkflow } from "@/interfaces/approval.workflow";
import { Entity } from "@/interfaces/entity";
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 { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getEntitiesUsers } from "@/utils/users.be";
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 { BsChevronLeft, BsTrash } from "react-icons/bs";
import { MdFormatListBulletedAdd } from "react-icons/md";
+import { ToastContainer } from "react-toastify";
+import { v4 as uuidv4 } from 'uuid';
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
@@ -51,7 +50,7 @@ interface Props {
}
export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers, userEntitiesCorporates }: Props) {
- const [workflows, setWorkflows] = useState([]);
+ const [workflows, setWorkflows] = useState([]);
const [selectedWorkflowId, setSelectedWorkflowId] = useState(undefined);
const [entityId, setEntityId] = useState(null);
const [entityTeachers, setEntityTeachers] = useState([]);
@@ -80,7 +79,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
const ENTITY_OPTIONS = userEntitiesWithLabel.map(entity => ({
label: entity.label,
value: entity.id,
- filter: (x: ApprovalWorkflow) => x.entityId === entity.id,
+ filter: (x: EditableApprovalWorkflow) => x.entityId === entity.id,
}));
const handleSubmit = (e: React.FormEvent) => {
@@ -90,7 +89,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
const handleAddNewWorkflow = () => {
const newId = uuidv4();
- const newWorkflow: ApprovalWorkflow = {
+ const newWorkflow: EditableApprovalWorkflow = {
id: newId,
name: "",
entityId: "",
@@ -99,15 +98,15 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
startDate: Date.now(),
status: "pending",
steps: [
- { key: Date.now(), completed: false, stepType: "form-intake", stepNumber: 1, firstStep: true, assignees: [null] },
- { key: Date.now() + 1, completed: false, stepType: "approval-by", stepNumber: 2, finalStep: true, assignees: [null] },
+ { key: Date.now(), stepType: "form-intake", stepNumber: 1, firstStep: true, finalStep: false, assignees: [null] },
+ { key: Date.now() + 1, stepType: "approval-by", stepNumber: 2, firstStep: false, finalStep: true, assignees: [null] },
],
};
setWorkflows((prev) => [...prev, newWorkflow]);
handleSelectWorkflow(newId);
};
- const onWorkflowChange = (updatedWorkflow: ApprovalWorkflow) => {
+ const onWorkflowChange = (updatedWorkflow: EditableApprovalWorkflow) => {
setWorkflows(prev =>
prev.map(wf => (wf.id === updatedWorkflow.id ? updatedWorkflow : wf))
);
diff --git a/src/pages/approval-workflows/index.tsx b/src/pages/approval-workflows/index.tsx
index ef09c959..d909c3b8 100644
--- a/src/pages/approval-workflows/index.tsx
+++ b/src/pages/approval-workflows/index.tsx
@@ -34,12 +34,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
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 allowedEntities = findAllowedEntities(user, entities, "view_approval_workflows"); */
-
-
- // replace later with await getApprovalWorkflow(id). Don't think a hook is needed here;
+ // replace later with useApprovalWorkflows()
const workflows = approvalWorkflowsData as ApprovalWorkflow[];
const allAssigneeIds: string[] = [
@@ -47,8 +42,8 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
workflows
.map(workflow => workflow.steps
.map(step => step.assignees)
- .flat() as string[] // we are sure assignees coming from a db workflow are all valid strings.
- ).flat() as string[]
+ .flat()
+ ).flat()
)
];
@@ -166,10 +161,6 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
};
const columns = [
- /* columnHelper.accessor("id", {
- header: "ID",
- cell: (info) => info.getValue(),
- }), */
columnHelper.accessor("name", {
header: "NAME",
cell: (info) => (
@@ -216,7 +207,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
const steps = info.row.original.steps;
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);
return assignee?.name || "Unknown Assignee";
});
@@ -245,7 +236,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
return (
{currentStep
- ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType!]}`
+ ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}`
: "Completed"}
);