- initial selected step

- assignees id to name on table view
This commit is contained in:
Joao Correia
2025-01-24 17:09:37 +00:00
parent 41d09eaad8
commit 1f7639a30e
5 changed files with 97 additions and 49 deletions

View File

@@ -9,7 +9,7 @@ interface Props {
export default function UserWithProfilePic({ prefix, name, profileImage }: Props) { export default function UserWithProfilePic({ prefix, name, profileImage }: Props) {
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<p className="text-xs font-medium text-gray-800">{prefix} {name}</p> <p className="text-xs font-medium">{prefix} {name}</p>
<Image <Image
src={profileImage} src={profileImage}
alt={name} alt={name}

View File

@@ -23,8 +23,6 @@ export default function WorkflowStepComponent({
assigneesType, assigneesType,
onClick, onClick,
}: Props) { }: Props) {
console.log(workflowAssignees);
console.log(completedBy)
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));
@@ -59,31 +57,46 @@ export default function WorkflowStepComponent({
<> <>
<p className="text-sm font-medium text-gray-800">Form: Intake</p> <p className="text-sm font-medium text-gray-800">Form: Intake</p>
{completed && completedBy && ( {completed && completedBy && (
<p 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 })}>
Completed by {getUserTypeLabelShort(completedByUser?.type)} {completedByUser?.name} <UserWithProfilePic
</p> prefix={`Completed by: ${getUserTypeLabelShort(completedByUser!.type)}`}
name={completedByUser!.name}
profileImage={completedByUser!.profilePicture}
/>
</div>
)} )}
{!completed && completedBy && ( {!completed && completedBy && (
<> <div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
{assigneesUsers.map(user => ( In Progress... Assignees:
<p key={user.id} className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}> <div className="flex flex-row flex-wrap gap-3 items-center">
{getUserTypeLabelShort(user.type)} {user.name} {assigneesUsers.map(user => (
</p> <span key={user.id}>
))} <UserWithProfilePic
</> prefix={getUserTypeLabelShort(user.type)}
name={user.name}
profileImage={user.profilePicture}
/>
</span>
))}
</div>
</div>
)} )}
</> </>
) : ( ) : (
stepType === "approval-by" && ( stepType === "approval-by" && (
<> <>
<p className="text-sm font-medium text-gray-800">Approval: {getUserTypeLabel(assigneesType)}</p> <p className="text-sm font-medium text-gray-800">Approval: {getUserTypeLabel(assigneesType)}</p>
{completed ? ( {completed && completedBy ? (
<p 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 })}>
Approved by {workflowAssignees.find((assignee) => assignee.id === completedBy)?.name || "Unknown"} <UserWithProfilePic
</p> prefix={`Approved by: ${getUserTypeLabelShort(completedByUser!.type)}`}
name={completedByUser!.name}
profileImage={completedByUser!.profilePicture}
/>
</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 })}>
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 => (
<span key={user.id}> <span key={user.id}>
@@ -97,9 +110,9 @@ export default function WorkflowStepComponent({
</div> </div>
</div> </div>
) : ( ) : (
<p 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 })}>
Waiting for previous steps... Waiting for previous steps...
</p> </div>
)} )}
</> </>
) )

View File

@@ -1,5 +1,4 @@
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import useUser from "@/hooks/useUser";
import { ApprovalWorkflow, getUserTypeLabelShort } from "@/interfaces/approval.workflow"; import { ApprovalWorkflow, getUserTypeLabelShort } from "@/interfaces/approval.workflow";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import { redirect, serialize } from "@/utils"; import { redirect, serialize } from "@/utils";
@@ -17,10 +16,9 @@ import RequestedBy from "@/components/ApprovalWorkflows/RequestedBy";
import StartedOn from "@/components/ApprovalWorkflows/StartedOn"; import StartedOn from "@/components/ApprovalWorkflows/StartedOn";
import Status from "@/components/ApprovalWorkflows/Status"; import Status from "@/components/ApprovalWorkflows/Status";
import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent"; import WorkflowStepComponent from "@/components/ApprovalWorkflows/WorkflowStepComponent";
import { useState } from "react";
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { getSpecificUsers, getUser, getUsers } from "@/utils/users.be"; 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);
@@ -32,7 +30,6 @@ 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). Don't think a hook is needed here;
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);
@@ -67,7 +64,11 @@ interface Props {
export default function Home({ user, workflow, workflowAssignees, workflowRequester }: Props) { export default function Home({ user, workflow, workflowAssignees, workflowRequester }: Props) {
const steps = workflow.steps; const steps = workflow.steps;
const [selectedIndex, setSelectedIndex] = useState(steps.length - 1); let currentStep = steps.findIndex(step => !step.completed);
if (currentStep === -1)
currentStep = steps.length - 1;
const [selectedIndex, setSelectedIndex] = useState(currentStep);
const handleStepClick = (index: number) => { const handleStepClick = (index: number) => {
setSelectedIndex(index); setSelectedIndex(index);
@@ -124,7 +125,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
assignees={step.assignees} assignees={step.assignees}
assigneesType={step.assigneesType} assigneesType={step.assigneesType}
finalStep={index === steps.length - 1} finalStep={index === steps.length - 1}
currentStep={steps.findIndex(step => !step.completed) === index} currentStep={currentStep === index}
selected={index === selectedIndex} selected={index === selectedIndex}
onClick={() => handleStepClick(index)} onClick={() => handleStepClick(index)}
/> />

View File

@@ -104,7 +104,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
], ],
}; };
setWorkflows((prev) => [...prev, newWorkflow]); setWorkflows((prev) => [...prev, newWorkflow]);
setSelectedWorkflowId(newId); handleSelectWorkflow(newId);
}; };
const onWorkflowChange = (updatedWorkflow: ApprovalWorkflow) => { const onWorkflowChange = (updatedWorkflow: ApprovalWorkflow) => {
@@ -113,8 +113,14 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
); );
} }
const handleSelectWorkflow = (id: string) => { const handleSelectWorkflow = (id: string | undefined) => {
setSelectedWorkflowId(id); setSelectedWorkflowId(id);
const selectedWorkflow = workflows.find(wf => wf.id === id);
if (selectedWorkflow) {
setEntityId(selectedWorkflow.entityId || null);
} else {
setEntityId(null);
}
}; };
const handleDeleteWorkflow = (id: string) => { const handleDeleteWorkflow = (id: string) => {
@@ -125,7 +131,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
setWorkflows(updatedWorkflows); setWorkflows(updatedWorkflows);
if (selectedWorkflowId === id) { if (selectedWorkflowId === id) {
setSelectedWorkflowId(updatedWorkflows.find(wf => wf.id)?.id); handleSelectWorkflow(updatedWorkflows.find(wf => wf.id)?.id);
} }
}; };

View File

@@ -1,17 +1,16 @@
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
import { Module, ModuleTypeLabels } from "@/interfaces"; import { Module, ModuleTypeLabels } from "@/interfaces";
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow"; import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow";
import { Entity, EntityWithRoles } from "@/interfaces/entity"; import { Entity, EntityWithRoles } from "@/interfaces/entity";
import { TeacherUser, User } from "@/interfaces/user"; 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 { getEntities } from "@/utils/entities.be"; import { getEntities } from "@/utils/entities.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { getUsers } from "@/utils/users.be"; import { getSpecificUsers } from "@/utils/users.be";
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
@@ -24,6 +23,8 @@ import { FaRegEdit } from "react-icons/fa";
import { IoIosAddCircleOutline } from "react-icons/io"; import { IoIosAddCircleOutline } from "react-icons/io";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import approvalWorkflowsData from '../../demo/approval_workflows.json'; // to test locally
const columnHelper = createColumnHelper<ApprovalWorkflow>(); const columnHelper = createColumnHelper<ApprovalWorkflow>();
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
@@ -37,9 +38,25 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
const allowedEntities = findAllowedEntities(user, entities, "view_approval_workflows"); */ 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 allAssigneeIds: string[] = [
...new Set(
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[]
)
];
return { return {
props: serialize({ props: serialize({
user, user,
workflows,
workflowsAssignees: await getSpecificUsers(allAssigneeIds),
userEntitiesWithLabel: await getEntities(user.entities.map(entity => entity.id)), userEntitiesWithLabel: await getEntities(user.entities.map(entity => entity.id)),
}), }),
}; };
@@ -79,10 +96,12 @@ const STATUS_OPTIONS = [
interface Props { interface Props {
user: User, user: User,
workflows: ApprovalWorkflow[],
workflowsAssignees: User[],
userEntitiesWithLabel: Entity[], userEntitiesWithLabel: Entity[],
} }
export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props) { export default function ApprovalWorkflows({ user, workflows, workflowsAssignees, userEntitiesWithLabel }: Props) {
const ENTITY_OPTIONS = [ const ENTITY_OPTIONS = [
{ {
@@ -100,13 +119,11 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
.sort((a, b) => a.label.localeCompare(b.label)), .sort((a, b) => a.label.localeCompare(b.label)),
]; ];
const [filteredApprovalWorkflows, setFilteredApprovalWorkflows] = useState<ApprovalWorkflow[]>([]); const [filteredWorkflows, setFilteredWorkflows] = useState<ApprovalWorkflow[]>([]);
const [statusFilter, setStatusFilter] = useState<CustomStatus>("all"); const [statusFilter, setStatusFilter] = useState<CustomStatus>("all");
const [entityFilter, setEntityFilter] = useState<CustomEntity>("all"); const [entityFilter, setEntityFilter] = useState<CustomEntity>("all");
const { approvalWorkflows/* , reload */ } = useApprovalWorkflows();
useEffect(() => { useEffect(() => {
const filters = []; const filters = [];
if (statusFilter) { if (statusFilter) {
@@ -118,9 +135,9 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
if (filter) filters.push(filter); if (filter) filters.push(filter);
} }
setFilteredApprovalWorkflows([...filters.reduce((d, f) => d.filter(f), approvalWorkflows)]); setFilteredWorkflows([...filters.reduce((d, f) => d.filter(f), workflows)]);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [approvalWorkflows, statusFilter, entityFilter]); }, [workflows, statusFilter, entityFilter]);
const deleteApprovalWorkflow = (id: string, name: string) => { const deleteApprovalWorkflow = (id: string, name: string) => {
if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return; if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return;
@@ -193,21 +210,25 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
), ),
}), }),
columnHelper.accessor("steps", { columnHelper.accessor("steps", {
id: "currentApprovers", id: "currentAssignees",
header: "CURRENT APPROVERS", header: "CURRENT ASSIGNEES",
cell: (info) => { cell: (info) => {
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 approvers = currentStep?.assignees || [];
const assignees = currentStep?.assignees!.map((assigneeId) => {
const assignee = workflowsAssignees.find((user) => user.id === assigneeId);
return assignee?.name || "Unknown Assignee";
});
return ( return (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{approvers.map((approver: string | null | undefined, index: number) => ( {assignees?.map((assigneeName: string, index: number) => (
<span <span
key={index} key={index}
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-gray-100 border border-gray-300 text-gray-900" className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-gray-100 border border-gray-300 text-gray-900"
> >
{approver} {assigneeName}
</span> </span>
))} ))}
</div> </div>
@@ -236,10 +257,17 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
cell: ({ row }: { row: { original: ApprovalWorkflow } }) => { cell: ({ row }: { row: { original: ApprovalWorkflow } }) => {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
<Link data-tip="Edit" href={`/approval-workflows/${row.original.id}`} className="cursor-pointer tooltip"> <Link onClick={(e) => e.stopPropagation()} data-tip="Edit" href={`/approval-workflows/${row.original.id}/edit`} className="cursor-pointer tooltip">
<FaRegEdit className="hover:text-mti-purple-light transition ease-in-out duration-300" /> <FaRegEdit className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</Link> </Link>
<button data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteApprovalWorkflow(row.original.id, row.original.name)}> <button
data-tip="Delete"
className="cursor-pointer tooltip"
onClick={(e) => {
e.stopPropagation();
deleteApprovalWorkflow(row.original.id, row.original.name);
}}
>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" /> <BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</button> </button>
</div> </div>
@@ -249,7 +277,7 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
]; ];
const table = useReactTable({ const table = useReactTable({
data: filteredApprovalWorkflows, data: filteredWorkflows,
columns: columns, columns: columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
@@ -315,7 +343,7 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th key={header.id} className="px-2 py-3 text-left text-purple-900"> <th key={header.id} className="px-3 py-2 text-left text-purple-900">
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
@@ -338,7 +366,7 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
{row.getVisibleCells().map((cell, cellIndex) => { {row.getVisibleCells().map((cell, cellIndex) => {
const lastCellIndex = row.getVisibleCells().length - 1; const lastCellIndex = row.getVisibleCells().length - 1;
let cellClasses = "px-4 py-2 bg-purple-50 border-y-2 border-purple-300"; let cellClasses = "px-3 py-2 bg-purple-50 border-y-2 border-purple-300";
if (cellIndex === 0) { if (cellIndex === 0) {
cellClasses += " border-l-2 rounded-l-2xl"; cellClasses += " border-l-2 rounded-l-2xl";
} }