Files
encoach_frontend/src/pages/approval-workflows/index.tsx

368 lines
15 KiB
TypeScript

import Layout from "@/components/High/Layout";
import Button from "@/components/Low/Button";
import Select from "@/components/Low/Select";
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
import useUser from "@/hooks/useUser";
import { Module, ModuleTypeLabels } from "@/interfaces";
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow";
import { EntityWithRoles } from "@/interfaces/entity";
import { TeacherUser, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { getEntitiesWithRoles } from "@/utils/entities.be";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { findAllowedEntities } from "@/utils/permissions";
import { isAdmin } from "@/utils/users";
import { getUsers } from "@/utils/users.be";
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import axios, { all } from "axios";
import clsx from "clsx";
import { withIronSessionSsr } from "iron-session/next";
import Head from "next/head";
import Link from "next/link";
import { useEffect, useState } from "react";
import { BsTrash } from "react-icons/bs";
import { FaRegEdit } from "react-icons/fa";
import { IoIosAddCircleOutline } from "react-icons/io";
import { toast, ToastContainer } from "react-toastify";
const columnHelper = createColumnHelper<ApprovalWorkflow>();
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
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");
return {
props: serialize({
user,
teachers: await getUsers({ type: "teacher" }) as TeacherUser[],
allowedEntities,
}),
};
}, sessionOptions);
const StatusClassNames: { [key in ApprovalWorkflowStatus]: string } = {
approved: "bg-green-100 text-green-800 border border-green-300 before:content-[''] before:w-2 before:h-2 before:bg-green-500 before:rounded-full before:inline-block before:mr-2",
pending: "bg-orange-100 text-orange-800 border border-orange-300 before:content-[''] before:w-2 before:h-2 before:bg-orange-500 before:rounded-full before:inline-block before:mr-2",
rejected: "bg-red-100 text-red-800 border border-red-300 before:content-[''] before:w-2 before:h-2 before:bg-red-500 before:rounded-full before:inline-block before:mr-2",
};
type CustomStatus = ApprovalWorkflowStatus | "all";
type CustomEntity = EntityWithRoles["label"] | "all";
const STATUS_OPTIONS = [
{
label: "All",
value: "all",
filter: (x: ApprovalWorkflow) => true,
},
{
label: "Approved",
value: "approved",
filter: (x: ApprovalWorkflow) => x.status === "approved",
},
{
label: "Pending",
value: "pending",
filter: (x: ApprovalWorkflow) => x.status === "pending",
},
{
label: "Rejected",
value: "rejected",
filter: (x: ApprovalWorkflow) => x.status === "rejected",
},
];
interface Props {
user: User,
teachers: TeacherUser[],
allowedEntities: EntityWithRoles[],
}
export default function ApprovalWorkflows({ user, teachers, allowedEntities }: Props) {
console.log(user);
console.log(teachers);
console.log(allowedEntities);
const ENTITY_OPTIONS = [
{
label: "All",
value: "all",
filter: (x: ApprovalWorkflow) => true,
},
...allowedEntities
.map(entity => ({
label: entity.label,
value: entity.id,
filter: (x: ApprovalWorkflow) => x.entityId === entity.id,
}))
.sort((a, b) => a.label.localeCompare(b.label)),
];
const [filteredApprovalWorkflows, setFilteredApprovalWorkflows] = useState<ApprovalWorkflow[]>([]);
const [statusFilter, setStatusFilter] = useState<CustomStatus>("all");
const [entityFilter, setEntityFilter] = useState<CustomEntity>("all");
const { approvalWorkflows/* , reload */ } = useApprovalWorkflows();
useEffect(() => {
const filters = [];
if (statusFilter) {
const filter = STATUS_OPTIONS.find((x) => x.value === statusFilter)?.filter;
if (filter) filters.push(filter);
}
if (entityFilter) {
const filter = ENTITY_OPTIONS.find((x) => x.value === entityFilter)?.filter;
if (filter) filters.push(filter);
}
setFilteredApprovalWorkflows([...filters.reduce((d, f) => d.filter(f), approvalWorkflows)]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [approvalWorkflows, statusFilter, entityFilter]);
const deleteApprovalWorkflow = (id: string, name: string) => {
if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return;
axios
.delete(`/api/approval-workflows/${id}`)
.then(() => toast.success(`Deleted ${name} Approval Workflow.`))
.catch((reason) => {
if (reason.response.status === 404) {
toast.error("Approval Workflow not found!");
return;
}
if (reason.response.status === 403) {
toast.error("You do not have permission to delete an Approval Workflow!");
return;
}
toast.error("Something went wrong, please try again later.");
})
/* .finally(reload); */
};
const editApprovalWorkflow = (id: string) => {
};
const columns = [
/* columnHelper.accessor("id", {
header: "ID",
cell: (info) => info.getValue(),
}), */
columnHelper.accessor("name", {
header: "NAME",
cell: (info) => (
<span className="font-medium">
{info.getValue()}
</span>
),
}),
columnHelper.accessor("modules", {
header: "MODULES",
cell: (info) => (
<div className="flex flex-wrap gap-2">
{info.getValue().map((module: Module, index: number) => (
<span
key={index}
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-indigo-100 border border-indigo-300 text-indigo-900"
>
{ModuleTypeLabels[module]}
</span>
))}
</div>
),
}),
columnHelper.accessor("status", {
header: "STATUS",
cell: (info) => (
<span className={clsx("inline-block rounded-full px-3 py-1 text-sm font-medium text-left w-[110px]", StatusClassNames[info.getValue()])}>
{ApprovalWorkflowStatusLabel[info.getValue()]}
</span>
),
}),
columnHelper.accessor("steps", {
id: "currentApprovers",
header: "CURRENT APPROVERS",
cell: (info) => {
const steps = info.row.original.steps;
const currentStep = steps.find((step) => !step.completed);
const approvers = currentStep?.assignees || [];
return (
<div className="flex flex-wrap gap-2">
{approvers.map((approver: 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}
</span>
))}
</div>
);
},
}),
columnHelper.accessor("steps", {
id: "currentStep",
header: "CURRENT STEP",
cell: (info) => {
const steps = info.row.original.steps;
const currentStep = steps.find((step) => !step.completed);
return (
<span className="font-medium">
{currentStep
? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType!]}`
: "Completed"}
</span>
);
},
}),
{
header: "ACTIONS",
id: "actions",
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">
<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)}>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</button>
</div>
);
},
},
];
const table = useReactTable({
data: filteredApprovalWorkflows,
columns: columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<>
<Head>
<title>Approval Workflows Panel | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<Layout user={user} className="gap-6">
<h1 className="text-2xl font-semibold">Approval Workflows</h1>
<Link href={"/approval-workflows/create"}>
<Button
color="purple"
variant="solid"
className="max-w-fit text-lg font-medium flex items-center gap-2 text-left"
>
<IoIosAddCircleOutline className="size-6" />
Configure New Workflows
</Button>
</Link>
<div className="flex w-full items-center gap-4">
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">Status</label>
<Select
options={STATUS_OPTIONS}
value={STATUS_OPTIONS.find((x) => x.value === statusFilter)}
onChange={(value) => setStatusFilter((value?.value as ApprovalWorkflowStatus) ?? undefined)}
isClearable
placeholder="Status..."
/>
</div>
<div className="flex w-full flex-col gap-3">
<label className="text-mti-gray-dim text-base font-normal">Entity</label>
<Select
options={ENTITY_OPTIONS}
value={ENTITY_OPTIONS.find((x) => x.value === entityFilter)}
onChange={(value) => setEntityFilter((value?.value as string) ?? undefined)}
isClearable
placeholder="Entity..."
/>
</div>
</div>
<div className="px-6 pb-4 bg-purple-100 rounded-2xl">
<table
className="w-full table-auto border-separate border-spacing-y-2"
style={{ tableLayout: "auto" }}
>
<thead>
{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">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr
key={row.id}
onClick={() => window.location.href = `/approval-workflows/${row.original.id}`}
style={{ cursor: "pointer" }}
className="hover:bg-purple-100"
>
{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";
if (cellIndex === 0) {
cellClasses += " border-l-2 rounded-l-2xl";
}
if (cellIndex === lastCellIndex) {
cellClasses += " border-r-2 rounded-r-2xl";
}
return (
<td key={cellIndex} className={cellClasses}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</Layout>
)}
</>
);
}