- initial selected step
- assignees id to name on table view
This commit is contained in:
@@ -9,7 +9,7 @@ interface Props {
|
||||
export default function UserWithProfilePic({ prefix, name, profileImage }: Props) {
|
||||
return (
|
||||
<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
|
||||
src={profileImage}
|
||||
alt={name}
|
||||
|
||||
@@ -23,8 +23,6 @@ export default function WorkflowStepComponent({
|
||||
assigneesType,
|
||||
onClick,
|
||||
}: Props) {
|
||||
console.log(workflowAssignees);
|
||||
console.log(completedBy)
|
||||
|
||||
const completedByUser = workflowAssignees.find((assignee) => assignee.id === completedBy);
|
||||
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>
|
||||
{completed && completedBy && (
|
||||
<p className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
|
||||
Completed by {getUserTypeLabelShort(completedByUser?.type)} {completedByUser?.name}
|
||||
</p>
|
||||
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
|
||||
<UserWithProfilePic
|
||||
prefix={`Completed by: ${getUserTypeLabelShort(completedByUser!.type)}`}
|
||||
name={completedByUser!.name}
|
||||
profileImage={completedByUser!.profilePicture}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!completed && completedBy && (
|
||||
<>
|
||||
{assigneesUsers.map(user => (
|
||||
<p key={user.id} className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
|
||||
{getUserTypeLabelShort(user.type)} {user.name}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
|
||||
In Progress... Assignees:
|
||||
<div className="flex flex-row flex-wrap gap-3 items-center">
|
||||
{assigneesUsers.map(user => (
|
||||
<span key={user.id}>
|
||||
<UserWithProfilePic
|
||||
prefix={getUserTypeLabelShort(user.type)}
|
||||
name={user.name}
|
||||
profileImage={user.profilePicture}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
stepType === "approval-by" && (
|
||||
<>
|
||||
<p className="text-sm font-medium text-gray-800">Approval: {getUserTypeLabel(assigneesType)}</p>
|
||||
{completed ? (
|
||||
<p 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"}
|
||||
</p>
|
||||
{completed && completedBy ? (
|
||||
<div className={clsx("text-xs font-medium", { "text-mti-purple-ultradark": selected, "text-gray-800": !selected })}>
|
||||
<UserWithProfilePic
|
||||
prefix={`Approved by: ${getUserTypeLabelShort(completedByUser!.type)}`}
|
||||
name={completedByUser!.name}
|
||||
profileImage={completedByUser!.profilePicture}
|
||||
/>
|
||||
</div>
|
||||
) : !completed && currentStep ? (
|
||||
<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">
|
||||
{assigneesUsers.map(user => (
|
||||
<span key={user.id}>
|
||||
@@ -97,9 +110,9 @@ export default function WorkflowStepComponent({
|
||||
</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...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Layout from "@/components/High/Layout";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import { ApprovalWorkflow, getUserTypeLabelShort } from "@/interfaces/approval.workflow";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { redirect, serialize } from "@/utils";
|
||||
@@ -17,10 +16,9 @@ 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 { useState } from "react";
|
||||
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
|
||||
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 }) => {
|
||||
const user = await requestUser(req, res);
|
||||
@@ -32,7 +30,6 @@ 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;
|
||||
|
||||
const approvalWorkflowsDataAsWorkflows = approvalWorkflowsData as ApprovalWorkflow[];
|
||||
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) {
|
||||
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) => {
|
||||
setSelectedIndex(index);
|
||||
@@ -124,7 +125,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
assignees={step.assignees}
|
||||
assigneesType={step.assigneesType}
|
||||
finalStep={index === steps.length - 1}
|
||||
currentStep={steps.findIndex(step => !step.completed) === index}
|
||||
currentStep={currentStep === index}
|
||||
selected={index === selectedIndex}
|
||||
onClick={() => handleStepClick(index)}
|
||||
/>
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
|
||||
],
|
||||
};
|
||||
setWorkflows((prev) => [...prev, newWorkflow]);
|
||||
setSelectedWorkflowId(newId);
|
||||
handleSelectWorkflow(newId);
|
||||
};
|
||||
|
||||
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);
|
||||
const selectedWorkflow = workflows.find(wf => wf.id === id);
|
||||
if (selectedWorkflow) {
|
||||
setEntityId(selectedWorkflow.entityId || null);
|
||||
} else {
|
||||
setEntityId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorkflow = (id: string) => {
|
||||
@@ -125,7 +131,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesTeachers
|
||||
setWorkflows(updatedWorkflows);
|
||||
|
||||
if (selectedWorkflowId === id) {
|
||||
setSelectedWorkflowId(updatedWorkflows.find(wf => wf.id)?.id);
|
||||
handleSelectWorkflow(updatedWorkflows.find(wf => wf.id)?.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import Layout from "@/components/High/Layout";
|
||||
import Button from "@/components/Low/Button";
|
||||
import Select from "@/components/Low/Select";
|
||||
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
|
||||
import { Module, ModuleTypeLabels } from "@/interfaces";
|
||||
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow";
|
||||
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
||||
import { TeacherUser, User } from "@/interfaces/user";
|
||||
import { 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 { getUsers } from "@/utils/users.be";
|
||||
import { getSpecificUsers } from "@/utils/users.be";
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
@@ -24,6 +23,8 @@ import { FaRegEdit } from "react-icons/fa";
|
||||
import { IoIosAddCircleOutline } from "react-icons/io";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
|
||||
import approvalWorkflowsData from '../../demo/approval_workflows.json'; // to test locally
|
||||
|
||||
const columnHelper = createColumnHelper<ApprovalWorkflow>();
|
||||
|
||||
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 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 {
|
||||
props: serialize({
|
||||
user,
|
||||
workflows,
|
||||
workflowsAssignees: await getSpecificUsers(allAssigneeIds),
|
||||
userEntitiesWithLabel: await getEntities(user.entities.map(entity => entity.id)),
|
||||
}),
|
||||
};
|
||||
@@ -79,10 +96,12 @@ const STATUS_OPTIONS = [
|
||||
|
||||
interface Props {
|
||||
user: User,
|
||||
workflows: ApprovalWorkflow[],
|
||||
workflowsAssignees: User[],
|
||||
userEntitiesWithLabel: Entity[],
|
||||
}
|
||||
|
||||
export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props) {
|
||||
export default function ApprovalWorkflows({ user, workflows, workflowsAssignees, userEntitiesWithLabel }: Props) {
|
||||
|
||||
const ENTITY_OPTIONS = [
|
||||
{
|
||||
@@ -100,13 +119,11 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
|
||||
.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 [entityFilter, setEntityFilter] = useState<CustomEntity>("all");
|
||||
|
||||
const { approvalWorkflows/* , reload */ } = useApprovalWorkflows();
|
||||
|
||||
useEffect(() => {
|
||||
const filters = [];
|
||||
if (statusFilter) {
|
||||
@@ -118,9 +135,9 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
|
||||
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
|
||||
}, [approvalWorkflows, statusFilter, entityFilter]);
|
||||
}, [workflows, statusFilter, entityFilter]);
|
||||
|
||||
const deleteApprovalWorkflow = (id: string, name: string) => {
|
||||
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", {
|
||||
id: "currentApprovers",
|
||||
header: "CURRENT APPROVERS",
|
||||
id: "currentAssignees",
|
||||
header: "CURRENT ASSIGNEES",
|
||||
cell: (info) => {
|
||||
const steps = info.row.original.steps;
|
||||
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 (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{approvers.map((approver: string | null | undefined, index: number) => (
|
||||
{assignees?.map((assigneeName: string, index: number) => (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{approver}
|
||||
{assigneeName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -236,10 +257,17 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
|
||||
cell: ({ row }: { row: { original: ApprovalWorkflow } }) => {
|
||||
return (
|
||||
<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" />
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -249,7 +277,7 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredApprovalWorkflows,
|
||||
data: filteredWorkflows,
|
||||
columns: columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
@@ -315,7 +343,7 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{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
|
||||
? null
|
||||
: flexRender(
|
||||
@@ -338,7 +366,7 @@ export default function ApprovalWorkflows({ user, userEntitiesWithLabel }: Props
|
||||
{row.getVisibleCells().map((cell, cellIndex) => {
|
||||
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) {
|
||||
cellClasses += " border-l-2 rounded-l-2xl";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user