From 46b3baa7766d0568e9882cd2195659fc5b97ffdc Mon Sep 17 00:00:00 2001 From: Chinedu Olebu Date: Fri, 10 Jan 2025 16:07:17 -0800 Subject: [PATCH 1/8] feat(4276): add cancel operations for private/public clouds --- Makefile | 2 +- .../[idOrAccountCoding]/download/route.tsx | 4 +- .../api/private-cloud/requests/[id]/route.ts | 38 +++++++- .../[licencePlate]/review-mou/route.ts | 2 +- .../products/_operations/create.ts | 2 +- .../products/_operations/update.ts | 22 ++++- .../api/public-cloud/requests/[id]/route.ts | 91 ++++++++++++++++++- .../requests/(request)/[id]/request/page.tsx | 10 +- .../(product)/[licencePlate]/edit/page.tsx | 4 +- .../requests/(request)/[id]/original/page.tsx | 2 +- .../requests/(request)/[id]/page.tsx | 2 +- .../requests/(request)/[id]/request/page.tsx | 24 +++-- .../PublicCloudBillingDownloadButton.tsx | 4 + .../billing/PublicCloudBillingInfo.tsx | 12 ++- app/components/buttons/CancelButton.tsx | 24 +++++ app/components/form/ActiveRequestBox.tsx | 6 ++ .../modal/privateCloudRequestCancel.tsx | 58 ++++++++++++ .../modal/publicCloudProductEditSubmit.tsx | 2 +- .../modal/publicCloudRequestCancel.tsx | 58 ++++++++++++ app/constants/event.ts | 2 + app/core/auth-options.ts | 3 + app/emails/_components/ProviderDetails.tsx | 2 +- .../_components/public-cloud/Changes.tsx | 4 +- .../public-cloud/BillingReviewerMou.tsx | 2 +- .../public-cloud/EmouServiceAgreement.tsx | 2 +- .../public-cloud/ExpenseAuthorityMou.tsx | 2 +- .../ExpenseAuthorityMouProduct.tsx | 2 +- app/prisma/schema.prisma | 11 ++- .../backend/private-cloud/requests.ts | 5 + app/services/backend/public-cloud/requests.ts | 5 + app/services/ches/public-cloud/emails.ts | 6 ++ .../db/models/private-cloud-request.ts | 9 +- .../db/models/public-cloud-request.ts | 11 ++- .../db/tasks/sign-public-cloud-mou.ts | 2 +- app/services/nats/public-cloud/index.ts | 2 +- app/types/doc-decorate.ts | 2 + app/types/next-auth.d.ts | 3 + 37 files changed, 401 insertions(+), 41 deletions(-) create mode 100644 app/components/buttons/CancelButton.tsx create mode 100644 app/components/modal/privateCloudRequestCancel.tsx create mode 100644 app/components/modal/publicCloudRequestCancel.tsx diff --git a/Makefile b/Makefile index 82394fcbf..b3059a533 100755 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ localmac: .PHONY: dev dev: - @DATABASE_URL=$$(grep -m 1 '^DATABASE_URL=' app/.env.local | cut -d '=' -f 2-) \ + @DATABASE_URL=$$(grep -m 1 '^DATABASE_URL=' app/.env.apple.local | cut -d '=' -f 2-) \ npm run prisma-push --prefix app && \ npm run dev --prefix app diff --git a/app/app/api/billing/[idOrAccountCoding]/download/route.tsx b/app/app/api/billing/[idOrAccountCoding]/download/route.tsx index e410ab8f2..2e3703bb7 100644 --- a/app/app/api/billing/[idOrAccountCoding]/download/route.tsx +++ b/app/app/api/billing/[idOrAccountCoding]/download/route.tsx @@ -67,13 +67,13 @@ export const GET = apiHandler(async ({ pathParams, queryParams, session }) => { const canDownloadMou = session.permissions.downloadBillingMou || - product.members.some( + product?.members.some( (member) => member.userId === session.user.id && arraysIntersect(member.roles, [PublicCloudProductMemberRole.BILLING_VIEWER]), ); - if (!canDownloadMou) { + if (!canDownloadMou || !product) { return UnauthorizedResponse(); } diff --git a/app/app/api/private-cloud/requests/[id]/route.ts b/app/app/api/private-cloud/requests/[id]/route.ts index 59e542931..8b2a3efcc 100644 --- a/app/app/api/private-cloud/requests/[id]/route.ts +++ b/app/app/api/private-cloud/requests/[id]/route.ts @@ -1,8 +1,9 @@ +import { DecisionStatus, EventType } from '@prisma/client'; import { z } from 'zod'; import { GlobalRole } from '@/constants'; import createApiHandler from '@/core/api-handler'; import { OkResponse, UnauthorizedResponse } from '@/core/responses'; -import { models } from '@/services/db'; +import { createEvent, models } from '@/services/db'; const pathParamSchema = z.object({ id: z.string(), @@ -12,6 +13,7 @@ const apiHandler = createApiHandler({ roles: [GlobalRole.User], validations: { pathParams: pathParamSchema }, }); + export const GET = apiHandler(async ({ pathParams, queryParams, session }) => { const { id } = pathParams; @@ -23,3 +25,37 @@ export const GET = apiHandler(async ({ pathParams, queryParams, session }) => { return OkResponse(request); }); + +export const PUT = apiHandler(async ({ pathParams, session }) => { + const { id } = pathParams; + + const { data: request } = await models.privateCloudRequest.get({ where: { id } }, session); + + if (!request?._permissions.cancel) { + return UnauthorizedResponse(); + } + + const updatedRequest = await models.privateCloudRequest.update( + { + where: { + id, + decisionStatus: DecisionStatus.PENDING, + active: true, + }, + data: { + decisionStatus: DecisionStatus.CANCELLED, + }, + select: { + decisionStatus: true, + createdByEmail: true, + }, + }, + session, + ); + + if (updatedRequest) { + await createEvent(EventType.CANCEL_PRIVATE_CLOUD_REQUEST, session.user.id, { requestId: id }); + } + + return OkResponse(updatedRequest); +}); diff --git a/app/app/api/public-cloud/products/[licencePlate]/review-mou/route.ts b/app/app/api/public-cloud/products/[licencePlate]/review-mou/route.ts index 0fd4a422a..42dc98aab 100644 --- a/app/app/api/public-cloud/products/[licencePlate]/review-mou/route.ts +++ b/app/app/api/public-cloud/products/[licencePlate]/review-mou/route.ts @@ -33,7 +33,7 @@ export const POST = apiHandler(async ({ pathParams, body, session }) => { include: publicCloudRequestDetailInclude, }); - if (request) { + if (request && request.decisionData.billingId) { const billing = await prisma.billing.update({ where: { id: request?.decisionData.billingId, diff --git a/app/app/api/public-cloud/products/_operations/create.ts b/app/app/api/public-cloud/products/_operations/create.ts index 57af728d6..8773259ad 100644 --- a/app/app/api/public-cloud/products/_operations/create.ts +++ b/app/app/api/public-cloud/products/_operations/create.ts @@ -92,7 +92,7 @@ export default async function createOp({ session, body }: { session: Session; bo const proms = []; // Assign a task to the expense authority for new billing - if (newRequest.decisionData.expenseAuthorityId && !newRequest.decisionData.billing.signed) { + if (newRequest.decisionData.expenseAuthorityId && !newRequest.decisionData.billing?.signed) { const taskProm = tasks.create(TaskType.SIGN_PUBLIC_CLOUD_MOU, { request: newRequest }); proms.push(taskProm); } else { diff --git a/app/app/api/public-cloud/products/_operations/update.ts b/app/app/api/public-cloud/products/_operations/update.ts index 563f065c8..28abcb524 100644 --- a/app/app/api/public-cloud/products/_operations/update.ts +++ b/app/app/api/public-cloud/products/_operations/update.ts @@ -1,6 +1,7 @@ import { DecisionStatus, Prisma, RequestType, EventType, TaskType } from '@prisma/client'; import { Session } from 'next-auth'; import { TypeOf } from 'zod'; +import prisma from '@/core/prisma'; import { OkResponse, UnauthorizedResponse } from '@/core/responses'; import { comparePublicProductData } from '@/helpers/product-change'; import { sendEditRequestEmails } from '@/services/ches/public-cloud'; @@ -40,13 +41,28 @@ export default async function updateOp({ body.expenseAuthority?.email, ]); + const billingInfo = product.billingId + ? await prisma.billing.findFirst({ + where: { + id: product.billingId, + }, + }) + : null; + const decisionData = { ...rest, licencePlate: product.licencePlate, status: product.status, provider: product.provider, createdAt: product.createdAt, - billing: { connect: { id: product.billingId } }, + billing: product.billingId + ? { + connect: { + id: product.billingId, + code: billingInfo?.code, + }, + } + : undefined, // Handle the case where `billingId` is null projectOwner: { connect: { email: body.projectOwner.email } }, primaryTechnicalLead: { connect: { email: body.primaryTechnicalLead.email } }, secondaryTechnicalLead: body.secondaryTechnicalLead @@ -72,7 +88,9 @@ export default async function updateOp({ requestComment, changes: otherChangeMeta, originalData: { connect: { id: previousRequest?.decisionDataId } }, - decisionData: { create: decisionData }, + decisionData: { + create: decisionData, + }, requestData: { create: decisionData }, project: { connect: { licencePlate: product.licencePlate } }, }, diff --git a/app/app/api/public-cloud/requests/[id]/route.ts b/app/app/api/public-cloud/requests/[id]/route.ts index f3bcfc20f..12baa8d86 100644 --- a/app/app/api/public-cloud/requests/[id]/route.ts +++ b/app/app/api/public-cloud/requests/[id]/route.ts @@ -1,8 +1,10 @@ +import { DecisionStatus, EventType, PublicCloudRequest } from '@prisma/client'; import { z } from 'zod'; import { GlobalRole } from '@/constants'; import createApiHandler from '@/core/api-handler'; +import prisma from '@/core/prisma'; import { OkResponse, UnauthorizedResponse } from '@/core/responses'; -import { models } from '@/services/db'; +import { createEvent, models } from '@/services/db'; const pathParamSchema = z.object({ id: z.string(), @@ -23,3 +25,90 @@ export const GET = apiHandler(async ({ pathParams, queryParams, session }) => { return OkResponse(request); }); + +export const PUT = apiHandler(async ({ pathParams, session }) => { + const { id } = pathParams; + + const { data: request } = await models.publicCloudRequest.get({ where: { id } }, session); + + if (!request?._permissions.cancel) { + return UnauthorizedResponse(); + } + + const { data: updatedRequest } = await models.publicCloudRequest.update( + { + where: { + id, + decisionStatus: DecisionStatus.PENDING, + active: true, + }, + data: { + decisionStatus: DecisionStatus.CANCELLED, + }, + select: { + decisionStatus: true, + createdByEmail: true, + licencePlate: true, + decisionData: true, + decisionDataId: true, + requestDataId: true, + createdBy: { + select: { + id: true, + }, + }, + }, + }, + session, + ); + + const { decisionDataId, requestDataId, licencePlate } = updatedRequest as unknown as PublicCloudRequest; + + const billingData = await prisma.billing.findFirst({ + where: { + licencePlate, + }, + select: { + expenseAuthorityId: true, + }, + }); + + await prisma.publicCloudRequestedProject.updateMany({ + where: { + expenseAuthorityId: billingData?.expenseAuthorityId, + }, + data: { + expenseAuthorityId: null, + billingId: null, + }, + }); + + await prisma.billing.deleteMany({ + where: { + licencePlate, + signed: false, + }, + }); + + await prisma.task.deleteMany({ + where: { + data: { + equals: { + licencePlate, + }, + }, + }, + }); + + await prisma.publicCloudRequest.update({ + where: { id }, + data: { + decisionDataId, + requestDataId, + }, + }); + + await createEvent(EventType.CANCEL_PUBLIC_CLOUD_REQUEST, session.user.id, { requestId: id }); + + return OkResponse(true); +}); diff --git a/app/app/private-cloud/requests/(request)/[id]/request/page.tsx b/app/app/private-cloud/requests/(request)/[id]/request/page.tsx index 35f5fbe17..bdf7376f4 100644 --- a/app/app/private-cloud/requests/(request)/[id]/request/page.tsx +++ b/app/app/private-cloud/requests/(request)/[id]/request/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { PrivateCloudProject } from '@prisma/client'; +import { DecisionStatus, PrivateCloudProject } from '@prisma/client'; import { IconInfoCircle, IconUsersGroup, @@ -9,9 +9,11 @@ import { IconMessage, IconWebhook, } from '@tabler/icons-react'; +import { useSession } from 'next-auth/react'; import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { z } from 'zod'; +import CancelRequest from '@/components/buttons/CancelButton'; import PreviousButton from '@/components/buttons/Previous'; import ProjectDescription from '@/components/form/ProjectDescriptionPrivate'; import TeamContacts from '@/components/form/TeamContacts'; @@ -133,6 +135,9 @@ export default privateCloudRequestRequest(({}) => { }); } + const { data: session } = useSession(); + const canCancel = session?.user.email === snap.currentRequest.createdByEmail; + return (
@@ -141,6 +146,9 @@ export default privateCloudRequestRequest(({}) => {
+ {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && canCancel && ( + + )}
diff --git a/app/app/public-cloud/products/(product)/[licencePlate]/edit/page.tsx b/app/app/public-cloud/products/(product)/[licencePlate]/edit/page.tsx index 592367a82..70cca1606 100644 --- a/app/app/public-cloud/products/(product)/[licencePlate]/edit/page.tsx +++ b/app/app/public-cloud/products/(product)/[licencePlate]/edit/page.tsx @@ -49,7 +49,7 @@ export default publicCloudProductEdit(({}) => { defaultValues: { ...snap.currentProduct, isAgMinistryChecked: true, - accountCoding: snap.currentProduct?.billing.accountCoding, + accountCoding: snap.currentProduct?.billing?.accountCoding, }, }); @@ -131,7 +131,7 @@ export default publicCloudProductEdit(({}) => { label: 'Billing (Account coding)', description: '', Component: AccountCoding, - componentArgs: { accountCodingInitial: snap.currentProduct?.billing.accountCoding, disabled: true }, + componentArgs: { accountCodingInitial: snap.currentProduct?.billing?.accountCoding, disabled: true }, }, ]; diff --git a/app/app/public-cloud/requests/(request)/[id]/original/page.tsx b/app/app/public-cloud/requests/(request)/[id]/original/page.tsx index 8c2afa650..2e8786fd6 100644 --- a/app/app/public-cloud/requests/(request)/[id]/original/page.tsx +++ b/app/app/public-cloud/requests/(request)/[id]/original/page.tsx @@ -118,7 +118,7 @@ export default publicCloudRequestOriginal(({ router }) => { description: '', Component: AccountCoding, componentArgs: { - accountCodingInitial: snap.currentRequest.originalData?.billing.accountCoding, + accountCodingInitial: snap.currentRequest.originalData?.billing?.accountCoding, disabled: true, }, }, diff --git a/app/app/public-cloud/requests/(request)/[id]/page.tsx b/app/app/public-cloud/requests/(request)/[id]/page.tsx index 4ffa88b61..3eab20060 100644 --- a/app/app/public-cloud/requests/(request)/[id]/page.tsx +++ b/app/app/public-cloud/requests/(request)/[id]/page.tsx @@ -98,7 +98,7 @@ export default publicCloudRequest(({ getPathParams }) => { label: 'Billing (account coding)', description: '', Component: AccountCoding, - componentArgs: { accountCodingInitial: request.decisionData.billing.accountCoding, disabled: true }, + componentArgs: { accountCodingInitial: request.decisionData.billing?.accountCoding, disabled: true }, }, ]; diff --git a/app/app/public-cloud/requests/(request)/[id]/request/page.tsx b/app/app/public-cloud/requests/(request)/[id]/request/page.tsx index 59a6a3989..95dc5ac96 100644 --- a/app/app/public-cloud/requests/(request)/[id]/request/page.tsx +++ b/app/app/public-cloud/requests/(request)/[id]/request/page.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@mantine/core'; -import { RequestType } from '@prisma/client'; +import { DecisionStatus, RequestType } from '@prisma/client'; import { IconInfoCircle, IconUsersGroup, @@ -12,10 +12,12 @@ import { IconMoneybag, IconReceipt2, } from '@tabler/icons-react'; +import { useSession } from 'next-auth/react'; import { useEffect, useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { useForm, FormProvider } from 'react-hook-form'; import { z } from 'zod'; import PublicCloudBillingInfo from '@/components/billing/PublicCloudBillingInfo'; +import CancelRequest from '@/components/buttons/CancelButton'; import PreviousButton from '@/components/buttons/Previous'; import AccountCoding from '@/components/form/AccountCoding'; import AccountEnvironmentsPublic from '@/components/form/AccountEnvironmentsPublic'; @@ -46,6 +48,7 @@ const publicCloudProductRequest = createClientPage({ roles: [GlobalRole.User], validations: { pathParams: pathParamSchema }, }); + export default publicCloudProductRequest(({ router }) => { const [, snap] = usePublicProductState(); const [secondTechLead, setSecondTechLead] = useState(false); @@ -78,7 +81,7 @@ export default publicCloudProductRequest(({ router }) => { decision: RequestDecision.APPROVED as RequestDecision, type: snap.currentRequest?.type, ...snap.currentRequest?.decisionData, - accountCoding: snap.currentRequest?.decisionData.billing.accountCoding, + accountCoding: snap.currentRequest?.decisionData.billing?.accountCoding, }, }); @@ -150,7 +153,7 @@ export default publicCloudProductRequest(({ router }) => { description: '', Component: AccountCoding, componentArgs: { - accountCodingInitial: snap.currentRequest.decisionData?.billing.accountCoding, + accountCodingInitial: snap.currentRequest.decisionData?.billing?.accountCoding, disabled: true, }, }, @@ -174,6 +177,9 @@ export default publicCloudProductRequest(({ router }) => { }); } + const { data: session } = useSession(); + const canCancel = session?.user.email === snap.currentRequest.createdByEmail; + return (
@@ -198,6 +204,9 @@ export default publicCloudProductRequest(({ router }) => {
+ {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && canCancel && ( + + )} {snap.currentRequest._permissions.review && ( <> )}
diff --git a/app/components/billing/PublicCloudBillingDownloadButton.tsx b/app/components/billing/PublicCloudBillingDownloadButton.tsx index 4768fb272..2452dd308 100644 --- a/app/components/billing/PublicCloudBillingDownloadButton.tsx +++ b/app/components/billing/PublicCloudBillingDownloadButton.tsx @@ -17,6 +17,10 @@ export default function BillingDownloadButton({ product }: { product: Product }) size="xs" className="mt-2" onClick={async () => { + if (!product.billing?.accountCoding) { + return; + } + setLoading(true); await downloadBilling( product.billing.accountCoding, diff --git a/app/components/billing/PublicCloudBillingInfo.tsx b/app/components/billing/PublicCloudBillingInfo.tsx index 07b55f84b..891e34362 100644 --- a/app/components/billing/PublicCloudBillingInfo.tsx +++ b/app/components/billing/PublicCloudBillingInfo.tsx @@ -27,7 +27,7 @@ export default function PublicCloudBillingInfo({ const { licencePlate, billing } = product; let content = null; - if (billing.approved) { + if (billing?.approved) { content = ( <>
  • Sign-Off complete
  • @@ -39,7 +39,7 @@ export default function PublicCloudBillingInfo({ )} ); - } else if (billing.signed) { + } else if (billing?.signed) { content = ( <>
  • @@ -52,7 +52,7 @@ export default function PublicCloudBillingInfo({ <>
  • Current Step:Pending sign-off from the Ministry Expense Authority ( - {formatFullName(billing.expenseAuthority)}) + {formatFullName(billing?.expenseAuthority)})
  • Next Step:Sign-off by the OCIO Cloud Director @@ -64,7 +64,7 @@ export default function PublicCloudBillingInfo({ return ( } className={cn(className)}>
      {content}
    - {(session?.permissions.downloadBillingMou || product._permissions?.downloadMou) && billing.approved && ( + {(session?.permissions.downloadBillingMou || product._permissions?.downloadMou) && billing?.approved && ( )} {product._permissions?.signMou && ( @@ -93,6 +93,10 @@ export default function PublicCloudBillingInfo({ size="xs" className="mt-2" onClick={async () => { + if (!product.billingId) { + return; + } + const res = await openPublicCloudMouReviewModal<{ confirmed: boolean }>({ licencePlate: product.licencePlate, billingId: product.billingId, diff --git a/app/components/buttons/CancelButton.tsx b/app/components/buttons/CancelButton.tsx new file mode 100644 index 000000000..9d5ae884f --- /dev/null +++ b/app/components/buttons/CancelButton.tsx @@ -0,0 +1,24 @@ +import { Button } from '@mantine/core'; +import { IconCancel } from '@tabler/icons-react'; +import { openPrivateCloudRequestCancelModal } from '../modal/privateCloudRequestCancel'; +import { openPublicCloudRequestCancelModal } from '../modal/publicCloudRequestCancel'; + +export default function CancelRequest({ id, context }: any) { + const handleCancel = async () => { + if (context === 'PRIVATE') { + await openPrivateCloudRequestCancelModal({ + requestId: id, + }); + } else { + await openPublicCloudRequestCancelModal({ + requestId: id, + }); + } + }; + + return ( + + ); +} diff --git a/app/components/form/ActiveRequestBox.tsx b/app/components/form/ActiveRequestBox.tsx index 3f4ed5282..401fa122b 100644 --- a/app/components/form/ActiveRequestBox.tsx +++ b/app/components/form/ActiveRequestBox.tsx @@ -9,6 +9,7 @@ import { IconCircleCheck, IconPoint, IconBan, + IconCancel, } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; @@ -122,6 +123,11 @@ export default function ActiveRequestBox({ decisionText = 'Provisioned'; DecisionIcon = IconCircleCheck; break; + case DecisionStatus.CANCELLED: + decisionColor = 'pink'; + decisionText = 'Cancelled'; + DecisionIcon = IconCancel; + break; } const badges = ( diff --git a/app/components/modal/privateCloudRequestCancel.tsx b/app/components/modal/privateCloudRequestCancel.tsx new file mode 100644 index 000000000..bb341ba68 --- /dev/null +++ b/app/components/modal/privateCloudRequestCancel.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { Button, LoadingOverlay, Box } from '@mantine/core'; +import { useMutation } from '@tanstack/react-query'; +import { createModal } from '@/core/modal'; +import { cancelPrivateCloudRequest } from '@/services/backend/private-cloud/requests'; +import { success, failure } from '../notification'; +import { openNotificationModal } from './notification'; + +interface ModalProps { + requestId: string; +} + +interface ModalState { + success: boolean; +} + +export const openPrivateCloudRequestCancelModal = createModal({ + settings: { + size: 'md', + title: 'Cancel Request?', + }, + Component: function ({ requestId, state, closeModal }) { + const { mutateAsync: cancelRequest, isPending: isCancelingRequest } = useMutation({ + mutationFn: () => cancelPrivateCloudRequest(requestId), + onSuccess: () => { + state.success = true; + success(); + closeModal(); + openNotificationModal({ + callbackUrl: '/private-cloud/requests/all', + content:

    The request for the product has been successfully cancelled.

    , + }); + }, + onError: (error) => { + failure({ error }); + }, + }); + + return ( + + +

    + Are you sure you want to cancel this request? This action cannot be undone! +

    +
    + + +
    +
    + ); + }, + onClose: () => {}, +}); diff --git a/app/components/modal/publicCloudProductEditSubmit.tsx b/app/components/modal/publicCloudProductEditSubmit.tsx index 471c457ac..d5cf7dd4c 100644 --- a/app/components/modal/publicCloudProductEditSubmit.tsx +++ b/app/components/modal/publicCloudProductEditSubmit.tsx @@ -63,7 +63,7 @@ export const openPublicCloudProductEditSubmitModal = createModal { const _changes = comparePublicProductData( - { ...snap.currentProduct, accountCoding: snap.currentProduct?.billing.accountCoding }, + { ...snap.currentProduct, accountCoding: snap.currentProduct?.billing?.accountCoding }, originalProductData, ); setChange(_changes); diff --git a/app/components/modal/publicCloudRequestCancel.tsx b/app/components/modal/publicCloudRequestCancel.tsx new file mode 100644 index 000000000..979ebda91 --- /dev/null +++ b/app/components/modal/publicCloudRequestCancel.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { Button, LoadingOverlay, Box } from '@mantine/core'; +import { useMutation } from '@tanstack/react-query'; +import { createModal } from '@/core/modal'; +import { cancelPublicCloudRequest } from '@/services/backend/public-cloud/requests'; +import { success, failure } from '../notification'; +import { openNotificationModal } from './notification'; + +interface ModalProps { + requestId: string; +} + +interface ModalState { + success: boolean; +} + +export const openPublicCloudRequestCancelModal = createModal({ + settings: { + size: 'md', + title: 'Cancel Request?', + }, + Component: function ({ requestId, state, closeModal }) { + const { mutateAsync: cancelRequest, isPending: isCancelingRequest } = useMutation({ + mutationFn: () => cancelPublicCloudRequest(requestId), + onSuccess: () => { + state.success = true; + success(); + closeModal(); + openNotificationModal({ + callbackUrl: '/public-cloud/requests/all', + content:

    The request for the product has been successfully cancelled.

    , + }); + }, + onError: (error) => { + failure({ error }); + }, + }); + + return ( + + +

    + Are you sure you want to cancel this request? This action cannot be undone! +

    +
    + + +
    +
    + ); + }, + onClose: () => {}, +}); diff --git a/app/constants/event.ts b/app/constants/event.ts index c5bd52ad6..3bf05e0b5 100644 --- a/app/constants/event.ts +++ b/app/constants/event.ts @@ -15,9 +15,11 @@ export const eventTypeNames: Record = { [EventType.REVIEW_PRIVATE_CLOUD_REQUEST]: 'Review Private Cloud Request', [EventType.RESEND_PRIVATE_CLOUD_REQUEST]: 'Resend Private Cloud Request', [EventType.REPROVISION_PRIVATE_CLOUD_PRODUCT]: 'Reprovision Private Cloud Product', + [EventType.CANCEL_PRIVATE_CLOUD_REQUEST]: 'Cancel Private Cloud Request', [EventType.CREATE_PUBLIC_CLOUD_PRODUCT]: 'Create Public Cloud Product', [EventType.UPDATE_PUBLIC_CLOUD_PRODUCT]: 'Update Public Cloud Product', [EventType.DELETE_PUBLIC_CLOUD_PRODUCT]: 'Delete Public Cloud Product', [EventType.EXPORT_PUBLIC_CLOUD_PRODUCT]: 'Export Public Cloud Product', [EventType.REVIEW_PUBLIC_CLOUD_REQUEST]: 'Review Public Cloud Request', + [EventType.CANCEL_PUBLIC_CLOUD_REQUEST]: 'Cancel Public Cloud Request', }; diff --git a/app/core/auth-options.ts b/app/core/auth-options.ts index ece92f19d..d9132265f 100644 --- a/app/core/auth-options.ts +++ b/app/core/auth-options.ts @@ -290,6 +290,9 @@ export async function generateSession({ viewUsers: session.isAdmin || session.isUserReader, viewEvents: session.isAdmin || session.isEventReader, editUsers: session.isAdmin, + + cancelPrivateCloudRequest: session.isUser, + cancelPublicCloudRequest: session.isUser, }; session.permissionList = Object.keys(session.permissions).filter( diff --git a/app/emails/_components/ProviderDetails.tsx b/app/emails/_components/ProviderDetails.tsx index d14c57092..3ddfac378 100644 --- a/app/emails/_components/ProviderDetails.tsx +++ b/app/emails/_components/ProviderDetails.tsx @@ -51,7 +51,7 @@ export default function ProviderDetails({ product }: Props) {
  • Account Coding - {billing.accountCoding} + {billing?.accountCoding}
    ); diff --git a/app/emails/_components/public-cloud/Changes.tsx b/app/emails/_components/public-cloud/Changes.tsx index da28f51ef..cba98cf8a 100644 --- a/app/emails/_components/public-cloud/Changes.tsx +++ b/app/emails/_components/public-cloud/Changes.tsx @@ -67,8 +67,8 @@ export default function Changes({ request }: { request: PublicCloudRequestDetail ); diff --git a/app/emails/_templates/public-cloud/BillingReviewerMou.tsx b/app/emails/_templates/public-cloud/BillingReviewerMou.tsx index 42678399b..abd75a355 100644 --- a/app/emails/_templates/public-cloud/BillingReviewerMou.tsx +++ b/app/emails/_templates/public-cloud/BillingReviewerMou.tsx @@ -14,7 +14,7 @@ export default function BillingReviewerMou({ request }: Props) { const { name, expenseAuthority, billing } = request.decisionData; - const { accountCoding } = billing; + const accountCoding = billing?.accountCoding; return ( diff --git a/app/emails/_templates/public-cloud/EmouServiceAgreement.tsx b/app/emails/_templates/public-cloud/EmouServiceAgreement.tsx index 9c84357be..e734db934 100644 --- a/app/emails/_templates/public-cloud/EmouServiceAgreement.tsx +++ b/app/emails/_templates/public-cloud/EmouServiceAgreement.tsx @@ -13,7 +13,7 @@ export default function EmouServiceAgreement({ request }: Props) { const { name, billing } = request.decisionData; - const { accountCoding } = billing; + const accountCoding = billing?.accountCoding; return ( diff --git a/app/emails/_templates/public-cloud/ExpenseAuthorityMou.tsx b/app/emails/_templates/public-cloud/ExpenseAuthorityMou.tsx index 0b14b37fc..aa9afb018 100644 --- a/app/emails/_templates/public-cloud/ExpenseAuthorityMou.tsx +++ b/app/emails/_templates/public-cloud/ExpenseAuthorityMou.tsx @@ -14,7 +14,7 @@ export default function ExpenseAuthorityMou({ request }: Props) { const { name, expenseAuthority, billing } = request.decisionData; - const { accountCoding } = billing; + const accountCoding = billing?.accountCoding; return ( diff --git a/app/emails/_templates/public-cloud/ExpenseAuthorityMouProduct.tsx b/app/emails/_templates/public-cloud/ExpenseAuthorityMouProduct.tsx index c6a95b0ff..823415c35 100644 --- a/app/emails/_templates/public-cloud/ExpenseAuthorityMouProduct.tsx +++ b/app/emails/_templates/public-cloud/ExpenseAuthorityMouProduct.tsx @@ -14,7 +14,7 @@ export default function ExpenseAuthorityMouProduct({ product }: Props) { const { name, expenseAuthority, billing } = product; - const { accountCoding } = billing; + const accountCoding = billing?.accountCoding; return ( diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 2fef162f4..0ff727699 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -285,8 +285,8 @@ model PublicCloudProject { status ProjectStatus createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - billingId String @db.ObjectId - billing Billing @relation("billing", fields: [billingId], references: [id]) + billingId String? @db.ObjectId + billing Billing? @relation("billing", fields: [billingId], references: [id]) budget Budget projectOwnerId String @db.ObjectId projectOwner User @relation("projectOwner", fields: [projectOwnerId], references: [id]) @@ -317,8 +317,8 @@ model PublicCloudRequestedProject { description String status ProjectStatus createdAt DateTime @default(now()) - billingId String @db.ObjectId - billing Billing @relation("billing", fields: [billingId], references: [id]) + billingId String? @db.ObjectId + billing Billing? @relation("billing", fields: [billingId], references: [id]) budget Budget projectOwnerId String @db.ObjectId projectOwner User @relation("projectOwner", fields: [projectOwnerId], references: [id]) @@ -561,6 +561,7 @@ enum DecisionStatus { REJECTED PARTIALLY_PROVISIONED PROVISIONED + CANCELLED } enum RequestType { @@ -673,12 +674,14 @@ enum EventType { EXPORT_PRIVATE_CLOUD_PRODUCT REVIEW_PRIVATE_CLOUD_REQUEST RESEND_PRIVATE_CLOUD_REQUEST + CANCEL_PRIVATE_CLOUD_REQUEST REPROVISION_PRIVATE_CLOUD_PRODUCT CREATE_PUBLIC_CLOUD_PRODUCT UPDATE_PUBLIC_CLOUD_PRODUCT DELETE_PUBLIC_CLOUD_PRODUCT EXPORT_PUBLIC_CLOUD_PRODUCT REVIEW_PUBLIC_CLOUD_REQUEST + CANCEL_PUBLIC_CLOUD_REQUEST } enum TaskType { diff --git a/app/services/backend/private-cloud/requests.ts b/app/services/backend/private-cloud/requests.ts index c86dac44f..78f529202 100644 --- a/app/services/backend/private-cloud/requests.ts +++ b/app/services/backend/private-cloud/requests.ts @@ -67,3 +67,8 @@ export async function resendPrivateCloudRequest(id: string) { const result = await instance.get(`/${id}/resend`).then((res) => res.data); return result as true; } + +export async function cancelPrivateCloudRequest(id: string) { + const result = await instance.put(`/${id}`).then((res) => res.data); + return result as PrivateCloudRequestDetail; +} diff --git a/app/services/backend/public-cloud/requests.ts b/app/services/backend/public-cloud/requests.ts index 63176f0fa..e3093257e 100644 --- a/app/services/backend/public-cloud/requests.ts +++ b/app/services/backend/public-cloud/requests.ts @@ -62,3 +62,8 @@ export async function makePublicCloudRequestDecision(id: string, data: any) { const result = await instance.post(`/${id}/decision`, data).then((res) => res.data); return result as PublicCloudRequestDetail; } + +export async function cancelPublicCloudRequest(id: string) { + const result = await instance.put(`/${id}`).then((res) => res.data); + return result as PublicCloudRequestDetail; +} diff --git a/app/services/ches/public-cloud/emails.ts b/app/services/ches/public-cloud/emails.ts index 151037300..49ae859aa 100644 --- a/app/services/ches/public-cloud/emails.ts +++ b/app/services/ches/public-cloud/emails.ts @@ -178,7 +178,13 @@ export async function sendBillingReviewerMou(request: PublicCloudRequestDetailDe export async function sendEmouServiceAgreement(request: PublicCloudRequestDetailDecorated) { const content = await getContent(EmouServiceAgreementTemplate({ request })); + + if (!request.decisionData.billing) { + return; + } + const emouPdfBuff = await generateEmouPdf(request.decisionData, request.decisionData.billing); + const billingReviewerEmails = await findUserEmailsByAuthRole(GlobalRole.BillingReviewer); return sendEmail({ diff --git a/app/services/db/models/private-cloud-request.ts b/app/services/db/models/private-cloud-request.ts index 78a0fb0e4..93424ded6 100644 --- a/app/services/db/models/private-cloud-request.ts +++ b/app/services/db/models/private-cloud-request.ts @@ -51,7 +51,10 @@ async function decorate (task.data as { requestId: string }).requestId) .includes(doc.id); - const canEdit = canReview && doc.type !== RequestType.DELETE; + const canEdit = + (canReview && doc.type !== RequestType.DELETE) || + (session.user.email === doc.createdBy?.email && doc.type !== RequestType.DELETE); + const canResend = (doc.decisionStatus === DecisionStatus.APPROVED || doc.decisionStatus === DecisionStatus.AUTO_APPROVED) && session.permissions.reviewAllPrivateCloudRequests; @@ -60,6 +63,8 @@ async function decorate Date: Fri, 10 Jan 2025 16:13:48 -0800 Subject: [PATCH 2/8] chore(4276): revert makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b3059a533..82394fcbf 100755 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ localmac: .PHONY: dev dev: - @DATABASE_URL=$$(grep -m 1 '^DATABASE_URL=' app/.env.apple.local | cut -d '=' -f 2-) \ + @DATABASE_URL=$$(grep -m 1 '^DATABASE_URL=' app/.env.local | cut -d '=' -f 2-) \ npm run prisma-push --prefix app && \ npm run dev --prefix app From 6be8d8672034181e317d38c5293c6708b412264a Mon Sep 17 00:00:00 2001 From: Chinedu Olebu Date: Sun, 12 Jan 2025 18:12:58 -0800 Subject: [PATCH 3/8] chore(4276): optimize and reorganize code --- .../billing/[idOrAccountCoding]/download/route.tsx | 2 +- app/app/api/private-cloud/requests/[id]/route.ts | 12 +++--------- .../public-cloud/products/_operations/update.ts | 2 +- app/app/api/public-cloud/requests/[id]/route.ts | 9 --------- .../requests/(request)/[id]/decision/page.tsx | 7 ++++++- .../requests/(request)/[id]/request/page.tsx | 12 +++++------- .../requests/(request)/[id]/request/page.tsx | 14 ++++++-------- .../billing/PublicCloudBillingDownloadButton.tsx | 4 +--- app/components/buttons/CancelButton.tsx | 3 ++- app/components/modal/privateCloudRequestCancel.tsx | 2 +- app/components/modal/publicCloudRequestCancel.tsx | 2 +- app/services/ches/public-cloud/emails.ts | 1 - app/services/db/models/private-cloud-request.ts | 10 +++++----- app/services/db/models/public-cloud-request.ts | 12 ++++++------ 14 files changed, 38 insertions(+), 54 deletions(-) diff --git a/app/app/api/billing/[idOrAccountCoding]/download/route.tsx b/app/app/api/billing/[idOrAccountCoding]/download/route.tsx index 2e3703bb7..1a49c0ea2 100644 --- a/app/app/api/billing/[idOrAccountCoding]/download/route.tsx +++ b/app/app/api/billing/[idOrAccountCoding]/download/route.tsx @@ -62,7 +62,7 @@ export const GET = apiHandler(async ({ pathParams, queryParams, session }) => { return BadRequestResponse('invalid account coding'); } - product = req.decisionData; + product = req.decisionData as Product; } const canDownloadMou = diff --git a/app/app/api/private-cloud/requests/[id]/route.ts b/app/app/api/private-cloud/requests/[id]/route.ts index 8b2a3efcc..af77d840b 100644 --- a/app/app/api/private-cloud/requests/[id]/route.ts +++ b/app/app/api/private-cloud/requests/[id]/route.ts @@ -35,7 +35,7 @@ export const PUT = apiHandler(async ({ pathParams, session }) => { return UnauthorizedResponse(); } - const updatedRequest = await models.privateCloudRequest.update( + await models.privateCloudRequest.update( { where: { id, @@ -45,17 +45,11 @@ export const PUT = apiHandler(async ({ pathParams, session }) => { data: { decisionStatus: DecisionStatus.CANCELLED, }, - select: { - decisionStatus: true, - createdByEmail: true, - }, }, session, ); - if (updatedRequest) { - await createEvent(EventType.CANCEL_PRIVATE_CLOUD_REQUEST, session.user.id, { requestId: id }); - } + await createEvent(EventType.CANCEL_PRIVATE_CLOUD_REQUEST, session.user.id, { requestId: id }); - return OkResponse(updatedRequest); + return OkResponse(true); }); diff --git a/app/app/api/public-cloud/products/_operations/update.ts b/app/app/api/public-cloud/products/_operations/update.ts index 28abcb524..0cbfde924 100644 --- a/app/app/api/public-cloud/products/_operations/update.ts +++ b/app/app/api/public-cloud/products/_operations/update.ts @@ -62,7 +62,7 @@ export default async function updateOp({ code: billingInfo?.code, }, } - : undefined, // Handle the case where `billingId` is null + : undefined, projectOwner: { connect: { email: body.projectOwner.email } }, primaryTechnicalLead: { connect: { email: body.primaryTechnicalLead.email } }, secondaryTechnicalLead: body.secondaryTechnicalLead diff --git a/app/app/api/public-cloud/requests/[id]/route.ts b/app/app/api/public-cloud/requests/[id]/route.ts index 12baa8d86..036821acf 100644 --- a/app/app/api/public-cloud/requests/[id]/route.ts +++ b/app/app/api/public-cloud/requests/[id]/route.ts @@ -46,17 +46,9 @@ export const PUT = apiHandler(async ({ pathParams, session }) => { decisionStatus: DecisionStatus.CANCELLED, }, select: { - decisionStatus: true, - createdByEmail: true, licencePlate: true, - decisionData: true, decisionDataId: true, requestDataId: true, - createdBy: { - select: { - id: true, - }, - }, }, }, session, @@ -78,7 +70,6 @@ export const PUT = apiHandler(async ({ pathParams, session }) => { expenseAuthorityId: billingData?.expenseAuthorityId, }, data: { - expenseAuthorityId: null, billingId: null, }, }); diff --git a/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx b/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx index ba546ab95..35efbd39b 100644 --- a/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx +++ b/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@mantine/core'; -import { PrivateCloudProject, RequestType } from '@prisma/client'; +import { DecisionStatus, PrivateCloudProject, ProjectContext, RequestType } from '@prisma/client'; import { IconInfoCircle, IconUsersGroup, @@ -14,6 +14,7 @@ import { import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { z } from 'zod'; +import CancelRequest from '@/components/buttons/CancelButton'; import PreviousButton from '@/components/buttons/Previous'; import ProjectDescription from '@/components/form/ProjectDescriptionPrivate'; import TeamContacts from '@/components/form/TeamContacts'; @@ -213,6 +214,10 @@ export default privateCloudRequestDecision(({ getPathParams, session, router }) )} + {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && + snap.currentRequest._permissions.cancel && ( + + )} diff --git a/app/app/private-cloud/requests/(request)/[id]/request/page.tsx b/app/app/private-cloud/requests/(request)/[id]/request/page.tsx index bdf7376f4..e302a7382 100644 --- a/app/app/private-cloud/requests/(request)/[id]/request/page.tsx +++ b/app/app/private-cloud/requests/(request)/[id]/request/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { DecisionStatus, PrivateCloudProject } from '@prisma/client'; +import { DecisionStatus, PrivateCloudProject, ProjectContext } from '@prisma/client'; import { IconInfoCircle, IconUsersGroup, @@ -135,9 +135,6 @@ export default privateCloudRequestRequest(({}) => { }); } - const { data: session } = useSession(); - const canCancel = session?.user.email === snap.currentRequest.createdByEmail; - return (
    @@ -146,9 +143,10 @@ export default privateCloudRequestRequest(({}) => {
    - {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && canCancel && ( - - )} + {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && + snap.currentRequest._permissions.cancel && ( + + )}
    diff --git a/app/app/public-cloud/requests/(request)/[id]/request/page.tsx b/app/app/public-cloud/requests/(request)/[id]/request/page.tsx index 95dc5ac96..b76728a8c 100644 --- a/app/app/public-cloud/requests/(request)/[id]/request/page.tsx +++ b/app/app/public-cloud/requests/(request)/[id]/request/page.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@mantine/core'; -import { DecisionStatus, RequestType } from '@prisma/client'; +import { DecisionStatus, ProjectContext, RequestType } from '@prisma/client'; import { IconInfoCircle, IconUsersGroup, @@ -177,9 +177,6 @@ export default publicCloudProductRequest(({ router }) => { }); } - const { data: session } = useSession(); - const canCancel = session?.user.email === snap.currentRequest.createdByEmail; - return (
    @@ -204,9 +201,10 @@ export default publicCloudProductRequest(({ router }) => {
    - {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && canCancel && ( - - )} + {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && + snap.currentRequest._permissions.cancel && ( + + )} {snap.currentRequest._permissions.review && ( <> )}
    diff --git a/app/components/billing/PublicCloudBillingDownloadButton.tsx b/app/components/billing/PublicCloudBillingDownloadButton.tsx index 2452dd308..686edcb26 100644 --- a/app/components/billing/PublicCloudBillingDownloadButton.tsx +++ b/app/components/billing/PublicCloudBillingDownloadButton.tsx @@ -17,9 +17,7 @@ export default function BillingDownloadButton({ product }: { product: Product }) size="xs" className="mt-2" onClick={async () => { - if (!product.billing?.accountCoding) { - return; - } + if (!product.billing) return; setLoading(true); await downloadBilling( diff --git a/app/components/buttons/CancelButton.tsx b/app/components/buttons/CancelButton.tsx index 9d5ae884f..d5481040b 100644 --- a/app/components/buttons/CancelButton.tsx +++ b/app/components/buttons/CancelButton.tsx @@ -1,11 +1,12 @@ import { Button } from '@mantine/core'; +import { ProjectContext } from '@prisma/client'; import { IconCancel } from '@tabler/icons-react'; import { openPrivateCloudRequestCancelModal } from '../modal/privateCloudRequestCancel'; import { openPublicCloudRequestCancelModal } from '../modal/publicCloudRequestCancel'; export default function CancelRequest({ id, context }: any) { const handleCancel = async () => { - if (context === 'PRIVATE') { + if (context === ProjectContext.PRIVATE) { await openPrivateCloudRequestCancelModal({ requestId: id, }); diff --git a/app/components/modal/privateCloudRequestCancel.tsx b/app/components/modal/privateCloudRequestCancel.tsx index bb341ba68..b49685b51 100644 --- a/app/components/modal/privateCloudRequestCancel.tsx +++ b/app/components/modal/privateCloudRequestCancel.tsx @@ -48,7 +48,7 @@ export const openPrivateCloudRequestCancelModal = createModal
    diff --git a/app/components/modal/publicCloudRequestCancel.tsx b/app/components/modal/publicCloudRequestCancel.tsx index 979ebda91..1bc6ba84f 100644 --- a/app/components/modal/publicCloudRequestCancel.tsx +++ b/app/components/modal/publicCloudRequestCancel.tsx @@ -48,7 +48,7 @@ export const openPublicCloudRequestCancelModal = createModal
    diff --git a/app/services/ches/public-cloud/emails.ts b/app/services/ches/public-cloud/emails.ts index 49ae859aa..437a86163 100644 --- a/app/services/ches/public-cloud/emails.ts +++ b/app/services/ches/public-cloud/emails.ts @@ -184,7 +184,6 @@ export async function sendEmouServiceAgreement(request: PublicCloudRequestDetail } const emouPdfBuff = await generateEmouPdf(request.decisionData, request.decisionData.billing); - const billingReviewerEmails = await findUserEmailsByAuthRole(GlobalRole.BillingReviewer); return sendEmail({ diff --git a/app/services/db/models/private-cloud-request.ts b/app/services/db/models/private-cloud-request.ts index 93424ded6..2f0ef6004 100644 --- a/app/services/db/models/private-cloud-request.ts +++ b/app/services/db/models/private-cloud-request.ts @@ -51,9 +51,11 @@ async function decorate (task.data as { requestId: string }).requestId) .includes(doc.id); - const canEdit = - (canReview && doc.type !== RequestType.DELETE) || - (session.user.email === doc.createdBy?.email && doc.type !== RequestType.DELETE); + const canCancel = + session.user.email === doc.createdBy?.email && + (doc.type === RequestType.DELETE || doc.type === RequestType.EDIT || doc.type === RequestType.CREATE); + + const canEdit = (canReview && doc.type !== RequestType.DELETE) || canCancel; const canResend = (doc.decisionStatus === DecisionStatus.APPROVED || doc.decisionStatus === DecisionStatus.AUTO_APPROVED) && @@ -63,8 +65,6 @@ async function decorate Date: Mon, 13 Jan 2025 00:48:55 -0800 Subject: [PATCH 4/8] chore(4276): use one cancel request modal --- app/components/buttons/CancelButton.tsx | 18 ++---- ...oudRequestCancel.tsx => CancelRequest.tsx} | 13 +++-- .../modal/publicCloudRequestCancel.tsx | 58 ------------------- 3 files changed, 15 insertions(+), 74 deletions(-) rename app/components/modal/{privateCloudRequestCancel.tsx => CancelRequest.tsx} (73%) delete mode 100644 app/components/modal/publicCloudRequestCancel.tsx diff --git a/app/components/buttons/CancelButton.tsx b/app/components/buttons/CancelButton.tsx index d5481040b..9b9a6a352 100644 --- a/app/components/buttons/CancelButton.tsx +++ b/app/components/buttons/CancelButton.tsx @@ -1,20 +1,14 @@ import { Button } from '@mantine/core'; import { ProjectContext } from '@prisma/client'; import { IconCancel } from '@tabler/icons-react'; -import { openPrivateCloudRequestCancelModal } from '../modal/privateCloudRequestCancel'; -import { openPublicCloudRequestCancelModal } from '../modal/publicCloudRequestCancel'; +import { openRequestCancelModal } from '../modal/CancelRequest'; -export default function CancelRequest({ id, context }: any) { +export default function CancelRequest({ id, context }: { id: string; context: ProjectContext }) { const handleCancel = async () => { - if (context === ProjectContext.PRIVATE) { - await openPrivateCloudRequestCancelModal({ - requestId: id, - }); - } else { - await openPublicCloudRequestCancelModal({ - requestId: id, - }); - } + await openRequestCancelModal({ + requestId: id, + context, + }); }; return ( diff --git a/app/components/modal/privateCloudRequestCancel.tsx b/app/components/modal/CancelRequest.tsx similarity index 73% rename from app/components/modal/privateCloudRequestCancel.tsx rename to app/components/modal/CancelRequest.tsx index b49685b51..7c9cf7017 100644 --- a/app/components/modal/privateCloudRequestCancel.tsx +++ b/app/components/modal/CancelRequest.tsx @@ -1,34 +1,39 @@ 'use client'; import { Button, LoadingOverlay, Box } from '@mantine/core'; +import { ProjectContext } from '@prisma/client'; import { useMutation } from '@tanstack/react-query'; +import { lowerCase } from 'lodash-es'; import { createModal } from '@/core/modal'; import { cancelPrivateCloudRequest } from '@/services/backend/private-cloud/requests'; +import { cancelPublicCloudRequest } from '@/services/backend/public-cloud/requests'; import { success, failure } from '../notification'; import { openNotificationModal } from './notification'; interface ModalProps { requestId: string; + context: 'PRIVATE' | 'PUBLIC'; } interface ModalState { success: boolean; } -export const openPrivateCloudRequestCancelModal = createModal({ +export const openRequestCancelModal = createModal({ settings: { size: 'md', title: 'Cancel Request?', }, - Component: function ({ requestId, state, closeModal }) { + Component: function ({ requestId, context, state, closeModal }) { const { mutateAsync: cancelRequest, isPending: isCancelingRequest } = useMutation({ - mutationFn: () => cancelPrivateCloudRequest(requestId), + mutationFn: async () => + context === ProjectContext.PRIVATE ? cancelPrivateCloudRequest(requestId) : cancelPublicCloudRequest(requestId), onSuccess: () => { state.success = true; success(); closeModal(); openNotificationModal({ - callbackUrl: '/private-cloud/requests/all', + callbackUrl: `/${lowerCase(context)}-cloud/requests/all`, content:

    The request for the product has been successfully cancelled.

    , }); }, diff --git a/app/components/modal/publicCloudRequestCancel.tsx b/app/components/modal/publicCloudRequestCancel.tsx deleted file mode 100644 index 1bc6ba84f..000000000 --- a/app/components/modal/publicCloudRequestCancel.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; - -import { Button, LoadingOverlay, Box } from '@mantine/core'; -import { useMutation } from '@tanstack/react-query'; -import { createModal } from '@/core/modal'; -import { cancelPublicCloudRequest } from '@/services/backend/public-cloud/requests'; -import { success, failure } from '../notification'; -import { openNotificationModal } from './notification'; - -interface ModalProps { - requestId: string; -} - -interface ModalState { - success: boolean; -} - -export const openPublicCloudRequestCancelModal = createModal({ - settings: { - size: 'md', - title: 'Cancel Request?', - }, - Component: function ({ requestId, state, closeModal }) { - const { mutateAsync: cancelRequest, isPending: isCancelingRequest } = useMutation({ - mutationFn: () => cancelPublicCloudRequest(requestId), - onSuccess: () => { - state.success = true; - success(); - closeModal(); - openNotificationModal({ - callbackUrl: '/public-cloud/requests/all', - content:

    The request for the product has been successfully cancelled.

    , - }); - }, - onError: (error) => { - failure({ error }); - }, - }); - - return ( - - -

    - Are you sure you want to cancel this request? This action cannot be undone! -

    -
    - - -
    -
    - ); - }, - onClose: () => {}, -}); From a7a1dd435d9c3b3b88c09056e1009b782569581a Mon Sep 17 00:00:00 2001 From: Chinedu Olebu Date: Mon, 13 Jan 2025 01:24:41 -0800 Subject: [PATCH 5/8] chore(4276): reword the modal message --- app/components/modal/CancelRequest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/modal/CancelRequest.tsx b/app/components/modal/CancelRequest.tsx index 7c9cf7017..174551f95 100644 --- a/app/components/modal/CancelRequest.tsx +++ b/app/components/modal/CancelRequest.tsx @@ -12,7 +12,7 @@ import { openNotificationModal } from './notification'; interface ModalProps { requestId: string; - context: 'PRIVATE' | 'PUBLIC'; + context: ProjectContext; } interface ModalState { @@ -34,7 +34,7 @@ export const openRequestCancelModal = createModal({ closeModal(); openNotificationModal({ callbackUrl: `/${lowerCase(context)}-cloud/requests/all`, - content:

    The request for the product has been successfully cancelled.

    , + content:

    This request has been successfully cancelled!

    , }); }, onError: (error) => { From 20b41da34cbfa5deb3d315d0cfb615aa37913cbc Mon Sep 17 00:00:00 2001 From: Chinedu Olebu Date: Mon, 13 Jan 2025 13:34:53 -0800 Subject: [PATCH 6/8] chore(4276): adjust type casting and optional property --- app/app/api/billing/[idOrAccountCoding]/download/route.tsx | 6 +++--- .../public-cloud/requests/(request)/[id]/request/page.tsx | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/app/api/billing/[idOrAccountCoding]/download/route.tsx b/app/app/api/billing/[idOrAccountCoding]/download/route.tsx index 1a49c0ea2..e410ab8f2 100644 --- a/app/app/api/billing/[idOrAccountCoding]/download/route.tsx +++ b/app/app/api/billing/[idOrAccountCoding]/download/route.tsx @@ -62,18 +62,18 @@ export const GET = apiHandler(async ({ pathParams, queryParams, session }) => { return BadRequestResponse('invalid account coding'); } - product = req.decisionData as Product; + product = req.decisionData; } const canDownloadMou = session.permissions.downloadBillingMou || - product?.members.some( + product.members.some( (member) => member.userId === session.user.id && arraysIntersect(member.roles, [PublicCloudProductMemberRole.BILLING_VIEWER]), ); - if (!canDownloadMou || !product) { + if (!canDownloadMou) { return UnauthorizedResponse(); } diff --git a/app/app/public-cloud/requests/(request)/[id]/request/page.tsx b/app/app/public-cloud/requests/(request)/[id]/request/page.tsx index b76728a8c..00a3b2b7f 100644 --- a/app/app/public-cloud/requests/(request)/[id]/request/page.tsx +++ b/app/app/public-cloud/requests/(request)/[id]/request/page.tsx @@ -12,7 +12,6 @@ import { IconMoneybag, IconReceipt2, } from '@tabler/icons-react'; -import { useSession } from 'next-auth/react'; import { useEffect, useState } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; import { z } from 'zod'; From 4ef83a106a3c16d0c91d00e4018a8c340ae29ae7 Mon Sep 17 00:00:00 2001 From: Chinedu Olebu Date: Tue, 14 Jan 2025 13:49:45 -0800 Subject: [PATCH 7/8] chore(4276): edit active states for requests --- app/app/api/private-cloud/requests/[id]/route.ts | 1 + app/app/api/public-cloud/requests/[id]/route.ts | 1 + .../requests/(request)/[id]/decision/page.tsx | 6 +----- .../requests/(request)/[id]/summary/page.tsx | 8 +++++++- .../requests/(request)/[id]/summary/page.tsx | 9 ++++++++- app/services/db/models/private-cloud-request.ts | 1 + app/services/db/models/public-cloud-request.ts | 3 ++- 7 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/app/api/private-cloud/requests/[id]/route.ts b/app/app/api/private-cloud/requests/[id]/route.ts index af77d840b..7d406d963 100644 --- a/app/app/api/private-cloud/requests/[id]/route.ts +++ b/app/app/api/private-cloud/requests/[id]/route.ts @@ -44,6 +44,7 @@ export const PUT = apiHandler(async ({ pathParams, session }) => { }, data: { decisionStatus: DecisionStatus.CANCELLED, + active: false, }, }, session, diff --git a/app/app/api/public-cloud/requests/[id]/route.ts b/app/app/api/public-cloud/requests/[id]/route.ts index 036821acf..21c9df92a 100644 --- a/app/app/api/public-cloud/requests/[id]/route.ts +++ b/app/app/api/public-cloud/requests/[id]/route.ts @@ -44,6 +44,7 @@ export const PUT = apiHandler(async ({ pathParams, session }) => { }, data: { decisionStatus: DecisionStatus.CANCELLED, + active: false, }, select: { licencePlate: true, diff --git a/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx b/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx index 35efbd39b..df5255951 100644 --- a/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx +++ b/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@mantine/core'; -import { DecisionStatus, PrivateCloudProject, ProjectContext, RequestType } from '@prisma/client'; +import { RequestType } from '@prisma/client'; import { IconInfoCircle, IconUsersGroup, @@ -214,10 +214,6 @@ export default privateCloudRequestDecision(({ getPathParams, session, router }) )} - {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && - snap.currentRequest._permissions.cancel && ( - - )} diff --git a/app/app/private-cloud/requests/(request)/[id]/summary/page.tsx b/app/app/private-cloud/requests/(request)/[id]/summary/page.tsx index bcc1cc47e..b8ff1781a 100644 --- a/app/app/private-cloud/requests/(request)/[id]/summary/page.tsx +++ b/app/app/private-cloud/requests/(request)/[id]/summary/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { Alert, Group, Avatar, Text, Accordion, Table, Badge, Button } from '@mantine/core'; -import { DecisionStatus, RequestType } from '@prisma/client'; +import { DecisionStatus, ProjectContext, RequestType } from '@prisma/client'; import { IconInfoCircle, IconCircleLetterO, @@ -10,6 +10,7 @@ import { IconAddressBook, } from '@tabler/icons-react'; import { z } from 'zod'; +import CancelRequest from '@/components/buttons/CancelButton'; import PageAccordion, { PageAccordionItem } from '@/components/generic/accordion/PageAccordion'; import ProductComparison from '@/components/ProductComparison'; import { GlobalRole } from '@/constants'; @@ -120,6 +121,11 @@ export default Layout(({}) => {
    +
    + {snap.currentRequest?.decisionStatus === DecisionStatus.PENDING && snap.currentRequest._permissions.cancel && ( + + )} +
    ); }); diff --git a/app/app/public-cloud/requests/(request)/[id]/summary/page.tsx b/app/app/public-cloud/requests/(request)/[id]/summary/page.tsx index fcb2ca6fe..a879683d6 100644 --- a/app/app/public-cloud/requests/(request)/[id]/summary/page.tsx +++ b/app/app/public-cloud/requests/(request)/[id]/summary/page.tsx @@ -1,9 +1,10 @@ 'use client'; import { Alert } from '@mantine/core'; -import { DecisionStatus, RequestType } from '@prisma/client'; +import { DecisionStatus, ProjectContext, RequestType } from '@prisma/client'; import { IconInfoCircle } from '@tabler/icons-react'; import { z } from 'zod'; +import CancelRequest from '@/components/buttons/CancelButton'; import ProductComparison from '@/components/ProductComparison'; import { GlobalRole } from '@/constants'; import createClientPage from '@/core/client-page'; @@ -36,6 +37,12 @@ export default Layout(({}) => { )} + +
    + {snap.currentRequest?.decisionStatus === DecisionStatus.PENDING && snap.currentRequest._permissions.cancel && ( + + )} +
    ); }); diff --git a/app/services/db/models/private-cloud-request.ts b/app/services/db/models/private-cloud-request.ts index 2f0ef6004..e44261337 100644 --- a/app/services/db/models/private-cloud-request.ts +++ b/app/services/db/models/private-cloud-request.ts @@ -52,6 +52,7 @@ async function decorate Date: Tue, 14 Jan 2025 16:29:27 -0800 Subject: [PATCH 8/8] chore(4276): add cancel request button to decision page --- .../private-cloud/requests/(request)/[id]/decision/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx b/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx index df5255951..abecb501b 100644 --- a/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx +++ b/app/app/private-cloud/requests/(request)/[id]/decision/page.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@mantine/core'; -import { RequestType } from '@prisma/client'; +import { DecisionStatus, ProjectContext, RequestType } from '@prisma/client'; import { IconInfoCircle, IconUsersGroup, @@ -191,6 +191,10 @@ export default privateCloudRequestDecision(({ getPathParams, session, router })
    + {snap.currentRequest.decisionStatus === DecisionStatus.PENDING && + snap.currentRequest._permissions.cancel && ( + + )} {snap.currentRequest._permissions.review && ( <>