Add entityId to workflow. Allow filter workflows based on entityId. Restrict creation of workflows based on user entities.

This commit is contained in:
Joao Correia
2025-01-22 16:39:18 +00:00
parent 8f8d5e5640
commit 4895f00184
5 changed files with 124 additions and 29 deletions

View File

@@ -2,6 +2,7 @@
{
"id": "kajhfakscbka-asacaca-acawesae",
"name": "English Exam 1st Quarter 2025",
"entityId": "ae64d98e-18e4-4978-b600-c542a5b43c2d",
"modules": [
"reading",
"writing"
@@ -70,6 +71,7 @@
{
"id": "aaaaaakscbka-asacaca-acawesae",
"name": "English Exam 2nd Quarter 2025",
"entityId": "85fc76e6-da50-45f6-a1ed-0fe3802ecf02",
"modules": [
"reading",
"writing",

View File

@@ -4,6 +4,7 @@ import { CorporateUser, MasterCorporateUser, TeacherUser, userTypeLabels } from
export interface ApprovalWorkflow {
id: string,
name: string,
entityId: string,
modules: Module[],
status: ApprovalWorkflowStatus,
steps: WorkflowStep[],

View File

@@ -1,7 +1,7 @@
import Layout from "@/components/High/Layout";
import useUser from "@/hooks/useUser";
import { sessionOptions } from "@/lib/session";
import { redirect } from "@/utils";
import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { withIronSessionSsr } from "iron-session/next";
@@ -17,6 +17,11 @@ import Input from "@/components/Low/Input";
import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
import { useState } from "react";
import { MdFormatListBulletedAdd } from "react-icons/md";
import { User } from "@/interfaces/user";
import Select from "@/components/Low/Select";
import { getUserWithEntity } from "@/utils/users.be";
import { getEntities } from "@/utils/entities.be";
import { Entity } from "@/interfaces/entity";
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
@@ -26,17 +31,31 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
return redirect("/")
return {
props: { user },
props: serialize({
user,
userEntitiesWithLabel: await getEntities(user.entities.map(entity => entity.id)),
}),
};
}, sessionOptions);
export default function Home() {
const { user } = useUser({ redirectTo: "/login" });
interface Props {
user: User,
userEntitiesWithLabel: Entity[],
}
export default function Home({ user, userEntitiesWithLabel }: Props) {
const [workflows, setWorkflows] = useState<ApprovalWorkflow[]>([]);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | undefined>(undefined);
const [entityId, setEntityId] = useState<string | undefined>(undefined);
const currentWorkflow = workflows.find(wf => wf.id === selectedWorkflowId);
const ENTITY_OPTIONS = userEntitiesWithLabel.map(entity => ({
label: entity.label,
value: entity.id,
filter: (x: ApprovalWorkflow) => x.entityId === entity.id,
}));
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Handle form submission logic
@@ -48,6 +67,7 @@ export default function Home() {
const newWorkflow: ApprovalWorkflow = {
id: newId,
name: "",
entityId: "",
modules: [],
status: "pending",
steps: [
@@ -74,7 +94,7 @@ export default function Home() {
setWorkflows(prev => prev.filter(wf => wf.id !== id));
if (selectedWorkflowId === id) {
setSelectedWorkflowId(null);
setSelectedWorkflowId(undefined);
}
};
@@ -138,7 +158,7 @@ export default function Home() {
<form onSubmit={handleSubmit}>
{currentWorkflow && (
<>
<div className="mb-8 flex flex-row">
<div className="mb-8 flex flex-row gap-6">
<Input
type="text"
name={currentWorkflow.name}
@@ -153,12 +173,26 @@ export default function Home() {
onWorkflowChange(updatedWorkflow);
}}
/>
<Select
options={ENTITY_OPTIONS}
onChange={(selectedEntity) => {
if (selectedEntity?.value) {
const updatedWorkflow = {
...currentWorkflow,
entityId: selectedEntity.value,
};
onWorkflowChange(updatedWorkflow);
}
}}
isClearable
placeholder="Entity..."
/>
<Button
color="purple"
variant="solid"
onClick={() => handleDeleteWorkflow(currentWorkflow.id)}
type="button"
className="min-w-fit text-lg font-medium flex items-center gap-2 text-left ml-4"
className="min-w-fit text-lg font-medium flex items-center gap-2 text-left"
>
Delete Workflow
<BsTrash className="size-6" />

View File

@@ -5,12 +5,18 @@ 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 { redirect } from "@/utils";
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 from "axios";
import axios, { all } from "axios";
import clsx from "clsx";
import { withIronSessionSsr } from "iron-session/next";
import Head from "next/head";
@@ -30,8 +36,16 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
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: { user },
props: serialize({
user,
teachers: await getUsers({ type: "teacher" }) as TeacherUser[],
allowedEntities,
}),
};
}, sessionOptions);
@@ -41,7 +55,8 @@ const StatusClassNames: { [key in ApprovalWorkflowStatus]: string } = {
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" | "pending";
type CustomStatus = ApprovalWorkflowStatus | "all";
type CustomEntity = EntityWithRoles["label"] | "all";
const STATUS_OPTIONS = [
{
@@ -66,13 +81,37 @@ const STATUS_OPTIONS = [
},
];
export default function ApprovalWorkflows() {
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 [selectedApprovalWorkflow, setSelectedApprovalWorkflow] = useState<ApprovalWorkflow>(); */
const [statusFilter, setStatusFilter] = useState<CustomStatus>("all");
const { user } = useUser({ redirectTo: "/login" });
const [entityFilter, setEntityFilter] = useState<CustomEntity>("all");
const { approvalWorkflows/* , reload */ } = useApprovalWorkflows();
@@ -82,9 +121,14 @@ export default function ApprovalWorkflows() {
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]);
}, [approvalWorkflows, statusFilter, entityFilter]);
const deleteApprovalWorkflow = (id: string, name: string) => {
if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return;
@@ -132,7 +176,7 @@ export default function ApprovalWorkflows() {
{info.getValue().map((module: Module, index: number) => (
<span
key={index}
className="inline-block rounded-full px-3 py-1 text-sm font-medium bg-purple-100 border border-purple-300 text-purple-900"
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>
@@ -149,6 +193,7 @@ export default function ApprovalWorkflows() {
),
}),
columnHelper.accessor("steps", {
id: "currentApprovers",
header: "CURRENT APPROVERS",
cell: (info) => {
const steps = info.row.original.steps;
@@ -170,6 +215,7 @@ export default function ApprovalWorkflows() {
},
}),
columnHelper.accessor("steps", {
id: "currentStep",
header: "CURRENT STEP",
cell: (info) => {
const steps = info.row.original.steps;
@@ -246,6 +292,16 @@ export default function ApprovalWorkflows() {
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">
@@ -271,13 +327,12 @@ export default function ApprovalWorkflows() {
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{/*
Might be an overkill way to add rounded borders to the rows, but couldn't figure out another way...
border round and margin does not seem to work properly on tr
Another way to do it was with grid but that puts the same width in all rows, which is inconvenient
Regardless, it works and all calcs are pretty simple so shouldnt be too inefficient
*/}
<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;
@@ -290,7 +345,7 @@ export default function ApprovalWorkflows() {
}
return (
<td key={cell.id} className={cellClasses}>
<td key={cellIndex} className={cellClasses}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
@@ -298,6 +353,7 @@ export default function ApprovalWorkflows() {
</tr>
))}
</tbody>
</table>
</div>

View File

@@ -65,7 +65,8 @@ export type RolePermission =
"view_student_record" |
"download_student_record" |
"pay_entity" |
"view_payment_record"
"view_payment_record" |
"view_approval_workflows"
export const DEFAULT_PERMISSIONS: RolePermission[] = [
"view_students",
@@ -74,7 +75,8 @@ export const DEFAULT_PERMISSIONS: RolePermission[] = [
"view_classrooms",
"view_entity_roles",
"view_statistics",
"download_statistics_report"
"download_statistics_report",
"view_approval_workflows"
]
export const ADMIN_PERMISSIONS: RolePermission[] = [