campains
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-07-04 11:56:32 +03:30
parent a4eaa50fdf
commit 95814d6b00
12 changed files with 433 additions and 5 deletions
+2
View File
@@ -36,6 +36,8 @@ export const Pages = {
groups: "/customers/groups",
groupsAdd: "/customers/groups/add",
groupsUpdate: "/customers/groups/update/",
campaigns: "/customers/campaigns",
campaignsAdd: "/customers/campaigns/add",
},
discount: {
list: "/discounts/list",
+2 -1
View File
@@ -105,7 +105,8 @@
"add_discount": "افزودن تخفیف",
"customers_list": "لیست مشتریان",
"add_customer": "افزودن مشتری",
"groups": "گروه مشتری"
"groups": "گروه مشتری",
"campaigns": "کمپین‌ها"
},
"guide": {
"list_guide": "لیست راهنمای سرویس",
+152
View File
@@ -0,0 +1,152 @@
import Button from "@/components/Button";
import Input from "@/components/Input";
import MultiSelect from "@/components/MultiSelect";
import Select from "@/components/Select";
import { extractErrorMessage } from "@/config/func";
import { Pages } from "@/config/Pages";
import type { ErrorType } from "@/helpers/types";
import { useFormik } from "formik";
import { TickCircle } from "iconsax-react";
import { type FC, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import * as Yup from "yup";
import { useGetUserGroups } from "@/pages/customers/groups/hooks/useUserGroupData";
import { useCreateDiscountCampaign } from "./hooks/useCampaignData";
import type { CreateDiscountCampaignType } from "./types/Types";
const sentTypeOptions = [
{ value: "push", label: "پوش نوتیفیکیشن" },
{ value: "sms", label: "پیامک" },
];
const CreateCampaign: FC = () => {
const navigate = useNavigate();
const { mutate: createCampaign, isPending } = useCreateDiscountCampaign();
const { data: groupsData } = useGetUserGroups();
const groupOptions = useMemo(
() =>
(groupsData?.data || []).map((group) => ({
value: group.id,
label: group.name,
})),
[groupsData]
);
const formik = useFormik<CreateDiscountCampaignType>({
initialValues: {
title: "",
groups: [],
sentType: "push",
discountCode: "",
ocasion: "",
},
validationSchema: Yup.object().shape({
title: Yup.string().required("عنوان کمپین الزامی است"),
groups: Yup.array()
.of(Yup.string())
.min(1, "حداقل یک گروه باید انتخاب شود"),
sentType: Yup.string().required("نوع ارسال الزامی است"),
discountCode: Yup.string().required("کد تخفیف الزامی است"),
ocasion: Yup.string().required("مناسبت الزامی است"),
}),
onSubmit: (values) => {
createCampaign(values, {
onSuccess: () => {
toast.success("کمپین با موفقیت ایجاد شد");
navigate(Pages.customers.campaigns);
},
onError: (error: ErrorType) => {
toast.error(extractErrorMessage(error));
},
});
},
});
return (
<div className="mt-5">
<div className="flex w-full justify-between items-center">
<div className="text-lg font-light">افزودن کمپین تخفیف</div>
<Button
className="px-5 w-fit"
onClick={() => formik.handleSubmit()}
isloading={isPending}
>
<div className="flex gap-2 items-center">
<TickCircle className="size-5" color="white" />
<div>ثبت کمپین</div>
</div>
</Button>
</div>
<div className="mt-6">
<div className="bg-white rounded-3xl p-6 flex flex-col gap-6">
<Input
label="عنوان کمپین"
name="title"
value={formik.values.title}
onChange={formik.handleChange}
error_text={
formik.touched.title && formik.errors.title
? formik.errors.title
: ""
}
/>
<Input
label="کد تخفیف"
name="discountCode"
value={formik.values.discountCode}
onChange={formik.handleChange}
error_text={
formik.touched.discountCode && formik.errors.discountCode
? formik.errors.discountCode
: ""
}
/>
<Input
label="مناسبت"
name="ocasion"
value={formik.values.ocasion}
onChange={formik.handleChange}
error_text={
formik.touched.ocasion && formik.errors.ocasion
? formik.errors.ocasion
: ""
}
/>
<Select
label="نوع ارسال"
name="sentType"
items={sentTypeOptions}
value={formik.values.sentType}
onChange={formik.handleChange}
error_text={
formik.touched.sentType && formik.errors.sentType
? formik.errors.sentType
: ""
}
/>
<MultiSelect
label="گروه‌های مشتری"
items={groupOptions}
value={formik.values.groups}
onChange={(values) => formik.setFieldValue("groups", values)}
placeholder="انتخاب گروه‌ها"
error_text={
formik.touched.groups && formik.errors.groups
? String(formik.errors.groups)
: ""
}
/>
</div>
</div>
</div>
);
};
export default CreateCampaign;
+54
View File
@@ -0,0 +1,54 @@
import Button from "@/components/Button";
import Filters from "@/components/Filters";
import Table from "@/components/Table";
import { Pages } from "@/config/Pages";
import { Add } from "iconsax-react";
import { type FC } from "react";
import { Link } from "react-router-dom";
import { getCampaignTableColumns } from "./components/CampaignTableColumns";
import { useGetCampaigns } from "./hooks/useCampaignData";
import { useCampaignFilters } from "./hooks/useCampaignFilters";
const CampaignsList: FC = () => {
const { filters, currentPage, apiParams, handleFiltersChange, handlePageChange, limit } = useCampaignFilters();
const { data: campaignsData, isLoading } = useGetCampaigns(apiParams);
const campaigns = campaignsData?.data || [];
const totalPages = campaignsData?.meta?.totalPages || Math.max(1, Math.ceil(campaigns.length / limit) || 1);
const columns = getCampaignTableColumns();
return (
<div className="mt-5">
<div className="flex justify-between items-center">
<h1 className="text-lg font-light">کمپینها</h1>
<Link to={Pages.customers.campaignsAdd}>
<Button className="w-fit px-6">
<div className="flex gap-2 items-center">
<Add color="#fff" size={20} />
<span>افزودن کمپین</span>
</div>
</Button>
</Link>
</div>
<div className="mt-8">
<Filters fields={[]} onChange={handleFiltersChange} initialValues={filters} searchField="search" />
</div>
<Table
columns={columns}
data={campaigns}
isloading={isLoading}
pagination={{
currentPage,
totalPages,
onPageChange: handlePageChange,
}}
/>
</div>
);
};
export default CampaignsList;
@@ -0,0 +1,54 @@
import type { ColumnType } from "@/components/types/TableTypes";
import { formatOptionalDate } from "@/helpers/func";
import type { Campaign } from "../types/Types";
const getGroupNames = (groups: Campaign["groups"]) => {
if (!groups?.length) return "-";
return groups.map((group) => (typeof group === "string" ? group : group.name)).join(" ، ");
};
const sentTypeLabels: Record<string, string> = {
push: "پوش نوتیفیکیشن",
sms: "پیامک",
};
export const getCampaignTableColumns = (): ColumnType<Campaign>[] => {
return [
{
key: "title",
title: "عنوان",
render: (item: Campaign) => item.title || "-",
},
{
key: "discountCode",
title: "کد تخفیف",
render: (item: Campaign) => item.discountCode || "-",
},
{
key: "ocasion",
title: "مناسبت",
render: (item: Campaign) => item.ocasion || "-",
},
{
key: "sentType",
title: "نوع ارسال",
render: (item: Campaign) => sentTypeLabels[item.sentType] || item.sentType,
},
{
key: "groups",
title: "گروه‌ها",
render: (item: Campaign) => getGroupNames(item.groups),
},
{
key: "createdAt",
title: "تاریخ ایجاد",
render: (item: Campaign) => formatOptionalDate(item.createdAt),
},
// {
// key: "actions",
// title: "",
// render: (item: Campaign) => (onDelete ? <TrashWithConfrim onDelete={() => onDelete(item.id)} isloading={isDeleting} /> : null),
// },
];
};
@@ -0,0 +1,38 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/CampaignService";
import type { GetCampaignsParams } from "../types/Types";
export const useGetCampaigns = (params?: GetCampaignsParams) => {
return useQuery({
queryKey: ["campaigns", params],
queryFn: () => api.getCampaigns(params),
});
};
export const useGetCampaignById = (id: string) => {
return useQuery({
queryKey: ["campaign", id],
queryFn: () => api.getCampaignById(id),
enabled: !!id,
});
};
export const useCreateDiscountCampaign = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createDiscountCampaign,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["campaigns"] });
},
});
};
export const useDeleteCampaign = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteCampaign,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["campaigns"] });
},
});
};
@@ -0,0 +1,43 @@
import { useMemo, useState } from "react";
import type { FilterValues } from "@/components/Filters";
import type { GetCampaignsParams } from "../types/Types";
const DEFAULT_PAGE = 1;
const DEFAULT_LIMIT = 10;
export const useCampaignFilters = () => {
const [filters, setFilters] = useState<FilterValues>({});
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
const [limit] = useState(DEFAULT_LIMIT);
const apiParams = useMemo<GetCampaignsParams>(() => {
const params: GetCampaignsParams = {
page: currentPage,
limit,
};
if (filters.search) {
params.search = filters.search as string;
}
return params;
}, [filters, currentPage, limit]);
const handleFiltersChange = (newFilters: FilterValues) => {
setFilters(newFilters);
setCurrentPage(DEFAULT_PAGE);
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return {
filters,
currentPage,
apiParams,
handleFiltersChange,
handlePageChange,
limit,
};
};
@@ -0,0 +1,33 @@
import axios from "@/config/axios";
import type {
CampaignResponse,
CampaignsResponse,
CreateDiscountCampaignType,
GetCampaignsParams,
} from "../types/Types";
export const getCampaigns = async (
params?: GetCampaignsParams
): Promise<CampaignsResponse> => {
const { data } = await axios.get<CampaignsResponse>("/admin/campaigns", {
params,
});
return data;
};
export const getCampaignById = async (id: string): Promise<CampaignResponse> => {
const { data } = await axios.get<CampaignResponse>(`/admin/campaigns/${id}`);
return data;
};
export const createDiscountCampaign = async (
params: CreateDiscountCampaignType
) => {
const { data } = await axios.post("/admin/campaigns/discount", params);
return data;
};
export const deleteCampaign = async (id: string) => {
const { data } = await axios.delete(`/admin/campaigns/${id}`);
return data;
};
@@ -0,0 +1,42 @@
import type { UserGroup } from "@/pages/customers/groups/types/Types";
import type { IResponse } from "@/types/response.types";
export type CampaignSentType = "push" | "sms";
export type Campaign = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
title: string;
groups: UserGroup[] | string[];
sentType: CampaignSentType;
discountCode: string;
ocasion: string;
};
export type PaginationMeta = {
total: number;
page: number;
limit: number;
totalPages: number;
};
export type GetCampaignsParams = {
page?: number;
limit?: number;
search?: string;
};
export type CampaignsResponse = IResponse<Campaign[]> & {
meta?: PaginationMeta;
};
export type CampaignResponse = IResponse<Campaign>;
export type CreateDiscountCampaignType = {
title: string;
groups: string[];
sentType: CampaignSentType;
discountCode: string;
ocasion: string;
};
+8 -4
View File
@@ -80,10 +80,14 @@ const UpdateUserGroup: FC = () => {
</Button>
</div>
<div className="mt-6">
<div className="bg-white rounded-3xl p-6">
<Input label="نام گروه" name="name" value={formik.values.name} onChange={formik.handleChange} error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ""} />
</div>
<div className="mt-6 bg-white rounded-3xl p-6">
<Input
label="نام گروه"
name="name"
value={formik.values.name}
onChange={formik.handleChange}
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ""}
/>
</div>
{id && <GroupUsersSection groupId={id} />}
+4
View File
@@ -53,6 +53,8 @@ import UpdateSlider from '@/pages/sliders/Update'
import UserGroupsList from '@/pages/customers/groups/List'
import CreateUserGroup from '@/pages/customers/groups/Create'
import UpdateUserGroup from '@/pages/customers/groups/Update'
import CampaignsList from '@/pages/customers/campaigns/List'
import CreateCampaign from '@/pages/customers/campaigns/Create'
const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore()
@@ -84,6 +86,8 @@ const MainRouter: FC = () => {
<Route path={Pages.customers.groups} element={<UserGroupsList />} />
<Route path={Pages.customers.groupsAdd} element={<CreateUserGroup />} />
<Route path={Pages.customers.groupsUpdate + ':id'} element={<UpdateUserGroup />} />
<Route path={Pages.customers.campaigns} element={<CampaignsList />} />
<Route path={Pages.customers.campaignsAdd} element={<CreateCampaign />} />
<Route path={Pages.orders.list} element={<OrdersList />} />
<Route path={Pages.orders.add} element={<CreateOrder />} />
<Route path={Pages.orders.foodReport} element={<FoodOrderReport />} />
@@ -25,6 +25,7 @@ const CustomersSubMenu: FC = () => {
<SubMenuItem title={t("submenu.add_customer")} isActive={isActive("add")} link={Pages.customers.add} />
<SubMenuItem title={t("submenu.groups")} isActive={isActive("groups")} link={Pages.customers.groups} />
<SubMenuItem title={t("submenu.campaigns")} isActive={isActive("campaigns")} link={Pages.customers.campaigns} />
</div>
</div>
);