From 41d09eaad876c84d12590167bafdca97721537a9 Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Fri, 24 Jan 2025 14:14:07 +0000 Subject: [PATCH] Make data dynamic in workflow view. Add requester and startDate to workflows. --- .../ApprovalWorkflows/RequestedBy.tsx | 5 +- .../ApprovalWorkflows/StartedOn.tsx | 26 ++++- .../ApprovalWorkflows/UserWithProfilePic.tsx | 22 ++++ .../WorkflowStepComponent.tsx | 55 ++++++--- src/demo/approval_workflows.json | 108 +++++++++--------- src/interfaces/approval.workflow.ts | 26 ++--- src/interfaces/user.ts | 10 ++ src/pages/approval-workflows/[id].tsx | 51 ++++++--- src/pages/approval-workflows/create.tsx | 8 +- src/pages/approval-workflows/index.tsx | 3 +- 10 files changed, 207 insertions(+), 107 deletions(-) create mode 100644 src/components/ApprovalWorkflows/UserWithProfilePic.tsx diff --git a/src/components/ApprovalWorkflows/RequestedBy.tsx b/src/components/ApprovalWorkflows/RequestedBy.tsx index 0f13ae32..e3c8d7da 100644 --- a/src/components/ApprovalWorkflows/RequestedBy.tsx +++ b/src/components/ApprovalWorkflows/RequestedBy.tsx @@ -3,11 +3,12 @@ import React from "react"; import { FaRegUser } from "react-icons/fa"; interface Props { + prefix: string; name: string; profileImage: string; } -export default function RequestedBy({ name, profileImage }: Props) { +export default function RequestedBy({ prefix, name, profileImage }: Props) { return (
@@ -16,7 +17,7 @@ export default function RequestedBy({ name, profileImage }: Props) {

Requested by

-

{name}

+

{prefix} {name}

{name}
- +

Started on

-

{date}

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

+ {yearMonthDay} +

diff --git a/src/components/ApprovalWorkflows/UserWithProfilePic.tsx b/src/components/ApprovalWorkflows/UserWithProfilePic.tsx new file mode 100644 index 00000000..63ff9434 --- /dev/null +++ b/src/components/ApprovalWorkflows/UserWithProfilePic.tsx @@ -0,0 +1,22 @@ +import Image from "next/image"; + +interface Props { + prefix: string; + name: string; + profileImage: string; +} + +export default function UserWithProfilePic({ prefix, name, profileImage }: Props) { + return ( +
+

{prefix} {name}

+ {name} +
+ ); +}; \ No newline at end of file diff --git a/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx b/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx index e7fc77ed..266238bd 100644 --- a/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx +++ b/src/components/ApprovalWorkflows/WorkflowStepComponent.tsx @@ -1,10 +1,17 @@ -import { getUserTypeLabel, WorkflowStep } from "@/interfaces/approval.workflow"; +import { getUserTypeLabel, getUserTypeLabelShort, WorkflowStep } from "@/interfaces/approval.workflow"; import WorkflowStepNumber from "./WorkflowStepNumber"; import clsx from "clsx"; import { RiThumbUpLine } from "react-icons/ri"; import { FaWpforms } from "react-icons/fa6"; +import { User } from "@/interfaces/user"; +import UserWithProfilePic from "./UserWithProfilePic"; + +interface Props extends WorkflowStep { + workflowAssignees: User[], +} export default function WorkflowStepComponent({ + workflowAssignees, stepType, stepNumber, completed, @@ -15,12 +22,17 @@ export default function WorkflowStepComponent({ assignees, assigneesType, onClick, -}: WorkflowStep) { +}: Props) { + console.log(workflowAssignees); + console.log(completedBy) + + const completedByUser = workflowAssignees.find((assignee) => assignee.id === completedBy); + const assigneesUsers = workflowAssignees.filter(user => assignees!.includes(user.id)); return (
@@ -29,7 +41,7 @@ export default function WorkflowStepComponent({ {/* Vertical Bar connecting steps */} {!finalStep && ( -
+
)}
@@ -46,15 +58,19 @@ export default function WorkflowStepComponent({ {stepType === "form-intake" ? ( <>

Form: Intake

- {completed && ( + {completed && completedBy && (

- Completed by {completedBy} + Completed by {getUserTypeLabelShort(completedByUser?.type)} {completedByUser?.name}

)} - {!completed && ( -

- In progress... -

+ {!completed && completedBy && ( + <> + {assigneesUsers.map(user => ( +

+ {getUserTypeLabelShort(user.type)} {user.name} +

+ ))} + )} ) : ( @@ -63,12 +79,23 @@ export default function WorkflowStepComponent({

Approval: {getUserTypeLabel(assigneesType)}

{completed ? (

- Approved by {completedBy} + Approved by {workflowAssignees.find((assignee) => assignee.id === completedBy)?.name || "Unknown"}

) : !completed && currentStep ? ( -

- In progress... -

+
+ In Progress... Assignees: +
+ {assigneesUsers.map(user => ( + + + + ))} +
+
) : (

Waiting for previous steps... diff --git a/src/demo/approval_workflows.json b/src/demo/approval_workflows.json index 226167e9..47592b7f 100644 --- a/src/demo/approval_workflows.json +++ b/src/demo/approval_workflows.json @@ -7,64 +7,61 @@ "reading", "writing" ], + "requester": "ffdIipRyXTRmm10Sq2eg7P97rLB2", + "startDate": 1737712243906, "status": "pending", "steps": [ { "stepType": "form-intake", "stepNumber": 1, "completed": true, - "completedBy": "Prof. X", + "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 2, "completed": true, - "completedBy": "Prof. Y", + "completedBy": "c5fc1514-1a94-4f8c-a046-a62099097a50", "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 3, "completed": false, "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 4, "completed": false, "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 5, "completed": false, "assignees": [ - "Dir. X", - "Dir. Y", - "Dir. Z" - ], - "assigneesType": "corporate" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] } ] }, @@ -79,67 +76,64 @@ "speaking", "listening" ], + "requester": "231c84b2-a65a-49a9-803c-c664d84b13e0", + "startDate": 1737712243906, "status": "approved", "steps": [ { "stepType": "form-intake", "stepNumber": 1, "completed": true, - "completedBy": "Prof. X", + "completedBy": "fd5fce42-4bcc-4150-a143-b484e750b265", "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 2, "completed": true, - "completedBy": "Prof. Y", + "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 3, "completed": true, - "completedBy": "Prof. Y", + "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 4, "completed": true, - "completedBy": "Prof. Y", + "completedBy": "231c84b2-a65a-49a9-803c-c664d84b13e0", "assignees": [ - "Prof. X", - "Prof. Y", - "Prof. Z" - ], - "assigneesType": "teacher" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] }, { "stepType": "approval-by", "stepNumber": 5, "completed": true, - "completedBy": "Prof. Y", + "completedBy": "c5fc1514-1a94-4f8c-a046-a62099097a50", "assignees": [ - "Dir. X", - "Dir. Y", - "Dir. Z" - ], - "assigneesType": "corporate" + "fd5fce42-4bcc-4150-a143-b484e750b265", + "231c84b2-a65a-49a9-803c-c664d84b13e0", + "c5fc1514-1a94-4f8c-a046-a62099097a50" + ] } ] } diff --git a/src/interfaces/approval.workflow.ts b/src/interfaces/approval.workflow.ts index 62458976..07d19850 100644 --- a/src/interfaces/approval.workflow.ts +++ b/src/interfaces/approval.workflow.ts @@ -1,11 +1,12 @@ -import {Module} from "."; -import Option from "./option"; -import { CorporateUser, MasterCorporateUser, TeacherUser, userTypeLabels } from "./user"; +import { Module } from "."; +import { Type, User, userTypeLabels, userTypeLabelsShort } from "./user"; export interface ApprovalWorkflow { id: string, name: string, entityId: string, + requester: User["id"], + startDate: number, modules: Module[], status: ApprovalWorkflowStatus, steps: WorkflowStep[], @@ -17,35 +18,34 @@ export const StepTypeLabel: Record = { "approval-by": "Approval", }; -type AssigneesType = TeacherUser["type"] | CorporateUser["type"] | MasterCorporateUser["type"]; - export interface WorkflowStep { key?: number, stepType?: StepType, stepNumber: number, completed?: boolean, - completedBy?: string, - assignees?: (string | 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?: AssigneesType, - editView?: 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, firstStep?: boolean, currentStep?: boolean, finalStep?: boolean, selected?: boolean, - requestedBy?: string, - //requestedBy: TeacherUser | CorporateUser | MasterCorporateUser, onDelete?: () => void; onClick?: React.MouseEventHandler } -export function getUserTypeLabel(type: AssigneesType | undefined): string { +export function getUserTypeLabel(type: Type | undefined): string { if (type) return userTypeLabels[type]; return ''; } +export function getUserTypeLabelShort(type: Type | undefined): string { + if (type) return userTypeLabelsShort[type]; + return ''; +} export type ApprovalWorkflowStatus = "approved" | "pending" | "rejected"; export const ApprovalWorkflowStatusLabel: Record = { approved: "Approved", pending: "Pending", rejected: "Rejected", -}; \ No newline at end of file +}; diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index e61ceece..e16f7e0d 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -180,4 +180,14 @@ export const userTypeLabels: Record = { mastercorporate: "Master Corporate", }; +export const userTypeLabelsShort: Record = { + student: "", + teacher: "Prof.", + corporate: "Dir.", + admin: "Admin", + developer: "Dev.", + agent: "Agent", + mastercorporate: "Dir.", +}; + export type WithUser = T extends { participants: string[] } ? Omit & { participants: User[] } : T; diff --git a/src/pages/approval-workflows/[id].tsx b/src/pages/approval-workflows/[id].tsx index c05d0f74..0f197be2 100644 --- a/src/pages/approval-workflows/[id].tsx +++ b/src/pages/approval-workflows/[id].tsx @@ -1,8 +1,8 @@ import Layout from "@/components/High/Layout"; import useUser from "@/hooks/useUser"; -import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; +import { ApprovalWorkflow, getUserTypeLabelShort } from "@/interfaces/approval.workflow"; import { sessionOptions } from "@/lib/session"; -import { redirect } from "@/utils"; +import { redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { withIronSessionSsr } from "iron-session/next"; @@ -11,7 +11,6 @@ import Link from "next/link"; import { BsChevronLeft } from "react-icons/bs"; import { ToastContainer } from "react-toastify"; - import approvalWorkflowsData from '../../demo/approval_workflows.json'; // to test locally import RequestedBy from "@/components/ApprovalWorkflows/RequestedBy"; @@ -19,6 +18,9 @@ import StartedOn from "@/components/ApprovalWorkflows/StartedOn"; import Status from "@/components/ApprovalWorkflows/Status"; import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent"; import { useState } from "react"; +import useApprovalWorkflows from "@/hooks/useApprovalWorkflows"; +import { User } from "@/interfaces/user"; +import { getSpecificUsers, getUser, getUsers } from "@/utils/users.be"; export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => { const user = await requestUser(req, res); @@ -29,19 +31,40 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params } const { id } = params as { id: string }; - const workflow = approvalWorkflowsData.find(workflow => workflow.id === id); // await getApprovalWorkflow(id); + // replace later with await getApprovalWorkflow(id). Don't think a hook is needed here; + + const approvalWorkflowsDataAsWorkflows = approvalWorkflowsData as ApprovalWorkflow[]; + const workflow: ApprovalWorkflow | undefined = approvalWorkflowsDataAsWorkflows.find(workflow => workflow.id === id); if (!workflow) return redirect("/approval-workflows") + 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. + ) + ]; + return { - props: { user, workflow }, + props: serialize({ + user, + workflow, + workflowAssignees: await getSpecificUsers(allAssigneeIds), + workflowRequester: await getUser(workflow.requester), + }), }; }, sessionOptions); -export default function Home({ workflow }: { workflow: ApprovalWorkflow }) { - const { user } = useUser({ redirectTo: "/login" }); +interface Props { + user: User, + workflow: ApprovalWorkflow, + workflowAssignees: User[], + workflowRequester: User, +} +export default function Home({ user, workflow, workflowAssignees, workflowRequester }: Props) { const steps = workflow.steps; const [selectedIndex, setSelectedIndex] = useState(steps.length - 1); @@ -77,20 +100,22 @@ export default function Home({ workflow }: { workflow: ApprovalWorkflow }) {

{steps.map((step, index) => ( !step.completed) === index} - selected={index === selectedIndex} - onClick={() => handleStepClick(index)} + selected={index === selectedIndex} + onClick={() => handleStepClick(index)} /> ))}
diff --git a/src/pages/approval-workflows/create.tsx b/src/pages/approval-workflows/create.tsx index 3ce00bd5..a806186d 100644 --- a/src/pages/approval-workflows/create.tsx +++ b/src/pages/approval-workflows/create.tsx @@ -95,10 +95,12 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers name: "", entityId: "", modules: [], + requester: user.id, + startDate: Date.now(), status: "pending", steps: [ - { key: Date.now(), completed: false, editView: true, stepType: "form-intake", stepNumber: 1, firstStep: true, assignees: [null] }, - { key: Date.now() + 1, completed: false, editView: true, stepType: "approval-by", stepNumber: 2, finalStep: true, assignees: [null] }, + { 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] }, ], }; setWorkflows((prev) => [...prev, newWorkflow]); @@ -253,7 +255,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} > - + )} diff --git a/src/pages/approval-workflows/index.tsx b/src/pages/approval-workflows/index.tsx index dce7b489..a06a9b1b 100644 --- a/src/pages/approval-workflows/index.tsx +++ b/src/pages/approval-workflows/index.tsx @@ -79,11 +79,10 @@ const STATUS_OPTIONS = [ interface Props { user: User, - teachers: TeacherUser[], userEntitiesWithLabel: Entity[], } -export default function ApprovalWorkflows({ user, teachers, userEntitiesWithLabel }: Props) { +export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props) { const ENTITY_OPTIONS = [ {