Optimize permissions
This commit is contained in:
parent
dc4379b482
commit
9ba63c2ec9
|
|
@ -1,7 +1,7 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
import checkPermission from "@/features/auth/tools/checkPermission";
|
||||||
import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData";
|
import permissionFormDataSchema, { PermissionFormData } from "../../../../modules/permission/formSchemas/PermissionFormData";
|
||||||
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
||||||
import prisma from "@/db";
|
import prisma from "@/db";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
|
||||||
import prisma from "@/db";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a user from the database based on their ID.
|
|
||||||
*
|
|
||||||
* @param {string} id The unique identifier of the user to be deleted.
|
|
||||||
* @returns A promise that resolves to an object indicating the success or failure of the operation.
|
|
||||||
*/
|
|
||||||
export default async function deleteUser(id: string) {
|
|
||||||
|
|
||||||
// Check user permission
|
|
||||||
if (!(await checkPermission())) return {
|
|
||||||
success: false,
|
|
||||||
message: "Unauthorized"
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Find the user in the database
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle case where user is not found
|
|
||||||
if (!user) return {
|
|
||||||
success: false,
|
|
||||||
message: "User not found"
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Delete the user
|
|
||||||
await prisma.user.delete({
|
|
||||||
where: { id }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Revalidate cache
|
|
||||||
revalidatePath(".");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `User ${user.name} has been successfully deleted`
|
|
||||||
} as const;
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import prisma from "@/db";
|
|
||||||
import userFormDataSchema, { UserFormData } from "../formSchemas/userFormDataSchema";
|
|
||||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edits user data in the database based on the provided form data.
|
|
||||||
*
|
|
||||||
* @param formData The user data to be updated.
|
|
||||||
* @returns A promise that resolves to an object indicating the success or failure of the operation.
|
|
||||||
*/
|
|
||||||
export default async function editUser(formData: UserFormData) {
|
|
||||||
|
|
||||||
// Check user permission
|
|
||||||
if (!await checkPermission("authenticated-only")) return {
|
|
||||||
success: false,
|
|
||||||
message: "Unauthorized"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate form data
|
|
||||||
const validatedFields = userFormDataSchema.safeParse(formData);
|
|
||||||
if (!validatedFields.success){
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Invalid Form Data",
|
|
||||||
errors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors)
|
|
||||||
} as const
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for valid ID
|
|
||||||
if (!validatedFields.data.id){
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Invalid Form Data",
|
|
||||||
errors: {
|
|
||||||
id: "Invalid ID"
|
|
||||||
}
|
|
||||||
} as const
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user data in the database
|
|
||||||
try {
|
|
||||||
await prisma.user.update({
|
|
||||||
where: { id: validatedFields.data.id },
|
|
||||||
data: {
|
|
||||||
email: validatedFields.data.email,
|
|
||||||
name: validatedFields.data.name,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Revalidate the cache
|
|
||||||
revalidatePath(".");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `User ${validatedFields.data.name} has been successfully updated`
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// Consider handling specific database errors here
|
|
||||||
console.error('Error updating user data', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Error updating user data"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import "server-only"
|
|
||||||
import prisma from "@/db";
|
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import checkPermission from "@/features/auth/tools/checkPermission";
|
|
||||||
import { unauthorized } from "@/BaseError";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves detailed information of a user by their ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the user.
|
|
||||||
* @returns The user's detailed information or an error response.
|
|
||||||
*/
|
|
||||||
export default async function getUserDetailById(id: string){
|
|
||||||
|
|
||||||
// Check user permission
|
|
||||||
if (!checkPermission("authenticated-only")) return unauthorized();
|
|
||||||
|
|
||||||
// Retrieve user data from the database
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: { id },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
photoProfile: {
|
|
||||||
select: {
|
|
||||||
path: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if user exists
|
|
||||||
if (!user) return notFound();
|
|
||||||
|
|
||||||
// Format user data
|
|
||||||
const formattedUser = {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email ?? "",
|
|
||||||
name: user.name ?? "",
|
|
||||||
photoProfileUrl: user.photoProfile?.path ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedUser;
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { unauthorized } from "@/BaseError"
|
|
||||||
import prisma from "@/db";
|
|
||||||
import checkPermission from "@/features/auth/tools/checkPermission"
|
|
||||||
import "server-only"
|
|
||||||
|
|
||||||
const getUsers = async () => {
|
|
||||||
|
|
||||||
if (!await checkPermission("authenticated-only")) unauthorized();
|
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
photoProfile: {
|
|
||||||
select: {
|
|
||||||
path: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = users.map((user) => ({
|
|
||||||
...user,
|
|
||||||
photoUrl: user.photoProfile?.path ?? null,
|
|
||||||
photoProfile: undefined
|
|
||||||
}))
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getUsers;
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export interface UserFormData {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
photoProfileUrl: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userFormDataSchema = z.object({
|
|
||||||
id: z.string().nullable(),
|
|
||||||
name: z.string(),
|
|
||||||
photoProfileUrl: z.union([z.string(), z.null()]),
|
|
||||||
email: z.string().email(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default userFormDataSchema;
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
|
import checkMultiplePermissions from "@/modules/dashboard/services/checkMultiplePermissions";
|
||||||
|
import getAllPermissions from "@/modules/permission/actions/getAllPermissions";
|
||||||
|
import PermissionsTable from "@/modules/permission/tables/PermissionTable/PermissionTable";
|
||||||
import { Card, Stack, Title } from "@mantine/core";
|
import { Card, Stack, Title } from "@mantine/core";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// import RolesTable from "./_tables/RolesTable/RolesTable";
|
|
||||||
// import getRoles from "@/features/dashboard/roles/data/getRoles";
|
|
||||||
import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions";
|
|
||||||
import { PermissionTable } from "@/features/dashboard/permissions/tables";
|
|
||||||
import getPermissions from "@/features/dashboard/permissions/data/getPermissions";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: {
|
searchParams: {
|
||||||
|
|
@ -28,14 +26,15 @@ export default async function RolesPage({ searchParams }: Props) {
|
||||||
update: "permission.update",
|
update: "permission.update",
|
||||||
delete: "permission.delete",
|
delete: "permission.delete",
|
||||||
});
|
});
|
||||||
|
|
||||||
const permissionData = await getPermissions()
|
const res = await getAllPermissions();
|
||||||
|
if (!res.success) throw new Error();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title order={1}>Permissions</Title>
|
<Title order={1}>Permissions</Title>
|
||||||
<Card>
|
<Card>
|
||||||
<PermissionTable permissions={permissions} permissionData={permissionData} />
|
<PermissionsTable permissions={permissions} permissionData={res.data} />
|
||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
27
src/modules/permission/actions/deletePermission.ts
Normal file
27
src/modules/permission/actions/deletePermission.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import prisma from "@/db";
|
||||||
|
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||||
|
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||||
|
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||||
|
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
export default async function deletePermission(id: string): Promise<ServerResponseAction> {
|
||||||
|
try {
|
||||||
|
if (!(await checkPermission("permission.delete"))) unauthorized();
|
||||||
|
const permission = await prisma.permission.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
revalidatePath(".")
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "The permission has been deleted successfully",
|
||||||
|
};
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return handleCatch(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/modules/permission/actions/getAllPermissions.ts
Normal file
56
src/modules/permission/actions/getAllPermissions.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
"use server"
|
||||||
|
import prisma from "@/db";
|
||||||
|
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||||
|
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||||
|
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||||
|
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||||
|
import "server-only";
|
||||||
|
import Permission from "../types/Permission";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all permissions along with the count of associated permissions and users.
|
||||||
|
* Authorization check is performed for the operation.
|
||||||
|
*
|
||||||
|
* @returns An array of permission objects each including details and counts of related permissions and users.
|
||||||
|
*/
|
||||||
|
export default async function getAllPermissions(): Promise<ServerResponseAction<Permission[]>> {
|
||||||
|
// Authorization check
|
||||||
|
if (!(await checkPermission("permissions.readAll"))) {
|
||||||
|
unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch permissions from the database
|
||||||
|
const permissions = await prisma.permission.findMany({
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
roles: true,
|
||||||
|
directUsers: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform the data into the desired format
|
||||||
|
const permissionsData = permissions.map(
|
||||||
|
({ id, code, name, description, isActive, _count }) => ({
|
||||||
|
id,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
isActive,
|
||||||
|
roleCount: _count.roles,
|
||||||
|
//User count counts only direct user
|
||||||
|
userCount: _count.directUsers,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: permissionsData
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return handleCatch(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/modules/permission/actions/getPermissionById.ts
Normal file
49
src/modules/permission/actions/getPermissionById.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import prisma from "@/db";
|
||||||
|
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||||
|
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||||
|
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||||
|
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function getPermissionById(
|
||||||
|
id: string
|
||||||
|
): Promise<ServerResponseAction<Permission>> {
|
||||||
|
try {
|
||||||
|
if (!(await checkPermission("permissions.read"))) unauthorized();
|
||||||
|
|
||||||
|
const permission = await prisma.permission.findFirst({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
code: true,
|
||||||
|
description: true,
|
||||||
|
id: true,
|
||||||
|
isActive: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Permission not found",
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Permission fetched successfully",
|
||||||
|
data: permission,
|
||||||
|
} as const;
|
||||||
|
} catch (e) {
|
||||||
|
return handleCatch(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/modules/permission/actions/upsertPermission.ts
Normal file
83
src/modules/permission/actions/upsertPermission.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData";
|
||||||
|
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
|
||||||
|
import prisma from "@/db";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
|
||||||
|
import checkPermission from "@/modules/dashboard/services/checkPermission";
|
||||||
|
import unauthorized from "@/modules/dashboard/utils/unauthorized";
|
||||||
|
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||||
|
import handleCatch from "@/modules/dashboard/utils/handleCatch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upserts a permission based on the provided PermissionFormData.
|
||||||
|
* If the permission already exists (determined by `id`), it updates the permission; otherwise, it creates a new permission.
|
||||||
|
* Authorization checks are performed based on whether it's a create or update operation.
|
||||||
|
*
|
||||||
|
* @param data - The data for creating or updating the permission.
|
||||||
|
* @returns An object containing the success status, message, and any errors.
|
||||||
|
*/
|
||||||
|
export default async function upsertPermission(
|
||||||
|
data: PermissionFormData
|
||||||
|
): Promise<ServerResponseAction> {
|
||||||
|
try {
|
||||||
|
const isInsert = !data.id;
|
||||||
|
|
||||||
|
// Authorization check
|
||||||
|
const permissionType = isInsert ? "permission.create" : "permission.update";
|
||||||
|
if (!(await checkPermission(permissionType))) {
|
||||||
|
unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate form data
|
||||||
|
const validatedFields = permissionFormDataSchema.safeParse(data);
|
||||||
|
if (!validatedFields.success) {
|
||||||
|
throw new DashboardError({
|
||||||
|
errorCode: "INVALID_FORM_DATA",
|
||||||
|
formErrors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const permissionData = {
|
||||||
|
code: validatedFields.data.code,
|
||||||
|
description: validatedFields.data.description,
|
||||||
|
name: validatedFields.data.name,
|
||||||
|
isActive: validatedFields.data.isActive,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Database operation
|
||||||
|
if (isInsert) {
|
||||||
|
if (await prisma.permission.findFirst({
|
||||||
|
where: {
|
||||||
|
code: permissionData.code
|
||||||
|
}
|
||||||
|
})){
|
||||||
|
throw new DashboardError({
|
||||||
|
errorCode: "INVALID_FORM_DATA",
|
||||||
|
formErrors: {
|
||||||
|
code: "The code is already exists"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await prisma.permission.create({ data: permissionData });
|
||||||
|
} else {
|
||||||
|
await prisma.permission.update({
|
||||||
|
where: { id: validatedFields.data.id! },
|
||||||
|
data: permissionData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revalidate the cache
|
||||||
|
revalidatePath(".");
|
||||||
|
|
||||||
|
// Return success message
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Permission ${validatedFields.data.name} has been successfully ${
|
||||||
|
isInsert ? "created" : "updated"
|
||||||
|
}.`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleCatch(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/modules/permission/modals/PermissionDeleteModal.tsx
Normal file
105
src/modules/permission/modals/PermissionDeleteModal.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
"use client";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Modal,
|
||||||
|
Text,
|
||||||
|
Alert,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { showNotification } from "@/utils/notifications";
|
||||||
|
import { error } from "console";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import withServerAction from "@/modules/dashboard/utils/withServerAction";
|
||||||
|
import deletePermission from "../actions/deletePermission";
|
||||||
|
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||||
|
|
||||||
|
export interface DeleteModalProps {
|
||||||
|
data?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeleteModal(props: DeleteModalProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the modal. It won't close if a submission is in progress.
|
||||||
|
*/
|
||||||
|
const closeModal = () => {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
setErrorMessage("")
|
||||||
|
props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmAction = () => {
|
||||||
|
if (!props.data?.id) return;
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
withServerAction(deletePermission, props.data!.id)
|
||||||
|
.then((response) => {
|
||||||
|
showNotification(
|
||||||
|
response.message ?? "Permission deleted successfully"
|
||||||
|
);
|
||||||
|
setSubmitting(false);
|
||||||
|
props.onClose()
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof DashboardError){
|
||||||
|
setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`)
|
||||||
|
}
|
||||||
|
else if (e instanceof Error) {
|
||||||
|
setErrorMessage(`ERROR: ${e.message}`)
|
||||||
|
} else {
|
||||||
|
setErrorMessage(`Unkown error is occured. Please contact administrator`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setSubmitting(false)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={!!props.data}
|
||||||
|
onClose={closeModal}
|
||||||
|
title={`Delete confirmation`}
|
||||||
|
>
|
||||||
|
<Text size="sm">
|
||||||
|
Are you sure you want to delete permission{" "}
|
||||||
|
<Text span fw={700}>
|
||||||
|
{props.data?.name}
|
||||||
|
</Text>
|
||||||
|
? This action is irreversible.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{errorMessage && <Alert color="red">{errorMessage}</Alert>}
|
||||||
|
{/* Buttons */}
|
||||||
|
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={closeModal}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
// leftSection={<TbDeviceFloppy size={20} />}
|
||||||
|
type="submit"
|
||||||
|
color="red"
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={confirmAction}
|
||||||
|
>
|
||||||
|
Delete Permission
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
213
src/modules/permission/modals/PermissionFormModal.tsx
Normal file
213
src/modules/permission/modals/PermissionFormModal.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { showNotification } from "@/utils/notifications";
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
ScrollArea,
|
||||||
|
Checkbox,
|
||||||
|
Skeleton,
|
||||||
|
Fieldset,
|
||||||
|
Alert,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { TbDeviceFloppy } from "react-icons/tb";
|
||||||
|
import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData";
|
||||||
|
import getPermissionById from "../actions/getPermissionById";
|
||||||
|
import withServerAction from "@/modules/dashboard/utils/withServerAction";
|
||||||
|
import upsertPermission from "../actions/upsertPermission";
|
||||||
|
import DashboardError from "@/modules/dashboard/errors/DashboardError";
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
title: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
id?: string;
|
||||||
|
opened: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component for rendering a modal with a form to create or edit a permission.
|
||||||
|
*
|
||||||
|
* @param props - The props for the component.
|
||||||
|
* @returns The rendered element.
|
||||||
|
*/
|
||||||
|
export default function FormModal(props: ModalProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
|
const [isFetching, setFetching] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
|
const form = useForm<PermissionFormData>({
|
||||||
|
initialValues: {
|
||||||
|
code: "",
|
||||||
|
description: "",
|
||||||
|
id: "",
|
||||||
|
isActive: false,
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
validate: zodResolver(permissionFormDataSchema),
|
||||||
|
validateInputOnChange: false,
|
||||||
|
onValuesChange: (values) => {
|
||||||
|
console.log(values);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches permission data by ID and populates the form if the modal is opened and an ID is provided.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.opened || !props.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFetching(true);
|
||||||
|
getPermissionById(props.id)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
form.setValues({
|
||||||
|
code: data.code,
|
||||||
|
description: data.description,
|
||||||
|
id: data.id,
|
||||||
|
isActive: data.isActive,
|
||||||
|
name: data.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
//TODO: Handle error
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setFetching(false);
|
||||||
|
});
|
||||||
|
}, [props.opened, props.id]);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
form.reset()
|
||||||
|
props.onClose ? props.onClose() : router.replace("?");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (values: PermissionFormData) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
withServerAction(upsertPermission, values)
|
||||||
|
.then((response) => {
|
||||||
|
showNotification(response.message!, "success");
|
||||||
|
closeModal();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof DashboardError) {
|
||||||
|
if (e.errorCode === "INVALID_FORM_DATA") {
|
||||||
|
form.setErrors(e.formErrors ?? {});
|
||||||
|
} else {
|
||||||
|
setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`);
|
||||||
|
}
|
||||||
|
} else if (e instanceof Error) {
|
||||||
|
setErrorMessage(`ERROR: ${e.message}`);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(
|
||||||
|
`Unkown error is occured. Please contact administrator`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={props.opened}
|
||||||
|
onClose={closeModal}
|
||||||
|
title={props.title}
|
||||||
|
scrollAreaComponent={ScrollArea.Autosize}
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<Stack mt="sm" gap="lg" px="lg">
|
||||||
|
{errorMessage && <Alert color="red">{errorMessage}</Alert>}
|
||||||
|
{/* ID */}
|
||||||
|
{form.values.id ? (
|
||||||
|
<TextInput
|
||||||
|
label="ID"
|
||||||
|
readOnly
|
||||||
|
variant="filled"
|
||||||
|
{...form.getInputProps("id")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Code */}
|
||||||
|
<Skeleton visible={isFetching}>
|
||||||
|
<TextInput
|
||||||
|
data-autofocus
|
||||||
|
label="Code"
|
||||||
|
readOnly={props.readonly}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...form.getInputProps("code")}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
|
||||||
|
{/* Name */}
|
||||||
|
<Skeleton visible={isFetching}>
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
readOnly={props.readonly}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Skeleton visible={isFetching}>
|
||||||
|
<Textarea
|
||||||
|
label="Description"
|
||||||
|
readOnly={props.readonly}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...form.getInputProps("description")}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
|
||||||
|
<Skeleton visible={isFetching}>
|
||||||
|
<Checkbox
|
||||||
|
label="Active"
|
||||||
|
labelPosition="right"
|
||||||
|
{...form.getInputProps("isActive", {
|
||||||
|
type: "checkbox",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={closeModal}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
{!props.readonly && (
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
leftSection={<TbDeviceFloppy size={20} />}
|
||||||
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
"use client";
|
||||||
|
import CrudPermissions from "@/modules/dashboard/types/CrudPermissions";
|
||||||
|
import { Table, Text, Flex, Button, Center } from "@mantine/core";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { TbPlus } from "react-icons/tb";
|
||||||
|
import getPermissions from "../../actions/getAllPermissions";
|
||||||
|
import getAllPermissions from "../../actions/getAllPermissions";
|
||||||
|
import FormModal, { ModalProps } from "../../modals/PermissionFormModal";
|
||||||
|
import DeleteModal, { DeleteModalProps } from "../../modals/PermissionDeleteModal";
|
||||||
|
import Permission from "../../types/Permission";
|
||||||
|
import createColumns from "./columns";
|
||||||
|
import DashboardTable from "@/modules/dashboard/components/DashboardTable";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
permissions: Partial<CrudPermissions>;
|
||||||
|
permissionData: Permission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PermissionsTable(props: Props) {
|
||||||
|
const [modalProps, setModalProps] = useState<ModalProps>({
|
||||||
|
opened: false,
|
||||||
|
title: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [deleteModalProps, setDeleteModalProps] = useState<
|
||||||
|
Omit<DeleteModalProps, "onClose">
|
||||||
|
>({
|
||||||
|
data: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: props.permissionData,
|
||||||
|
columns: createColumns({
|
||||||
|
permissions: props.permissions,
|
||||||
|
actions: {
|
||||||
|
detail: (id: string) => openFormModal("detail", id),
|
||||||
|
edit: (id: string) => openFormModal("edit", id),
|
||||||
|
delete: (id: string, name: string) => openDeleteModal(id, name),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
defaultColumn: {
|
||||||
|
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const openFormModal = (type: "create" | "edit" | "detail", id?: string) => {
|
||||||
|
const openCreateModal = () => {
|
||||||
|
setModalProps({
|
||||||
|
id,
|
||||||
|
opened: true,
|
||||||
|
title: "Create new permission",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDetailModal = () => {
|
||||||
|
setModalProps({
|
||||||
|
id,
|
||||||
|
opened: true,
|
||||||
|
title: "Permission detail",
|
||||||
|
readonly: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = () => {
|
||||||
|
setModalProps({
|
||||||
|
id,
|
||||||
|
opened: true,
|
||||||
|
title: "Edit permission",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type === "create"
|
||||||
|
? openCreateModal()
|
||||||
|
: type === "detail"
|
||||||
|
? openDetailModal()
|
||||||
|
: openEditModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteModal = (id: string, name: string) => {
|
||||||
|
setDeleteModalProps({
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalProps({
|
||||||
|
id: "",
|
||||||
|
opened: false,
|
||||||
|
title: "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Add view when data is empty
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
{props.permissions.create && (
|
||||||
|
<Button
|
||||||
|
leftSection={<TbPlus />}
|
||||||
|
onClick={() => openFormModal("create")}
|
||||||
|
>
|
||||||
|
New Permission
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<DashboardTable table={table} />
|
||||||
|
|
||||||
|
<FormModal {...modalProps} onClose={closeModal} />
|
||||||
|
<DeleteModal
|
||||||
|
{...deleteModalProps}
|
||||||
|
onClose={() => setDeleteModalProps({})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/modules/permission/tables/PermissionTable/columns.tsx
Normal file
105
src/modules/permission/tables/PermissionTable/columns.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { Badge, Flex } from "@mantine/core";
|
||||||
|
import { TbEye, TbPencil, TbTrash } from "react-icons/tb";
|
||||||
|
import CrudPermissions from "@/modules/dashboard/types/CrudPermissions";
|
||||||
|
import createActionButtons from "@/modules/dashboard/utils/createActionButton";
|
||||||
|
|
||||||
|
export interface PermissionRow {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
roleCount: number;
|
||||||
|
userCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColumnOptions {
|
||||||
|
permissions: Partial<CrudPermissions>;
|
||||||
|
actions: {
|
||||||
|
detail: (id: string) => void;
|
||||||
|
edit: (id: string) => void;
|
||||||
|
delete: (id: string, name: string) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createColumns = (options: ColumnOptions) => {
|
||||||
|
const columnHelper = createColumnHelper<PermissionRow>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor("id", {
|
||||||
|
id: "sequence",
|
||||||
|
header: "#",
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("code", {
|
||||||
|
header: "Code",
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: "Name",
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("isActive", {
|
||||||
|
header: "Status",
|
||||||
|
cell: (props) => {
|
||||||
|
props.getValue() ? (
|
||||||
|
<Badge color="green">Enabled</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge color="orange">Disabled</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("roleCount", {
|
||||||
|
header: "Roles",
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("userCount", {
|
||||||
|
header: "Users",
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.display({
|
||||||
|
id: "Actions",
|
||||||
|
header: "Actions",
|
||||||
|
cell: (props) => (
|
||||||
|
<Flex gap="xs">
|
||||||
|
{createActionButtons([
|
||||||
|
{
|
||||||
|
label: "Detail",
|
||||||
|
permission: options.permissions.read,
|
||||||
|
action: () =>
|
||||||
|
options.actions.detail(props.row.original.id),
|
||||||
|
color: "green",
|
||||||
|
icon: <TbEye />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
permission: options.permissions.update,
|
||||||
|
action: () =>
|
||||||
|
options.actions.edit(props.row.original.id),
|
||||||
|
color: "yellow",
|
||||||
|
icon: <TbPencil />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Delete",
|
||||||
|
permission: options.permissions.delete,
|
||||||
|
action: () =>
|
||||||
|
options.actions.delete(
|
||||||
|
props.row.original.id,
|
||||||
|
props.row.original.name
|
||||||
|
),
|
||||||
|
color: "red",
|
||||||
|
icon: <TbTrash />,
|
||||||
|
},
|
||||||
|
])}
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createColumns;
|
||||||
11
src/modules/permission/types/Permission.d.ts
vendored
Normal file
11
src/modules/permission/types/Permission.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
type Permission = {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
roleCount: number;
|
||||||
|
userCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Permission;
|
||||||
Loading…
Reference in New Issue
Block a user