разделение доп тарифов

This commit is contained in:
Nastya 2024-08-18 07:18:59 +03:00
parent 9bee2da343
commit fbd5783061
13 changed files with 570 additions and 84 deletions

@ -21,3 +21,28 @@ export const getTariffs = async (
return [null, `Ошибка при получении списка тарифов. ${error}`];
}
};
import axios from "axios";
const apiUrl = process.env.REACT_APP_DOMAIN + "/requestquiz";
export async function sendContactFormRequest(body: {
contact: string;
whoami: string;
}) {
try {
const a = await axios(apiUrl + "/callme", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
});
return [a];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при отправке запроса. ${error}`];
}
}

@ -81,39 +81,38 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({ isModalOpen, handleClo
}}
>
<Box>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
<Typography
<Box
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
padding: "20px",
color: theme.palette.grey2.main,
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
Интеграция с {companyName ? companyName : "партнером"}
</Typography>
</Box>
<IconButton
onClick={handleCloseModal}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
padding: "20px",
color: theme.palette.grey2.main,
}}
>
Интеграция с {companyName ? companyName : "партнером"}
</Typography>
</Box>
<IconButton
onClick={handleCloseModal}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
</Box>
<Box
className="родитель"
sx={{
display: "flex",
flexDirection: "column",

@ -1,7 +1,10 @@
import { useLocation } from "react-router-dom";
import { Box } from "@mui/material";
import { Box, SxProps, Theme } from "@mui/material";
export default function CloseIcon() {
interface Props {
sx?: SxProps<Theme>;
}
export default function CloseIcon({ sx }: Props) {
const location = useLocation();
return (
<Box
@ -15,6 +18,7 @@ export default function CloseIcon() {
"&:hover path": {
stroke: "#7E2AEA",
},
...sx
}}
>
<svg

@ -0,0 +1,260 @@
import { Box, Button, IconButton, Modal, Typography, useMediaQuery, useTheme } from "@mui/material"
import CloseIcon from "../Landing/images/icons/CloseIcon"
import InputTextfield from "@/ui_kit/InputTextfield"
import { Form, Formik, useFormik } from "formik"
import CustomTextField from "@/ui_kit/CustomTextField"
import Info from "@icons/Info";
import { sendContactFormRequest } from "@/api/tariff"
import { TimePicker } from "@mui/x-date-pickers"
import moment, { Moment } from "moment"
import { enqueueSnackbar } from "notistack"
interface Props {
open: boolean
onClose: () => void
}
interface Values {
contact: string;
dogiebusiness: string;
imagination: string;
name: string;
time: Moment | null;
}
const initialValues: Values = {
contact: "",//phone number
// whoami: {},
dogiebusiness: "",
imagination: "",
name: "",
time: null
};
interface FP {
title: string
desc?: string
placeholder: string
value: string
onChange: any
rows?: number
}
const Field = ({
title,
desc,
placeholder,
value,
onChange,
rows,
}: FP) => {
return (
<Box
sx={{
m: "15px 0"
}}
>
<Typography
sx={{
fontSize: "18px",
fontWeight: 500,
lineHeight: "21.33px",
mb: "10px",
}}
>{title}</Typography>
{desc && <Typography
sx={{
fontSize: "18px",
mb: "10px",
color: "#9A9AAF"
}}
>{desc}</Typography>}
<CustomTextField
value={value}
placeholder={placeholder}
maxLength={200}
onChange={onChange}
rows={rows || 0}
sx={rows !== undefined ? { height: "68px" } : {}}
/>
</Box>
)
}
export const ModalRequestCreate = ({
open,
onClose
}: Props) => {
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down(650));
return (
<Modal
open={open}
onClose={onClose}
>
<Box sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
boxShadow: 24,
width: isMobile ? "344px" : "620px",
borderRadius: "10px"
}}>
<Box>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
borderRadius: "10px 10px 0 0"
}}
>
<Typography
sx={{
fontSize: "18px",
padding: "20px",
color: theme.palette.grey2.main,
borderRadius: "10px 10px 0 0"
}}
>
Заполните форму, чтобы оставить заявку на создание квиза
</Typography>
</Box>
<IconButton
onClick={onClose}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
</Box>
<Box
sx={{
p: "15px 70px 50px 50px",
}}
>
<Formik
initialValues={initialValues}
onSubmit={async (values, formikHelpers) => {
if (values.contact.length < 8) return enqueueSnackbar("Пожалуйста, оставьте контактные данные")
const resp = await sendContactFormRequest({
contact: values.contact,
whoami: JSON.stringify({
dogiebusiness: values.dogiebusiness,
imagination: values.imagination,
name: values.name,
time: moment(values.time).format("hh:mm")
})
})
console.log(resp)
if (resp[0]?.status === 200) {
enqueueSnackbar("Запрос успешно отправлен")
onClose()
}
if (resp[1]) {
enqueueSnackbar(resp[1])
}
}}
>
{({ values, isSubmitting, setFieldValue }) => (<>
<Form>
<Field
title="Ваше имя"
placeholder="Иван"
value={values.name}
onChange={({ target }: any) => setFieldValue("name", target.value)}
/>
<Field
title="Какой у вас бизнес?"
placeholder="Логистика"
value={values.dogiebusiness}
onChange={({ target }: any) => setFieldValue("dogiebusiness", target.value)}
/>
<Field
title="Ваши контактные данные"
placeholder="Не менее 8 символов"
value={values.contact}
onChange={({ target }: any) => setFieldValue("contact", target.value)}
desc="(Telegram, WhatsApp, номер телефона)"
/>
<Field
title="Ваши пожелания к квизу"
placeholder="Введите свой текст здесь"
value={values.imagination}
onChange={({ target }: any) => setFieldValue("imagination", target.value)}
rows={2}
/>
<Box
sx={{
m: "15px 0"
}}
>
<Typography
sx={{
fontSize: "18px",
fontWeight: 500,
lineHeight: "21.33px",
mb: "10px",
}}
>Во сколько вам можно позвонить? </Typography>
<Typography
sx={{
fontSize: "18px",
mb: "10px",
color: "#9A9AAF"
}}
>Москва (GMT+3)</Typography>
<TimePicker value={values.time}
onChange={(e) => setFieldValue("time", e)}
views={['hours', 'minutes']} format="hh:mm"
ampm={false}
/>
</Box>
<Box
sx={{
display: "flex"
}}>
<Button
variant="contained"
fullWidth
type="submit"
disabled={isSubmitting}
sx={{
py: "12px",
"&:hover": {
backgroundColor: "#581CA7",
},
"&:active": {
color: "white",
backgroundColor: "black",
},
}}
>Отправить</Button>
<Info sx={{
ml: "15px",
width: "48px"
}} />
</Box>
</Form>
</>)}
</Formik>
</Box>
</Box>
</Modal >
)
}

@ -4,7 +4,7 @@ import { CustomTab } from "./CustomTab";
type TabsProps = {
names: string[];
items: string[];
selectedItem: "day" | "count" | "dop";
selectedItem: "day" | "count" | "dop" | "hide" | "create";
setSelectedItem: (num: "day" | "count" | "dop") => void;
};
@ -25,7 +25,46 @@ export const Tabs = ({
scrollButtons={false}
>
{items.map((item, index) => (
<CustomTab key={item + index} value={item} label={names[index]} />
<CustomTab key={item + index} value={item}
sx={{
textUnderlinePosition: "under",
color:
selectedItem === "create" && item === "dop" ?
"#7e2aea"
:
selectedItem === "hide" && item === "dop" ?
"#7e2aea"
:
selectedItem === item ? "#7e2aea" : "black",
textDecoration:
selectedItem === "create" && item === "dop" ?
"underline #7e2aea"
:
selectedItem === "hide" && item === "dop" ?
"underline #7e2aea"
:
selectedItem === item ? "underline #7e2aea" : "none",
"&.Mui-selected": {
textDecoration:
selectedItem === "create" && item === "dop" ?
"underline #7e2aea"
:
selectedItem === "hide" && item === "dop" ?
"underline #7e2aea"
:
selectedItem === item ? "underline #7e2aea" : "none",
},
}}
label={
selectedItem === "create" && item === "dop" ?
"Доп. услуги — Создать квиз на заказ"
:
selectedItem === "hide" && item === "dop" ?
"Доп. услуги — Убрать логотип “PenaQuiz”"
:
names[index]
} />
))}
</MuiTabs>
);

@ -38,6 +38,8 @@ import { getUser } from "@api/user";
import { getTariffs } from "@api/tariff";
import type { Discount } from "@model/discounts";
import { Other } from "./pages/Other";
import { ModalRequestCreate } from "./ModalRequestCreate";
const StepperText: Record<string, string> = {
day: "Тарифы на время",
@ -46,6 +48,7 @@ const StepperText: Record<string, string> = {
};
function TariffPage() {
const userPrivilegies = useUserStore(store => store.userAccount?.privileges);
const theme = useTheme();
const token = useToken();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
@ -55,11 +58,10 @@ function TariffPage() {
const [tariffs, setTariffs] = useState<Tariff[]>([]);
const [user, setUser] = useState();
const [discounts, setDiscounts] = useState<Discount[]>([]);
const [isRequestCreate, setIsRequestCreate] = useState(false);
const [openModal, setOpenModal] = useState({});
const { cashString, cashCop, cashRub } = useWallet();
const [selectedItem, setSelectedItem] = useState<"count" | "day" | "dop">(
"day",
);
const [selectedItem, setSelectedItem] = useState<TypePages>("day");
const { isTestServer } = useDomainDefine();
const [promocodeField, setPromocodeField] = useState<string>("");
@ -174,15 +176,7 @@ function TariffPage() {
);
});
const filteredBadgeTariffs = tariffs.filter((tariff) => {
return (
tariff.privileges[0].serviceKey === "squiz" &&
!tariff.isDeleted &&
!tariff.isCustom &&
tariff.privileges[0].privilegeId === "squizHideBadge" &&
tariff.privileges[0]?.type === "day"
);
});
const filteredBaseTariffs = filteredTariffs.filter((tariff) => {
return tariff.privileges[0].privilegeId !== "squizHideBadge";
});
@ -215,6 +209,10 @@ function TariffPage() {
}
}
const startRequestCreate = () => {
setIsRequestCreate(true)
}
return (
<>
<Container
@ -288,6 +286,7 @@ function TariffPage() {
items={Object.keys(StepperText)}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
toDop={() => setSelectedItem("dop")}
/>
<Box
@ -296,9 +295,8 @@ function TariffPage() {
display: selectedItem === "dop" ? "flex" : "grid",
gap: "40px",
p: "20px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${
isTablet ? "436px" : "360px"
}))`,
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
}))`,
flexDirection: selectedItem === "dop" ? "column" : undefined,
}}
>
@ -318,29 +316,29 @@ function TariffPage() {
discounts,
openModalHC,
)}
{selectedItem === "dop" && (
<>
<Typography fontWeight={500}>Убрать логотип "PenaQuiz"</Typography>
<Box
sx={{
justifyContent: "left",
display: "grid",
gap: "40px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${
isTablet ? "436px" : "360px"
}))`,
}}
>
{createTariffElements(
filteredBadgeTariffs,
false,
user,
discounts,
openModalHC,
)}
</Box>
</>
)}
{(selectedItem === "dop" || selectedItem === "hide" || selectedItem === "create")
&& (
<Other
selectedItem={selectedItem}
content={[
{
title: `Убрать логотип “PenaQuiz”`,
onClick: () => setSelectedItem("hide")
},
{
title: "Создать квиз на заказ",
onClick: () => setSelectedItem("create")
},
]}
tariffs={tariffs}
user={user}
discounts={discounts}
openModalHC={openModalHC}
userPrivilegies={userPrivilegies}
startRequestCreate={startRequestCreate}
/>
)}
</Box>
<Modal
open={Object.values(openModal).length > 0}
@ -373,6 +371,7 @@ function TariffPage() {
</Button>
</Paper>
</Modal>
<ModalRequestCreate open={isRequestCreate} onClose={() => setIsRequestCreate(false)} />
</>
);
}
@ -383,7 +382,7 @@ export const Tariffs = withErrorBoundary(TariffPage, {
Ошибка загрузки тарифов
</Typography>
),
onError: () => {},
onError: () => { },
});
const LoadingPage = () => (
@ -426,20 +425,20 @@ export const inCart = () => {
const outCart = (cart: string[]) => {
//Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
if (cart.length > 0) {
cart.forEach(async (id: string) => {
const [_, deleteError] = await cartApi.delete(id);
cart.forEach(async (id: string) => {
const [_, deleteError] = await cartApi.delete(id);
if (deleteError) {
console.error(deleteError);
if (deleteError) {
console.error(deleteError);
return;
}
return;
}
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || [];
if (!Array.isArray(saveCart)) saveCart = []
saveCart = saveCart.push(id);
localStorage.setItem("saveCart", JSON.stringify(saveCart));
});
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || [];
if (!Array.isArray(saveCart)) saveCart = []
saveCart = saveCart.push(id);
localStorage.setItem("saveCart", JSON.stringify(saveCart));
});
}
};

@ -0,0 +1,38 @@
import SimpleArrowDown from "@/ui_kit/SimpleArrowDown";
import { Box, ButtonBase, Typography } from "@mui/material"
interface Props {
title: string;
onClick: () => void;
}
export const NavCard = ({
title,
onClick
}: Props) => {
return (
<ButtonBase onClick={onClick}
sx={{
maxWidth: "570px",
height: "70px",
borderRadius: "12px",
bgcolor: "white",
display: "flex",
justifyContent: "space-between",
p: "0 20px 0 30px",
m: "10px",
minWidth: "343px",
width: "100%"
}}
>
<Typography>{title}</Typography>
<SimpleArrowDown
sx={{
transform: "rotate(270deg)"
}}
/>
</ButtonBase>
)
}

@ -0,0 +1,95 @@
import { Box, useMediaQuery, useTheme } from "@mui/material"
import { NavCard } from "../components/NavCard"
import { createTariffElements } from "../tariffsUtils/createTariffElements"
interface Props {
content: {
title: string,
onClick: () => void
}[]
selectedItem: TypePages
}
export const Other = ({
content,
selectedItem,
tariffs,
user,
discounts,
openModalHC,
userPrivilegies,
startRequestCreate
}: any) => {
const theme = useTheme()
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const sendRequestToCreate = userPrivilegies?.quizManual.amount > 0 ? startRequestCreate : undefined
console.log("sendRequestToCreate")
console.log(sendRequestToCreate)
switch (selectedItem) {
case "hide":
const filteredBadgeTariffs = tariffs.filter((tariff) => {
return (
tariff.privileges[0].serviceKey === "squiz" &&
!tariff.isDeleted &&
!tariff.isCustom &&
tariff.privileges[0].privilegeId === "squizHideBadge" &&
tariff.privileges[0]?.type === "day"
);
});
return <Box
sx={{
justifyContent: "left",
display: "grid",
gap: "40px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
}))`,
}}
>
{createTariffElements(
filteredBadgeTariffs,
false,
user,
discounts,
openModalHC,
)}
</Box>
case "create":
const filteredCreateTariffs = tariffs.filter((tariff) => {
return (
tariff.privileges[0].serviceKey === "squiz" &&
!tariff.isDeleted &&
!tariff.isCustom &&
tariff.privileges[0].privilegeId === "quizManual" &&
tariff.privileges[0]?.type === "count"
);
});
return <Box
sx={{
justifyContent: "left",
display: "grid",
gap: "40px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
}))`,
}}
>
{createTariffElements(
filteredCreateTariffs,
false,
user,
discounts,
openModalHC,
sendRequestToCreate
)}
</Box>
default:
return <Box
sx={{
display: "flex",
flexWrap: "wrap",
width: "100%"
}}>
{content.map(data => <NavCard {...data} key={data.title} />)}
</Box>
}
}

@ -23,6 +23,7 @@ interface Props {
text?: string;
};
price?: ReactNode;
sendRequestToCreate?: () => void
}
export default function TariffCard({
@ -33,6 +34,7 @@ export default function TariffCard({
price,
buttonProps,
discount,
sendRequestToCreate,
}: Props) {
text = Array.isArray(text) ? text : [text];
const theme = useTheme();
@ -132,12 +134,20 @@ export default function TariffCard({
))}
</Box>
</Tooltip>
<Box
sx={{
display: "flex",
width: "100%",
alignItems: "center",
}}>
{buttonProps && (
<Button
onClick={buttonProps.onClick}
variant="outlined"
sx={{
mt: "10px",
// mt: "10px",
color: "#7e2aea",
minWidth: "180px",
background: "transparent",
@ -157,6 +167,17 @@ export default function TariffCard({
{buttonProps.text}
</Button>
)}
{Boolean(sendRequestToCreate) && (
<Button
sx={{
ml: "30px"
}}
onClick={sendRequestToCreate}
>
Запросить
</Button>
)}
</Box>
</Box >
);
}

@ -11,6 +11,7 @@ export const createTariffElements = (
user: any,
discounts: any,
onclick: any,
sendRequestToCreate?: () => void
) => {
const tariffElements = filteredTariffs
.filter((tariff) => tariff.privileges.length > 0)
@ -43,13 +44,14 @@ export const createTariffElements = (
/>
}
buttonProps={{
text: "Выбрать",
text: "Купить",
onClick: () =>
onclick({
id: tariff._id,
price: Math.trunc(priceAfterDiscounts) / 100,
}),
}}
sendRequestToCreate={sendRequestToCreate}
headerText={tariff.name}
text={tariff.description}
price={

@ -0,0 +1 @@
type TypePages = "count" | "day" | "dop" | "hide" | "create"

@ -70,6 +70,9 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
case 403:
return ["Доступ ограничен.", error.status];
case 429:
return ["Слишком частые запросы", error.status];
case 401:
return ["Ошибка авторизации.", error.status];