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 upsertRole from "@/features/dashboard/roles/actions/upsertRole";
import roleFormDataSchema, { import roleFormDataSchema, {
RoleFormData, RoleFormData,
@ -14,59 +15,101 @@ import {
Button, Button,
ScrollArea, ScrollArea,
Checkbox, Checkbox,
Skeleton,
Fieldset,
} from "@mantine/core"; } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form"; import { useForm, zodResolver } from "@mantine/form";
import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import React, { useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { TbDeviceFloppy } from "react-icons/tb"; import { TbDeviceFloppy } from "react-icons/tb";
interface Props { export interface ModalProps {
title: string; title: string;
readonly?: boolean; readonly?: boolean;
data: RoleFormData; id?: string;
opened: boolean; opened: boolean;
onClose?: () => void; 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 router = useRouter();
const [isSubmitting, setSubmitting] = useState(false);
const [isFetching, setFetching] = useState(false);
const [isSubmitting, setSubmitting] = useState(false); const form = useForm<RoleFormData>({
const [value, setValue] = useState(""); initialValues: {
code: "",
description: "",
id: "",
isActive: false,
name: "",
},
validate: zodResolver(roleFormDataSchema),
validateInputOnChange: false,
onValuesChange: (values) => {
console.log(values);
},
});
const form = useForm<RoleFormData>({ /**
initialValues: props.data, * Fetches role data by ID and populates the form if the modal is opened and an ID is provided.
validate: zodResolver(roleFormDataSchema), */
validateInputOnChange: false, useEffect(() => {
onValuesChange: (values) => { if (!props.opened || !props.id) {
console.log(values); 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 = () => { const closeModal = () => {
form.reset();
props.onClose ? props.onClose() : router.replace("?"); props.onClose ? props.onClose() : router.replace("?");
}; };
const handleSubmit = (values: RoleFormData) => { const handleSubmit = (values: RoleFormData) => {
upsertRole(values) upsertRole(values)
.then((response) => { .then((response) => {
if (response.success){ if (response.success) {
showNotification(response.message,"success"); showNotification(response.message, "success");
return closeModal() return closeModal();
} else { } else {
form.setErrors(response.errors ?? {}); form.setErrors(response.errors ?? {});
if (!response.errors){ if (!response.errors) {
showNotification(response.message, "error") showNotification(response.message, "error");
} }
} }
}) })
.catch(e =>{ .catch((e) => {
//TODO: Handle Error //TODO: Handle Error
console.log(e) console.log(e);
}) });
} };
return ( return (
<Modal <Modal
@ -74,11 +117,12 @@ export default function FormModal(props: Props) {
onClose={closeModal} onClose={closeModal}
title={props.title} title={props.title}
scrollAreaComponent={ScrollArea.Autosize} scrollAreaComponent={ScrollArea.Autosize}
size="xl"
> >
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack mt="sm" gap="lg" px="lg"> <Stack mt="sm" gap="lg" px="lg">
{/* ID */} {/* ID */}
{props.data.id ? ( {form.values.id ? (
<TextInput <TextInput
label="ID" label="ID"
readOnly readOnly
@ -90,37 +134,45 @@ export default function FormModal(props: Props) {
)} )}
{/* Code */} {/* Code */}
<TextInput <Skeleton visible={isFetching}>
data-autofocus <TextInput
label="Code" data-autofocus
readOnly={props.readonly} label="Code"
disabled={isSubmitting} readOnly={props.readonly}
{...form.getInputProps("code")} disabled={isSubmitting}
/> {...form.getInputProps("code")}
/>
</Skeleton>
{/* Name */} {/* Name */}
<TextInput <Skeleton visible={isFetching}>
label="Name" <TextInput
readOnly={props.readonly} label="Name"
disabled={isSubmitting} readOnly={props.readonly}
{...form.getInputProps("name")} disabled={isSubmitting}
/> {...form.getInputProps("name")}
/>
</Skeleton>
{/* Description */} {/* Description */}
<Textarea <Skeleton visible={isFetching}>
label="Description" <Textarea
readOnly={props.readonly} label="Description"
disabled={isSubmitting} readOnly={props.readonly}
{...form.getInputProps("description")} disabled={isSubmitting}
/> {...form.getInputProps("description")}
/>
</Skeleton>
<Checkbox <Skeleton visible={isFetching}>
label="Active" <Checkbox
labelPosition="right" label="Active"
{...form.getInputProps("isActive", { labelPosition="right"
type: "checkbox", {...form.getInputProps("isActive", {
})} type: "checkbox",
/> })}
/>
</Skeleton>
{/* Buttons */} {/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg"> <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 { export { DeleteModal, FormModal };
CreateModal
}

View File

@ -1,25 +1,48 @@
"use client"; "use client";
import { Table, Text } from "@mantine/core"; import { Table, Text, Flex, Button, Center } from "@mantine/core";
import { import {
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import React from "react"; import React, { useState } from "react";
import CrudPermissions from "@/features/auth/types/CrudPermissions"; import CrudPermissions from "@/features/auth/types/CrudPermissions";
import getRoles from "@/features/dashboard/roles/data/getRoles"; import getRoles from "@/features/dashboard/roles/data/getRoles";
import createColumns from "./columns"; 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 { interface Props {
permissions: Partial<CrudPermissions>, permissions: Partial<CrudPermissions>;
roles: Awaited<ReturnType<typeof getRoles>> roles: Awaited<ReturnType<typeof getRoles>>;
} }
export default function RolesTable(props: Props) { 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({ const table = useReactTable({
data: props.roles, data: props.roles,
columns: createColumns({ 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(), getCoreRowModel: getCoreRowModel(),
defaultColumn: { defaultColumn: {
@ -27,54 +50,130 @@ export default function RolesTable(props: Props) {
}, },
}); });
// TODO: Add view when data is empty 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 ( return (
<Table verticalSpacing="xs" horizontalSpacing="xs"> <>
{/* Thead */} <Flex justify="flex-end">
<Table.Thead> {props.permissions.create && (
{table.getHeaderGroups().map((headerGroup) => ( <Button
<Table.Tr key={headerGroup.id}> leftSection={<TbPlus />}
{headerGroup.headers.map((header) => ( onClick={() => openFormModal("create")}
<Table.Th >
key={header.id} New Role
style={{ </Button>
maxWidth: `${header.column.columnDef.maxSize}px`, )}
width: `${header.getSize()}`, </Flex>
}} <Table verticalSpacing="xs" horizontalSpacing="xs">
> {/* Thead */}
{header.isPlaceholder <Table.Thead>
? null {table.getHeaderGroups().map((headerGroup) => (
: flexRender( <Table.Tr key={headerGroup.id}>
header.column.columnDef.header, {headerGroup.headers.map((header) => (
header.getContext() <Table.Th
)} key={header.id}
</Table.Th> style={{
))} maxWidth: `${header.column.columnDef.maxSize}px`,
</Table.Tr> width: `${header.getSize()}`,
))} }}
</Table.Thead> >
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.Th>
))}
</Table.Tr>
))}
</Table.Thead>
{/* Tbody */} {/* Tbody */}
<Table.Tbody> <Table.Tbody>
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.length > 0 ? (
<Table.Tr key={row.id}> table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => ( <Table.Tr key={row.id}>
<Table.Td {row.getVisibleCells().map((cell) => (
key={cell.id} <Table.Td
style={{ key={cell.id}
maxWidth: `${cell.column.columnDef.maxSize}px`, style={{
}} maxWidth: `${cell.column.columnDef.maxSize}px`,
> }}
{flexRender( >
cell.column.columnDef.cell, {flexRender(
cell.getContext() cell.column.columnDef.cell,
)} cell.getContext()
)}
</Table.Td>
))}
</Table.Tr>
))
) : (
<Table.Tr>
<Table.Td colSpan={table.getAllColumns().length}>
<Center>- No Data -</Center>
</Table.Td> </Table.Td>
))} </Table.Tr>
</Table.Tr> )}
))} </Table.Tbody>
</Table.Tbody> </Table>
</Table>
<FormModal {...modalProps} onClose={closeModal} />
<DeleteModal
{...deleteModalProps}
onClose={() => setDeleteModalProps({})}
/>
</>
); );
} }

View File

@ -1,4 +1,5 @@
import CrudPermissions from "@/features/auth/types/CrudPermissions"; import CrudPermissions from "@/features/auth/types/CrudPermissions";
import { RoleFormData } from "@/features/dashboard/roles/formSchemas/RoleFormData";
import createActionButtons from "@/features/dashboard/utils/createActionButtons"; import createActionButtons from "@/features/dashboard/utils/createActionButtons";
import { Badge, Flex, Tooltip, ActionIcon } from "@mantine/core"; import { Badge, Flex, Tooltip, ActionIcon } from "@mantine/core";
import { createColumnHelper } from "@tanstack/react-table"; import { createColumnHelper } from "@tanstack/react-table";
@ -17,10 +18,10 @@ export interface RoleRow {
interface ColumnOptions { interface ColumnOptions {
permissions: Partial<CrudPermissions>; permissions: Partial<CrudPermissions>;
actions?: { actions: {
detail?: () => void; detail: (id: string) => void;
edit?: () => void; edit: (id: string) => void;
delete?: () => void; delete: (id: string, name: string) => void;
}; };
} }
@ -67,28 +68,28 @@ const createColumns = (options: ColumnOptions) => {
meta: { meta: {
className: "w-fit", className: "w-fit",
}, },
cell: () => ( cell: (props) => (
<Flex gap="xs"> <Flex gap="xs">
{ {
createActionButtons([ createActionButtons([
{ {
label: "Detail", label: "Detail",
permission: options.permissions.read, permission: options.permissions.read,
action: options.actions?.detail, action: () => options.actions.detail(props.row.original.id),
color: "green", color: "green",
icon: <TbEye /> icon: <TbEye />
}, },
{ {
label: "Edit", label: "Edit",
permission: options.permissions.update, permission: options.permissions.update,
action: options.actions?.edit, action: () => options.actions.edit(props.row.original.id),
color: "yellow", color: "yellow",
icon: <TbPencil /> icon: <TbPencil />
}, },
{ {
label: "Delete", label: "Delete",
permission: options.permissions.delete, permission: options.permissions.delete,
action: options.actions?.delete, action: () => options.actions.delete(props.row.original.id, props.row.original.name),
color: "red", color: "red",
icon: <TbTrash /> 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 { Metadata } from "next";
import React from "react"; import React from "react";
import RolesTable from "./_tables/RolesTable/RolesTable"; 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 getRoles from "@/features/dashboard/roles/data/getRoles";
import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions"; import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions";
interface Props { interface Props {
searchParams: { searchParams: {
detail?: string, detail?: string;
edit?: string, edit?: string;
delete?: string, delete?: string;
create?: 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({ const permissions = await checkMultiplePermissions({
create: "role.create", create: "role.create",
readAll: "role.readAll", readAll: "role.readAll",
read: 'role.read', read: "role.read",
update: 'role.update', update: "role.update",
delete: 'role.delete' delete: "role.delete",
}) });
const roles = await getRoles() const roles = await getRoles();
return ( return (
<Stack> <Stack>
<Title order={1}>Roles</Title> <Title order={1}>Roles</Title>
<Card> <Card>
<Flex justify="flex-end">
{ permissions.create && <CreateButton />}
</Flex>
<RolesTable permissions={permissions} roles={roles} /> <RolesTable permissions={permissions} roles={roles} />
</Card> </Card>
</Stack> </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;
}