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

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}`]; 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>
<Box <Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
<Typography
sx={{ sx={{
fontSize: isMobile ? "20px" : "24px", width: "100%",
fontWeight: "500", height: "68px",
padding: "20px", backgroundColor: theme.palette.background.default,
color: theme.palette.grey2.main,
}} }}
> >
Интеграция с {companyName ? companyName : "партнером"} <Typography
</Typography> sx={{
</Box> fontSize: isMobile ? "20px" : "24px",
<IconButton fontWeight: "500",
onClick={handleCloseModal} padding: "20px",
sx={{ color: theme.palette.grey2.main,
width: "12px", }}
height: "12px", >
position: "absolute", Интеграция с {companyName ? companyName : "партнером"}
right: "15px", </Typography>
top: "15px", </Box>
}} <IconButton
> onClick={handleCloseModal}
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} /> sx={{
</IconButton> width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
</Box> </Box>
<Box <Box
className="родитель"
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",

@ -1,7 +1,10 @@
import { useLocation } from "react-router-dom"; 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(); const location = useLocation();
return ( return (
<Box <Box
@ -15,6 +18,7 @@ export default function CloseIcon() {
"&:hover path": { "&:hover path": {
stroke: "#7E2AEA", stroke: "#7E2AEA",
}, },
...sx
}} }}
> >
<svg <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 = { type TabsProps = {
names: string[]; names: string[];
items: string[]; items: string[];
selectedItem: "day" | "count" | "dop"; selectedItem: "day" | "count" | "dop" | "hide" | "create";
setSelectedItem: (num: "day" | "count" | "dop") => void; setSelectedItem: (num: "day" | "count" | "dop") => void;
}; };
@ -25,7 +25,46 @@ export const Tabs = ({
scrollButtons={false} scrollButtons={false}
> >
{items.map((item, index) => ( {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> </MuiTabs>
); );

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

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

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

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