diff --git a/apps/admin/src/generated/graphql.ts b/apps/admin/src/generated/graphql.ts index d3bcf90..006ac19 100644 --- a/apps/admin/src/generated/graphql.ts +++ b/apps/admin/src/generated/graphql.ts @@ -127,7 +127,9 @@ export type Mutation = { deleteCategory: Scalars["Boolean"]; deleteDiscount?: Maybe; deleteFromCart: Scalars["Boolean"]; + emailInvoice: Scalars["Boolean"]; forgotPassword: Scalars["Boolean"]; + generateInvoice?: Maybe; login: UserResponse; logout: Scalars["Boolean"]; register: UserResponse; @@ -204,10 +206,19 @@ export type MutationDeleteFromCartArgs = { quantity: Scalars["Int"]; }; +export type MutationEmailInvoiceArgs = { + email: Scalars["String"]; + orderId: Scalars["String"]; +}; + export type MutationForgotPasswordArgs = { email: Scalars["String"]; }; +export type MutationGenerateInvoiceArgs = { + orderId: Scalars["String"]; +}; + export type MutationLoginArgs = { email: Scalars["String"]; password: Scalars["String"]; @@ -440,11 +451,9 @@ export type Query = { allReviews?: Maybe>; categories: Array; categoriesSummary?: Maybe>; - emailInvoice: Scalars["Boolean"]; favourites: Array; favouritesWithProduct: Array; fetchCartItems?: Maybe>; - generate?: Maybe; hello: Scalars["String"]; me?: Maybe; orderById?: Maybe; @@ -466,14 +475,6 @@ export type QueryAllReviewsArgs = { productId: Scalars["Int"]; }; -export type QueryEmailInvoiceArgs = { - orderId: Scalars["String"]; -}; - -export type QueryGenerateArgs = { - orderId: Scalars["String"]; -}; - export type QueryOrderByIdArgs = { orderId: Scalars["String"]; }; diff --git a/apps/api/src/resolvers/invoice.ts b/apps/api/src/resolvers/invoice.ts index 327cdc1..a7bf7b2 100644 --- a/apps/api/src/resolvers/invoice.ts +++ b/apps/api/src/resolvers/invoice.ts @@ -1,62 +1,41 @@ import { COMPANY } from "../constants"; import { OrderDetail } from "../entities/OrderDetail"; -import { Arg, Ctx, Query, Resolver, UseMiddleware } from "type-graphql"; +import { Arg, Mutation, Resolver, UseMiddleware } from "type-graphql"; import easyinvoice from "easyinvoice"; -import fs from "fs"; import { sendEmailWithAttachment } from "../utils/sendEmail"; -import { type MyContext } from "../types"; -import { User } from "../entities/User"; import { invoiceTemplate } from "../static/invoiceTemplate"; import { isAuth } from "../middlewares/isAuth"; @Resolver() export class InvoiceResolver { - @Query(() => String, { nullable: true }) + @Mutation(() => String, { nullable: true }) @UseMiddleware(isAuth) - async generate( + async generateInvoice( @Arg("orderId", () => String) orderId: string ): Promise { return (await easyinvoice.createInvoice(await getSampleData(orderId))).pdf; } - @Query(() => Boolean) + @Mutation(() => Boolean) @UseMiddleware(isAuth) async emailInvoice( @Arg("orderId", () => String) orderId: string, - @Ctx() { req }: MyContext + @Arg("email", () => String) email: string ): Promise { - const user = await User.findOne({ where: { id: req.session?.userId } }); - - if (!user) { - return false; - } - await easyinvoice.createInvoice( await getSampleData(orderId), async function (result) { - // TODO: Email doesn't attach the file yet. - // fs.writeFile(`${orderId}.pdf`, result.pdf, "base64", function (err) { - // if (err) { - // console.error(err); - // return; - // } - // }); await sendEmailWithAttachment( - user?.email, + email, `Invoice for ${orderId}`, invoiceTemplate(orderId), [ { - path: `${orderId}.pdf`, + filename: `INVOICE-${orderId}.pdf`, + path: `data:application/pdf;base64,${result.pdf}`, }, ] ); - // fs.unlink(`${orderId}.pdf`, (err) => { - // if (err) { - // console.error(err); - // return; - // } - // }); } ); return true; diff --git a/apps/api/src/utils/sendEmail.ts b/apps/api/src/utils/sendEmail.ts index 7f3688a..b1df932 100644 --- a/apps/api/src/utils/sendEmail.ts +++ b/apps/api/src/utils/sendEmail.ts @@ -3,20 +3,22 @@ import { COMPANY, __prod__ } from "../constants"; import { Attachment } from "nodemailer/lib/mailer"; import SMTPTransport from "nodemailer/lib/smtp-transport"; +const transportOptions: SMTPTransport.Options = { + host: process.env.RESEND_HOST, + port: parseInt(process.env.RESEND_PORT!), + secure: __prod__, + auth: { + user: process.env.RESEND_AUTH_USER, + pass: process.env.RESEND_AUTH_PASS, + }, +}; + export async function sendEmail( to: string, subject: string, html: string ): Promise { - const transporter = nodemailer.createTransport({ - host: "smtp.resend.com", - port: 465, - secure: true, - auth: { - user: "resend", - pass: process.env.RESEND_API_KEY, - }, - }); + const transporter = nodemailer.createTransport(transportOptions); const info = await transporter.sendMail({ from: `"${COMPANY.name} 👻" `, @@ -37,15 +39,7 @@ export async function sendEmailWithAttachment( html: string, attachments: Attachment[] ): Promise { - const transporter = nodemailer.createTransport({ - host: "smtp.resend.com", - port: 465, - secure: __prod__, - auth: { - user: "resend", - pass: process.env.RESEND_API_KEY, - }, - }); + const transporter = nodemailer.createTransport(transportOptions); const info = await transporter.sendMail({ from: `"${COMPANY.name} 👻" `, diff --git a/apps/storefront/src/components/pages/account/order/EmailInvoice.tsx b/apps/storefront/src/components/pages/account/order/EmailInvoice.tsx new file mode 100644 index 0000000..884f062 --- /dev/null +++ b/apps/storefront/src/components/pages/account/order/EmailInvoice.tsx @@ -0,0 +1,92 @@ +import { Button, Card, HStack, Stack, useToast } from "@chakra-ui/react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as Yup from "yup"; +import { BiMailSend } from "react-icons/bi"; +import { useEmailInvoiceMutation } from "@/generated/graphql"; +import InputField from "@/components/ui/InputField"; + +interface FormValues { + email: string; +} + +const EmailInvoiceFormSchema = Yup.object({ + email: Yup.string().email().required("Required"), +}); + +const EmailInvoice = ({ orderId }: { orderId: string }) => { + const toast = useToast(); + + const { + register, + handleSubmit, + formState: { errors, touchedFields, isSubmitting }, + } = useForm({ + defaultValues: { + email: "", + }, + resolver: yupResolver(EmailInvoiceFormSchema), + }); + + const [emailInvoiceMutation, { error }] = useEmailInvoiceMutation(); + + return ( + +
{ + const res = await emailInvoiceMutation({ + variables: { + email: values.email, + orderId, + }, + }); + + if (!res.data?.emailInvoice) { + toast({ + title: "An Error Occured", + description: error?.message ?? "Please try again later.", + status: "error", + duration: 4000, + isClosable: true, + }); + } else { + toast({ + title: "Email sent", + description: "We've sent you an email with an invoice.", + status: "success", + duration: 4000, + isClosable: true, + }); + } + })} + > + + + + + + +
+
+ ); +}; + +export default EmailInvoice; diff --git a/apps/storefront/src/components/pages/account/order/OrderCard.tsx b/apps/storefront/src/components/pages/account/order/OrderCard.tsx index 4b280d4..22bc16b 100644 --- a/apps/storefront/src/components/pages/account/order/OrderCard.tsx +++ b/apps/storefront/src/components/pages/account/order/OrderCard.tsx @@ -25,7 +25,7 @@ import { Link } from "@chakra-ui/next-js"; import { OrderDetail, OrderItem, - useGenerateInvoiceLazyQuery, + useGenerateInvoiceMutation, } from "@/generated/graphql"; import { OrderInfo, @@ -35,8 +35,11 @@ import { import { PriceTag } from "@/components/shared/product/PriceTag"; import { capitalize } from "@/utils/helpers"; import ConfirmationModal from "@/components/helpers/ConfirmationModal"; +import ModalButton from "@/components/ui/ModalButton"; +import DividerWithText from "@/components/ui/DividerWithText"; import { KHALTI_LOGO } from "../../cart/checkout/PaymentSelector"; import { CreateReviewButton } from "../../product/review/ProductReview"; +import EmailInvoice from "./EmailInvoice"; interface OrderCardProps { orderItem: OrderDetail; @@ -48,8 +51,8 @@ const OrderCard = ({ orderItem }: OrderCardProps) => { (payment) => payment.status === "COMPLETED" ); - const [generateInvoice, { loading: invoiceLoading }] = - useGenerateInvoiceLazyQuery(); + const [generateInvoiceMutation, { loading: invoiceLoading }] = + useGenerateInvoiceMutation(); const subTotal = useMemo( () => @@ -86,24 +89,34 @@ const OrderCard = ({ orderItem }: OrderCardProps) => { - + + + OR + + + diff --git a/apps/storefront/src/generated/graphql.ts b/apps/storefront/src/generated/graphql.ts index 6cdb254..cf8d742 100644 --- a/apps/storefront/src/generated/graphql.ts +++ b/apps/storefront/src/generated/graphql.ts @@ -127,7 +127,9 @@ export type Mutation = { deleteCategory: Scalars["Boolean"]; deleteDiscount?: Maybe; deleteFromCart: Scalars["Boolean"]; + emailInvoice: Scalars["Boolean"]; forgotPassword: Scalars["Boolean"]; + generateInvoice?: Maybe; login: UserResponse; logout: Scalars["Boolean"]; register: UserResponse; @@ -204,10 +206,19 @@ export type MutationDeleteFromCartArgs = { quantity: Scalars["Int"]; }; +export type MutationEmailInvoiceArgs = { + email: Scalars["String"]; + orderId: Scalars["String"]; +}; + export type MutationForgotPasswordArgs = { email: Scalars["String"]; }; +export type MutationGenerateInvoiceArgs = { + orderId: Scalars["String"]; +}; + export type MutationLoginArgs = { email: Scalars["String"]; password: Scalars["String"]; @@ -440,11 +451,9 @@ export type Query = { allReviews?: Maybe>; categories: Array; categoriesSummary?: Maybe>; - emailInvoice: Scalars["Boolean"]; favourites: Array; favouritesWithProduct: Array; fetchCartItems?: Maybe>; - generate?: Maybe; hello: Scalars["String"]; me?: Maybe; orderById?: Maybe; @@ -466,14 +475,6 @@ export type QueryAllReviewsArgs = { productId: Scalars["Int"]; }; -export type QueryEmailInvoiceArgs = { - orderId: Scalars["String"]; -}; - -export type QueryGenerateArgs = { - orderId: Scalars["String"]; -}; - export type QueryOrderByIdArgs = { orderId: Scalars["String"]; }; @@ -1237,6 +1238,25 @@ export type RemoveFromFavouriteMutation = { removeFromFavourite: boolean; }; +export type EmailInvoiceMutationVariables = Exact<{ + email: Scalars["String"]; + orderId: Scalars["String"]; +}>; + +export type EmailInvoiceMutation = { + __typename?: "Mutation"; + emailInvoice: boolean; +}; + +export type GenerateInvoiceMutationVariables = Exact<{ + orderId: Scalars["String"]; +}>; + +export type GenerateInvoiceMutation = { + __typename?: "Mutation"; + generateInvoice?: string | null; +}; + export type CreateOrderMutationVariables = Exact<{ options: CreateOrderInput; }>; @@ -1835,15 +1855,6 @@ export type FavouritesWithProductQuery = { }>; }; -export type GenerateInvoiceQueryVariables = Exact<{ - orderId: Scalars["String"]; -}>; - -export type GenerateInvoiceQuery = { - __typename?: "Query"; - generate?: string | null; -}; - export type OrderByIdQueryVariables = Exact<{ orderId: Scalars["String"]; }>; @@ -3211,6 +3222,103 @@ export type RemoveFromFavouriteMutationOptions = Apollo.BaseMutationOptions< RemoveFromFavouriteMutation, RemoveFromFavouriteMutationVariables >; +export const EmailInvoiceDocument = gql` + mutation EmailInvoice($email: String!, $orderId: String!) { + emailInvoice(email: $email, orderId: $orderId) + } +`; +export type EmailInvoiceMutationFn = Apollo.MutationFunction< + EmailInvoiceMutation, + EmailInvoiceMutationVariables +>; + +/** + * __useEmailInvoiceMutation__ + * + * To run a mutation, you first call `useEmailInvoiceMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useEmailInvoiceMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [emailInvoiceMutation, { data, loading, error }] = useEmailInvoiceMutation({ + * variables: { + * email: // value for 'email' + * orderId: // value for 'orderId' + * }, + * }); + */ +export function useEmailInvoiceMutation( + baseOptions?: Apollo.MutationHookOptions< + EmailInvoiceMutation, + EmailInvoiceMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + EmailInvoiceMutation, + EmailInvoiceMutationVariables + >(EmailInvoiceDocument, options); +} +export type EmailInvoiceMutationHookResult = ReturnType< + typeof useEmailInvoiceMutation +>; +export type EmailInvoiceMutationResult = + Apollo.MutationResult; +export type EmailInvoiceMutationOptions = Apollo.BaseMutationOptions< + EmailInvoiceMutation, + EmailInvoiceMutationVariables +>; +export const GenerateInvoiceDocument = gql` + mutation GenerateInvoice($orderId: String!) { + generateInvoice(orderId: $orderId) + } +`; +export type GenerateInvoiceMutationFn = Apollo.MutationFunction< + GenerateInvoiceMutation, + GenerateInvoiceMutationVariables +>; + +/** + * __useGenerateInvoiceMutation__ + * + * To run a mutation, you first call `useGenerateInvoiceMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGenerateInvoiceMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [generateInvoiceMutation, { data, loading, error }] = useGenerateInvoiceMutation({ + * variables: { + * orderId: // value for 'orderId' + * }, + * }); + */ +export function useGenerateInvoiceMutation( + baseOptions?: Apollo.MutationHookOptions< + GenerateInvoiceMutation, + GenerateInvoiceMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + GenerateInvoiceMutation, + GenerateInvoiceMutationVariables + >(GenerateInvoiceDocument, options); +} +export type GenerateInvoiceMutationHookResult = ReturnType< + typeof useGenerateInvoiceMutation +>; +export type GenerateInvoiceMutationResult = + Apollo.MutationResult; +export type GenerateInvoiceMutationOptions = Apollo.BaseMutationOptions< + GenerateInvoiceMutation, + GenerateInvoiceMutationVariables +>; export const CreateOrderDocument = gql` mutation CreateOrder($options: CreateOrderInput!) { createOrder(options: $options) @@ -4249,62 +4357,6 @@ export type FavouritesWithProductQueryResult = Apollo.QueryResult< FavouritesWithProductQuery, FavouritesWithProductQueryVariables >; -export const GenerateInvoiceDocument = gql` - query GenerateInvoice($orderId: String!) { - generate(orderId: $orderId) - } -`; - -/** - * __useGenerateInvoiceQuery__ - * - * To run a query within a React component, call `useGenerateInvoiceQuery` and pass it any options that fit your needs. - * When your component renders, `useGenerateInvoiceQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGenerateInvoiceQuery({ - * variables: { - * orderId: // value for 'orderId' - * }, - * }); - */ -export function useGenerateInvoiceQuery( - baseOptions: Apollo.QueryHookOptions< - GenerateInvoiceQuery, - GenerateInvoiceQueryVariables - > -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery( - GenerateInvoiceDocument, - options - ); -} -export function useGenerateInvoiceLazyQuery( - baseOptions?: Apollo.LazyQueryHookOptions< - GenerateInvoiceQuery, - GenerateInvoiceQueryVariables - > -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery< - GenerateInvoiceQuery, - GenerateInvoiceQueryVariables - >(GenerateInvoiceDocument, options); -} -export type GenerateInvoiceQueryHookResult = ReturnType< - typeof useGenerateInvoiceQuery ->; -export type GenerateInvoiceLazyQueryHookResult = ReturnType< - typeof useGenerateInvoiceLazyQuery ->; -export type GenerateInvoiceQueryResult = Apollo.QueryResult< - GenerateInvoiceQuery, - GenerateInvoiceQueryVariables ->; export const OrderByIdDocument = gql` query OrderById($orderId: String!) { orderById(orderId: $orderId) { diff --git a/apps/storefront/src/graphql/mutation/invoice/emailInvoice.graphql b/apps/storefront/src/graphql/mutation/invoice/emailInvoice.graphql new file mode 100644 index 0000000..baceb59 --- /dev/null +++ b/apps/storefront/src/graphql/mutation/invoice/emailInvoice.graphql @@ -0,0 +1,3 @@ +mutation EmailInvoice($email: String!, $orderId: String!) { + emailInvoice(email: $email, orderId: $orderId) +} diff --git a/apps/storefront/src/graphql/mutation/invoice/generateInvoice.graphql b/apps/storefront/src/graphql/mutation/invoice/generateInvoice.graphql new file mode 100644 index 0000000..62bf3b2 --- /dev/null +++ b/apps/storefront/src/graphql/mutation/invoice/generateInvoice.graphql @@ -0,0 +1,3 @@ +mutation GenerateInvoice($orderId: String!) { + generateInvoice(orderId: $orderId) +} diff --git a/apps/storefront/src/graphql/query/invoice/generate.graphql b/apps/storefront/src/graphql/query/invoice/generate.graphql deleted file mode 100644 index 72815aa..0000000 --- a/apps/storefront/src/graphql/query/invoice/generate.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query GenerateInvoice($orderId: String!) { - generate(orderId: $orderId) -}