Added modal for input link

This commit is contained in:
sianida26 2024-02-18 03:45:18 +07:00
parent f16e356555
commit bf7f8ebbf7
16 changed files with 569 additions and 132 deletions

View File

@ -3,7 +3,7 @@ import React from 'react'
export default async function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<h1 className='font-bold bg-red-500'>Dashboard</h1>
</div>
)
}

View File

@ -0,0 +1,39 @@
import getUserRoles from "@/modules/auth/utils/getUserRoles";
import checkMultiplePermissions from "@/modules/dashboard/services/checkMultiplePermissions";
import checkPermission from "@/modules/dashboard/services/checkPermission";
import getAllLinkRequests from "@/modules/resellerOffice365/actions/getAllLinkRequests";
import getLinkRequests from "@/modules/resellerOffice365/actions/getLinkRequests";
import ListOfRequestTable from "@/modules/resellerOffice365/tables/ListOfRequestTable/ListOfRequestTable";
import RequestTable from "@/modules/resellerOffice365/tables/RequestTable/RequestTable";
import { Card, Stack, Title } from "@mantine/core";
import { notFound } from "next/navigation";
import React from "react";
export default async function RequestLinkPage() {
const permissions = await checkMultiplePermissions({
create: "office-365-link.create",
readAll: "office-365-link.readAll",
read: "office-365-link.read",
update: "office-365-link.update",
delete: "office-365-link.delete",
});
if (!permissions.readAll) notFound();
const data = await getAllLinkRequests();
if (!data.success) {
//todo: handle error
console.error(data.error);
throw new Error("Error while fetch data");
}
const tableData = data.data;
return (
<Stack>
<Title order={1}>List Link Office 365</Title>
<Card>
<ListOfRequestTable permissions={permissions} tableData={tableData} />
</Card>
</Stack>
);
}

View File

@ -22,7 +22,7 @@ export default async function RequestLinkPage() {
if (!data.success){
//todo: handle error
console.error(data.error)
throw new Error("Error while fetch permission")
throw new Error("Error while fetch data")
}
const tableData = data.data

View File

@ -8,8 +8,8 @@ declare global {
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}
const prisma = globalThis.prisma ?? prismaClientSingleton();
const db = globalThis.prisma ?? prismaClientSingleton();
export default prisma;
export default db;
if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
if (process.env.NODE_ENV !== "production") globalThis.prisma = db;

View File

@ -8,7 +8,7 @@ declare global {
var prisma: undefined | ReturnType<typeof prismaClientSingleton>
}
const prisma = globalThis.prisma ?? prismaClientSingleton()
const db = globalThis.prisma ?? prismaClientSingleton()
export default prisma

View File

@ -31,13 +31,13 @@ const sidebarMenus: SidebarMenu[] = [
allowedPermissions: ["*"],
children: [
{
label: "Request Link",
label: "My Request Links",
link: "/reseller-office-365/request",
allowedRoles: ["*"]
},
{
label: "Respond Request Link",
link: "#",
label: "Process Request Link",
link: "/reseller-office-365/list",
allowedRoles: ["*"]
}
]

View File

@ -10,7 +10,6 @@ import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
import db from "@/core/db";
import getCurrentUser from "@/modules/auth/utils/getCurrentUser";
import { revalidatePath } from "next/cache";
import {server} from "../../../../server/socket";
export default async function createLinkRequest(
formData: RequestLinkForm
@ -55,8 +54,6 @@ export default async function createLinkRequest(
revalidatePath(".")
server.publish(`mwrl-${currentUser.id}`, "update")
return {
success: true,
message:

View File

@ -0,0 +1,55 @@
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 db from "@/core/db";
import RequestLinkWithIssuerData from "../types/RequestLinkWithIssuerData";
export default async function getAllLinkRequests(): Promise<
ServerResponseAction<RequestLinkWithIssuerData[]>
> {
try {
//TODO: Fix permission check
if (!(await checkPermission("authenticated-only")))
return unauthorized();
const requestLinks = await db.office365LinkRequest.findMany({
orderBy: [
{
status: "desc"
},
{
requestedAt: "desc"
}
],
select: {
id: true,
creator: {
select: {
id: true,
name: true,
photoProfile: true,
email: true,
},
},
status: true,
requestedAt: true,
_count: {
select: {
links: true,
},
},
},
});
return {
success: true,
data: requestLinks.map((item) => ({
...item,
userCount: item._count.links,
})),
};
} catch (e) {
return handleCatch(e);
}
}

View File

@ -0,0 +1,70 @@
"use server"
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 RequestLinkWithIssuerData from "../types/RequestLinkWithIssuerData";
import db from "@/core/db";
import { string } from "zod";
import notFound from "@/modules/dashboard/utils/notFound";
async function getOffice365LinkRequestData(id: string) {
const data = await db.office365LinkRequest.findFirst({
where: { id },
select: {
acceptedAt: true,
cancelledAt: true,
creator: {
select: {
name: true,
id: true,
email: true,
},
},
id: true,
links: {
select: {
activePeriod: true,
email: true,
id: true,
link: true,
numberOfUsers: true,
},
},
rejectedAt: true,
requestedAt: true,
status: true,
},
});
return data;
}
export default async function getLinkRequestDataById(
id: string
): Promise<
ServerResponseAction<
NonNullable<Awaited<ReturnType<typeof getOffice365LinkRequestData>>>
>
> {
try {
//TODO: Adjust permission
if (!(await checkPermission("authenticated-only")))
return unauthorized();
const data = await getOffice365LinkRequestData(id);
if (!data) {
return notFound({
message: "The requested link request item is not found",
});
}
return {
success: true,
data,
};
} catch (e) {
return handleCatch(e);
}
}

View File

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
Alert,
Button,
@ -13,9 +14,10 @@ import {
TextInput,
Loader,
Text,
Skeleton,
} from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import {
TbAt,
TbCalendarTime,
@ -30,27 +32,82 @@ import withServerAction from "@/modules/dashboard/utils/withServerAction";
import createLinkRequest from "../actions/createLinkRequest";
import { notifications } from "@mantine/notifications";
import DashboardError from "@/modules/dashboard/errors/DashboardError";
import getLinkRequestDataById from "../actions/getLinkRequestDataById";
import { isPagesAPIRouteMatch } from "next/dist/server/future/route-matches/pages-api-route-match";
export interface ModalProps {
title: string;
opened: boolean;
readonly: boolean;
onClose?: () => void;
type: "create" | "detail" | "waiting" | "input link";
detailId: string | null;
}
export default function RequestModal(props: ModalProps) {
const [formState, setFormState] = useState<
"idle" | "submitting" | "waiting"
"idle" | "submitting" | "waiting" | "fetching" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState("");
const closeModal = () => {
if (formState === "submitting") return; //prevents closing
//reset state
setErrorMessage("");
setFormState("idle");
form.reset();
props.onClose ? props.onClose() : "";
};
useEffect(() => {
const fetchDataById = async (id: string) => {
const { data } = await withServerAction(getLinkRequestDataById, id);
if (!props.opened) return;
return data;
};
switch (props.type) {
case "input link": {
if (!props.detailId || !props.opened) return;
setFormState("fetching");
fetchDataById(props.detailId)
.then((data) => {
if (!data) {
closeModal();
notifications.show({
message:
"The returned data from server is empty. Please try again",
color: "red",
});
return;
}
form.setValues({
numberOfLinks: data.links.length,
id: data.id,
details: data.links.map((item) => ({
activePeriod: item.activePeriod,
email: item.email,
endUserQty: item.numberOfUsers,
})),
});
})
.catch((e) => {
if (e instanceof Error) {
setErrorMessage(e.message);
} else {
setErrorMessage("Unkown error occured");
}
})
.finally(() => {
setFormState("idle");
});
}
}
}, [props]);
const form = useForm<RequestLinkForm>({
initialValues: {
id: undefined,
numberOfLinks: 1,
details: [
{
@ -90,51 +147,67 @@ export default function RequestModal(props: ModalProps) {
},
});
const disableChange = formState !== "idle";
const handleSubmit = (values: RequestLinkForm) => {
const submitableState = ["idle"];
const submitableState: (typeof formState)[] = ["idle"];
if (!submitableState.includes(formState)) return; //prevent submit
if (!submitableState.includes(formState)) return; //prevent submit when not in subitable state
setFormState("submitting");
withServerAction(createLinkRequest, values)
.then((response) => {
notifications.show({
message: response.message,
color: "green",
});
setFormState("waiting");
})
.catch((e) => {
if (e instanceof DashboardError) {
if (e.errorCode === "INVALID_FORM_DATA") {
if (e.formErrors) {
form.setErrors(e.formErrors);
switch (props.type) {
case "create": {
withServerAction(createLinkRequest, values)
.then((response) => {
notifications.show({
message: response.message,
color: "green",
});
setFormState("waiting");
})
.catch((e) => {
if (e instanceof DashboardError) {
if (e.errorCode === "INVALID_FORM_DATA") {
if (e.formErrors) {
form.setErrors(e.formErrors);
} else {
setErrorMessage(e.message);
}
} else {
setErrorMessage(
`ERROR: ${e.message} (${e.errorCode})`
);
}
} else if (e instanceof Error) {
setErrorMessage(`ERROR: ${e.message}`);
} else {
setErrorMessage(e.message);
setErrorMessage(
`Unkown error is occured. Please contact administrator`
);
}
} 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`
);
}
setFormState("idle");
});
setFormState("idle");
});
break;
}
case "input link": {
//TODO: Handle add link
}
}
};
const disableChange = formState !== "idle";
const readonly = props.type === "input link";
const showSkeleton = formState === "fetching";
return (
<Modal
size="sm"
opened={props.opened}
title={formState === "waiting" ? "Link Request Detail" : "Create New Request"}
title={
formState === "waiting"
? "Link Request Detail"
: "Create New Request"
}
onClose={closeModal}
scrollAreaComponent={ScrollArea.Autosize}
>
@ -146,16 +219,21 @@ export default function RequestModal(props: ModalProps) {
</Alert>
)}
<NumberInput
label="Please input the number of links you request"
min={1}
max={3}
allowDecimal={false}
clampBehavior="strict"
leftSection={<TbLink />}
disabled={disableChange}
{...form.getInputProps("numberOfLinks")}
/>
{errorMessage && <Alert color="red">{errorMessage}</Alert>}
<Skeleton visible={showSkeleton}>
<NumberInput
label="Please input the number of links you request"
min={1}
max={3}
allowDecimal={false}
clampBehavior="strict"
leftSection={<TbLink />}
disabled={disableChange}
readOnly={readonly}
{...form.getInputProps("numberOfLinks")}
/>
</Skeleton>
<Divider
label="End User Information"
@ -165,39 +243,60 @@ export default function RequestModal(props: ModalProps) {
<Stack>
{form.values.details.map((item, i) => (
<Fieldset key={i} legend={`Information ${i + 1}`}>
<TextInput
leftSection={<TbAt />}
label="Email"
disabled={disableChange}
{...form.getInputProps(
`details.${i}.email`
<Stack gap="xs">
<Skeleton visible={showSkeleton}>
<TextInput
leftSection={<TbAt />}
label="Email"
readOnly={readonly}
disabled={disableChange}
{...form.getInputProps(
`details.${i}.email`
)}
/>
</Skeleton>
<Flex gap="md">
<Skeleton visible={showSkeleton}>
<Select
data={
resellerOffice365Config.activePeriods
}
label="Active Period"
disabled={disableChange}
readOnly={readonly}
leftSection={<TbCalendarTime />}
{...form.getInputProps(
`details.${i}.activePeriod`
)}
/>
</Skeleton>
<Skeleton visible={showSkeleton}>
<NumberInput
label="End User Quantity"
leftSection={<TbUsers />}
min={1}
max={5}
disabled={disableChange}
allowDecimal={false}
readOnly={readonly}
clampBehavior="strict"
{...form.getInputProps(
`details.${i}.endUserQty`
)}
/>
</Skeleton>
</Flex>
{["input link", "detail"].includes(
props.type
) && (
<Skeleton visible={showSkeleton}>
<TextInput
label="Activation Link"
required
/>
</Skeleton>
)}
/>
<Flex gap="md">
<Select
data={
resellerOffice365Config.activePeriods
}
label="Active Period"
disabled={disableChange}
leftSection={<TbCalendarTime />}
{...form.getInputProps(
`details.${i}.activePeriod`
)}
/>
<NumberInput
label="End User Quantity"
leftSection={<TbUsers />}
min={1}
max={5}
disabled={disableChange}
allowDecimal={false}
clampBehavior="strict"
{...form.getInputProps(
`details.${i}.endUserQty`
)}
/>
</Flex>
</Stack>
</Fieldset>
))}
</Stack>
@ -211,7 +310,7 @@ export default function RequestModal(props: ModalProps) {
>
Close
</Button>
{(!props.readonly || formState === "waiting") && (
{formState === "waiting" && (
<Button
variant="filled"
leftSection={<TbDeviceFloppy size={20} />}

View File

@ -0,0 +1,66 @@
"use client";
import DashboardTable from "@/modules/dashboard/components/DashboardTable";
import { Button, Flex, Text } from "@mantine/core";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import React, { ReactNode, useState } from "react";
import { TbPlus } from "react-icons/tb";
import createColumns from "./columns";
import CrudPermissions from "@/modules/dashboard/types/CrudPermissions";
import RequestLink from "../../types/RequestLink";
import RequestModal, { ModalProps } from "../../modals/RequestModal";
import RequestLinkWithIssuerData from "../../types/RequestLinkWithIssuerData";
interface Props {
permissions: Partial<CrudPermissions>;
tableData: RequestLinkWithIssuerData[];
}
const defaultModalProps: ModalProps = {
opened: false,
title: "Create new Link",
type: "create",
detailId: null,
};
export default function ListOfRequestTable(props: Props) {
const [modalProps, setModalProps] = useState<ModalProps>(defaultModalProps);
// const [openModal, setOpenModal] = useState(false);
const table = useReactTable({
data: props.tableData,
columns: createColumns({
permissions: props.permissions,
actions: {
detail: (id) => {
openFormModal(id);
},
},
}),
getCoreRowModel: getCoreRowModel(),
defaultColumn: {
cell: (props) => <Text>{props.getValue() as ReactNode}</Text>,
},
});
const openFormModal = (id: string) => {
setModalProps({
opened: true,
title: "Request Detail",
type: "input link",
detailId: id,
});
};
const closeModal = () => {
setModalProps(defaultModalProps);
};
return (
<>
<DashboardTable table={table} />
<RequestModal {...modalProps} onClose={closeModal} />
</>
);
}

View File

@ -0,0 +1,95 @@
import { createColumnHelper } from "@tanstack/react-table";
import { Avatar, Badge, Flex, Stack, Text } 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";
import RequestLinkWithIssuerData from "../../types/RequestLinkWithIssuerData";
interface ColumnOptions {
permissions: Partial<CrudPermissions>;
actions: {
detail: (id: string) => void;
// edit: (id: string) => void;
// delete: (id: string) => void;
};
}
const createColumns = (options: ColumnOptions) => {
const columnHelper = createColumnHelper<RequestLinkWithIssuerData>();
const columns = [
columnHelper.accessor("id", {
id: "sequence",
header: "#",
cell: (props) => props.row.index + 1,
}),
columnHelper.accessor("requestedAt", {
header: "Request Date",
cell: (props) => {
const date = new Date(props.row.original.requestedAt);
return `${date.toDateString()}; ${date.toLocaleTimeString()}`;
},
}),
columnHelper.accessor("creator", {
header: "Issuer",
cell: (props) => {
return (<Flex gap="sm" align="center">
<Avatar src={props.cell.getValue().photoProfile} />
<div>
<Text>{props.cell.getValue().name}</Text>
<Text size="xs" c="gray">{props.cell.getValue().email}</Text>
</div>
</Flex>)
}
}),
columnHelper.accessor("userCount", {
header: "User Count",
}),
columnHelper.accessor("status", {
header: "Status",
cell: (props) => {
switch (props.row.original.status) {
case "WAITING":
return <Badge color="cyan">WAITING</Badge>;
break;
case "ACCEPTED":
return <Badge color="green">ACCEPTED</Badge>;
break;
case "CANCELLED":
return <Badge color="gray">CANCELLED</Badge>;
break;
case "REJECTED":
return <Badge color="red">REJECTED</Badge>;
break;
}
},
}),
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 />,
},
])}
</Flex>
),
}),
];
return columns;
};
export default createColumns;

View File

@ -14,21 +14,25 @@ interface Props {
tableData: RequestLink[];
}
const defaultModalProps: ModalProps = {
opened: false,
title: "",
type: "create",
detailId: null,
};
export default function RequestTable(props: Props) {
// const [modalProps, setModalProps] = useState<ModalProps>({
// opened: false,
// title: "",
// readonly: false,
// });
const [openModal, setOpenModal] = useState(false);
const [modalProps, setModalProps] = useState<ModalProps>(defaultModalProps);
const table = useReactTable({
data: props.tableData,
columns: createColumns({
permissions: props.permissions,
actions: {
detail: (id) => {console.log(id)}
}
detail: (id) => {
console.log(id);
},
},
}),
getCoreRowModel: getCoreRowModel(),
defaultColumn: {
@ -36,33 +40,24 @@ export default function RequestTable(props: Props) {
},
});
const openFormModall = () => {
// setModalProps({
// opened: true,
// title: "Request new link",
// readonly: false,
// });
// console.log('hai')
setOpenModal(true);
const openCreateModal = () => {
setModalProps({
opened: true,
title: "Create New Office 365 Link Request",
detailId: null,
type: "create",
});
};
const closeModal = () => {
// setModalProps({
// opened: false,
// title: "",
// readonly: false,
// });
setOpenModal(false)
setModalProps(defaultModalProps);
};
return (
<>
<Flex justify="flex-end">
{
<Button
leftSection={<TbPlus />}
onClick={() => openFormModall()}
>
<Button leftSection={<TbPlus />} onClick={openCreateModal}>
New Link Request
</Button>
}
@ -70,7 +65,7 @@ export default function RequestTable(props: Props) {
<DashboardTable table={table} />
<RequestModal opened={openModal} readonly={false} title="Create new Link Request" onClose={closeModal} />
<RequestModal {...modalProps} onClose={closeModal} />
</>
);
}

View File

@ -6,9 +6,9 @@ import createActionButtons from "@/modules/dashboard/utils/createActionButton";
export interface RequestLinkRow {
id: string;
requestDate: string,
userCount: number,
status: string
requestDate: string;
userCount: number;
status: string;
}
interface ColumnOptions {
@ -34,8 +34,8 @@ const createColumns = (options: ColumnOptions) => {
header: "Request Date",
cell: (props) => {
const date = new Date(props.row.original.requestDate);
return `${date.toDateString()}; ${date.toLocaleTimeString()}`
}
return `${date.toDateString()}; ${date.toLocaleTimeString()}`;
},
}),
columnHelper.accessor("userCount", {
@ -44,6 +44,22 @@ const createColumns = (options: ColumnOptions) => {
columnHelper.accessor("status", {
header: "Status",
cell: (props) => {
switch (props.row.original.status) {
case "WAITING":
return <Badge color="cyan">WAITING</Badge>;
break;
case "ACCEPTED":
return <Badge color="green">ACCEPTED</Badge>;
break;
case "CANCELLED":
return <Badge color="gray">CANCELLED</Badge>;
break;
case "REJECTED":
return <Badge color="red">REJECTED</Badge>;
break;
}
},
}),
columnHelper.display({
@ -68,23 +84,13 @@ const createColumns = (options: ColumnOptions) => {
color: "yellow",
icon: <TbPencil />,
},
{
label: "Delete",
permission: options.permissions.delete,
// action: () =>
// options.actions.delete(
// props.row.original.id
// ),
color: "red",
icon: <TbTrash />,
},
])}
</Flex>
),
}),
];
return columns;
return columns;
};
export default createColumns;

View File

@ -1,4 +1,5 @@
interface RequestLinkForm {
id: string | undefined;
numberOfLinks: number;
details: {
email: string;

View File

@ -0,0 +1,14 @@
import { Office365LinkRequestStatus, Prisma } from "@prisma/client";
export default interface RequestLinkWithIssuerData {
id: string;
status: Office365LinkRequestStatus;
requestedAt: Date;
creator: {
id: string;
name: string | null;
email: string | null;
photoProfile: string | null;
};
userCount: number
}