Skip to content

Commit

Permalink
WIP: Invite customer
Browse files Browse the repository at this point in the history
  • Loading branch information
ravirajput10 committed Sep 16, 2024
1 parent 985a16e commit f7e12de
Show file tree
Hide file tree
Showing 15 changed files with 373 additions and 10 deletions.
4 changes: 4 additions & 0 deletions apps/web/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export const { auth, signIn, signOut, handlers } = NextAuth({
domain: domain._id,
email: sanitizedEmail,
});
if (user && user.invited) {
user.invited = false;
await user.save();
}
if (!user) {
user = await createUser({
domain,
Expand Down
179 changes: 179 additions & 0 deletions apps/web/components/admin/products/new-customer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"use client";

import React, { useCallback, useEffect, useState } from "react";
import { Address, AppMessage } from "@courselit/common-models";
import {
Form,
FormField,
Section,
Link,
Button,
ComboBox,
Breadcrumbs,
} from "@courselit/components-library";
import { AppDispatch, AppState } from "@courselit/state-management";
import { FetchBuilder } from "@courselit/utils";
import { connect } from "react-redux";
import {
BTN_GO_BACK,
BTN_INVITE,
PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER,
USER_TAGS_SUBHEADER,
} from "../../../ui-config/strings";
import {
networkAction,
setAppMessage,
} from "@courselit/state-management/dist/action-creators";

interface NewCustomerProps {
address: Address;
dispatch: AppDispatch;
networkAction: boolean;
courseId: string;
}

function NewCustomer({
courseId,
address,
dispatch,
networkAction: loading,
}: NewCustomerProps) {
const [email, setEmail] = useState("");
const [tags, setTags] = useState([]);

const getTags = useCallback(async () => {
const query = `
query {
tags
}
`;
const fetch = new FetchBuilder()
.setUrl(`${address.backend}/api/graph`)
.setPayload(query)
.setIsGraphQLEndpoint(true)
.build();
try {
dispatch(networkAction(true));
const response = await fetch.exec();
if (response.tags) {
setTags(response.tags);
}
} catch (err) {
} finally {
dispatch(networkAction(false));
}
}, [address.backend, dispatch]);

useEffect(() => {
getTags();
}, [getTags]);

const inviteCustomer = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

const query = `
query InviteCustomer($email: String!, $tags: [String]!, $courseId: ID!) {
user: inviteCustomer(email: $email, tags: $tags, id: $courseId) {
name,
email,
id,
subscribedToUpdates,
active,
permissions,
userId,
tags,
invited
}
}
`;
const fetch = new FetchBuilder()
.setUrl(`${address.backend}/api/graph`)
.setPayload({
query,
variables: {
email: email,
tags: tags,
courseId: courseId,
},
})
.setIsGraphQLEndpoint(true)
.build();
try {
dispatch(networkAction(true));
const response = await fetch.exec();
if (response.user) {
setEmail("");
dispatch(
setAppMessage(
new AppMessage(
`${response.user.email} has been invited.`,
),
),
);
}
} catch (err: any) {
dispatch(setAppMessage(new AppMessage(err.message)));
} finally {
dispatch(networkAction(false));
}
};

return (
<div className="flex flex-col gap-4">
<Breadcrumbs aria-label="breakcrumb">
<Link href="/dashboard/products/">Products</Link>
<Link href={`/dashboard/product/${courseId}/reports`}>Product</Link>
<p>{PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER}</p>
</Breadcrumbs>
<Section>
<div className="flex flex-col">
<h1 className="text-4xl font-semibold mb-4">
{PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER}
</h1>
<Form
onSubmit={inviteCustomer}
className="flex flex-col gap-4"
>
<FormField
required
label="Email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<div className="flex flex-col gap-2">
<p>{USER_TAGS_SUBHEADER}</p>
<ComboBox
side="bottom"
options={tags}
selectedOptions={new Set(tags)}
onChange={(values: string[]) => setTags(values)}
/>
</div>
<div className="flex gap-2">
<Button
disabled={!email}
onClick={inviteCustomer}
sx={{ mr: 1 }}
>
{BTN_INVITE}
</Button>
<Link href={`/dashboard/products`}>
<Button variant="soft">{BTN_GO_BACK}</Button>
</Link>
</div>
</Form>
</div>
</Section>
</div>
);
}

const mapStateToProps = (state: AppState) => ({
address: state.address,
networkAction: state.networkAction,
});

const mapDispatchToProps = (dispatch: AppDispatch) => ({ dispatch });

export default connect(mapStateToProps, mapDispatchToProps)(NewCustomer);
10 changes: 10 additions & 0 deletions apps/web/components/admin/products/product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PRODUCT_STATUS_PUBLISHED,
PRODUCT_TABLE_CONTEXT_MENU_DELETE_PRODUCT,
PRODUCT_TABLE_CONTEXT_MENU_EDIT_PAGE,
PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER,
VIEW_PAGE_MENU_ITEM,
} from "../../../ui-config/strings";
import { MoreVert } from "@courselit/icons";
Expand Down Expand Up @@ -122,6 +123,15 @@ function Product({
{PRODUCT_TABLE_CONTEXT_MENU_EDIT_PAGE}
</Link>
</MenuItem>
<div className="flex w-full border-b border-slate-300"></div>
<MenuItem>
<Link
href={`/dashboard/product/${product.courseId}/customer/new`}
>
{PRODUCT_TABLE_CONTEXT_MENU_INVITE_A_CUSTOMER}
</Link>
</MenuItem>
<div className="flex w-full border-b border-slate-300"></div>
<MenuItem
component="dialog"
title={PRODUCT_TABLE_CONTEXT_MENU_DELETE_PRODUCT}
Expand Down
14 changes: 10 additions & 4 deletions apps/web/components/admin/users/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ const UsersManager = ({ address, dispatch, loading }: UserManagerProps) => {
email,
permissions,
createdAt,
updatedAt
updatedAt,
invited,
avatar {
mediaId,
originalFileName,
Expand Down Expand Up @@ -116,7 +117,8 @@ const UsersManager = ({ address, dispatch, loading }: UserManagerProps) => {
email,
permissions,
createdAt,
updatedAt
updatedAt,
invited,
avatar {
mediaId,
originalFileName,
Expand Down Expand Up @@ -313,8 +315,12 @@ const UsersManager = ({ address, dispatch, loading }: UserManagerProps) => {
</td>
<td align="right">
{user.updatedAt !== user.createdAt
? user.updatedAt
? formattedLocaleDate(user.updatedAt)
? !user.invited
? user.updatedAt
? formattedLocaleDate(
user.updatedAt,
)
: ""
: ""
: ""}
</td>
Expand Down
7 changes: 3 additions & 4 deletions apps/web/graphql/courses/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { User } from "../../models/User";
import Page from "../../models/Page";
import slugify from "slugify";
import { addGroup } from "./logic";
import { Banner, Footer, Header } from "@courselit/common-widgets";

const validatePaymentMethod = async (domain: string) => {
try {
Expand Down Expand Up @@ -186,15 +185,15 @@ export const setupBlog = async ({
const getInitialLayout = () => {
return [
{
name: Header.metadata.name,
name: "header",
deleteable: false,
shared: true,
},
{
name: Banner.metadata.name,
name: "banner",
},
{
name: Footer.metadata.name,
name: "footer",
deleteable: false,
shared: true,
},
Expand Down
67 changes: 66 additions & 1 deletion apps/web/graphql/users/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import {
} from "@courselit/common-models";
import { recordActivity } from "../../lib/record-activity";
import { triggerSequences } from "../../lib/trigger-sequences";
import finalizePurchase from "@/lib/finalize-purchase";
import { getCourseOrThrow } from "../courses/logic";
import pug from "pug";
import courseEnrollTemplate from "@/templates/course-enroll";
import { send } from "../../services/mail";

const removeAdminFieldsFromUserObject = ({
id,
Expand Down Expand Up @@ -97,7 +102,10 @@ const checkForInvalidPermissions = (user) => {
}
};

export const updateUser = async (userData: any, ctx: GQLContext) => {
export const updateUser = async (
userData: Record<string, unknown>,
ctx: GQLContext,
) => {
checkIfAuthenticated(ctx);
const { id } = userData;

Expand Down Expand Up @@ -143,6 +151,60 @@ export const updateUser = async (userData: any, ctx: GQLContext) => {
return user;
};

export const inviteCustomer = async (
email: string,
tags: string[],
id: string,
ctx: GQLContext,
) => {
checkIfAuthenticated(ctx);
if (!checkPermission(ctx.user.permissions, [permissions.manageUsers])) {
throw new Error(responses.action_not_allowed);
}

const course = await getCourseOrThrow(undefined, ctx, id);
const sanitizedEmail = (email as string).toLowerCase();
let user = await UserModel.findOne({
email: sanitizedEmail,
domain: ctx.subdomain._id,
});
if (!user) {
user = await createUser({
domain: ctx.subdomain!,
email: sanitizedEmail,
subscribedToUpdates: true,
invited: true,
});
}
if (
!user.purchases.some(
(purchase) => purchase.courseId === course.courseId,
)
) {
await finalizePurchase(user.userId, id);

try {
const emailBody = pug.render(courseEnrollTemplate, {
courseName: course.title,
loginLink: `${ctx.address}/login`,
hideCourseLitBranding:
ctx.subdomain.settings.hideCourseLitBranding,
});

await send({
to: [user.email],
subject: `You have been invited in ${course.title}`,
body: emailBody,
});
} catch (error) {
// eslint-disable-next-line no-console
console.log("error", error);
}
}
user = await updateUser({ id: user._id, tags }, ctx);
return user;
};

const updateCoursesForCreatorName = async (creatorId, creatorName) => {
await Course.updateMany(
{
Expand Down Expand Up @@ -262,6 +324,7 @@ export async function createUser({
lead,
superAdmin = false,
subscribedToUpdates = true,
invited,
}: {
domain: Domain;
name?: string;
Expand All @@ -272,6 +335,7 @@ export async function createUser({
| typeof constants.leadApi;
superAdmin?: boolean;
subscribedToUpdates?: boolean;
invited?: boolean;
}): Promise<User> {
const newUser: Partial<User> = {
domain: domain._id,
Expand All @@ -282,6 +346,7 @@ export async function createUser({
permissions: [],
lead: lead || constants.leadWebsite,
subscribedToUpdates,
invited,
};
if (superAdmin) {
newUser.permissions = [
Expand Down
Loading

0 comments on commit f7e12de

Please sign in to comment.