Merge branch 'dev' into 'main'
ЛК авторизация. Запрещена автоподстановка в регристрацию и настроку кабинета.... See merge request frontend/marketplace!10
This commit is contained in:
commit
ecb8d8cf2e
20
src/api/cart.ts
Normal file
20
src/api/cart.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
|
|
||||||
|
const apiUrl = process.env.NODE_ENV === "production" ? "/customer" : "https://hub.pena.digital/customer";
|
||||||
|
|
||||||
|
export function patchCart(tariffId: string) {
|
||||||
|
return makeRequest<never, string[]>({
|
||||||
|
url: apiUrl + `/cart?id=${tariffId}`,
|
||||||
|
method: "PATCH",
|
||||||
|
useToken: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCart(tariffId: string) {
|
||||||
|
return makeRequest<never, string[]>({
|
||||||
|
url: apiUrl + `/cart?id=${tariffId}`,
|
||||||
|
method: "DELETE",
|
||||||
|
useToken: true,
|
||||||
|
});
|
||||||
|
}
|
@ -1,15 +1,21 @@
|
|||||||
import { makeRequest } from "@frontend/kitui";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { CustomTariff } from "@root/model/customTariffs";
|
import { CreateTariffBody, CustomTariff } from "@root/model/customTariffs";
|
||||||
import { PrivilegeWithoutPrice } from "@root/model/privilege";
|
import { Tariff } from "@root/model/tariff";
|
||||||
|
|
||||||
|
|
||||||
export function createTariff<
|
export function createTariff(tariff: CreateTariffBody) {
|
||||||
T = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; }
|
return makeRequest<CreateTariffBody, CustomTariff>({
|
||||||
>(tariff: T) {
|
|
||||||
return makeRequest<T, CustomTariff>({
|
|
||||||
url: `https://admin.pena.digital/strator/tariff`,
|
url: `https://admin.pena.digital/strator/tariff`,
|
||||||
method: "post",
|
method: "post",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
body: tariff,
|
body: tariff,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTariffById(tariffId:string){
|
||||||
|
return makeRequest<never, Tariff>({
|
||||||
|
url: `https://admin.pena.digital/strator/tariff/${tariffId}`,
|
||||||
|
method: "get",
|
||||||
|
useToken: true,
|
||||||
|
});
|
||||||
}
|
}
|
@ -2,156 +2,159 @@ import { useState } from "react";
|
|||||||
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
import { basketStore } from "@root/stores/BasketStore";
|
|
||||||
import { cardShadow } from "@root/utils/themes/shadow";
|
import { cardShadow } from "@root/utils/themes/shadow";
|
||||||
|
import { ServiceCartData } from "@root/model/cart";
|
||||||
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
|
import { removeTariffFromCart } from "@root/stores/user";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
|
|
||||||
|
|
||||||
|
const name: Record<string, string> = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" };
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "templ" | "squiz" | "reducer";
|
serviceData: ServiceCartData;
|
||||||
content: {
|
|
||||||
name: string;
|
|
||||||
desc: string;
|
|
||||||
id: string;
|
|
||||||
privelegeid: string;
|
|
||||||
amount: number;
|
|
||||||
price: number;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomWrapperDrawer({ type, content }: Props) {
|
export default function CustomWrapperDrawer({ serviceData }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
const { remove } = basketStore();
|
function handleItemDeleteClick(tariffId: string) {
|
||||||
|
removeTariffFromCart(tariffId).then(() => {
|
||||||
|
enqueueSnackbar("Тариф удален");
|
||||||
|
}).catch(error => {
|
||||||
|
const message = getMessageFromFetchError(error);
|
||||||
|
if (message) enqueueSnackbar(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const totalSum = Object.values(content).reduce((accamulator, { price }) => (accamulator += price), 0);
|
return (
|
||||||
const name: Record<string, string> = { templ: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "12px",
|
|
||||||
boxShadow: cardShadow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "white",
|
|
||||||
"&:first-of-type": {
|
|
||||||
borderTopLeftRadius: "12px",
|
|
||||||
borderTopRightRadius: "12px",
|
|
||||||
},
|
|
||||||
"&:last-of-type": {
|
|
||||||
borderBottomLeftRadius: "12px",
|
|
||||||
borderBottomRightRadius: "12px",
|
|
||||||
},
|
|
||||||
"&:not(:last-of-type)": {
|
|
||||||
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setIsExpanded((prev) => !prev)}
|
sx={{
|
||||||
sx={{
|
overflow: "hidden",
|
||||||
height: "72px",
|
borderRadius: "12px",
|
||||||
|
boxShadow: cardShadow,
|
||||||
display: "flex",
|
}}
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
cursor: "pointer",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: upMd ? "20px" : "16px",
|
|
||||||
lineHeight: upMd ? undefined : "19px",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
px: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name[type]}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
height: "100%",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{ pr: "11px", color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}
|
|
||||||
>
|
|
||||||
{totalSum} руб.
|
|
||||||
</Typography>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
paddingLeft: upSm ? "24px" : 0,
|
backgroundColor: "white",
|
||||||
height: "100%",
|
"&:first-of-type": {
|
||||||
display: "flex",
|
borderTopLeftRadius: "12px",
|
||||||
justifyContent: "center",
|
borderTopRightRadius: "12px",
|
||||||
alignItems: "center",
|
},
|
||||||
}}
|
"&:last-of-type": {
|
||||||
></Box>
|
borderBottomLeftRadius: "12px",
|
||||||
</Box>
|
borderBottomRightRadius: "12px",
|
||||||
</Box>
|
},
|
||||||
{isExpanded &&
|
"&:not(:last-of-type)": {
|
||||||
Object.values(content).map(({ desc, id, privelegeid, amount, price }, index) => (
|
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
||||||
<Box
|
},
|
||||||
key={index}
|
}}
|
||||||
sx={{
|
|
||||||
py: upMd ? "10px" : undefined,
|
|
||||||
pt: upMd ? undefined : "15px",
|
|
||||||
pb: upMd ? undefined : "20px",
|
|
||||||
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "15px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Typography
|
<Box
|
||||||
sx={{
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
width: "200px",
|
sx={{
|
||||||
fontSize: upMd ? undefined : "16px",
|
height: "72px",
|
||||||
lineHeight: upMd ? undefined : "19px",
|
|
||||||
color: theme.palette.grey3.main,
|
display: "flex",
|
||||||
}}
|
alignItems: "center",
|
||||||
>
|
justifyContent: "space-between",
|
||||||
{desc}
|
cursor: "pointer",
|
||||||
</Typography>
|
userSelect: "none",
|
||||||
<Box
|
}}
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: "10px",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.grey3.main,
|
|
||||||
fontSize: "20px",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{price} руб.
|
<Typography
|
||||||
</Typography>
|
sx={{
|
||||||
|
fontSize: upMd ? "20px" : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
px: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name[serviceData.serviceKey]}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<SvgIcon
|
<Box
|
||||||
sx={{ cursor: "pointer", color: "#7E2AEA" }}
|
sx={{
|
||||||
onClick={() => remove(type, id)}
|
display: "flex",
|
||||||
component={ClearIcon}
|
justifyContent: "flex-end",
|
||||||
/>
|
height: "100%",
|
||||||
</Box>
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{ pr: "11px", color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(serviceData.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
paddingLeft: upSm ? "24px" : 0,
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{isExpanded &&
|
||||||
|
serviceData.privileges.map(privilege => (
|
||||||
|
<Box
|
||||||
|
key={privilege.tariffId + privilege.privilegeId}
|
||||||
|
sx={{
|
||||||
|
py: upMd ? "10px" : undefined,
|
||||||
|
pt: upMd ? undefined : "15px",
|
||||||
|
pb: upMd ? undefined : "20px",
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
width: "200px",
|
||||||
|
fontSize: upMd ? undefined : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{privilege.name}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "10px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(privilege.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<SvgIcon
|
||||||
|
sx={{ cursor: "pointer", color: "#7E2AEA" }}
|
||||||
|
onClick={() => handleItemDeleteClick(privilege.tariffId)}
|
||||||
|
component={ClearIcon}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
</Box>
|
||||||
</Box>
|
);
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,228 +1,185 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { Typography, Drawer, useMediaQuery, useTheme, Box, IconButton, SvgIcon, Icon } from "@mui/material";
|
import { Typography, Drawer, useMediaQuery, useTheme, Box, IconButton, SvgIcon, Icon } from "@mui/material";
|
||||||
import { IconsCreate } from "@root/lib/IconsCreate";
|
import { IconsCreate } from "@root/lib/IconsCreate";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
|
|
||||||
import { basketStore } from "@root/stores/BasketStore";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import BasketIcon from "../assets/Icons/BasketIcon.svg";
|
import BasketIcon from "../assets/Icons/BasketIcon.svg";
|
||||||
import SectionWrapper from "./SectionWrapper";
|
import SectionWrapper from "./SectionWrapper";
|
||||||
import CustomWrapperDrawer from "./CustomWrapperDrawer";
|
import CustomWrapperDrawer from "./CustomWrapperDrawer";
|
||||||
import CustomButton from "./CustomButton";
|
import CustomButton from "./CustomButton";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { useCart } from "@root/utils/hooks/useCart";
|
||||||
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
|
import { closeCartDrawer, openCartDrawer, useCartStore } from "@root/stores/cart";
|
||||||
|
|
||||||
interface TabPanelProps {
|
|
||||||
index: number;
|
|
||||||
value: number;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
mt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type BasketItem = {
|
|
||||||
name: string;
|
|
||||||
desc: string;
|
|
||||||
id: string;
|
|
||||||
privelegeid: string;
|
|
||||||
amount: number;
|
|
||||||
price: number;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
function TabPanel({ index, value, children, mt }: TabPanelProps) {
|
|
||||||
return (
|
|
||||||
<Box hidden={index !== value} sx={{ mt }}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Drawers() {
|
export default function Drawers() {
|
||||||
const [tabIndex, setTabIndex] = useState<number>(0);
|
const navigate = useNavigate();
|
||||||
const [basketQuantity, setBasketQuantity] = useState<number>();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const { templ, squiz, reducer, open, openDrawer } = basketStore();
|
const isDrawerOpen = useCartStore(state => state.isDrawerOpen);
|
||||||
const theme = useTheme();
|
const cart = useCart();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
|
||||||
|
|
||||||
const newArray: BasketItem = [...Object.values(templ), ...Object.values(squiz), ...Object.values(reducer)];
|
return (
|
||||||
const sum = newArray.reduce((accamulator, { price }) => (accamulator += price), 0);
|
<IconButton sx={{ p: 0 }}>
|
||||||
|
<Typography onClick={openCartDrawer} component="div" sx={{ position: "absolute" }}>
|
||||||
useEffect(() => {
|
<IconsCreate svg={BasketIcon} bgcolor="#F2F3F7" />
|
||||||
setBasketQuantity(Object.keys(templ).length + Object.keys(squiz).length + Object.keys(reducer).length);
|
|
||||||
}, [templ, squiz, reducer]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton sx={{ p: 0 }}>
|
|
||||||
<Typography onClick={open(true)} component="div" sx={{ position: "absolute" }}>
|
|
||||||
<IconsCreate svg={BasketIcon} bgcolor="#F2F3F7" />
|
|
||||||
</Typography>
|
|
||||||
{basketQuantity && (
|
|
||||||
<Icon
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
position: "relative",
|
|
||||||
left: "8px",
|
|
||||||
bottom: "7px",
|
|
||||||
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
backgroundColor: "#7E2AEA",
|
|
||||||
borderRadius: "12px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
fontSize: "12px",
|
|
||||||
mt: "4.5px",
|
|
||||||
width: "100%",
|
|
||||||
height: "9px",
|
|
||||||
color: "white",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{basketQuantity}
|
|
||||||
</Typography>
|
|
||||||
</Icon>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Drawer anchor={"right"} open={openDrawer} onClose={open(false)}>
|
|
||||||
<SectionWrapper
|
|
||||||
maxWidth="lg"
|
|
||||||
sx={{
|
|
||||||
pl: "0px",
|
|
||||||
pr: "0px",
|
|
||||||
width: "450px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
pt: "20px",
|
|
||||||
pb: "20px",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
bgcolor: "#F2F3F7",
|
|
||||||
gap: "10px",
|
|
||||||
pl: "20px",
|
|
||||||
pr: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!upMd && (
|
|
||||||
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
|
||||||
<ArrowBackIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
<Typography
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
fontSize: "18px",
|
|
||||||
lineHeight: "21px",
|
|
||||||
font: "Rubick",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Корзина
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<SvgIcon onClick={open(false)} sx={{ cursor: "pointer" }} component={ClearIcon} />
|
{cart.itemCount && (
|
||||||
</Box>
|
<Icon
|
||||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
component="div"
|
||||||
<TabPanel value={tabIndex} index={0} mt={"10px"}>
|
|
||||||
{Object.keys(templ).length > 0 ? (
|
|
||||||
<CustomWrapperDrawer type="templ" content={Object.values(templ)} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{Object.keys(squiz).length > 0 ? (
|
|
||||||
<CustomWrapperDrawer type="squiz" content={Object.values(squiz)} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{Object.keys(reducer).length > 0 ? (
|
|
||||||
<CustomWrapperDrawer type="reducer" content={Object.values(reducer)} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</TabPanel>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: "40px",
|
|
||||||
pt: upMd ? "30px" : undefined,
|
|
||||||
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: upMd ? "100%" : undefined,
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
|
|
||||||
Итоговая цена
|
|
||||||
</Typography>
|
|
||||||
<Typography color={theme.palette.grey3.main}>
|
|
||||||
Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет
|
|
||||||
Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет
|
|
||||||
Текст-заполнитель
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.grey3.main,
|
|
||||||
pb: "100px",
|
|
||||||
pt: "38px",
|
|
||||||
pl: upMd ? "20px" : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: upMd ? "column" : "row",
|
|
||||||
alignItems: upMd ? "start" : "center",
|
|
||||||
mt: upMd ? "10px" : "30px",
|
|
||||||
gap: "15px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
color={theme.palette.orange.main}
|
|
||||||
sx={{
|
sx={{
|
||||||
textDecoration: "line-through",
|
position: "relative",
|
||||||
order: upMd ? 1 : 2,
|
left: "8px",
|
||||||
|
bottom: "7px",
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
|
borderRadius: "12px",
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
20 190 руб.
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="p1"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: "26px",
|
|
||||||
lineHeight: "31px",
|
|
||||||
order: upMd ? 2 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sum} руб.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<CustomButton
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => navigate("/basket")}
|
|
||||||
sx={{
|
|
||||||
mt: "25px",
|
|
||||||
backgroundColor: theme.palette.brightPurple.main,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Оплатить
|
<Typography
|
||||||
</CustomButton>
|
component="div"
|
||||||
</Box>
|
sx={{
|
||||||
</Box>
|
display: "flex",
|
||||||
</Box>
|
fontSize: "12px",
|
||||||
</SectionWrapper>
|
mt: "4.5px",
|
||||||
</Drawer>
|
width: "100%",
|
||||||
</IconButton>
|
height: "9px",
|
||||||
);
|
color: "white",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cart.itemCount}
|
||||||
|
</Typography>
|
||||||
|
</Icon>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Drawer anchor={"right"} open={isDrawerOpen} onClose={closeCartDrawer}>
|
||||||
|
<SectionWrapper
|
||||||
|
maxWidth="lg"
|
||||||
|
sx={{
|
||||||
|
pl: "0px",
|
||||||
|
pr: "0px",
|
||||||
|
width: "450px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
pt: "20px",
|
||||||
|
pb: "20px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
gap: "10px",
|
||||||
|
pl: "20px",
|
||||||
|
pr: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!upMd && (
|
||||||
|
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
lineHeight: "21px",
|
||||||
|
font: "Rubick",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Корзина
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={closeCartDrawer} sx={{ p: 0 }}>
|
||||||
|
<SvgIcon component={ClearIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||||
|
{cart.services.map(serviceData =>
|
||||||
|
<CustomWrapperDrawer
|
||||||
|
key={serviceData.serviceKey}
|
||||||
|
serviceData={serviceData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: "40px",
|
||||||
|
pt: upMd ? "30px" : undefined,
|
||||||
|
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: upMd ? "100%" : undefined,
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
|
||||||
|
Итоговая цена
|
||||||
|
</Typography>
|
||||||
|
<Typography color={theme.palette.grey3.main}>
|
||||||
|
Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет
|
||||||
|
Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет
|
||||||
|
Текст-заполнитель
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
pb: "100px",
|
||||||
|
pt: "38px",
|
||||||
|
pl: upMd ? "20px" : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: upMd ? "column" : "row",
|
||||||
|
alignItems: upMd ? "start" : "center",
|
||||||
|
mt: upMd ? "10px" : "30px",
|
||||||
|
gap: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
color={theme.palette.orange.main}
|
||||||
|
sx={{
|
||||||
|
textDecoration: "line-through",
|
||||||
|
order: upMd ? 1 : 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(cart.priceBeforeDiscounts / 100)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="p1"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: "26px",
|
||||||
|
lineHeight: "31px",
|
||||||
|
order: upMd ? 2 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(cart.priceAfterDiscounts / 100)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<CustomButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => navigate("/basket")}
|
||||||
|
sx={{
|
||||||
|
mt: "25px",
|
||||||
|
backgroundColor: theme.palette.brightPurple.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Оплатить
|
||||||
|
</CustomButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SectionWrapper>
|
||||||
|
</Drawer>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Box, Button, Container, IconButton, Typography, useTheme } from "@mui/material";
|
import { Box, Button, Container, IconButton, Typography, useTheme } from "@mui/material";
|
||||||
|
|
||||||
import SectionWrapper from "../SectionWrapper";
|
import SectionWrapper from "../SectionWrapper";
|
||||||
import { basketStore } from "@stores/BasketStore";
|
|
||||||
|
|
||||||
import LogoutIcon from "../icons/LogoutIcon";
|
import LogoutIcon from "../icons/LogoutIcon";
|
||||||
import WalletIcon from "../icons/WalletIcon";
|
import WalletIcon from "../icons/WalletIcon";
|
||||||
import CustomAvatar from "./Avatar";
|
import CustomAvatar from "./Avatar";
|
||||||
@ -26,14 +22,6 @@ export default function NavbarFull({ isLoggedIn }: Props) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useUserStore((state) => state.user);
|
const user = useUserStore((state) => state.user);
|
||||||
|
|
||||||
const { open } = basketStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (location.pathname === "/basket") {
|
|
||||||
open(false);
|
|
||||||
}
|
|
||||||
}, [location.pathname, open]);
|
|
||||||
|
|
||||||
async function handleLogoutClick() {
|
async function handleLogoutClick() {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
|
@ -1,25 +1,16 @@
|
|||||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { basketStore } from "@root/stores/BasketStore";
|
|
||||||
|
|
||||||
import CustomButton from "./CustomButton";
|
import CustomButton from "./CustomButton";
|
||||||
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
|
|
||||||
export default function TotalPrice() {
|
interface Props {
|
||||||
|
price: number;
|
||||||
|
priceWithDiscounts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TotalPrice({price,priceWithDiscounts}:Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const { templ, squiz, reducer } = basketStore();
|
|
||||||
|
|
||||||
type BasketItem = {
|
|
||||||
name: string;
|
|
||||||
desc: string;
|
|
||||||
id: string;
|
|
||||||
privelegeid: string;
|
|
||||||
amount: number;
|
|
||||||
price: number;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
const newArray: BasketItem = [...Object.values(templ), ...Object.values(squiz), ...Object.values(reducer)];
|
|
||||||
|
|
||||||
const sum = newArray.reduce((accamulator, { price }) => (accamulator += price), 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -71,7 +62,7 @@ export default function TotalPrice() {
|
|||||||
order: upMd ? 1 : 2,
|
order: upMd ? 1 : 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
20 190 руб.
|
{currencyFormatter.format(price / 100)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="p1"
|
variant="p1"
|
||||||
@ -82,7 +73,7 @@ export default function TotalPrice() {
|
|||||||
order: upMd ? 2 : 1,
|
order: upMd ? 2 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sum} руб.
|
{currencyFormatter.format(priceWithDiscounts / 100)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
|
120
src/components/passwordInput.tsx
Normal file
120
src/components/passwordInput.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
InputLabel,
|
||||||
|
SxProps,
|
||||||
|
TextField,
|
||||||
|
TextFieldProps,
|
||||||
|
Theme,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import * as React from 'react';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
bold?: boolean;
|
||||||
|
gap?: string;
|
||||||
|
color?: string;
|
||||||
|
FormInputSx?: SxProps<Theme>;
|
||||||
|
TextfieldProps: TextFieldProps;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
bold = false,
|
||||||
|
gap = "10px",
|
||||||
|
onChange,
|
||||||
|
TextfieldProps,
|
||||||
|
color,
|
||||||
|
FormInputSx,
|
||||||
|
}: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
|
||||||
|
const labelFont = upMd
|
||||||
|
? bold
|
||||||
|
? theme.typography.p1
|
||||||
|
: { ...theme.typography.body1, fontWeight: 500 }
|
||||||
|
: theme.typography.body2;
|
||||||
|
|
||||||
|
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
|
|
||||||
|
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||||
|
|
||||||
|
const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
sx={{
|
||||||
|
gap,
|
||||||
|
// mt: "10px",
|
||||||
|
...FormInputSx,
|
||||||
|
position: "relative"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputLabel
|
||||||
|
shrink
|
||||||
|
htmlFor={id}
|
||||||
|
sx={{
|
||||||
|
position: "inherit",
|
||||||
|
color: "black",
|
||||||
|
transform: "none",
|
||||||
|
...labelFont,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
{...TextfieldProps}
|
||||||
|
fullWidth
|
||||||
|
id={id}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label="toggle password visibility"
|
||||||
|
onClick={handleClickShowPassword}
|
||||||
|
onMouseDown={handleMouseDownPassword}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
sx: {
|
||||||
|
backgroundColor: color,
|
||||||
|
borderRadius: "8px",
|
||||||
|
height: "48px",
|
||||||
|
py: 0,
|
||||||
|
color: "black",
|
||||||
|
...placeholderFont,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={onChange}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
19
src/model/cart.ts
Normal file
19
src/model/cart.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export type PrivilegeCartData = {
|
||||||
|
tariffId: string;
|
||||||
|
privilegeId: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceCartData = {
|
||||||
|
serviceKey: string;
|
||||||
|
privileges: PrivilegeCartData[];
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CartData = {
|
||||||
|
services: ServiceCartData[];
|
||||||
|
priceBeforeDiscounts: number;
|
||||||
|
priceAfterDiscounts: number;
|
||||||
|
itemCount: number;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { PrivilegeWithAmount } from "./privilege";
|
import { PrivilegeWithAmount, PrivilegeWithoutPrice } from "./privilege";
|
||||||
|
|
||||||
|
|
||||||
export type CustomTariffUserValues = Record<string, number>;
|
export type CustomTariffUserValues = Record<string, number>;
|
||||||
@ -15,4 +15,6 @@ export interface CustomTariff {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
isDeleted?: boolean;
|
isDeleted?: boolean;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };
|
@ -9,6 +9,7 @@ export interface GetTariffsResponse {
|
|||||||
export interface Tariff {
|
export interface Tariff {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
/** Кастомная цена, undefined если isCustom === true */
|
||||||
price?: number;
|
price?: number;
|
||||||
isCustom: boolean;
|
isCustom: boolean;
|
||||||
privilegies: PrivilegeWithAmount[];
|
privilegies: PrivilegeWithAmount[];
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import CustomButton from "@components/CustomButton";
|
import CustomButton from "@components/CustomButton";
|
||||||
import InputTextfield from "@components/InputTextfield";
|
import InputTextfield from "@components/InputTextfield";
|
||||||
|
import PasswordInput from "@components/passwordInput";
|
||||||
import SectionWrapper from "@components/SectionWrapper";
|
import SectionWrapper from "@components/SectionWrapper";
|
||||||
import ComplexNavText from "@root/components/ComplexNavText";
|
import ComplexNavText from "@root/components/ComplexNavText";
|
||||||
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user";
|
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user";
|
||||||
@ -29,7 +30,7 @@ export default function AccountSettings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function handleSendDataClick() {
|
function handleSendDataClick() {
|
||||||
sendUserData().then(result => {
|
sendUserData().then(() => {
|
||||||
enqueueSnackbar("Информация обновлена");
|
enqueueSnackbar("Информация обновлена");
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
const message = getMessageFromFetchError(error);
|
const message = getMessageFromFetchError(error);
|
||||||
@ -46,7 +47,6 @@ export default function AccountSettings() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DocumentsDialog />
|
<DocumentsDialog />
|
||||||
<ComplexNavText text1="Настройки аккаунта" />
|
|
||||||
<Typography variant="h4" mt="20px">Настройки аккаунта</Typography>
|
<Typography variant="h4" mt="20px">Настройки аккаунта</Typography>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
mt: "40px",
|
mt: "40px",
|
||||||
@ -145,13 +145,13 @@ export default function AccountSettings() {
|
|||||||
label="Телефон"
|
label="Телефон"
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
/>
|
/>
|
||||||
<InputTextfield
|
<PasswordInput
|
||||||
TextfieldProps={{
|
TextfieldProps={{
|
||||||
placeholder: "Не менее 8 символов",
|
placeholder: "Не менее 8 символов",
|
||||||
value: fields.password.value || "",
|
value: fields.password.value || "",
|
||||||
helperText: fields.password.touched && fields.password.error,
|
helperText: fields.password.touched && fields.password.error,
|
||||||
error: fields.password.touched && Boolean(fields.password.error),
|
error: fields.password.touched && Boolean(fields.password.error),
|
||||||
type: "password",
|
autoComplete: "new-password"
|
||||||
}}
|
}}
|
||||||
onChange={e => setSettingsField("password", e.target.value)}
|
onChange={e => setSettingsField("password", e.target.value)}
|
||||||
id="password"
|
id="password"
|
||||||
|
@ -1,72 +1,54 @@
|
|||||||
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import SectionWrapper from "@components/SectionWrapper";
|
import SectionWrapper from "@components/SectionWrapper";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import { useState } from "react";
|
|
||||||
import TotalPrice from "@components/TotalPrice";
|
import TotalPrice from "@components/TotalPrice";
|
||||||
import { basketStore } from "@root/stores/BasketStore";
|
|
||||||
import CustomWrapper from "./CustomWrapper";
|
import CustomWrapper from "./CustomWrapper";
|
||||||
import ComplexNavText from "@root/components/ComplexNavText";
|
import ComplexNavText from "@root/components/ComplexNavText";
|
||||||
|
import { useCart } from "@root/utils/hooks/useCart";
|
||||||
|
|
||||||
interface TabPanelProps {
|
|
||||||
index: number;
|
|
||||||
value: number;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
mt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabPanel({ index, value, children, mt }: TabPanelProps) {
|
|
||||||
return (
|
|
||||||
<Box hidden={index !== value} sx={{ mt }}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Basket() {
|
export default function Basket() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
const cart = useCart();
|
||||||
|
|
||||||
const [tabIndex, setTabIndex] = useState<number>(0);
|
return (
|
||||||
const { templ, squiz, reducer, open } = basketStore();
|
<SectionWrapper
|
||||||
|
maxWidth="lg"
|
||||||
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
sx={{
|
||||||
setTabIndex(newValue);
|
mt: upMd ? "25px" : "20px",
|
||||||
};
|
mb: upMd ? "70px" : "37px",
|
||||||
|
}}
|
||||||
open(false);
|
>
|
||||||
|
{upMd && <ComplexNavText text1="Все тарифы — " text2="Корзина" />}
|
||||||
return (
|
<Box
|
||||||
<SectionWrapper
|
sx={{
|
||||||
maxWidth="lg"
|
mt: "20px",
|
||||||
sx={{
|
mb: upMd ? "40px" : "20px",
|
||||||
mt: upMd ? "25px" : "20px",
|
display: "flex",
|
||||||
mb: upMd ? "70px" : "37px",
|
gap: "10px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{upMd && <ComplexNavText text1="Все тарифы — " text2="Корзина" />}
|
{!upMd && (
|
||||||
<Box
|
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||||
sx={{
|
<ArrowBackIcon />
|
||||||
mt: "20px",
|
</IconButton>
|
||||||
mb: upMd ? "40px" : "20px",
|
)}
|
||||||
display: "flex",
|
<Typography component="h4" variant="h4">
|
||||||
gap: "10px",
|
Корзина
|
||||||
}}
|
</Typography>
|
||||||
>
|
</Box>
|
||||||
{!upMd && (
|
<Box sx={{
|
||||||
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
mt: upMd ? "27px" : "10px",
|
||||||
<ArrowBackIcon />
|
}}>
|
||||||
</IconButton>
|
{cart.services.map(serviceData =>
|
||||||
)}
|
<CustomWrapper
|
||||||
<Typography component="h4" variant="h4">
|
key={serviceData.serviceKey}
|
||||||
Корзина
|
serviceData={serviceData}
|
||||||
</Typography>
|
/>
|
||||||
</Box>
|
)}
|
||||||
<TabPanel value={tabIndex} index={0} mt={upMd ? "27px" : "10px"}>
|
</Box>
|
||||||
{Object.keys(templ).length > 0 ? <CustomWrapper type="templ" content={templ} /> : <></>}
|
<TotalPrice price={cart.priceBeforeDiscounts} priceWithDiscounts={cart.priceAfterDiscounts} />
|
||||||
{Object.keys(squiz).length > 0 ? <CustomWrapper type="squiz" content={squiz} /> : <></>}
|
</SectionWrapper>
|
||||||
{Object.keys(reducer).length > 0 ? <CustomWrapper type="reducer" content={reducer} /> : <></>}
|
);
|
||||||
</TabPanel>
|
|
||||||
<TotalPrice />
|
|
||||||
</SectionWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,177 +1,176 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
|
||||||
import ExpandIcon from "@components/icons/ExpandIcon";
|
import ExpandIcon from "@components/icons/ExpandIcon";
|
||||||
|
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
import { basketStore } from "@root/stores/BasketStore";
|
|
||||||
import { cardShadow } from "@root/utils/themes/shadow";
|
import { cardShadow } from "@root/utils/themes/shadow";
|
||||||
|
import { ServiceCartData } from "@root/model/cart";
|
||||||
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
|
import { removeTariffFromCart } from "@root/stores/user";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
|
|
||||||
interface Templ {
|
|
||||||
name: string;
|
const name: Record<string, string> = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Сокращатель ссылок" };
|
||||||
desc: string;
|
|
||||||
id: string;
|
|
||||||
privelegeid?: string;
|
|
||||||
amount: number;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "templ" | "squiz" | "reducer";
|
serviceData: ServiceCartData;
|
||||||
content: Record<string, Templ>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomWrapper({ type, content }: Props) {
|
export default function CustomWrapper({ serviceData }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
const { remove } = basketStore();
|
function handleItemDeleteClick(tariffId: string) {
|
||||||
|
removeTariffFromCart(tariffId).then(() => {
|
||||||
|
enqueueSnackbar("Тариф удален");
|
||||||
|
}).catch(error => {
|
||||||
|
const message = getMessageFromFetchError(error);
|
||||||
|
if (message) enqueueSnackbar(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const totalSum = Object.values(content).reduce((accamulator, { price }) => (accamulator += price), 0);
|
return (
|
||||||
const name: Record<string, string> = { templ: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "12px",
|
|
||||||
boxShadow: cardShadow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "white",
|
|
||||||
"&:first-of-type": {
|
|
||||||
borderTopLeftRadius: "12px",
|
|
||||||
borderTopRightRadius: "12px",
|
|
||||||
},
|
|
||||||
"&:last-of-type": {
|
|
||||||
borderBottomLeftRadius: "12px",
|
|
||||||
borderBottomRightRadius: "12px",
|
|
||||||
},
|
|
||||||
"&:not(:last-of-type)": {
|
|
||||||
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setIsExpanded((prev) => !prev)}
|
sx={{
|
||||||
sx={{
|
overflow: "hidden",
|
||||||
height: "72px",
|
borderRadius: "12px",
|
||||||
px: "20px",
|
boxShadow: cardShadow,
|
||||||
|
}}
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
cursor: "pointer",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: upMd ? "20px" : "16px",
|
|
||||||
lineHeight: upMd ? undefined : "19px",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
px: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name[type]}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
height: "100%",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: upSm ? "111px" : "17px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography sx={{ color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}>
|
|
||||||
{totalSum} руб.
|
|
||||||
</Typography>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
|
||||||
borderLeft: upSm ? "1px solid #9A9AAF" : "none",
|
|
||||||
paddingLeft: upSm ? "24px" : 0,
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExpandIcon isExpanded={isExpanded} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{isExpanded &&
|
|
||||||
Object.values(content).map(({ desc, id, privelegeid, amount, price }, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
sx={{
|
|
||||||
px: "20px",
|
|
||||||
py: upMd ? "25px" : undefined,
|
|
||||||
pt: upMd ? undefined : "15px",
|
|
||||||
pb: upMd ? undefined : "25px",
|
|
||||||
backgroundColor: "#F1F2F6",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "15px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: upMd ? undefined : "16px",
|
backgroundColor: "white",
|
||||||
lineHeight: upMd ? undefined : "19px",
|
"&:first-of-type": {
|
||||||
color: theme.palette.grey3.main,
|
borderTopLeftRadius: "12px",
|
||||||
|
borderTopRightRadius: "12px",
|
||||||
|
},
|
||||||
|
"&:last-of-type": {
|
||||||
|
borderBottomLeftRadius: "12px",
|
||||||
|
borderBottomRightRadius: "12px",
|
||||||
|
},
|
||||||
|
"&:not(:last-of-type)": {
|
||||||
|
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{desc}
|
<Box
|
||||||
</Typography>
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: "10px",
|
|
||||||
alignItems: "center",
|
|
||||||
width: upSm ? "195px" : "123px",
|
|
||||||
marginRight: upSm ? "65px" : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.grey3.main,
|
|
||||||
fontSize: upSm ? "20px" : "16px",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{price} руб.
|
|
||||||
</Typography>
|
|
||||||
{upSm ? (
|
|
||||||
<Typography
|
|
||||||
component="div"
|
|
||||||
onClick={() => remove(type, id)}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.text.secondary,
|
height: "72px",
|
||||||
borderBottom: `1px solid ${theme.palette.text.secondary}`,
|
px: "20px",
|
||||||
width: "max-content",
|
|
||||||
lineHeight: "19px",
|
display: "flex",
|
||||||
cursor: "pointer",
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
cursor: "pointer",
|
||||||
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Удалить
|
<Typography
|
||||||
</Typography>
|
sx={{
|
||||||
) : (
|
fontSize: upMd ? "20px" : "16px",
|
||||||
<SvgIcon onClick={() => remove(type, id)} component={ClearIcon}></SvgIcon>
|
lineHeight: upMd ? undefined : "19px",
|
||||||
)}
|
fontWeight: 500,
|
||||||
</Box>
|
color: theme.palette.text.secondary,
|
||||||
|
px: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name[serviceData.serviceKey]}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
height: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: upSm ? "111px" : "17px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}>
|
||||||
|
{currencyFormatter.format(serviceData.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderLeft: upSm ? "1px solid #9A9AAF" : "none",
|
||||||
|
paddingLeft: upSm ? "24px" : 0,
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandIcon isExpanded={isExpanded} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{isExpanded &&
|
||||||
|
serviceData.privileges.map(privilege => (
|
||||||
|
<Box
|
||||||
|
key={privilege.tariffId + privilege.privilegeId}
|
||||||
|
sx={{
|
||||||
|
px: "20px",
|
||||||
|
py: upMd ? "25px" : undefined,
|
||||||
|
pt: upMd ? undefined : "15px",
|
||||||
|
pb: upMd ? undefined : "25px",
|
||||||
|
backgroundColor: "#F1F2F6",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: upMd ? undefined : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{privilege.name}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "10px",
|
||||||
|
alignItems: "center",
|
||||||
|
width: upSm ? "195px" : "123px",
|
||||||
|
marginRight: upSm ? "65px" : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: upSm ? "20px" : "16px",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(privilege.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
{upSm ? (
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
onClick={() => handleItemDeleteClick(privilege.tariffId)}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
borderBottom: `1px solid ${theme.palette.text.secondary}`,
|
||||||
|
width: "max-content",
|
||||||
|
lineHeight: "19px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<SvgIcon onClick={() => handleItemDeleteClick(privilege.tariffId)} component={ClearIcon}></SvgIcon>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
</Box>
|
||||||
</Box>
|
);
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,78 +1,31 @@
|
|||||||
import { Box, SxProps, Theme } from "@mui/material";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useNavigate } from "react-router-dom";
|
import TariffCard from "./TariffCard";
|
||||||
|
import NumberIcon from "@root/components/NumberIcon";
|
||||||
|
|
||||||
import CustomButton from "@components/CustomButton";
|
|
||||||
|
|
||||||
import { IconsCreate } from "../../lib/IconsCreate";
|
export default function FreeTariffCard() {
|
||||||
|
|
||||||
import ZeroIcons from "../../assets/Icons/ZeroIcons.svg";
|
return (
|
||||||
|
<TariffCard
|
||||||
interface Props {
|
icon={<NumberIcon
|
||||||
headerText: string;
|
number={0}
|
||||||
text: string;
|
color="#7E2AEA"
|
||||||
money?: string;
|
backgroundColor="white"
|
||||||
sx: SxProps<Theme>;
|
/>}
|
||||||
href: string;
|
buttonText="Выбрать"
|
||||||
}
|
headerText="бесплатно"
|
||||||
|
text="Текст-заполнитель — это текст, который имеет "
|
||||||
export default function FreeTariffCard({ headerText, text, sx, href, money = "0" }: Props) {
|
onButtonClick={undefined}
|
||||||
const navigate = useNavigate();
|
price={<Typography variant="price" color="white">0 руб.</Typography>}
|
||||||
const icon = <IconsCreate svg={ZeroIcons} bgcolor="#FFFFFF" />;
|
sx={{
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
return (
|
color: "white",
|
||||||
<Box
|
maxWidth: "360px",
|
||||||
component="div"
|
}}
|
||||||
sx={{
|
buttonSx={{
|
||||||
maxWidth: "360px",
|
color: "white",
|
||||||
width: "360px",
|
borderColor: "white",
|
||||||
bgcolor: "white",
|
}}
|
||||||
borderRadius: "12px",
|
/>
|
||||||
display: "flex",
|
);
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "start",
|
|
||||||
p: "20px",
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
component="div"
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-around",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography component="div">{icon}</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="h5"
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "right",
|
|
||||||
width: "100%",
|
|
||||||
color: "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{money} руб.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography variant="h5" sx={{ mt: "14px", mb: "10px" }}>
|
|
||||||
{headerText}
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ minHeight: "calc(1.185*2em)" }}>{text}</Typography>
|
|
||||||
<CustomButton
|
|
||||||
onClick={() => navigate(href)}
|
|
||||||
variant="outlined"
|
|
||||||
sx={{
|
|
||||||
color: "white",
|
|
||||||
borderColor: "white",
|
|
||||||
mt: "33px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Подробнее
|
|
||||||
</CustomButton>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ export default function TariffCard({ icon, headerText, text, sx, buttonText, onB
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
maxWidth: "360px",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
bgcolor: "white",
|
bgcolor: "white",
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
|
@ -46,6 +46,7 @@ export default function Tariffs() {
|
|||||||
text="безлимит на 1 месяц , 3 , 6 , 12"
|
text="безлимит на 1 месяц , 3 , 6 , 12"
|
||||||
buttonText="Подробнее"
|
buttonText="Подробнее"
|
||||||
onButtonClick={() => navigate("time")}
|
onButtonClick={() => navigate("time")}
|
||||||
|
sx={{ maxWidth: "360px" }}
|
||||||
/>
|
/>
|
||||||
<TariffCard
|
<TariffCard
|
||||||
icon={<PieChartIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
|
icon={<PieChartIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
|
||||||
@ -53,6 +54,7 @@ export default function Tariffs() {
|
|||||||
text="200 шаблонов, 1000 шаблонов, 5000 шаблонов, 10 000 шаблонов"
|
text="200 шаблонов, 1000 шаблонов, 5000 шаблонов, 10 000 шаблонов"
|
||||||
buttonText="Подробнее"
|
buttonText="Подробнее"
|
||||||
onButtonClick={() => navigate("volume")}
|
onButtonClick={() => navigate("volume")}
|
||||||
|
sx={{ maxWidth: "360px" }}
|
||||||
/>
|
/>
|
||||||
<TariffCard
|
<TariffCard
|
||||||
icon={<CustomIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
|
icon={<CustomIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
|
||||||
@ -60,6 +62,7 @@ export default function Tariffs() {
|
|||||||
text="Текст-заполнитель — это текст, который имеет "
|
text="Текст-заполнитель — это текст, который имеет "
|
||||||
buttonText="Подробнее"
|
buttonText="Подробнее"
|
||||||
onButtonClick={() => navigate("/tariffconstructor")}
|
onButtonClick={() => navigate("/tariffconstructor")}
|
||||||
|
sx={{ maxWidth: "360px" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography component="div">
|
<Typography component="div">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { Box, Tabs, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Tabs, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import SectionWrapper from "@components/SectionWrapper";
|
import SectionWrapper from "@components/SectionWrapper";
|
||||||
import ComplexNavText from "@root/components/ComplexNavText";
|
import ComplexNavText from "@root/components/ComplexNavText";
|
||||||
import { useTariffs } from "@root/utils/hooks/useTariffs";
|
import { useTariffs } from "@root/utils/hooks/useTariffs";
|
||||||
import { setTariffs, useTariffStore } from "@root/stores/tariffs";
|
import { updateTariffs, useTariffStore } from "@root/stores/tariffs";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { CustomTab } from "@root/components/CustomTab";
|
import { CustomTab } from "@root/components/CustomTab";
|
||||||
import TariffCard from "./TariffCard";
|
import TariffCard from "./TariffCard";
|
||||||
@ -12,6 +12,8 @@ import NumberIcon from "@root/components/NumberIcon";
|
|||||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
import { calcTariffPrices } from "@root/utils/calcTariffPrices";
|
import { calcTariffPrices } from "@root/utils/calcTariffPrices";
|
||||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
|
import FreeTariffCard from "./FreeTariffCard";
|
||||||
|
import { addTariffToCart } from "@root/stores/user";
|
||||||
|
|
||||||
|
|
||||||
export default function TariffPage() {
|
export default function TariffPage() {
|
||||||
@ -26,20 +28,58 @@ export default function TariffPage() {
|
|||||||
const StepperText: Record<string, string> = { volume: "Тарифы на объём", time: "Тарифы на время" };
|
const StepperText: Record<string, string> = { volume: "Тарифы на объём", time: "Тарифы на время" };
|
||||||
|
|
||||||
useTariffs({
|
useTariffs({
|
||||||
url: "https://admin.pena.digital/strator/tariff",
|
|
||||||
apiPage: 0,
|
apiPage: 0,
|
||||||
tariffsPerPage: 100,
|
tariffsPerPage: 100,
|
||||||
onNewTariffs: setTariffs,
|
onNewTariffs: updateTariffs,
|
||||||
onError: useCallback(error => {
|
onError: error => {
|
||||||
const errorMessage = getMessageFromFetchError(error);
|
const errorMessage = getMessageFromFetchError(error);
|
||||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||||
}, [])
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleTariffItemClick(tariffId: string) {
|
||||||
|
addTariffToCart(tariffId).then(() => {
|
||||||
|
enqueueSnackbar("Тариф добавлен в корзину");
|
||||||
|
}).catch(error => {
|
||||||
|
const message = getMessageFromFetchError(error);
|
||||||
|
if (message) enqueueSnackbar(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const filteredTariffs = tariffs.filter(tariff => {
|
const filteredTariffs = tariffs.filter(tariff => {
|
||||||
return tariff.privilegies.map(p => p.type).includes("day") === (unit === "time");
|
return tariff.privilegies.map(p => p.type).includes("day") === (unit === "time");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tariffElements = filteredTariffs.map((tariff, index) => {
|
||||||
|
const { price, priceWithDiscounts } = calcTariffPrices(tariff);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TariffCard
|
||||||
|
key={tariff._id}
|
||||||
|
icon={<NumberIcon
|
||||||
|
number={(index + 1) % 10}
|
||||||
|
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
|
||||||
|
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
|
||||||
|
/>}
|
||||||
|
buttonText="Выбрать"
|
||||||
|
headerText={tariff.name}
|
||||||
|
text={tariff.privilegies.map(p => `${p.name} - ${p.amount}`)}
|
||||||
|
onButtonClick={() => handleTariffItemClick(tariff._id)}
|
||||||
|
price={<>
|
||||||
|
{price !== undefined && price !== priceWithDiscounts &&
|
||||||
|
<Typography variant="oldPrice">{currencyFormatter.format(price / 100)}</Typography>
|
||||||
|
}
|
||||||
|
{priceWithDiscounts !== undefined &&
|
||||||
|
<Typography variant="price">{currencyFormatter.format(priceWithDiscounts / 100)}</Typography>
|
||||||
|
}
|
||||||
|
</>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tariffElements.length < 6) tariffElements.push(<FreeTariffCard key="free_tariff_card" />);
|
||||||
|
else tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWrapper
|
<SectionWrapper
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
@ -72,52 +112,7 @@ export default function TariffPage() {
|
|||||||
gap: "40px",
|
gap: "40px",
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
||||||
}}>
|
}}>
|
||||||
{filteredTariffs.map((tariff, index) => {
|
{tariffElements}
|
||||||
const { price, priceWithDiscounts } = calcTariffPrices(tariff);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TariffCard
|
|
||||||
key={tariff._id}
|
|
||||||
icon={<NumberIcon
|
|
||||||
number={(index + 1) % 10}
|
|
||||||
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
|
|
||||||
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
|
|
||||||
/>}
|
|
||||||
buttonText="Выбрать"
|
|
||||||
headerText={tariff.name}
|
|
||||||
text={tariff.privilegies.map(p => `${p.name} - ${p.amount}`)}
|
|
||||||
onButtonClick={undefined}
|
|
||||||
price={<>
|
|
||||||
{price !== undefined && price !== priceWithDiscounts &&
|
|
||||||
<Typography variant="oldPrice">{currencyFormatter.format(price / 100)}</Typography>
|
|
||||||
}
|
|
||||||
{priceWithDiscounts !== undefined &&
|
|
||||||
<Typography variant="price">{currencyFormatter.format(priceWithDiscounts / 100)}</Typography>
|
|
||||||
}
|
|
||||||
</>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<TariffCard
|
|
||||||
icon={<NumberIcon
|
|
||||||
number={0}
|
|
||||||
color="#7E2AEA"
|
|
||||||
backgroundColor="white"
|
|
||||||
/>}
|
|
||||||
buttonText="Выбрать"
|
|
||||||
headerText="бесплатно"
|
|
||||||
text="Текст-заполнитель — это текст, который имеет "
|
|
||||||
onButtonClick={undefined}
|
|
||||||
price={<Typography variant="price" color="white">0 руб.</Typography>}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#7E2AEA",
|
|
||||||
color: "white",
|
|
||||||
}}
|
|
||||||
buttonSx={{
|
|
||||||
color: "white",
|
|
||||||
borderColor: "white",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ import { setUserId, useUserStore } from "@root/stores/user";
|
|||||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
import { makeRequest } from "@frontend/kitui";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { cardShadow } from "@root/utils/themes/shadow";
|
import { cardShadow } from "@root/utils/themes/shadow";
|
||||||
|
import PasswordInput from "@root/components/passwordInput";
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
login: string;
|
login: string;
|
||||||
@ -147,7 +148,7 @@ export default function SigninDialog() {
|
|||||||
label="Логин"
|
label="Логин"
|
||||||
gap={upMd ? "10px" : "10px"}
|
gap={upMd ? "10px" : "10px"}
|
||||||
/>
|
/>
|
||||||
<InputTextfield
|
<PasswordInput
|
||||||
TextfieldProps={{
|
TextfieldProps={{
|
||||||
value: formik.values.password,
|
value: formik.values.password,
|
||||||
placeholder: "Не менее 8 символов",
|
placeholder: "Не менее 8 символов",
|
||||||
|
@ -14,6 +14,7 @@ import { setUserId, useUserStore } from "@root/stores/user";
|
|||||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||||
import { makeRequest } from "@frontend/kitui";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { cardShadow } from "@root/utils/themes/shadow";
|
import { cardShadow } from "@root/utils/themes/shadow";
|
||||||
|
import PasswordInput from "@root/components/passwordInput";
|
||||||
|
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
@ -50,7 +51,7 @@ export default function SignupDialog() {
|
|||||||
body: {
|
body: {
|
||||||
login: values.login.trim(),
|
login: values.login.trim(),
|
||||||
password: values.password.trim(),
|
password: values.password.trim(),
|
||||||
phoneNumber: "-",
|
phoneNumber: "+7",
|
||||||
},
|
},
|
||||||
useToken: false,
|
useToken: false,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@ -146,14 +147,14 @@ export default function SignupDialog() {
|
|||||||
label="Логин"
|
label="Логин"
|
||||||
gap={upMd ? "10px" : "10px"}
|
gap={upMd ? "10px" : "10px"}
|
||||||
/>
|
/>
|
||||||
<InputTextfield
|
<PasswordInput
|
||||||
TextfieldProps={{
|
TextfieldProps={{
|
||||||
value: formik.values.password,
|
value: formik.values.password,
|
||||||
placeholder: "Не менее 8 символов",
|
placeholder: "Не менее 8 символов",
|
||||||
onBlur: formik.handleBlur,
|
onBlur: formik.handleBlur,
|
||||||
error: formik.touched.password && Boolean(formik.errors.password),
|
error: formik.touched.password && Boolean(formik.errors.password),
|
||||||
helperText: formik.touched.password && formik.errors.password,
|
helperText: formik.touched.password && formik.errors.password,
|
||||||
type: "password",
|
autoComplete: "new-password"
|
||||||
}}
|
}}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
color="#F2F3F7"
|
color="#F2F3F7"
|
||||||
@ -161,14 +162,14 @@ export default function SignupDialog() {
|
|||||||
label="Пароль"
|
label="Пароль"
|
||||||
gap={upMd ? "10px" : "10px"}
|
gap={upMd ? "10px" : "10px"}
|
||||||
/>
|
/>
|
||||||
<InputTextfield
|
<PasswordInput
|
||||||
TextfieldProps={{
|
TextfieldProps={{
|
||||||
value: formik.values.repeatPassword,
|
value: formik.values.repeatPassword,
|
||||||
placeholder: "Не менее 8 символов",
|
placeholder: "Не менее 8 символов",
|
||||||
onBlur: formik.handleBlur,
|
onBlur: formik.handleBlur,
|
||||||
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
|
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
|
||||||
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
|
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
|
||||||
type: "password",
|
autoComplete: "new-password"
|
||||||
}}
|
}}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
color="#F2F3F7"
|
color="#F2F3F7"
|
||||||
|
80
src/stores/cart.ts
Normal file
80
src/stores/cart.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { CartData } from "@root/model/cart";
|
||||||
|
import { Tariff } from "@root/model/tariff";
|
||||||
|
import { calcCart } from "@root/utils/calcCart";
|
||||||
|
import { produce } from "immer";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { devtools } from "zustand/middleware";
|
||||||
|
|
||||||
|
|
||||||
|
interface CartStore {
|
||||||
|
cartTariffMap: Record<string, Tariff | "loading" | "not found">;
|
||||||
|
cart: CartData;
|
||||||
|
isDrawerOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCartStore = create<CartStore>()(
|
||||||
|
devtools(
|
||||||
|
(get, set) => ({
|
||||||
|
cartTariffMap: {},
|
||||||
|
cart: {
|
||||||
|
services: [],
|
||||||
|
priceBeforeDiscounts: 0,
|
||||||
|
priceAfterDiscounts: 0,
|
||||||
|
itemCount: 0,
|
||||||
|
},
|
||||||
|
isDrawerOpen: false,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "Cart",
|
||||||
|
enabled: process.env.NODE_ENV === "development",
|
||||||
|
trace: true,
|
||||||
|
actionsBlacklist: "rejected",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setCartTariffStatus = (tariffId: string, status: "loading" | "not found") => useCartStore.setState(
|
||||||
|
produce<CartStore>(state => {
|
||||||
|
state.cartTariffMap[tariffId] = status;
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
type: "setCartTariffStatus",
|
||||||
|
tariffId,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const addCartTariffs = (tariffs: Tariff[]) => useCartStore.setState(
|
||||||
|
produce<CartStore>(state => {
|
||||||
|
tariffs.forEach(tariff => {
|
||||||
|
state.cartTariffMap[tariff._id] = tariff;
|
||||||
|
});
|
||||||
|
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
|
||||||
|
state.cart = calcCart(cartTariffs);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
type: tariffs.length > 0 ? "addCartTariffs" : "rejected",
|
||||||
|
tariffIds: tariffs.map(tariff => tariff._id),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const removeMissingTariffs = (tariffIds: string[]) => useCartStore.setState(
|
||||||
|
produce<CartStore>(state => {
|
||||||
|
for (const key in state.cartTariffMap) {
|
||||||
|
if (!tariffIds.includes(key)) delete state.cartTariffMap[key];
|
||||||
|
}
|
||||||
|
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
|
||||||
|
state.cart = calcCart(cartTariffs);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
type: "removeMissingTariffs",
|
||||||
|
tariffIds,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const openCartDrawer = () => useCartStore.setState({ isDrawerOpen: true });
|
||||||
|
|
||||||
|
export const closeCartDrawer = () => useCartStore.setState({ isDrawerOpen: false });
|
@ -21,7 +21,7 @@ export const useCustomTariffsStore = create<CustomTariffsStore>()(
|
|||||||
summaryPrice: {},
|
summaryPrice: {},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "Custom tariffs store",
|
name: "Custom tariffs",
|
||||||
enabled: process.env.NODE_ENV === "development",
|
enabled: process.env.NODE_ENV === "development",
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -42,7 +42,7 @@ export const setCustomTariffsUserValue = (
|
|||||||
value: number,
|
value: number,
|
||||||
) => useCustomTariffsStore.setState(
|
) => useCustomTariffsStore.setState(
|
||||||
produce<CustomTariffsStore>(state => {
|
produce<CustomTariffsStore>(state => {
|
||||||
if (!state.userValues[serviceKey]) state.userValues[serviceKey] = {};
|
state.userValues[serviceKey] ??= {};
|
||||||
state.userValues[serviceKey][privilegeId] = value;
|
state.userValues[serviceKey][privilegeId] = value;
|
||||||
|
|
||||||
const sum = state.customTariffs[serviceKey].reduce((acc, tariff) => {
|
const sum = state.customTariffs[serviceKey].reduce((acc, tariff) => {
|
||||||
|
@ -14,7 +14,7 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
discounts: mockDiscounts
|
discounts: mockDiscounts
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "Message store (marketplace)",
|
name: "Discounts",
|
||||||
enabled: process.env.NODE_ENV === "development",
|
enabled: process.env.NODE_ENV === "development",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -21,16 +21,16 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
isPreventAutoscroll: false,
|
isPreventAutoscroll: false,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "Message store (marketplace)"
|
name: "Messages"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
|
export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
|
||||||
const state = useMessageStore.getState();
|
const messages = useMessageStore.getState().messages;
|
||||||
const messageIdToMessageMap: { [messageId: string]: TicketMessage; } = {};
|
const messageIdToMessageMap: { [messageId: string]: TicketMessage; } = {};
|
||||||
|
|
||||||
[...state.messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message);
|
[...messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message);
|
||||||
|
|
||||||
const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime);
|
const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime);
|
||||||
|
|
||||||
|
@ -13,10 +13,34 @@ export const useTariffStore = create<TariffStore>()(
|
|||||||
tariffs: [],
|
tariffs: [],
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "Tariff store",
|
name: "Tariffs",
|
||||||
enabled: process.env.NODE_ENV === "development",
|
enabled: process.env.NODE_ENV === "development",
|
||||||
|
trace: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const setTariffs = (tariffs: Tariff[]) => useTariffStore.setState({tariffs})
|
export const updateTariffs = (tariffs: TariffStore["tariffs"]) => useTariffStore.setState(
|
||||||
|
state => {
|
||||||
|
const tariffMap: Record<string, Tariff> = {};
|
||||||
|
|
||||||
|
[...state.tariffs, ...tariffs].forEach(tariff => tariffMap[tariff._id] = tariff);
|
||||||
|
|
||||||
|
const sortedTariffs = Object.values(tariffMap).sort(sortTariffsByCreatedAt);
|
||||||
|
|
||||||
|
return { tariffs: sortedTariffs };
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
type: "updateTariffs",
|
||||||
|
tariffsLength: tariffs.length,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function sortTariffsByCreatedAt(tariff1: Tariff, tariff2: Tariff) {
|
||||||
|
if (!tariff1.createdAt || !tariff2.createdAt) throw new Error("Trying to sort tariffs without createdAt field");
|
||||||
|
|
||||||
|
const date1 = new Date(tariff1.createdAt).getTime();
|
||||||
|
const date2 = new Date(tariff2.createdAt).getTime();
|
||||||
|
return date1 - date2;
|
||||||
|
}
|
||||||
|
@ -20,7 +20,7 @@ export const useTicketStore = create<TicketStore>()(
|
|||||||
ticketsPerPage: 10,
|
ticketsPerPage: 10,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "Tickets store (marketplace)"
|
name: "Tickets"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -29,7 +29,7 @@ export const useUnauthTicketStore = create<UnauthTicketStore>()(
|
|||||||
isPreventAutoscroll: false,
|
isPreventAutoscroll: false,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "Unauth ticket store"
|
name: "Unauth tickets"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ import { StringSchema, string } from "yup";
|
|||||||
import { patchUser } from "@root/api/user";
|
import { patchUser } from "@root/api/user";
|
||||||
import { UserAccount, UserAccountSettingsFieldStatus, UserName, VerificationStatus } from "@root/model/account";
|
import { UserAccount, UserAccountSettingsFieldStatus, UserName, VerificationStatus } from "@root/model/account";
|
||||||
import { patchUserAccount } from "@root/api/account";
|
import { patchUserAccount } from "@root/api/account";
|
||||||
|
import { deleteCart, patchCart } from "@root/api/cart";
|
||||||
|
|
||||||
|
|
||||||
interface UserStore {
|
interface UserStore {
|
||||||
@ -60,7 +61,7 @@ const initialState: UserStore = {
|
|||||||
"ИНН": { ...defaultDocument },
|
"ИНН": { ...defaultDocument },
|
||||||
"Устав": { ...defaultDocument },
|
"Устав": { ...defaultDocument },
|
||||||
"Свидетельство о регистрации НКО": { ...defaultDocument },
|
"Свидетельство о регистрации НКО": { ...defaultDocument },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUserStore = create<UserStore>()(
|
export const useUserStore = create<UserStore>()(
|
||||||
@ -68,8 +69,9 @@ export const useUserStore = create<UserStore>()(
|
|||||||
devtools(
|
devtools(
|
||||||
(set, get) => initialState,
|
(set, get) => initialState,
|
||||||
{
|
{
|
||||||
name: "User store",
|
name: "User",
|
||||||
enabled: process.env.NODE_ENV === "development",
|
enabled: process.env.NODE_ENV === "development",
|
||||||
|
trace: true,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
@ -108,6 +110,17 @@ export const setUserAccount = (user: UserAccount) => useUserStore.setState(
|
|||||||
state.settingsFields.secondname.value = user?.name.secondname ?? "";
|
state.settingsFields.secondname.value = user?.name.secondname ?? "";
|
||||||
state.settingsFields.middlename.value = user?.name.middlename ?? "";
|
state.settingsFields.middlename.value = user?.name.middlename ?? "";
|
||||||
state.settingsFields.orgname.value = user?.name.orgname ?? "";
|
state.settingsFields.orgname.value = user?.name.orgname ?? "";
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
type: "setUserAccount",
|
||||||
|
payload: user,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setCart = (cart: string[]) => useUserStore.setState(
|
||||||
|
produce<UserStore>(state => {
|
||||||
|
if (state.userAccount) state.userAccount.cart = cart;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -203,7 +216,7 @@ export const setSettingsField = (
|
|||||||
state.settingsFields[fieldName].error = errorMessage;
|
state.settingsFields[fieldName].error = errorMessage;
|
||||||
|
|
||||||
state.settingsFields.hasError = Object.values(state.settingsFields).reduce((acc: boolean, field) => {
|
state.settingsFields.hasError = Object.values(state.settingsFields).reduce((acc: boolean, field) => {
|
||||||
if (typeof field == "boolean") return acc;
|
if (typeof field === "boolean") return acc;
|
||||||
|
|
||||||
if (field.error !== null) return true;
|
if (field.error !== null) return true;
|
||||||
return acc;
|
return acc;
|
||||||
@ -239,13 +252,20 @@ export const sendUserData = async () => {
|
|||||||
orgname: state.settingsFields.orgname.value,
|
orgname: state.settingsFields.orgname.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [user, userAccount] = await Promise.all([
|
await Promise.all([
|
||||||
isPatchingUser && patchUser(userPayload),
|
isPatchingUser && patchUser(userPayload),
|
||||||
isPatchingUserAccount && patchUserAccount(userAccountPayload),
|
isPatchingUserAccount && patchUserAccount(userAccountPayload),
|
||||||
]);
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
// if (user) setUser(user);
|
export const addTariffToCart = async (tariffId: string) => {
|
||||||
// if (userAccount) setUserAccount(userAccount);
|
const result = await patchCart(tariffId);
|
||||||
|
setCart(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeTariffFromCart = async (tariffId: string) => {
|
||||||
|
const result = await deleteCart(tariffId);
|
||||||
|
setCart(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validators: Record<UserSettingsField | keyof UserName, StringSchema> = {
|
const validators: Record<UserSettingsField | keyof UserName, StringSchema> = {
|
||||||
@ -256,18 +276,18 @@ const validators: Record<UserSettingsField | keyof UserName, StringSchema> = {
|
|||||||
skipAbsent: true,
|
skipAbsent: true,
|
||||||
test(value, ctx) {
|
test(value, ctx) {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
if (value.length == 0) return true
|
if (value.length === 0) return true;
|
||||||
if (!/^[.,:;-_+\d\w]+$/.test(value)) {
|
if (!/^[.,:;-_+\d\w]+$/.test(value)) {
|
||||||
return ctx.createError({ message: 'Некорректные символы в пароле' })
|
return ctx.createError({ message: 'Некорректные символы в пароле' });
|
||||||
}
|
}
|
||||||
if (value.length > 0 && value.length < 8) {
|
if (value.length > 0 && value.length < 8) {
|
||||||
return ctx.createError({ message: 'Минимум 8 символов' })
|
return ctx.createError({ message: 'Минимум 8 символов' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы в пароле"),
|
// min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы в пароле"),
|
||||||
firstname: string(),
|
firstname: string(),
|
||||||
secondname: string(),
|
secondname: string(),
|
||||||
middlename: string(),
|
middlename: string(),
|
||||||
|
55
src/utils/calcCart.ts
Normal file
55
src/utils/calcCart.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { mockDiscounts } from "@root/__mocks__/discounts";
|
||||||
|
import { CartData, PrivilegeCartData } from "@root/model/cart";
|
||||||
|
import { AnyDiscount } from "@root/model/discount";
|
||||||
|
import { Tariff } from "@root/model/tariff";
|
||||||
|
import { findPrivilegeDiscount, findServiceDiscount } from "./calcTariffPrices";
|
||||||
|
|
||||||
|
|
||||||
|
export function calcCart(tariffs: Tariff[], discounts: AnyDiscount[] = mockDiscounts): CartData {
|
||||||
|
const cartData: CartData = {
|
||||||
|
services: [],
|
||||||
|
priceBeforeDiscounts: 0,
|
||||||
|
priceAfterDiscounts: 0,
|
||||||
|
itemCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
tariffs.forEach(tariff => {
|
||||||
|
if (tariff.price && tariff.price > 0) cartData.priceBeforeDiscounts += tariff.price;
|
||||||
|
|
||||||
|
tariff.privilegies.forEach(privilege => {
|
||||||
|
let serviceData = cartData.services.find(service => service.serviceKey === privilege.serviceKey);
|
||||||
|
if (!serviceData) {
|
||||||
|
serviceData = {
|
||||||
|
serviceKey: privilege.serviceKey,
|
||||||
|
privileges: [],
|
||||||
|
price: 0,
|
||||||
|
};
|
||||||
|
cartData.services.push(serviceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let privilegePrice = privilege.amount * privilege.price;
|
||||||
|
|
||||||
|
if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice;
|
||||||
|
|
||||||
|
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
|
||||||
|
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
|
||||||
|
|
||||||
|
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts);
|
||||||
|
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
|
||||||
|
|
||||||
|
const privilegeData: PrivilegeCartData = {
|
||||||
|
tariffId: tariff._id,
|
||||||
|
privilegeId: privilege.privilegeId,
|
||||||
|
name: privilege.description,
|
||||||
|
price: privilegePrice,
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceData.privileges.push(privilegeData);
|
||||||
|
serviceData.price += privilegePrice;
|
||||||
|
cartData.priceAfterDiscounts += privilegePrice;
|
||||||
|
cartData.itemCount++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return cartData;
|
||||||
|
}
|
@ -1,22 +1,22 @@
|
|||||||
import { Tariff } from "@root/model/tariff";
|
import { Tariff } from "@root/model/tariff";
|
||||||
import { mockDiscounts } from "../__mocks__/discounts";
|
import { mockDiscounts } from "../__mocks__/discounts";
|
||||||
import { PrivilegeWithAmount } from "@root/model/privilege";
|
import { PrivilegeWithAmount } from "@root/model/privilege";
|
||||||
import { PrivilegeDiscount, ServiceDiscount } from "../model/discount";
|
import { AnyDiscount, PrivilegeDiscount, ServiceDiscount } from "../model/discount";
|
||||||
|
|
||||||
|
|
||||||
export function calcTariffPrices(tariff: Tariff): {
|
export function calcTariffPrices(tariff: Tariff, discounts: AnyDiscount[] = mockDiscounts): {
|
||||||
price: number | undefined;
|
price: number | undefined;
|
||||||
priceWithDiscounts: number | undefined;
|
priceWithDiscounts: number | undefined;
|
||||||
} {
|
} {
|
||||||
let price = tariff.price ?? tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0);
|
let price = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0);
|
||||||
|
|
||||||
const priceWithDiscounts = tariff.privilegies.reduce((sum, privilege) => {
|
const priceWithDiscounts = tariff.privilegies.reduce((sum, privilege) => {
|
||||||
let privilegePrice = privilege.amount * privilege.price;
|
let privilegePrice = privilege.amount * privilege.price;
|
||||||
|
|
||||||
const privilegeDiscount = findPrivilegeDiscount(privilege);
|
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
|
||||||
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
|
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
|
||||||
|
|
||||||
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice);
|
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts);
|
||||||
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
|
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
|
||||||
|
|
||||||
return sum + privilegePrice;
|
return sum + privilegePrice;
|
||||||
@ -28,8 +28,8 @@ export function calcTariffPrices(tariff: Tariff): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPrivilegeDiscount(privilege: PrivilegeWithAmount): PrivilegeDiscount | null {
|
export function findPrivilegeDiscount(privilege: PrivilegeWithAmount, discounts: AnyDiscount[]): PrivilegeDiscount | null {
|
||||||
const applicableDiscounts = mockDiscounts.filter((discount): discount is PrivilegeDiscount => {
|
const applicableDiscounts = discounts.filter((discount): discount is PrivilegeDiscount => {
|
||||||
return (
|
return (
|
||||||
discount.conditionType === "privilege" &&
|
discount.conditionType === "privilege" &&
|
||||||
privilege.privilegeId === discount.condition.privilege.id &&
|
privilege.privilegeId === discount.condition.privilege.id &&
|
||||||
@ -46,11 +46,12 @@ function findPrivilegeDiscount(privilege: PrivilegeWithAmount): PrivilegeDiscoun
|
|||||||
return maxValueDiscount;
|
return maxValueDiscount;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findServiceDiscount(
|
export function findServiceDiscount(
|
||||||
serviceKey: string,
|
serviceKey: string,
|
||||||
currentPrice: number,
|
currentPrice: number,
|
||||||
|
discounts: AnyDiscount[],
|
||||||
): ServiceDiscount | null {
|
): ServiceDiscount | null {
|
||||||
const discountsForTariffService = mockDiscounts.filter((discount): discount is ServiceDiscount => {
|
const discountsForTariffService = discounts.filter((discount): discount is ServiceDiscount => {
|
||||||
return (
|
return (
|
||||||
discount.conditionType === "service" &&
|
discount.conditionType === "service" &&
|
||||||
discount.condition.service.id === serviceKey &&
|
discount.condition.service.id === serviceKey &&
|
||||||
|
46
src/utils/hooks/useCart.ts
Normal file
46
src/utils/hooks/useCart.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { devlog } from "@frontend/kitui";
|
||||||
|
import { getTariffById } from "@root/api/tariff";
|
||||||
|
import { useTariffStore } from "@root/stores/tariffs";
|
||||||
|
import { useUserStore } from "@root/stores/user";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { addCartTariffs, removeMissingTariffs, setCartTariffStatus, useCartStore } from "@root/stores/cart";
|
||||||
|
import { Tariff } from "@root/model/tariff";
|
||||||
|
|
||||||
|
|
||||||
|
export function useCart() {
|
||||||
|
const tariffs = useTariffStore(state => state.tariffs);
|
||||||
|
const cartTariffMap = useCartStore(state => state.cartTariffMap);
|
||||||
|
const cartTariffIds = useUserStore(state => state.userAccount?.cart);
|
||||||
|
const cart = useCartStore(state => state.cart);
|
||||||
|
|
||||||
|
useEffect(function addTariffsToCart() {
|
||||||
|
const knownTariffs: Tariff[] = [];
|
||||||
|
|
||||||
|
cartTariffIds?.forEach(tariffId => {
|
||||||
|
if (typeof cartTariffMap[tariffId] === "object") return;
|
||||||
|
|
||||||
|
const tariff = tariffs.find(tariff => tariff._id === tariffId);
|
||||||
|
if (tariff) return knownTariffs.push(tariff);
|
||||||
|
|
||||||
|
if (!cartTariffMap[tariffId]) {
|
||||||
|
setCartTariffStatus(tariffId, "loading");
|
||||||
|
|
||||||
|
getTariffById(tariffId).then(tariff => {
|
||||||
|
devlog("Unlisted tariff", tariff);
|
||||||
|
addCartTariffs([tariff]);
|
||||||
|
}).catch(error => {
|
||||||
|
devlog(`Error fetching unlisted tariff ${tariffId}`, error);
|
||||||
|
setCartTariffStatus(tariffId, "not found");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (knownTariffs.length > 0) addCartTariffs(knownTariffs);
|
||||||
|
}, [cartTariffIds, cartTariffMap, tariffs]);
|
||||||
|
|
||||||
|
useEffect(function cleanUpCart() {
|
||||||
|
if (cartTariffIds) removeMissingTariffs(cartTariffIds);
|
||||||
|
}, [cartTariffIds]);
|
||||||
|
|
||||||
|
return cart;
|
||||||
|
}
|
@ -1,39 +1,36 @@
|
|||||||
import { devlog, makeRequest } from "@frontend/kitui";
|
import { devlog, makeRequest } from "@frontend/kitui";
|
||||||
import { GetTariffsResponse, Tariff } from "@root/model/tariff";
|
import { GetTariffsResponse, Tariff } from "@root/model/tariff";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
|
||||||
export function useTariffs({ url, tariffsPerPage, apiPage, onNewTariffs, onError }: {
|
export function useTariffs({ baseUrl = "https://admin.pena.digital/strator/tariff", tariffsPerPage, apiPage, onNewTariffs, onError }: {
|
||||||
url: string;
|
baseUrl?: string;
|
||||||
tariffsPerPage: number;
|
tariffsPerPage: number;
|
||||||
apiPage: number;
|
apiPage: number;
|
||||||
onNewTariffs: (response: Tariff[]) => void;
|
onNewTariffs: (response: Tariff[]) => void;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
}) {
|
}) {
|
||||||
const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle");
|
const onNewTariffsRef = useRef<(response: Tariff[]) => void>(onNewTariffs);
|
||||||
|
const onErrorRef = useRef<(error: Error) => void>(onError);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
setFetchState("fetching");
|
|
||||||
makeRequest<never, GetTariffsResponse>({
|
makeRequest<never, GetTariffsResponse>({
|
||||||
url,
|
url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
devlog("GetTicketsResponse", result);
|
devlog("Tariffs", result);
|
||||||
if (result.tariffs.length > 0) {
|
if (result.tariffs.length > 0) {
|
||||||
onNewTariffs(result.tariffs);
|
onNewTariffsRef.current(result.tariffs);
|
||||||
setFetchState("idle");
|
}
|
||||||
} else setFetchState("all fetched");
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
devlog("Error fetching tariffs", error);
|
devlog("Error fetching tariffs", error);
|
||||||
onError(error);
|
onErrorRef.current(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [onError, onNewTariffs, apiPage, tariffsPerPage, url]);
|
}, [apiPage, tariffsPerPage, baseUrl]);
|
||||||
|
|
||||||
return fetchState;
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user