start implementation of approval workflows. Add nav, page, empty table, and status filtering
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
BsClipboardData,
|
BsClipboardData,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
|
import { GoWorkflow } from "react-icons/go";
|
||||||
import { CiDumbbell } from "react-icons/ci";
|
import { CiDumbbell } from "react-icons/ci";
|
||||||
import { RiLogoutBoxFill } from "react-icons/ri";
|
import { RiLogoutBoxFill } from "react-icons/ri";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -183,6 +184,16 @@ export default function Sidebar({
|
|||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{checkAccess(user, ["admin", "developer", "teacher", "corporate", "mastercorporate"]) && (
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={GoWorkflow}
|
||||||
|
label="Approval Workflows"
|
||||||
|
path={path}
|
||||||
|
keyPath="/approval-workflows"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
||||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized />
|
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized />
|
||||||
|
|||||||
1
src/demo/approval_workflows.json
Normal file
1
src/demo/approval_workflows.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
26
src/hooks/useApprovalWorkflows.tsx
Normal file
26
src/hooks/useApprovalWorkflows.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import approvalWorkflowsData from '../demo/approval_workflows.json' // to test locally
|
||||||
|
|
||||||
|
export default function useApprovalWorkflows() {
|
||||||
|
const [approvalWorkflows, setApprovalWorkflows] = useState<ApprovalWorkflow[]>(approvalWorkflowsData);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
/* const getData = useCallback(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get<ApprovalWorkflow[]>(`/api/approvalWorkflows`)
|
||||||
|
.then((response) => setApprovalWorkflows(response.data))
|
||||||
|
.catch((error) => {
|
||||||
|
setIsError(true);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(getData, [getData]); */
|
||||||
|
|
||||||
|
return { approvalWorkflows, isLoading, isError/* , reload: getData */ };
|
||||||
|
}
|
||||||
17
src/interfaces/approval.workflow.ts
Normal file
17
src/interfaces/approval.workflow.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {Module} from ".";
|
||||||
|
|
||||||
|
export interface ApprovalWorkflow {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
module: Module;
|
||||||
|
status: ApprovalWorkflowStatus;
|
||||||
|
approvers: string;
|
||||||
|
step: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApprovalWorkflowStatus = "approved" | "pending" | "rejected";
|
||||||
|
export const ApprovalWorkflowStatusLabel: {[key in ApprovalWorkflowStatus]: string} = {
|
||||||
|
approved: "Approved",
|
||||||
|
pending: "Pending",
|
||||||
|
rejected: "Rejected",
|
||||||
|
};
|
||||||
186
src/pages/approval-workflows.tsx
Normal file
186
src/pages/approval-workflows.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import Layout from "@/components/High/Layout";
|
||||||
|
import Select from "@/components/Low/Select";
|
||||||
|
import useApprovalWorkflows from "@/hooks/useApprovalWorkflows";
|
||||||
|
import useUser from "@/hooks/useUser";
|
||||||
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel } from "@/interfaces/approval.workflow";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { redirect } from "@/utils";
|
||||||
|
import { requestUser } from "@/utils/api";
|
||||||
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
|
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { 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("/")
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { user },
|
||||||
|
};
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
const StatusClassNames: { [key in ApprovalWorkflowStatus]: string } = {
|
||||||
|
approved: "bg-mti-green-light",
|
||||||
|
pending: "bg-mti-orange-light",
|
||||||
|
rejected: "bg-mti-red-light",
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapedURL = process.env.NEXT_PUBLIC_WEBSITE_URL || "".replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const fromHomepage = [new RegExp(`^${escapedURL}`), /\/contact$/];
|
||||||
|
|
||||||
|
type CustomStatus = ApprovalWorkflowStatus | "all" | "pending";
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ApprovalWorkflows() {
|
||||||
|
const [filteredApprovalWorkflows, setFilteredApprovalWorkflows] = useState<ApprovalWorkflow[]>([]);
|
||||||
|
const [selectedApprovalWorkflow, setSelectedApprovalWorkflow] = useState<ApprovalWorkflow>();
|
||||||
|
|
||||||
|
const [statusFilter, setStatusFilter] = useState<CustomStatus>("pending");
|
||||||
|
|
||||||
|
const { user } = useUser({ redirectTo: "/login" });
|
||||||
|
|
||||||
|
const { approvalWorkflows/* , reload */ } = useApprovalWorkflows();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filters = [];
|
||||||
|
if (statusFilter) {
|
||||||
|
const filter = STATUS_OPTIONS.find((x) => x.value === statusFilter)?.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]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
/* columnHelper.accessor("id", {
|
||||||
|
header: "ID",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}), */
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: "Name",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("module", {
|
||||||
|
header: "Module",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("status", {
|
||||||
|
header: "Status",
|
||||||
|
cell: (info) => (
|
||||||
|
<span className={clsx("rounded-lg p-1 px-2 text-white", StatusClassNames[info.getValue()])}>
|
||||||
|
{ApprovalWorkflowStatusLabel[info.getValue()]}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("approvers", {
|
||||||
|
header: "Approvers",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("step", {
|
||||||
|
header: "Step",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<table className="bg-mti-purple-ultralight/40 w-full rounded-xl">
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th className="px-4 py-4 text-left" key={header.id}>
|
||||||
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody className="px-2">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<tr
|
||||||
|
className={clsx(
|
||||||
|
"even:bg-mti-purple-ultralight/40 hover:bg-mti-purple-ultralight cursor-pointer rounded-lg py-2 odd:bg-white",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedApprovalWorkflow(row.original)}
|
||||||
|
key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td className="w-fit items-center px-4 py-2" key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Layout>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user