diff --git a/src/app/dashboard/(auth)/permissions/page.tsx b/src/app/dashboard/(auth)/permissions/page.tsx new file mode 100644 index 0000000..3b76d8a --- /dev/null +++ b/src/app/dashboard/(auth)/permissions/page.tsx @@ -0,0 +1,42 @@ +import { Card, Stack, Title } from "@mantine/core"; +import { Metadata } from "next"; +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 { + searchParams: { + detail?: string; + edit?: string; + delete?: string; + create?: string; + }; +} + +export const metadata: Metadata = { + title: "Permissions - Dashboard", +}; + +export default async function RolesPage({ searchParams }: Props) { + const permissions = await checkMultiplePermissions({ + create: "permission.create", + readAll: "permission.readAll", + read: "permission.read", + update: "permission.update", + delete: "permission.delete", + }); + + const permissionData = await getPermissions() + + return ( + + Permissions + + + + + ); +} diff --git a/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx b/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx index fcd444c..0513d11 100644 --- a/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx +++ b/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx @@ -16,6 +16,7 @@ import { RoleFormData } from "@/features/dashboard/roles/formSchemas/RoleFormDat import { string } from "zod"; import { DeleteModal } from "../../_modals"; import { DeleteModalProps } from "../../_modals/DeleteModal/DeleteModal"; +import { DashboardTable } from "@/features/dashboard/components"; interface Props { permissions: Partial; @@ -114,60 +115,8 @@ export default function RolesTable(props: Props) { )} - - {/* Thead */} - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {/* Tbody */} - - {table.getRowModel().rows.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - -
- No Data -
-
-
- )} -
-
+ + { + table: ReactTable +} + +export default function DashboardTable({table}: Props) { + return ( + + {/* Thead */} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {/* Tbody */} + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + +
- No Data -
+
+
+ )} +
+
+ ); +} diff --git a/src/features/dashboard/components/DashboardTable/index.ts b/src/features/dashboard/components/DashboardTable/index.ts new file mode 100644 index 0000000..6a3b717 --- /dev/null +++ b/src/features/dashboard/components/DashboardTable/index.ts @@ -0,0 +1 @@ +export { default } from "./DashboardTable"; diff --git a/src/features/dashboard/components/index.ts b/src/features/dashboard/components/index.ts new file mode 100644 index 0000000..248077e --- /dev/null +++ b/src/features/dashboard/components/index.ts @@ -0,0 +1,3 @@ +import DashboardTable from "./DashboardTable"; + +export { DashboardTable }; diff --git a/src/features/dashboard/permissions/actions/deletePermission.ts b/src/features/dashboard/permissions/actions/deletePermission.ts new file mode 100644 index 0000000..881b1ae --- /dev/null +++ b/src/features/dashboard/permissions/actions/deletePermission.ts @@ -0,0 +1,26 @@ +"use server"; + +import prisma from "@/db"; +import checkPermission from "@/features/auth/tools/checkPermission"; +import { handleCatch, unauthorized } from "../../errors/DashboardError"; +import ServerResponse from "@/types/Action"; +import { revalidatePath } from "next/cache"; + +export default async function deletePermission(id: string): Promise { + try { + if (!(await checkPermission("permission.delete"))) return 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) + } +} diff --git a/src/features/dashboard/permissions/actions/getPermissionById.ts b/src/features/dashboard/permissions/actions/getPermissionById.ts new file mode 100644 index 0000000..37543c9 --- /dev/null +++ b/src/features/dashboard/permissions/actions/getPermissionById.ts @@ -0,0 +1,33 @@ +"use server"; + +import { unauthorized } from "@/BaseError"; +import prisma from "@/db"; +import checkPermission from "@/features/auth/tools/checkPermission"; + +export default async function getPermissionById(id: string) { + if (!(await checkPermission("permission.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; +} diff --git a/src/features/dashboard/permissions/actions/upsertPermission.ts b/src/features/dashboard/permissions/actions/upsertPermission.ts new file mode 100644 index 0000000..4a92f7e --- /dev/null +++ b/src/features/dashboard/permissions/actions/upsertPermission.ts @@ -0,0 +1,81 @@ +"use server"; + +import checkPermission from "@/features/auth/tools/checkPermission"; +import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData"; +import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; +import prisma from "@/db"; +import { revalidatePath } from "next/cache"; +import ServerResponse from "@/types/Action"; +import DashboardError, { handleCatch, unauthorized } from "../../errors/DashboardError"; + +/** + * 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 { + try { + const isInsert = !data.id; + + // Authorization check + const permissionType = isInsert ? "permission.create" : "permission.update"; + if (!(await checkPermission(permissionType))) { + return 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) + } +} diff --git a/src/features/dashboard/permissions/data/getPermissions.ts b/src/features/dashboard/permissions/data/getPermissions.ts new file mode 100644 index 0000000..ad743d7 --- /dev/null +++ b/src/features/dashboard/permissions/data/getPermissions.ts @@ -0,0 +1,48 @@ +import { unauthorized } from "@/BaseError"; +import prisma from "@/db"; +import checkPermission from "@/features/auth/tools/checkPermission"; +import "server-only"; + +/** + * 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 getPermissions() { + // Authorization check + if (!(await checkPermission("permissions.getAll"))) { + return 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 + return 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, + }) + ); + } catch (error) { + console.error("Error retrieving permissions", error); + throw error; + } +} diff --git a/src/features/dashboard/permissions/formSchemas/PermissionFormData.ts b/src/features/dashboard/permissions/formSchemas/PermissionFormData.ts new file mode 100644 index 0000000..2e7941b --- /dev/null +++ b/src/features/dashboard/permissions/formSchemas/PermissionFormData.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +export interface PermissionFormData { + id: string; + name: string; + code: string; + description: string; + isActive: boolean; +} + +const permissionFormDataSchema = z.object({ + id: z.string().nullable(), + name: z.string().min(1), + code: z.string().min(1), + description: z.string(), + isActive: z.boolean(), +}) + +export default permissionFormDataSchema; diff --git a/src/features/dashboard/permissions/modals/DeleteModal/DeleteModal.tsx b/src/features/dashboard/permissions/modals/DeleteModal/DeleteModal.tsx new file mode 100644 index 0000000..c2dd4ec --- /dev/null +++ b/src/features/dashboard/permissions/modals/DeleteModal/DeleteModal.tsx @@ -0,0 +1,111 @@ +"use client"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; +import { + Avatar, + Button, + Center, + Flex, + Modal, + ScrollArea, + Text, + Stack, + TextInput, + Title, + Alert, +} from "@mantine/core"; +import { showNotification } from "@/utils/notifications"; +import deletePermission from "@/features/dashboard/permissions/actions/deletePermission"; +import withErrorHandling from "@/features/dashboard/utils/withServerAction"; +import { error } from "console"; +import DashboardError from "@/features/dashboard/errors/DashboardError"; +import { revalidatePath } from "next/cache"; + +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); + + withErrorHandling(() => 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 ( + + + Are you sure you want to delete permission{" "} + + {props.data?.name} + + ? This action is irreversible. + + + {errorMessage && {errorMessage}} + {/* Buttons */} + + + + + + ); +} diff --git a/src/features/dashboard/permissions/modals/DeleteModal/index.ts b/src/features/dashboard/permissions/modals/DeleteModal/index.ts new file mode 100644 index 0000000..34d6794 --- /dev/null +++ b/src/features/dashboard/permissions/modals/DeleteModal/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteModal"; diff --git a/src/features/dashboard/permissions/modals/FormModal/FormModal.tsx b/src/features/dashboard/permissions/modals/FormModal/FormModal.tsx new file mode 100644 index 0000000..d3d09a2 --- /dev/null +++ b/src/features/dashboard/permissions/modals/FormModal/FormModal.tsx @@ -0,0 +1,214 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import DashboardError from "@/features/dashboard/errors/DashboardError"; +import getPermissionById from "@/features/dashboard/permissions/actions/getPermissionById"; +import upsertPermission from "@/features/dashboard/permissions/actions/upsertPermission"; +import permissionFormDataSchema, { + PermissionFormData, +} from "@/features/dashboard/permissions/formSchemas/PermissionFormData"; +import withErrorHandling from "@/features/dashboard/utils/withServerAction"; +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"; + +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({ + 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 = () => { + props.onClose ? props.onClose() : router.replace("?"); + }; + + const handleSubmit = (values: PermissionFormData) => { + setSubmitting(true); + withErrorHandling(() => 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 ( + +
+ + {errorMessage && {errorMessage}} + {/* ID */} + {form.values.id ? ( + + ) : ( +
+ )} + + {/* Code */} + + + + + {/* Name */} + + + + + {/* Description */} + +