Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Tutor = lazy(() => import("@/pages/Tutor"));
const Tutors = lazy(() => import("@/pages/Tutors"));
const PlanInvites = lazy(() => import("@/pages/PlanInvites"));
const Lesson = lazy(() => import("@/pages/Lesson"));
const AdMessage = lazy(() => import("@/pages/AdMessage"));
const Main = lazy(() => import("@/pages/Main"));

const router = createBrowserRouter([
Expand Down Expand Up @@ -67,6 +68,10 @@ const router = createBrowserRouter([
path: Dashboard.Lesson,
element: <Page page={<Lesson />} />,
},
{
path: Dashboard.AdMessage,
element: <Page page={<AdMessage />} />,
},
],
},
]);
Expand Down
9 changes: 9 additions & 0 deletions apps/dashboard/src/components/Layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Tag from "@litespace/assets/Tag";
import Users from "@litespace/assets/Users";
import Video from "@litespace/assets/Video";
import Home from "@litespace/assets/Home";
import Messages from "@litespace/assets/Monitor";

import { router } from "@/lib/route";
import { Icon } from "@/types/common";
Expand Down Expand Up @@ -174,6 +175,13 @@ const Sidebar: React.FC = () => {
Icon: People,
};

const adMessage: LinkInfo = {
label: intl("dashboard.ad-message.title"),
route: Dashboard.AdMessage,
isActive: match(Dashboard.AdMessage),
Icon: Messages,
};

if (user?.role === IUser.Role.Studio) return [photoSession];

return [
Expand All @@ -187,6 +195,7 @@ const Sidebar: React.FC = () => {
photoSession,
lessons,
tutors,
adMessage,
];
}, [intl, location.pathname, user?.role]);

Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/src/hooks/authRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ const routeConfigMap: Record<Dashboard, RouteConfig> = {
[Dashboard.Lesson]: {
whitelist: [superAdmin, regularAdmin],
},
[Dashboard.AdMessage]: {
whitelist: [superAdmin, regularAdmin],
},
[Dashboard.Main]: {
whitelist: [superAdmin, regularAdmin],
},
Expand Down
93 changes: 93 additions & 0 deletions apps/dashboard/src/pages/AdMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import BackLink from "@/components/Common/BackLink";
import { useUsers } from "@litespace/headless/users";
import { useSendAdMessage } from "@litespace/headless/student";
import { IUser } from "@litespace/types";
import { Button } from "@litespace/ui/Button";
import { DateInput } from "@litespace/ui/DateInput";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { Typography } from "@litespace/ui/Typography";
import dayjs from "dayjs";
import { useCallback, useState } from "react";
import { useOnError } from "@/hooks/error";
import { useToast } from "@litespace/ui/Toast";

const AdMessage: React.FC = () => {
const intl = useFormatMessage();
const toast = useToast();

const [from, setFrom] = useState(
dayjs().subtract(1, "day").format("YYYY-MM-DD")
);
const [to, setTo] = useState(dayjs().format("YYYY-MM-DD"));

const findStudents = useUsers({
role: IUser.Role.Student,
createdAt: {
gte: from,
lte: to,
},
full: true,
});

const onError = useOnError({
type: "mutation",
handler: (error) => toast.error({ title: intl(error.messageId) }),
});

const sendAdMessageMutation = useSendAdMessage({
onSuccess: () => toast.success({ title: intl("labels.done") }),
onError,
});

const send = useCallback(() => {
sendAdMessageMutation.mutate({
payload: {
createdAt: {
gte: from,
lte: to,
},
},
});
}, [sendAdMessageMutation, from, to]);

return (
<div className="w-full flex flex-col gap-6 max-w-screen-2xl mx-auto p-6">
<BackLink />

<div className="flex flex-row gap-2 max-w-[350px] items-center">
<Typography tag="label">{intl("placeholders.from")}</Typography>
<DateInput value={from} onChange={setFrom} />
</div>

<div className="flex flex-row gap-2 max-w-[350px] items-center">
<Typography tag="label">{intl("placeholders.to")}</Typography>
<DateInput value={to} onChange={setTo} />
</div>

<div className="flex flex-col gap-1 max-h-[300px] overflow-auto">
{findStudents.query.data?.list.map((student) =>
student.phone ? (
<Typography tag="span">
{student.id} - {student.name}
</Typography>
) : null
)}
</div>

<div className="flex flex-row gap-2 max-w-[350px] items-center">
<Button
variant="primary"
size="large"
className="flex-1"
onClick={send}
loading={sendAdMessageMutation.isPending}
disabled={sendAdMessageMutation.isPending}
>
{intl("labels.send")}
</Button>
</div>
</div>
);
};

export default AdMessage;
6 changes: 6 additions & 0 deletions packages/atlas/src/api/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ export class Student extends Base {
): Promise<IStudent.FindByIdApiResponse> {
return this.get({ route: `api/v1/student/${query.id}` });
}

async sendAdMessage(
payload: IStudent.SendAdMessageApiPayload
): Promise<IStudent.SendAdMessageApiResponse> {
return this.post({ route: `api/v1/student/send-ad-message`, payload });
}
}
1 change: 1 addition & 0 deletions packages/headless/src/constants/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ export enum MutationKey {
FawryRefund = "fawry-refund",
PlanCheckout = "plan-checkout",
LessonCheckout = "lesson-checkout",
SendAdMessage = "send-ad-message",
}
23 changes: 23 additions & 0 deletions packages/headless/src/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,26 @@ export function useFindStudentById(id: number): UseQueryResult<IStudent.Self> {
queryKey: [QueryKey.FindStudentById],
});
}

export function useSendAdMessage({
onSuccess,
onError,
}: {
onSuccess?: OnSuccess<IStudent.SendAdMessageApiResponse>;
onError?: OnError;
}) {
const api = useApi();

const send = useCallback(
async ({ payload }: { payload: IStudent.SendAdMessageApiPayload }) =>
api.student.sendAdMessage(payload),
[api.student]
);

return useMutation({
mutationFn: send,
mutationKey: [MutationKey.SendAdMessage],
onSuccess,
onError,
});
}
3 changes: 3 additions & 0 deletions packages/models/src/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
countRows,
knex,
withDateFilter,
withListFilter,
WithOptionalTx,
withSkippablePagination,
Expand Down Expand Up @@ -151,6 +152,7 @@ export class Users extends Model<
gender,
city,
select,
createdAt,
...pagination
}: WithOptionalTx<IUser.FindModelQuery<T>>): Promise<
Paginated<Pick<IUser.Self, T>>
Expand All @@ -162,6 +164,7 @@ export class Users extends Model<
if (verified) base.andWhere(this.column("verified_email"), verified);
if (gender) base.andWhere(this.column("gender"), gender);
if (city) base.andWhere(this.column("city"), city);
withDateFilter(base, this.column("created_at"), createdAt);

const total = await countRows(base.clone(), {
column: this.column("id"),
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export type Template =
time: string;
url: string;
};
}
| {
name: "ad_message";
parameters: object;
};

export type Message = {
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,11 @@ export type FindApiResponse = Paginated<Self>;
export type FindByIdApiQuery = { id: number };

export type FindByIdApiResponse = Self;

export type SendAdMessageApiPayload = {
createdAt: IFilter.Date;
};

export type SendAdMessageApiResponse = {
count: number;
};
2 changes: 2 additions & 0 deletions packages/types/src/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,15 @@ export type FindModelQuery<T extends Field = Field> =
gender?: Gender;
city?: City;
select?: T[];
createdAt?: IFilter.Date;
};

export type FindUsersApiQuery = IFilter.SkippablePagination & {
role?: Role;
verified?: boolean;
gender?: Gender;
city?: City;
createdAt?: IFilter.Date;
};

export type FindStudentStatsApiResponse = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/locales/ar-eg.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@
"dashboard.lesson.before-session": "قبل الجلسة",
"dashboard.lesson.after-session": "بعد الجلسة",
"dashboard.lesson.at-session": "عند بدء الجلسة",
"dashboard.ad-message.title": "الرسائل الدعائية",
"global.labels.minutes": "د",
"webrtc-check.title": "تنبيه: متصفحك لا يدعم تقنيه WebRTC",
"webrtc-check.description": "بعض وظائف المنصة تتطلب تقنيه WebRTC و التي لا يدعمها متصفحك الحالي، للحصول علي افضل تجربة نوصي باستخدام متصفح مثل Chrome او Firefox او Safari او Edge باحدث اصداراته.",
Expand Down Expand Up @@ -1064,6 +1065,7 @@
"labels.contact-us": "تواصل معنا",
"labels.leave": "المغادرة",
"labels.enable-notifications": "تفعيل الإشعارات",
"labels.send": "إرسال",
"labels.percent": "{value}%",
"labels.hash-value": "{value}#",
"labels.upload.video": "جاري رفع الفيديو",
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/routes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export enum Dashboard {
Tutors = "/tutors",
PlanInvites = "/plan-invites",
SessionEvents = "/session-events",
AdMessage = "/ad-message",
}

export type StudentSettingsTabId =
Expand Down
32 changes: 22 additions & 10 deletions services/server/scripts/whatsapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,28 @@ const sendMessageCommand = new Command()
"-r, --receiver <string>",
"The phone number that should recieve the message"
)
.action(async ({ receiver }: { receiver: string }) => {
sendMsg({
to: receiver,
template: {
name: "hello_world",
parameters: {},
},
method: IUser.NotificationMethod.Whatsapp,
});
});
.option(
"-t, --template <string>",
"The template to be sent: hello_world or ad_message"
)
.action(
async ({
receiver,
template,
}: {
receiver: string;
template?: "hello_world" | "ad_message";
}) => {
sendMsg({
to: receiver,
template: {
name: template || "hello_world",
parameters: {},
},
method: IUser.NotificationMethod.Whatsapp,
});
}
);

new Command()
.name("WhatsApp API")
Expand Down
Loading