From bf7f8ebbf7d4d86048a33ec981f8820ffad14793 Mon Sep 17 00:00:00 2001 From: sianida26 Date: Sun, 18 Feb 2024 03:45:18 +0700 Subject: [PATCH] Added modal for input link --- src/app/dashboard/page.tsx | 2 +- .../reseller-office-365/list/page.tsx | 39 +++ .../reseller-office-365/request/page.tsx | 2 +- src/core/db/index.ts | 6 +- src/db/index.ts | 2 +- src/modules/dashboard/data/sidebarMenus.ts | 6 +- .../actions/createLinkRequest.ts | 3 - .../actions/getAllLinkRequests.ts | 55 ++++ .../actions/getLinkRequestDataById.ts | 70 +++++ .../resellerOffice365/modals/RequestModal.tsx | 253 ++++++++++++------ .../ListOfRequestTable/ListOfRequestTable.tsx | 66 +++++ .../tables/ListOfRequestTable/columns.tsx | 95 +++++++ .../tables/RequestTable/RequestTable.tsx | 49 ++-- .../tables/RequestTable/columns.tsx | 38 +-- .../types/RequestLinkForm.d.ts | 1 + .../types/RequestLinkWithIssuerData.d.ts | 14 + 16 files changed, 569 insertions(+), 132 deletions(-) create mode 100644 src/app/dashboard/reseller-office-365/list/page.tsx create mode 100644 src/modules/resellerOffice365/actions/getAllLinkRequests.ts create mode 100644 src/modules/resellerOffice365/actions/getLinkRequestDataById.ts create mode 100644 src/modules/resellerOffice365/tables/ListOfRequestTable/ListOfRequestTable.tsx create mode 100644 src/modules/resellerOffice365/tables/ListOfRequestTable/columns.tsx create mode 100644 src/modules/resellerOffice365/types/RequestLinkWithIssuerData.d.ts diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 82f1cfb..f58e804 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import React from 'react' export default async function Dashboard() { return (
-

Dashboard

+

Dashboard

) } diff --git a/src/app/dashboard/reseller-office-365/list/page.tsx b/src/app/dashboard/reseller-office-365/list/page.tsx new file mode 100644 index 0000000..9420636 --- /dev/null +++ b/src/app/dashboard/reseller-office-365/list/page.tsx @@ -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 ( + + List Link Office 365 + + + + + ); +} diff --git a/src/app/dashboard/reseller-office-365/request/page.tsx b/src/app/dashboard/reseller-office-365/request/page.tsx index dea4d45..3c81347 100644 --- a/src/app/dashboard/reseller-office-365/request/page.tsx +++ b/src/app/dashboard/reseller-office-365/request/page.tsx @@ -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 diff --git a/src/core/db/index.ts b/src/core/db/index.ts index 0273c94..faf0759 100644 --- a/src/core/db/index.ts +++ b/src/core/db/index.ts @@ -8,8 +8,8 @@ declare global { var prisma: undefined | ReturnType; } -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; diff --git a/src/db/index.ts b/src/db/index.ts index 77d6b4b..7f5046e 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -8,7 +8,7 @@ declare global { var prisma: undefined | ReturnType } -const prisma = globalThis.prisma ?? prismaClientSingleton() +const db = globalThis.prisma ?? prismaClientSingleton() export default prisma diff --git a/src/modules/dashboard/data/sidebarMenus.ts b/src/modules/dashboard/data/sidebarMenus.ts index 6a969ac..44fe8d5 100644 --- a/src/modules/dashboard/data/sidebarMenus.ts +++ b/src/modules/dashboard/data/sidebarMenus.ts @@ -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: ["*"] } ] diff --git a/src/modules/resellerOffice365/actions/createLinkRequest.ts b/src/modules/resellerOffice365/actions/createLinkRequest.ts index c54145a..16fdd6b 100644 --- a/src/modules/resellerOffice365/actions/createLinkRequest.ts +++ b/src/modules/resellerOffice365/actions/createLinkRequest.ts @@ -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: diff --git a/src/modules/resellerOffice365/actions/getAllLinkRequests.ts b/src/modules/resellerOffice365/actions/getAllLinkRequests.ts new file mode 100644 index 0000000..e095902 --- /dev/null +++ b/src/modules/resellerOffice365/actions/getAllLinkRequests.ts @@ -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 +> { + 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); + } +} diff --git a/src/modules/resellerOffice365/actions/getLinkRequestDataById.ts b/src/modules/resellerOffice365/actions/getLinkRequestDataById.ts new file mode 100644 index 0000000..9e266bf --- /dev/null +++ b/src/modules/resellerOffice365/actions/getLinkRequestDataById.ts @@ -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>> + > +> { + 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); + } +} diff --git a/src/modules/resellerOffice365/modals/RequestModal.tsx b/src/modules/resellerOffice365/modals/RequestModal.tsx index 99060ad..9810653 100644 --- a/src/modules/resellerOffice365/modals/RequestModal.tsx +++ b/src/modules/resellerOffice365/modals/RequestModal.tsx @@ -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({ 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 ( @@ -146,16 +219,21 @@ export default function RequestModal(props: ModalProps) { )} - } - disabled={disableChange} - {...form.getInputProps("numberOfLinks")} - /> + {errorMessage && {errorMessage}} + + + } + disabled={disableChange} + readOnly={readonly} + {...form.getInputProps("numberOfLinks")} + /> + {form.values.details.map((item, i) => (
- } - label="Email" - disabled={disableChange} - {...form.getInputProps( - `details.${i}.email` + + + } + label="Email" + readOnly={readonly} + disabled={disableChange} + {...form.getInputProps( + `details.${i}.email` + )} + /> + + + + } - {...form.getInputProps( - `details.${i}.activePeriod` - )} - /> - } - min={1} - max={5} - disabled={disableChange} - allowDecimal={false} - clampBehavior="strict" - {...form.getInputProps( - `details.${i}.endUserQty` - )} - /> - +
))} @@ -211,7 +310,7 @@ export default function RequestModal(props: ModalProps) { > Close - {(!props.readonly || formState === "waiting") && ( + {formState === "waiting" && ( } @@ -70,7 +65,7 @@ export default function RequestTable(props: Props) { - + ); } diff --git a/src/modules/resellerOffice365/tables/RequestTable/columns.tsx b/src/modules/resellerOffice365/tables/RequestTable/columns.tsx index 44fe0d1..fec8b69 100644 --- a/src/modules/resellerOffice365/tables/RequestTable/columns.tsx +++ b/src/modules/resellerOffice365/tables/RequestTable/columns.tsx @@ -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 WAITING; + break; + case "ACCEPTED": + return ACCEPTED; + break; + case "CANCELLED": + return CANCELLED; + break; + case "REJECTED": + return REJECTED; + break; + } + }, }), columnHelper.display({ @@ -68,23 +84,13 @@ const createColumns = (options: ColumnOptions) => { color: "yellow", icon: , }, - { - label: "Delete", - permission: options.permissions.delete, - // action: () => - // options.actions.delete( - // props.row.original.id - // ), - color: "red", - icon: , - }, ])} ), }), ]; - return columns; + return columns; }; export default createColumns; diff --git a/src/modules/resellerOffice365/types/RequestLinkForm.d.ts b/src/modules/resellerOffice365/types/RequestLinkForm.d.ts index 30d11de..3745dc3 100644 --- a/src/modules/resellerOffice365/types/RequestLinkForm.d.ts +++ b/src/modules/resellerOffice365/types/RequestLinkForm.d.ts @@ -1,4 +1,5 @@ interface RequestLinkForm { + id: string | undefined; numberOfLinks: number; details: { email: string; diff --git a/src/modules/resellerOffice365/types/RequestLinkWithIssuerData.d.ts b/src/modules/resellerOffice365/types/RequestLinkWithIssuerData.d.ts new file mode 100644 index 0000000..577a312 --- /dev/null +++ b/src/modules/resellerOffice365/types/RequestLinkWithIssuerData.d.ts @@ -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 +}