Added role actions

This commit is contained in:
Sianida26 2024-01-28 03:14:09 +07:00
parent 85daf8cfe7
commit 77bb120a00
12 changed files with 442 additions and 184 deletions

View File

@ -1,20 +0,0 @@
"use client"
import { Button } from '@mantine/core'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import React, { useState } from 'react'
import { TbPlus } from 'react-icons/tb'
import { CreateModal } from '../../_modals'
export default function CreateButton() {
const [isModalOpened, setModalOpened] = useState(false)
return (
<>
<Button leftSection={<TbPlus />} onClick={() => setModalOpened(true)}>New Role</Button>
<CreateModal opened={isModalOpened} onClose={() => setModalOpened(false)} />
</>
)
}

View File

@ -1,25 +0,0 @@
import React from "react";
import FormModal from "../FormModal";
interface Props {
opened: boolean
onClose?: () => void
}
export default function CreateModal(props: Props) {
return (
<FormModal
title="Create new role"
data={{
code: "",
description: "",
id: "",
isActive: false,
name: "",
}}
readonly={false}
opened={props.opened}
onClose={props.onClose}
/>
);
}

View File

@ -1 +0,0 @@
export { default } from "./CreateModal"

View File

@ -0,0 +1,93 @@
"use client";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import {
Avatar,
Button,
Center,
Flex,
Modal,
ScrollArea,
Text,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { showNotification } from "@/utils/notifications";
import deleteRole from "@/features/dashboard/roles/actions/deleteRole";
export interface DeleteModalProps {
data?: {
id: string,
name: string,
};
onClose: () => void
}
export default function DeleteModal(props: DeleteModalProps) {
const router = useRouter();
const [isSubmitting, setSubmitting] = useState(false);
/**
* Closes the modal. It won't close if a submission is in progress.
*/
const closeModal = () => {
if (isSubmitting) return;
props.onClose()
};
const confirmAction = () => {
if (!props.data?.id) return;
setSubmitting(true)
deleteRole(props.data.id)
.then((response) => {
if (response.success){
showNotification(response.message);
setSubmitting(false)
props.onClose()
return;
} else {
showNotification(response.message, "error")
}
})
.catch(() => {
//TODO: Handle Error
})
.finally(() => {
setSubmitting(false)
})
}
return (
<Modal opened={!!props.data} onClose={closeModal} title={`Delete confirmation`}>
<Text size="sm">
Are you sure you want to delete role{" "}
<Text span fw={700}>
{props.data?.name}
</Text>
? This action is irreversible.
</Text>
{/* 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 Role
</Button>
</Flex>
</Modal>
);
}

View File

@ -0,0 +1 @@
export { default } from "./DeleteModal";

View File

@ -1,4 +1,5 @@
"use client";
/* eslint-disable react-hooks/exhaustive-deps */
import getRoleById from "@/features/dashboard/roles/actions/getRoleById";
import upsertRole from "@/features/dashboard/roles/actions/upsertRole";
import roleFormDataSchema, {
RoleFormData,
@ -14,29 +15,41 @@ import {
Button,
ScrollArea,
Checkbox,
Skeleton,
Fieldset,
} from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { TbDeviceFloppy } from "react-icons/tb";
interface Props {
export interface ModalProps {
title: string;
readonly?: boolean;
data: RoleFormData;
id?: string;
opened: boolean;
onClose?: () => void;
}
export default function FormModal(props: Props) {
/**
* A component for rendering a modal with a form to create or edit a role.
*
* @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 [value, setValue] = useState("");
const [isFetching, setFetching] = useState(false);
const form = useForm<RoleFormData>({
initialValues: props.data,
initialValues: {
code: "",
description: "",
id: "",
isActive: false,
name: "",
},
validate: zodResolver(roleFormDataSchema),
validateInputOnChange: false,
onValuesChange: (values) => {
@ -44,29 +57,59 @@ export default function FormModal(props: Props) {
},
});
/**
* Fetches role 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);
getRoleById(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: RoleFormData) => {
upsertRole(values)
.then((response) => {
if (response.success){
showNotification(response.message,"success");
return closeModal()
if (response.success) {
showNotification(response.message, "success");
return closeModal();
} else {
form.setErrors(response.errors ?? {});
if (!response.errors){
showNotification(response.message, "error")
if (!response.errors) {
showNotification(response.message, "error");
}
}
})
.catch(e =>{
.catch((e) => {
//TODO: Handle Error
console.log(e)
})
}
console.log(e);
});
};
return (
<Modal
@ -74,11 +117,12 @@ export default function FormModal(props: Props) {
onClose={closeModal}
title={props.title}
scrollAreaComponent={ScrollArea.Autosize}
size="xl"
>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack mt="sm" gap="lg" px="lg">
{/* ID */}
{props.data.id ? (
{form.values.id ? (
<TextInput
label="ID"
readOnly
@ -90,6 +134,7 @@ export default function FormModal(props: Props) {
)}
{/* Code */}
<Skeleton visible={isFetching}>
<TextInput
data-autofocus
label="Code"
@ -97,23 +142,29 @@ export default function FormModal(props: Props) {
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"
@ -121,6 +172,7 @@ export default function FormModal(props: Props) {
type: "checkbox",
})}
/>
</Skeleton>
{/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg">

View File

@ -1,5 +1,4 @@
import CreateModal from "./CreateModal";
import FormModal from "./FormModal";
import DeleteModal from "./DeleteModal";
export {
CreateModal
}
export { DeleteModal, FormModal };

View File

@ -1,25 +1,48 @@
"use client";
import { Table, Text } from "@mantine/core";
import { Table, Text, Flex, Button, Center } from "@mantine/core";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import React from "react";
import React, { useState } from "react";
import CrudPermissions from "@/features/auth/types/CrudPermissions";
import getRoles from "@/features/dashboard/roles/data/getRoles";
import createColumns from "./columns";
import FormModal from "../../_modals/FormModal";
import { ModalProps } from "../../_modals/FormModal/FormModal";
import { TbPlus } from "react-icons/tb";
import { RoleFormData } from "@/features/dashboard/roles/formSchemas/RoleFormData";
import { string } from "zod";
import { DeleteModal } from "../../_modals";
import { DeleteModalProps } from "../../_modals/DeleteModal/DeleteModal";
interface Props {
permissions: Partial<CrudPermissions>,
roles: Awaited<ReturnType<typeof getRoles>>
permissions: Partial<CrudPermissions>;
roles: Awaited<ReturnType<typeof getRoles>>;
}
export default function RolesTable(props: Props) {
const [modalProps, setModalProps] = useState<ModalProps>({
opened: false,
title: "",
});
const [deleteModalProps, setDeleteModalProps] = useState<
Omit<DeleteModalProps, "onClose">
>({
data: undefined,
});
const table = useReactTable({
data: props.roles,
columns: createColumns({
permissions: props.permissions
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: {
@ -27,9 +50,70 @@ export default function RolesTable(props: Props) {
},
});
const openFormModal = (type: "create" | "edit" | "detail", id?: string) => {
const openCreateModal = () => {
setModalProps({
id,
opened: true,
title: "Create new role",
});
};
const openDetailModal = () => {
setModalProps({
id,
opened: true,
title: "Role detail",
readonly: true,
});
};
const openEditModal = () => {
setModalProps({
id,
opened: true,
title: "Edit role",
});
};
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 Role
</Button>
)}
</Flex>
<Table verticalSpacing="xs" horizontalSpacing="xs">
{/* Thead */}
<Table.Thead>
@ -57,7 +141,8 @@ export default function RolesTable(props: Props) {
{/* Tbody */}
<Table.Tbody>
{table.getRowModel().rows.map((row) => (
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map((row) => (
<Table.Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Table.Td
@ -73,8 +158,22 @@ export default function RolesTable(props: Props) {
</Table.Td>
))}
</Table.Tr>
))}
))
) : (
<Table.Tr>
<Table.Td colSpan={table.getAllColumns().length}>
<Center>- No Data -</Center>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
<FormModal {...modalProps} onClose={closeModal} />
<DeleteModal
{...deleteModalProps}
onClose={() => setDeleteModalProps({})}
/>
</>
);
}

View File

@ -1,4 +1,5 @@
import CrudPermissions from "@/features/auth/types/CrudPermissions";
import { RoleFormData } from "@/features/dashboard/roles/formSchemas/RoleFormData";
import createActionButtons from "@/features/dashboard/utils/createActionButtons";
import { Badge, Flex, Tooltip, ActionIcon } from "@mantine/core";
import { createColumnHelper } from "@tanstack/react-table";
@ -17,10 +18,10 @@ export interface RoleRow {
interface ColumnOptions {
permissions: Partial<CrudPermissions>;
actions?: {
detail?: () => void;
edit?: () => void;
delete?: () => void;
actions: {
detail: (id: string) => void;
edit: (id: string) => void;
delete: (id: string, name: string) => void;
};
}
@ -67,28 +68,28 @@ const createColumns = (options: ColumnOptions) => {
meta: {
className: "w-fit",
},
cell: () => (
cell: (props) => (
<Flex gap="xs">
{
createActionButtons([
{
label: "Detail",
permission: options.permissions.read,
action: options.actions?.detail,
action: () => options.actions.detail(props.row.original.id),
color: "green",
icon: <TbEye />
},
{
label: "Edit",
permission: options.permissions.update,
action: options.actions?.edit,
action: () => options.actions.edit(props.row.original.id),
color: "yellow",
icon: <TbPencil />
},
{
label: "Delete",
permission: options.permissions.delete,
action: options.actions?.delete,
action: () => options.actions.delete(props.row.original.id, props.row.original.name),
color: "red",
icon: <TbTrash />
}

View File

@ -1,45 +1,38 @@
import { Button, Card, Flex, Stack, Title } from "@mantine/core";
import { Card, Stack, Title } from "@mantine/core";
import { Metadata } from "next";
import React from "react";
import RolesTable from "./_tables/RolesTable/RolesTable";
import { TbPlus } from "react-icons/tb";
import checkPermission from "@/features/auth/tools/checkPermission";
import { unauthorized } from "@/BaseError";
import Link from "next/link";
import { CreateModal } from "./_modals";
import FormModal from "./_modals/FormModal";
import CreateButton from "./_components/CreateButton/CreateButton";
import getRoles from "@/features/dashboard/roles/data/getRoles";
import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions";
interface Props {
searchParams: {
detail?: string,
edit?: string,
delete?: string,
create?: string,
}
detail?: string;
edit?: string;
delete?: string;
create?: string;
};
}
export default async function RolesPage({searchParams}: Props) {
export const metadata: Metadata = {
title: "Roles - Dashboard",
};
export default async function RolesPage({ searchParams }: Props) {
const permissions = await checkMultiplePermissions({
create: "role.create",
readAll: "role.readAll",
read: 'role.read',
update: 'role.update',
delete: 'role.delete'
})
read: "role.read",
update: "role.update",
delete: "role.delete",
});
const roles = await getRoles()
const roles = await getRoles();
return (
<Stack>
<Title order={1}>Roles</Title>
<Card>
<Flex justify="flex-end">
{ permissions.create && <CreateButton />}
</Flex>
<RolesTable permissions={permissions} roles={roles} />
</Card>
</Stack>

View File

@ -0,0 +1,26 @@
"use server";
import { unauthorized } from "@/BaseError";
import prisma from "@/db";
import checkPermission from "@/features/auth/tools/checkPermission";
export default async function deleteRole(id: string) {
if (!(await checkPermission("role.delete"))) return unauthorized();
try {
const role = await prisma.role.delete({
where: { id },
});
return {
success: true,
message: "The role has been deleted successfully",
} as const;
} catch (e) {
//TODO: Handle error
return {
success: false,
message: "Unable to delete the role",
} as const;
}
}

View File

@ -0,0 +1,40 @@
"use server";
import { unauthorized } from "@/BaseError";
import prisma from "@/db";
import checkPermission from "@/features/auth/tools/checkPermission";
export default async function getRoleById(id: string) {
if (!(await checkPermission("role.read"))) unauthorized();
const role = await prisma.role.findFirst({
where: { id },
select: {
code: true,
description: true,
id: true,
isActive: true,
name: true,
permissions: {
select: {
id: true,
code: true,
name: true,
},
},
},
});
if (!role) {
return {
success: false,
message: "Role not found",
} as const;
}
return {
success: true,
message: "Role fetched successfully",
data: role,
} as const;
}