implement rejection of steps
This commit is contained in:
@@ -17,6 +17,7 @@ export default function WorkflowStepComponent({
|
||||
stepType,
|
||||
stepNumber,
|
||||
completed,
|
||||
rejected = false,
|
||||
completedBy,
|
||||
assignees,
|
||||
finalStep,
|
||||
@@ -31,11 +32,12 @@ export default function WorkflowStepComponent({
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={clsx("flex flex-row gap-5 w-[600px] p-6 mb-5 rounded-2xl transition ease-in-out duration-300 cursor-pointer", {
|
||||
"bg-mti-red-ultralight": rejected && selected,
|
||||
"bg-mti-purple-ultralight": selected,
|
||||
})}
|
||||
>
|
||||
<div className="relative flex flex-col items-center">
|
||||
<WorkflowStepNumber stepNumber={stepNumber} selected={selected} completed={completed} finalStep={finalStep} />
|
||||
<WorkflowStepNumber stepNumber={stepNumber} selected={selected} completed={completed} finalStep={finalStep} rejected={rejected} />
|
||||
|
||||
{/* Vertical Bar connecting steps */}
|
||||
{!finalStep && (
|
||||
@@ -53,50 +55,27 @@ export default function WorkflowStepComponent({
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex flex-col gap-0">
|
||||
{stepType === "form-intake" ? (
|
||||
<>
|
||||
<p className="text-sm font-medium text-gray-800">Form: Intake</p>
|
||||
{completed && completedBy && (
|
||||
{completed && completedBy && rejected ? (
|
||||
<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">{stepType === "approval-by" ? `Approval: ${getUserTypeLabel(completedByUser!.type)} Approval` : `Form Intake: ${getUserTypeLabel(completedByUser!.type)} Intake`} </p>
|
||||
<UserWithProfilePic
|
||||
prefix={`Rejected by: ${getUserTypeLabelShort(completedByUser!.type)}`}
|
||||
name={completedByUser!.name}
|
||||
profileImage={completedByUser!.profilePicture}
|
||||
/>
|
||||
</div>
|
||||
) : completed && completedBy && !rejected ? (
|
||||
<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">{stepType === "approval-by" ? `Approval: ${getUserTypeLabel(completedByUser!.type)} Approval` : `Form Intake: ${getUserTypeLabel(completedByUser!.type)} Intake`} </p>
|
||||
<UserWithProfilePic
|
||||
prefix={`Completed 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:
|
||||
<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" && (
|
||||
<>
|
||||
{completed && completedBy ? (
|
||||
<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
|
||||
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 })}>
|
||||
<p className="text-sm font-medium text-gray-800">Approval: </p>
|
||||
<p className="text-sm font-medium text-gray-800">{stepType === "approval-by" ? `Approval:` : `Form Intake:`} </p>
|
||||
In Progress... Assignees:
|
||||
<div className="flex flex-row flex-wrap gap-3 items-center">
|
||||
{assigneesUsers.map(user => (
|
||||
@@ -112,13 +91,10 @@ export default function WorkflowStepComponent({
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
<p className="text-sm font-medium text-gray-800">{stepType === "approval-by" ? `Approval:` : `Form Intake:`} </p>
|
||||
Waiting for previous steps...
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { WorkflowStep } from "@/interfaces/approval.workflow";
|
||||
import clsx from "clsx";
|
||||
import { IoCheckmarkDoneSharp, IoCheckmarkSharp } from "react-icons/io5";
|
||||
import { RxCross2 } from "react-icons/rx";
|
||||
|
||||
type Props = Pick<WorkflowStep, 'stepNumber' | 'completed' | 'finalStep' | 'selected'>
|
||||
type Props = Pick<WorkflowStep, 'stepNumber' | 'completed' | 'finalStep' | 'selected' | 'rejected'>
|
||||
|
||||
export default function WorkflowStepNumber({ stepNumber, selected = false, completed, finalStep }: Props) {
|
||||
export default function WorkflowStepNumber({ stepNumber, selected = false, completed, rejected, finalStep }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center min-w-11 min-h-11 rounded-full',
|
||||
{
|
||||
'bg-mti-red-dark text-mti-red-ultralight': rejected,
|
||||
'bg-mti-purple-dark text-mti-purple-ultralight': selected,
|
||||
'bg-mti-purple-ultralight text-gray-500': !selected,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{completed && finalStep ? (
|
||||
{rejected ? (
|
||||
<RxCross2 className="text-xl font-bold" size={25}/>
|
||||
) : completed && finalStep ? (
|
||||
<IoCheckmarkDoneSharp className="text-xl font-bold" size={25} />
|
||||
) : completed && !finalStep ? (
|
||||
<IoCheckmarkSharp className="text-xl font-bold" size={25} />
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface WorkflowStep {
|
||||
stepType: StepType,
|
||||
stepNumber: number,
|
||||
completed: boolean,
|
||||
rejected?: boolean,
|
||||
completedBy?: User["id"],
|
||||
completedDate?: number,
|
||||
assignees: (User["id"])[];
|
||||
|
||||
@@ -47,7 +47,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
|
||||
user,
|
||||
workflow,
|
||||
userEntitiesWithLabel,
|
||||
userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: {$in: ["teacher", "corporate", "mastercorporate", "developer"]} }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||
userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||
}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
@@ -134,9 +134,7 @@ export default function Home({ user, workflow, userEntitiesWithLabel, userEntiti
|
||||
.then(() => {
|
||||
toast.success("Approval Workflow cloned successfully.");
|
||||
setIsRedirecting(true);
|
||||
setTimeout(() => {
|
||||
router.push("/approval-workflows");
|
||||
}, 1000);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 401) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }
|
||||
props: serialize({
|
||||
user,
|
||||
workflow,
|
||||
workflowEntityApprovers: await getEntityUsers(workflow.entityId, undefined, { type: {$in: ["teacher", "corporate", "mastercorporate", "developer"]} }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||
workflowEntityApprovers: await getEntityUsers(workflow.entityId, undefined, { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||
}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
@@ -86,7 +86,7 @@ export default function Home({ user, workflow, workflowEntityApprovers }: Props)
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
if (!updatedWorkflow){
|
||||
if (!updatedWorkflow) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -105,9 +105,7 @@ export default function Home({ user, workflow, workflowEntityApprovers }: Props)
|
||||
.then(() => {
|
||||
toast.success("Approval Workflow edited successfully.");
|
||||
setIsRedirecting(true);
|
||||
setTimeout(() => {
|
||||
router.push("/approval-workflows");
|
||||
}, 1000);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 401) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { RiThumbUpLine } from "react-icons/ri";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
|
||||
import { FiSave } from "react-icons/fi";
|
||||
import { RxCrossCircled } from "react-icons/rx";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
|
||||
const user = await requestUser(req, res);
|
||||
@@ -71,7 +72,7 @@ interface Props {
|
||||
export default function Home({ user, workflow, workflowAssignees, workflowRequester }: Props) {
|
||||
const steps = workflow.steps;
|
||||
|
||||
let currentStep = steps.findIndex(step => !step.completed);
|
||||
let currentStep = steps.findIndex(step => !step.completed || step.rejected);
|
||||
if (currentStep === -1)
|
||||
currentStep = steps.length - 1;
|
||||
|
||||
@@ -110,9 +111,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
.then(() => {
|
||||
toast.success("Comments saved successfully.");
|
||||
setIsRedirecting(true);
|
||||
setTimeout(() => {
|
||||
router.reload();
|
||||
}, 1000);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 401) {
|
||||
@@ -129,7 +128,6 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
};
|
||||
|
||||
const handleApproveStep = () => {
|
||||
if (!confirm(`Are you sure you want to approve this step?`)) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const updatedWorkflow: ApprovalWorkflow = {
|
||||
@@ -151,9 +149,48 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
.then(() => {
|
||||
toast.success("Step approved successfully.");
|
||||
setIsRedirecting(true);
|
||||
setTimeout(() => {
|
||||
router.reload();
|
||||
}, 1000);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 401) {
|
||||
toast.error("Not logged in!");
|
||||
} else if (reason.response.status === 403) {
|
||||
toast.error("You do not have permission to approve this step!");
|
||||
} else {
|
||||
toast.error("Something went wrong, please try again later.");
|
||||
}
|
||||
setIsLoading(false);
|
||||
console.log("Submitted Values:", updatedWorkflow);
|
||||
return;
|
||||
})
|
||||
};
|
||||
|
||||
const handleRejectStep = () => {
|
||||
if (!confirm(`Are you sure you want to reject this step?`)) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const updatedWorkflow: ApprovalWorkflow = {
|
||||
...workflow,
|
||||
status: "rejected",
|
||||
steps: workflow.steps.map((step, index) =>
|
||||
index === selectedStepIndex ?
|
||||
{
|
||||
...step,
|
||||
completed: true,
|
||||
completedBy: user.id,
|
||||
completedDate: Date.now(),
|
||||
rejected: true,
|
||||
}
|
||||
: step
|
||||
)
|
||||
};
|
||||
|
||||
axios
|
||||
.put(`/api/approval-workflows/${workflow._id}`, updatedWorkflow)
|
||||
.then(() => {
|
||||
toast.success("Step rejected successfully.");
|
||||
setIsRedirecting(true);
|
||||
router.reload();
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 401) {
|
||||
@@ -219,6 +256,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
key={index}
|
||||
completed={step.completed}
|
||||
completedBy={step.completedBy}
|
||||
rejected={step.rejected}
|
||||
stepNumber={step.stepNumber}
|
||||
stepType={step.stepType}
|
||||
assignees={step.assignees}
|
||||
@@ -276,7 +314,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
|
||||
{selectedStep.completed ? (
|
||||
<div className={"text-base font-medium text-gray-500 flex flex-col gap-6"}>
|
||||
Approved on {new Date(selectedStep.completedDate!).toLocaleString("en-CA", {
|
||||
{selectedStep.rejected ? "Rejected" : "Approved"} on {new Date(selectedStep.completedDate!).toLocaleString("en-CA", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
@@ -286,7 +324,7 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
hour12: false,
|
||||
}).replace(", ", " at ")}
|
||||
<div className="flex flex-row gap-1 text-sm">
|
||||
<p className="text-base">Approved by:</p>
|
||||
<p className="text-base">{selectedStep.rejected ? "Rejected" : "Approved"} by:</p>
|
||||
{(() => {
|
||||
const assignee = workflowAssignees.find(
|
||||
(assignee) => assignee.id === selectedStep.completedBy
|
||||
@@ -324,7 +362,8 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedStepIndex === currentStep && !selectedStep.completed &&
|
||||
{selectedStepIndex === currentStep && !selectedStep.completed && !selectedStep.rejected &&
|
||||
<div className="flex flex-row gap-2 ">
|
||||
<Button
|
||||
type="submit"
|
||||
color="purple"
|
||||
@@ -351,6 +390,33 @@ export default function Home({ user, workflow, workflowAssignees, workflowReques
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
color="red"
|
||||
variant="solid"
|
||||
disabled={!selectedStep.assignees.includes(user.id) || isLoading}
|
||||
onClick={handleRejectStep}
|
||||
padding="px-6 py-2"
|
||||
className="mb-3 w-1/2 text-lg flex items-center justify-center gap-2 text-left"
|
||||
>
|
||||
{isRedirecting ? (
|
||||
<>
|
||||
<FaSpinner className="animate-spin size-5" />
|
||||
Reloading...
|
||||
</>
|
||||
) : isLoading ? (
|
||||
<>
|
||||
<FaSpinner className="animate-spin size-5" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RxCrossCircled size={20} />
|
||||
Reject Step
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<hr className="my-4 h-[4px] bg-mti-purple-ultralight rounded-full w-full" />
|
||||
|
||||
@@ -38,7 +38,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||
props: serialize({
|
||||
user,
|
||||
userEntitiesWithLabel,
|
||||
userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: {$in: ["teacher", "corporate", "mastercorporate", "developer"]} }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||
userEntitiesApprovers: await getEntitiesUsers(userEntitiesWithLabel.map(entity => entity.id), { type: { $in: ["teacher", "corporate", "mastercorporate", "developer"] } }) as (TeacherUser | CorporateUser | MasterCorporateUser | DeveloperUser)[],
|
||||
}),
|
||||
};
|
||||
}, sessionOptions);
|
||||
@@ -103,9 +103,7 @@ export default function Home({ user, userEntitiesWithLabel, userEntitiesApprover
|
||||
.then(() => {
|
||||
toast.success("Approval Workflows created successfully.");
|
||||
setIsRedirecting(true);
|
||||
setTimeout(() => {
|
||||
router.push("/approval-workflows");
|
||||
}, 1000);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 401) {
|
||||
|
||||
@@ -155,9 +155,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
|
||||
.delete(`/api/approval-workflows/${id}`)
|
||||
.then(() => {
|
||||
toast.success(`Successfully deleted ${name} Approval Workflow.`);
|
||||
setTimeout(() => {
|
||||
router.reload();
|
||||
}, 1000);
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (reason.response.status === 404) {
|
||||
@@ -217,6 +215,9 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
|
||||
cell: (info) => {
|
||||
const steps = info.row.original.steps;
|
||||
const currentStep = steps.find((step) => !step.completed);
|
||||
const rejected = steps.find((step) => step.rejected);
|
||||
|
||||
if(rejected) return "";
|
||||
|
||||
const assignees = currentStep?.assignees.map((assigneeId) => {
|
||||
const assignee = workflowsAssignees.find((user) => user.id === assigneeId);
|
||||
@@ -243,10 +244,11 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
|
||||
cell: (info) => {
|
||||
const steps = info.row.original.steps;
|
||||
const currentStep = steps.find((step) => !step.completed);
|
||||
const rejected = steps.find((step) => step.rejected);
|
||||
|
||||
return (
|
||||
<span className="font-medium">
|
||||
{currentStep
|
||||
{currentStep && !rejected
|
||||
? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}`
|
||||
: "Completed"}
|
||||
</span>
|
||||
@@ -259,6 +261,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
|
||||
cell: ({ row }) => {
|
||||
const steps = row.original.steps;
|
||||
const currentStep = steps.find((step) => !step.completed);
|
||||
const rejected = steps.find((step) => step.rejected);
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
@@ -282,7 +285,7 @@ export default function ApprovalWorkflows({ user, workflows, workflowsAssignees,
|
||||
<FaRegClone className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||
</Link>
|
||||
|
||||
{currentStep && (
|
||||
{currentStep && !rejected && (
|
||||
<Link
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-tip="Edit"
|
||||
|
||||
Reference in New Issue
Block a user