Update role columns
This commit is contained in:
parent
bf41292b28
commit
85daf8cfe7
|
|
@ -6,17 +6,21 @@ import {
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import columns from "./columns";
|
|
||||||
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||||
|
import getRoles from "@/features/dashboard/roles/data/getRoles";
|
||||||
|
import createColumns from "./columns";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
permissions: Partial<CrudPermissions>,
|
permissions: Partial<CrudPermissions>,
|
||||||
|
roles: Awaited<ReturnType<typeof getRoles>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RolesTable(props: Props) {
|
export default function RolesTable(props: Props) {
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: [],
|
data: props.roles,
|
||||||
columns,
|
columns: createColumns({
|
||||||
|
permissions: props.permissions
|
||||||
|
}),
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
defaultColumn: {
|
defaultColumn: {
|
||||||
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
|
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,105 @@
|
||||||
|
import CrudPermissions from "@/features/auth/types/CrudPermissions";
|
||||||
|
import createActionButtons from "@/features/dashboard/utils/createActionButtons";
|
||||||
|
import { Badge, Flex, Tooltip, ActionIcon } from "@mantine/core";
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import Link from "next/link";
|
||||||
import { StringifyOptions } from "querystring";
|
import { StringifyOptions } from "querystring";
|
||||||
|
import { TbEye, TbPencil, TbTrash } from "react-icons/tb";
|
||||||
|
|
||||||
export interface RoleRow {
|
export interface RoleRow {
|
||||||
id: string,
|
id: string;
|
||||||
code: string,
|
code: string;
|
||||||
name: string,
|
name: string;
|
||||||
permissionCount: number,
|
isActive: boolean;
|
||||||
userCount: number,
|
permissionCount: number;
|
||||||
|
userCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<RoleRow>()
|
interface ColumnOptions {
|
||||||
|
permissions: Partial<CrudPermissions>;
|
||||||
|
actions?: {
|
||||||
|
detail?: () => void;
|
||||||
|
edit?: () => void;
|
||||||
|
delete?: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createColumns = (options: ColumnOptions) => {
|
||||||
|
const columnHelper = createColumnHelper<RoleRow>();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
columnHelper.accessor("id", {
|
columnHelper.accessor("id", {
|
||||||
id: "sequence",
|
id: "sequence",
|
||||||
header: "#",
|
header: "#",
|
||||||
cell: props => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor("code", {
|
columnHelper.accessor("code", {
|
||||||
header: 'Code',
|
header: "Code",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: "Name"
|
header: "Name",
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("isActive", {
|
||||||
|
header: "Status",
|
||||||
|
cell: (props) =>
|
||||||
|
props.getValue() ? (
|
||||||
|
<Badge color="green">Enabled</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge color="orange">Disabled</Badge>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor("permissionCount", {
|
columnHelper.accessor("permissionCount", {
|
||||||
header: "Permissions"
|
header: "Permissions",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor("userCount", {
|
columnHelper.accessor("userCount", {
|
||||||
header: "Users"
|
header: "Users",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "Actions",
|
id: "Actions",
|
||||||
header: "Actions",
|
header: "Actions",
|
||||||
})
|
size: 10,
|
||||||
]
|
meta: {
|
||||||
|
className: "w-fit",
|
||||||
|
},
|
||||||
|
cell: () => (
|
||||||
|
<Flex gap="xs">
|
||||||
|
{
|
||||||
|
createActionButtons([
|
||||||
|
{
|
||||||
|
label: "Detail",
|
||||||
|
permission: options.permissions.read,
|
||||||
|
action: options.actions?.detail,
|
||||||
|
color: "green",
|
||||||
|
icon: <TbEye />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
permission: options.permissions.update,
|
||||||
|
action: options.actions?.edit,
|
||||||
|
color: "yellow",
|
||||||
|
icon: <TbPencil />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
permission: options.permissions.delete,
|
||||||
|
action: options.actions?.delete,
|
||||||
|
color: "red",
|
||||||
|
icon: <TbTrash />
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
export default columns;
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createColumns;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import Link from "next/link";
|
||||||
import { CreateModal } from "./_modals";
|
import { CreateModal } from "./_modals";
|
||||||
import FormModal from "./_modals/FormModal";
|
import FormModal from "./_modals/FormModal";
|
||||||
import CreateButton from "./_components/CreateButton/CreateButton";
|
import CreateButton from "./_components/CreateButton/CreateButton";
|
||||||
|
import getRoles from "@/features/dashboard/roles/data/getRoles";
|
||||||
|
import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: {
|
searchParams: {
|
||||||
|
|
@ -20,20 +22,25 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RolesPage({searchParams}: Props) {
|
export default async function RolesPage({searchParams}: Props) {
|
||||||
if (!(await checkPermission("role.readAll"))) {
|
|
||||||
return unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowCreate = await checkPermission("role.create")
|
const permissions = await checkMultiplePermissions({
|
||||||
|
create: "role.create",
|
||||||
|
readAll: "role.readAll",
|
||||||
|
read: 'role.read',
|
||||||
|
update: 'role.update',
|
||||||
|
delete: 'role.delete'
|
||||||
|
})
|
||||||
|
|
||||||
|
const roles = await getRoles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title order={1}>Roles</Title>
|
<Title order={1}>Roles</Title>
|
||||||
<Card>
|
<Card>
|
||||||
<Flex justify="flex-end">
|
<Flex justify="flex-end">
|
||||||
{ allowCreate && <CreateButton />}
|
{ permissions.create && <CreateButton />}
|
||||||
</Flex>
|
</Flex>
|
||||||
<RolesTable permissions={{}} />
|
<RolesTable permissions={permissions} roles={roles} />
|
||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
31
src/features/auth/tools/checkMultiplePermissions.ts
Normal file
31
src/features/auth/tools/checkMultiplePermissions.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { getUserFromToken } from "../authUtils";
|
||||||
|
import getCurrentUser from "./getCurrentUser";
|
||||||
|
|
||||||
|
type PermissionsInput = Record<string, string>;
|
||||||
|
|
||||||
|
type PermissionsOutput = Record<string, boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks multiple permissions for the current user and returns an object indicating
|
||||||
|
* whether each permission is granted.
|
||||||
|
*
|
||||||
|
* @param {PermissionsInput} permissions - An object with keys as permission names and values as the required roles/permissions.
|
||||||
|
* @returns {Promise<PermissionsOutput>} An object with keys as permission names and boolean values indicating whether the permission is granted.
|
||||||
|
*/
|
||||||
|
async function checkMultiplePermissions<T extends Record<string, string>>(permissions: T): Promise<{ [K in keyof T]: boolean }> {
|
||||||
|
const permissionResults: Partial<{ [K in keyof T]: boolean }> = {};
|
||||||
|
const currentUser = await getCurrentUser();
|
||||||
|
|
||||||
|
for (const permissionKey in permissions) {
|
||||||
|
if (permissions.hasOwnProperty(permissionKey)) {
|
||||||
|
const requiredPermission = permissions[permissionKey];
|
||||||
|
// const isPermissionGranted: boolean = currentUser ? currentUser.roles.includes(requiredPermission) : false;
|
||||||
|
const isPermissionGranted = true;
|
||||||
|
permissionResults[permissionKey] = isPermissionGranted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionResults as { [K in keyof T]: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default checkMultiplePermissions;
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"use server"
|
"use server";
|
||||||
|
|
||||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||||
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
|
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
|
||||||
import { unauthorized } from "@/BaseError";
|
import { unauthorized } from "@/BaseError";
|
||||||
|
|
@ -6,63 +7,64 @@ import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
||||||
import prisma from "@/db";
|
import prisma from "@/db";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upserts a role based on the provided RoleFormData.
|
||||||
|
* If the role already exists (determined by `id`), it updates the role; otherwise, it creates a new role.
|
||||||
|
* Authorization checks are performed based on whether it's a create or update operation.
|
||||||
|
*
|
||||||
|
* @param {RoleFormData} data - The data for creating or updating the role.
|
||||||
|
* @returns {Promise<object>} An object containing the success status, message, and any errors.
|
||||||
|
*/
|
||||||
export default async function upsertRole(data: RoleFormData) {
|
export default async function upsertRole(data: RoleFormData) {
|
||||||
|
const isInsert = !data.id;
|
||||||
|
|
||||||
const isInsert = !!data.id;
|
// Authorization check
|
||||||
|
const permissionType = isInsert ? "role.create" : "role.update";
|
||||||
if (isInsert && !await checkPermission("role.create")){
|
if (!await checkPermission(permissionType)) {
|
||||||
return unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInsert && !await checkPermission("role.update")){
|
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate form data
|
||||||
const validatedFields = roleFormDataSchema.safeParse(data);
|
const validatedFields = roleFormDataSchema.safeParse(data);
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid Form Data",
|
message: "Invalid Form Data",
|
||||||
errors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors)
|
errors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors),
|
||||||
} as const
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user data in the database
|
|
||||||
try {
|
try {
|
||||||
if (isInsert){
|
const roleData = {
|
||||||
await prisma.role.update({
|
|
||||||
where: { id: validatedFields.data.id!},
|
|
||||||
data: {
|
|
||||||
code: validatedFields.data.code,
|
|
||||||
description: validatedFields.data.description,
|
|
||||||
isActive: validatedFields.data.isActive,
|
|
||||||
name: validatedFields.data.name
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await prisma.role.create({
|
|
||||||
data: {
|
|
||||||
code: validatedFields.data.code,
|
code: validatedFields.data.code,
|
||||||
description: validatedFields.data.description,
|
description: validatedFields.data.description,
|
||||||
name: validatedFields.data.name,
|
name: validatedFields.data.name,
|
||||||
isActive: validatedFields.data.isActive
|
isActive: validatedFields.data.isActive,
|
||||||
}
|
};
|
||||||
})
|
|
||||||
|
// Database operation
|
||||||
|
if (isInsert) {
|
||||||
|
await prisma.role.create({ data: roleData });
|
||||||
|
} else {
|
||||||
|
await prisma.role.update({
|
||||||
|
where: { id: validatedFields.data.id! },
|
||||||
|
data: roleData,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revalidate the cache
|
// Revalidate the cache
|
||||||
revalidatePath(".");
|
revalidatePath(".");
|
||||||
|
|
||||||
|
// Return success message
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Role ${validatedFields.data.name} has been successfully ${isInsert ? "Updated" : "Created"}`
|
message: `Role ${validatedFields.data.name} has been successfully ${isInsert ? "created" : "updated"}.`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Consider handling specific database errors here
|
console.error('Error updating role data', error);
|
||||||
console.error('Error updating user data', error);
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Error updating user data"
|
message: "Error updating role data.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
45
src/features/dashboard/roles/data/getRoles.ts
Normal file
45
src/features/dashboard/roles/data/getRoles.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { unauthorized } from "@/BaseError";
|
||||||
|
import prisma from "@/db";
|
||||||
|
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||||
|
import "server-only";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all roles along with the count of associated permissions and users.
|
||||||
|
* Authorization check is performed for the operation.
|
||||||
|
*
|
||||||
|
* @returns An array of role objects each including details and counts of related permissions and users.
|
||||||
|
*/
|
||||||
|
export default async function getRoles() {
|
||||||
|
// Authorization check
|
||||||
|
if (!await checkPermission("roles.getAll")) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch roles from the database
|
||||||
|
const roles = await prisma.role.findMany({
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
permissions: true,
|
||||||
|
users: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform the data into the desired format
|
||||||
|
return roles.map(({ id, code, name, description, isActive, _count }) => ({
|
||||||
|
id,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
isActive,
|
||||||
|
permissionCount: _count.permissions,
|
||||||
|
userCount: _count.users
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrieving roles', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/features/dashboard/utils/createActionButtons.tsx
Normal file
50
src/features/dashboard/utils/createActionButtons.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
ActionIconVariant,
|
||||||
|
MantineColor,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface Action {
|
||||||
|
label: string;
|
||||||
|
action?: () => void | string;
|
||||||
|
variant?: ActionIconVariant;
|
||||||
|
permission?: boolean;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: MantineColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createActionButtons(actions: Action[]) {
|
||||||
|
const defaults = {
|
||||||
|
variant: "light",
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = actions.map((action, i) =>
|
||||||
|
action.permission ? (
|
||||||
|
<Tooltip label={action.label} key={i}>
|
||||||
|
{typeof action.action === "string" || action.action === undefined ? (
|
||||||
|
<ActionIcon
|
||||||
|
variant={action.variant ?? defaults.variant}
|
||||||
|
color={action.color}
|
||||||
|
component={Link}
|
||||||
|
href={action.action ?? "#"}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</ActionIcon>
|
||||||
|
) : (
|
||||||
|
<ActionIcon
|
||||||
|
variant={action.variant ?? defaults.variant}
|
||||||
|
color={action.color}
|
||||||
|
onClick={action.action}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return <>{elements}</>;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user