Added actions

This commit is contained in:
sianida26 2024-02-16 09:56:23 +07:00
parent f4c384665c
commit da678fd487
12 changed files with 279 additions and 17 deletions

View File

@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE `Office365LinkRequest` (
`id` VARCHAR(191) NOT NULL,
`status` ENUM('WAITING', 'ACCEPTED', 'CANCELLED', 'REJECTED') NOT NULL DEFAULT 'WAITING',
`requestedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`acceptedAt` DATETIME(3) NULL,
`cancelledAt` DATETIME(3) NULL,
`rejectedAt` DATETIME(3) NULL,
`createdBy` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Office365ReferralLink` (
`id` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`activePeriod` VARCHAR(191) NOT NULL,
`numberOfUsers` INTEGER NOT NULL,
`link` VARCHAR(191) NULL,
`requestId` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Office365LinkRequest` ADD CONSTRAINT `Office365LinkRequest_createdBy_fkey` FOREIGN KEY (`createdBy`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Office365ReferralLink` ADD CONSTRAINT `Office365ReferralLink_requestId_fkey` FOREIGN KEY (`requestId`) REFERENCES `Office365LinkRequest`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -16,6 +16,7 @@ model User {
photoProfile String?
directPermissions Permission[] @relation("PermissionToUser")
roles Role[] @relation("RoleToUser")
linkRequests Office365LinkRequest[]
}
model Role {
@ -37,3 +38,32 @@ model Permission {
roles Role[] @relation("PermissionToRole")
directUsers User[] @relation("PermissionToUser")
}
enum Office365LinkRequestStatus {
WAITING
ACCEPTED
CANCELLED
REJECTED
}
model Office365LinkRequest {
id String @id @default(cuid())
creator User @relation(fields: [createdBy], references: [id])
status Office365LinkRequestStatus @default(WAITING)
requestedAt DateTime @default(now())
acceptedAt DateTime?
cancelledAt DateTime?
rejectedAt DateTime?
createdBy String
links Office365ReferralLink[]
}
model Office365ReferralLink {
id String @id @default(cuid())
request Office365LinkRequest @relation(fields: [requestId], references: [id])
email String
activePeriod String
numberOfUsers Int
link String?
requestId String
}

View File

@ -81,6 +81,18 @@ export default async function permissionSeed(prisma: PrismaClient) {
description: "Allows deleting a user",
isActive: true,
},
{
code: "office-365-request.create",
name: "Create Office 365 Request",
description: "Allows create an Office 365 Reseller Request",
isActive: true
},
{
code: "office-365-request.getMine",
name: "Get my Office 365 Requests",
description: "Allows retrieve user's Office 365 Link Requests",
isActive: true
}
];
await Promise.all(

View File

@ -1,6 +1,7 @@
import getUserRoles from "@/modules/auth/utils/getUserRoles";
import checkMultiplePermissions from "@/modules/dashboard/services/checkMultiplePermissions";
import checkPermission from "@/modules/dashboard/services/checkPermission";
import getLinkRequests from "@/modules/resellerOffice365/actions/getLinkRequests";
import RequestTable from "@/modules/resellerOffice365/tables/RequestTable/RequestTable";
import { Card, Stack, Title } from "@mantine/core";
import { notFound } from "next/navigation";
@ -17,11 +18,19 @@ export default async function RequestLinkPage() {
if (!permissions.readAll) notFound();
const data = await getLinkRequests()
if (!data.success){
//todo: handle error
console.error(data.error)
throw new Error("Error while fetch permission")
}
const tableData = data.data
return (
<Stack>
<Title order={1}>Permohonan Link Office 365</Title>
<Card>
<RequestTable permissions={permissions} tableData={[]} />
<RequestTable permissions={permissions} tableData={tableData} />
</Card>
</Stack>
);

View File

@ -7,6 +7,7 @@ export const DashboardErrorCodes = [
"INVALID_JWT_TOKEN",
"JWT_SECRET_EMPTY",
"USER_ALREADY_EXISTS",
"INVALID_FORM_DATA"
] as const;
interface DashboardErrorOptions {

View File

@ -0,0 +1,65 @@
"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 "server-only";
import requestLinkFormSchema from "../formSchemas/requestLinkFormSchema";
import DashboardError from "@/modules/dashboard/errors/DashboardError";
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
import db from "@/core/db";
import getCurrentUser from "@/modules/auth/utils/getCurrentUser";
import { revalidatePath } from "next/cache";
export default async function createLinkRequest(
formData: RequestLinkForm
): Promise<ServerResponseAction> {
try {
if (!(await checkPermission("office-365-request.create")))
unauthorized();
const currentUser = await getCurrentUser();
if (!currentUser) return unauthorized();
const validatedFields = requestLinkFormSchema.safeParse(formData);
if (!validatedFields.success) {
throw new DashboardError({
errorCode: "INVALID_FORM_DATA",
formErrors: mapObjectToFirstValue(
validatedFields.error.flatten().fieldErrors
),
});
}
//database operations
await db.office365LinkRequest.create({
data: {
creator: {
connect: {
id: currentUser.id,
},
},
status: "WAITING",
links: {
createMany: {
data: validatedFields.data.details.map((detail) => ({
numberOfUsers: detail.endUserQty,
activePeriod: detail.activePeriod,
email: detail.email,
})),
},
},
},
});
revalidatePath(".")
return {
success: true,
message:
"Your request has been made. Please wait while our admin processing your request",
};
} catch (e) {
return handleCatch(e);
}
}

View File

@ -0,0 +1,49 @@
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
import RequestLink from "../types/RequestLink";
import handleCatch from "@/modules/dashboard/utils/handleCatch";
import checkPermission from "@/modules/dashboard/services/checkPermission";
import unauthorized from "@/modules/dashboard/utils/unauthorized";
import getCurrentUser from "@/modules/auth/utils/getCurrentUser";
import db from "@/core/db";
export default async function getLinkRequests(): Promise<
ServerResponseAction<RequestLink[]>
> {
try {
if (!(await checkPermission("office-365-request.getMine")))
unauthorized();
const user = await getCurrentUser();
if (!user) return unauthorized();
const requests = await db.office365LinkRequest.findMany({
where: {
creator: { id: user.id },
},
select: {
id: true,
requestedAt: true,
status: true,
links: true,
},
});
const result: RequestLink[] = requests.map((request) => ({
id: request.id,
requestDate: request.requestedAt.toISOString(),
status: request.status,
userCount: request.links.reduce(
(prev, curr) => prev + curr.numberOfUsers,
0
),
}));
return {
success: true,
data: result,
};
} catch (e) {
return handleCatch(e);
}
}

View File

@ -0,0 +1,15 @@
import { z } from "zod";
import resellerOffice365Config from "../config";
const requestLinkFormSchema = z.object({
numberOfLinks: z.number().min(1), // Assuming you need at least one link
details: z.array(
z.object({
email: z.string().email(), // Validate string as an email
activePeriod: z.enum(resellerOffice365Config.activePeriods), // Validate against the specific allowed values
endUserQty: z.number().min(1), // Assuming you need at least one end user
})
),
});
export default requestLinkFormSchema;

View File

@ -1,4 +1,5 @@
import {
Alert,
Button,
Divider,
Fieldset,
@ -10,8 +11,10 @@ import {
Select,
Stack,
TextInput,
Loader,
Text,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { useForm, zodResolver } from "@mantine/form";
import React, { useState } from "react";
import {
TbAt,
@ -22,6 +25,11 @@ import {
TbUsers,
} from "react-icons/tb";
import resellerOffice365Config from "../config";
import requestLinkFormSchema from "../formSchemas/requestLinkFormSchema";
import withServerAction from "@/modules/dashboard/utils/withServerAction";
import createLinkRequest from "../actions/createLinkRequest";
import { notifications } from "@mantine/notifications";
import DashboardError from "@/modules/dashboard/errors/DashboardError";
export interface ModalProps {
title: string;
@ -30,25 +38,18 @@ export interface ModalProps {
onClose?: () => void;
}
interface FormType {
numberOfLinks: number;
details: {
email: string;
activePeriod: (typeof resellerOffice365Config.activePeriods)[number];
endUserQty: number;
}[];
}
export default function RequestModal(props: ModalProps) {
const [formState, setFormState] = useState<
"idle" | "submitting" | "waiting"
>("idle");
const [errorMessage, setErrorMessage] = useState("");
const closeModal = () => {
props.onClose ? props.onClose() : "";
};
const form = useForm<FormType>({
const form = useForm<RequestLinkForm>({
initialValues: {
numberOfLinks: 1,
details: [
@ -59,6 +60,7 @@ export default function RequestModal(props: ModalProps) {
},
],
},
validate: zodResolver(requestLinkFormSchema),
onValuesChange: (values, prev) => {
// Check if numberOfLinks has changed
if (values.numberOfLinks !== prev.numberOfLinks) {
@ -90,24 +92,60 @@ export default function RequestModal(props: ModalProps) {
const disableChange = formState !== "idle";
const handleSubmit = (values: FormType) => {
const handleSubmit = (values: RequestLinkForm) => {
const submitableState = ["idle"];
if (!submitableState.includes(formState)) return; //prevent submit
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);
} else {
setErrorMessage(e.message);
}
} 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");
});
};
return (
<Modal
size="sm"
opened={props.opened}
title={props.title}
title={formState === "waiting" ? "Link Request Detail" : "Create New Request"}
onClose={closeModal}
scrollAreaComponent={ScrollArea.Autosize}
>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
{formState === "waiting" && (
<Alert color="orange">
Your request is being processed by administrator
</Alert>
)}
<NumberInput
label="Please input the number of links you request"
min={1}
@ -178,9 +216,10 @@ export default function RequestModal(props: ModalProps) {
variant="filled"
leftSection={<TbDeviceFloppy size={20} />}
type="submit"
loading={["submitting", "waiting"].includes(
disabled={["submitting", "waiting"].includes(
formState
)}
loading={["submitting"].includes(formState)}
>
Make Request
</Button>

View File

@ -6,7 +6,7 @@ import createActionButtons from "@/modules/dashboard/utils/createActionButton";
export interface RequestLinkRow {
id: string;
requestDate: Date,
requestDate: string,
userCount: number,
status: string
}
@ -32,6 +32,10 @@ const createColumns = (options: ColumnOptions) => {
columnHelper.accessor("requestDate", {
header: "Request Date",
cell: (props) => {
const date = new Date(props.row.original.requestDate);
return `${date.toDateString()}; ${date.toLocaleTimeString()}`
}
}),
columnHelper.accessor("userCount", {

View File

@ -1,6 +1,6 @@
export default interface RequestLink {
id: string;
requestDate: Date,
requestDate: string,
userCount: number,
status: string
}

View File

@ -0,0 +1,8 @@
interface RequestLinkForm {
numberOfLinks: number;
details: {
email: string;
activePeriod: (typeof resellerOffice365Config.activePeriods)[number];
endUserQty: number;
}[];
}