tariffs save fix

This commit is contained in:
ArtChaos189 2023-08-21 18:58:48 +03:00
parent 63ae005bb8
commit 52f1b30c5f
8 changed files with 377 additions and 261 deletions

@ -0,0 +1,79 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import ExpandIcon from "./icons/ExpandIcon";
import type { ReactNode } from "react";
interface Props {
header: ReactNode;
divide?: boolean;
privilege: ReactNode;
}
export default function CustomSaveAccordion({ header, divide = false, privilege }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upXs = useMediaQuery(theme.breakpoints.up("xs"));
const [isExpanded, setIsExpanded] = useState<boolean>(false);
return (
<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
onClick={() => setIsExpanded((prev) => !prev)}
sx={{
minHeight: "72px",
px: "20px",
display: "flex",
alignItems: "stretch",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
rowGap: "10px",
flexDirection: upXs ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: theme.palette.grey3.main,
px: 0,
}}
>
{header}
</Box>
<Box
sx={{
pl: "20px",
width: "52px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderLeft: divide ? "1px solid #000000" : "none",
}}
>
<ExpandIcon isExpanded={isExpanded} />
</Box>
</Box>
{isExpanded && privilege}
</Box>
);
}

@ -10,107 +10,116 @@ import { useNavigate } from "react-router-dom";
import { useState } from "react"; import { useState } from "react";
interface Props { interface Props {
priceBeforeDiscounts: number; priceBeforeDiscounts: number;
priceAfterDiscounts: number; priceAfterDiscounts: number;
} }
export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts }: Props) { export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0); const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0);
const navigate = useNavigate(); const navigate = useNavigate();
function handlePayClick() { function handlePayClick() {
payCart().then(result => { payCart()
setUserAccount(result); .then((result) => {
}).catch(error => { setUserAccount(result);
if (isAxiosError(error) && error.response?.status === 402) { })
const notEnoughMoneyAmount = parseInt((error.response.data.message as string).replace("insufficient funds: ", "")); .catch((error) => {
setNotEnoughMoneyAmount(notEnoughMoneyAmount); if (isAxiosError(error) && error.response?.status === 500) {
} else { enqueueSnackbar("В корзине нет товаров");
const message = getMessageFromFetchError(error); }
if (message) enqueueSnackbar(message); if (isAxiosError(error) && error.response?.status === 402) {
} const notEnoughMoneyAmount = parseInt(
}); (error.response.data.message as string).replace("insufficient funds: ", "")
} );
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
}
if (!isAxiosError(error)) {
enqueueSnackbar(error.response.data.message);
}
});
}
function handleReplenishWallet() { function handleReplenishWallet() {
navigate("/payment", { state: { notEnoughMoneyAmount } }); navigate("/payment", { state: { notEnoughMoneyAmount } });
} }
return ( return (
<Box sx={{ <Box
sx={{
display: "flex",
flexDirection: upMd ? "row" : "column",
mt: upMd ? "50px" : "70px",
pt: upMd ? "30px" : undefined,
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
pr: upMd ? "15%" : 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,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}
>
<Box
sx={{
display: "flex", display: "flex",
flexDirection: upMd ? "row" : "column", flexDirection: upMd ? "column" : "row",
mt: upMd ? "50px" : "70px", alignItems: upMd ? "start" : "center",
pt: upMd ? "30px" : undefined, mt: upMd ? "10px" : "30px",
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined, mb: "15px",
}}> gap: "15px",
<Box sx={{ }}
width: upMd ? "68.5%" : undefined, >
pr: upMd ? "15%" : undefined, <Typography variant="oldPrice" sx={{ order: upMd ? 1 : 2 }}>
display: "flex", {currencyFormatter.format(priceBeforeDiscounts / 100)}
flexWrap: "wrap", </Typography>
flexDirection: "column", <Typography
}}> variant="price"
<Typography variant="h4" mb={upMd ? "18px" : "30px"}> sx={{
Итоговая цена fontWeight: 500,
</Typography> fontSize: "26px",
<Typography color={theme.palette.grey3.main}> lineHeight: "31px",
Текст-заполнитель это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель order: upMd ? 2 : 1,
это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель }}
</Typography> >
</Box> {currencyFormatter.format(priceAfterDiscounts / 100)}
<Box sx={{ </Typography>
color: theme.palette.grey3.main,
width: upMd ? "31.5%" : undefined,
pl: upMd ? "33px" : undefined,
}}>
<Box sx={{
display: "flex",
flexDirection: upMd ? "column" : "row",
alignItems: upMd ? "start" : "center",
mt: upMd ? "10px" : "30px",
mb: "15px",
gap: "15px",
}}>
<Typography
variant="oldPrice"
sx={{ order: upMd ? 1 : 2 }}
>
{currencyFormatter.format(priceBeforeDiscounts / 100)}
</Typography>
<Typography
variant="price"
sx={{
fontWeight: 500,
fontSize: "26px",
lineHeight: "31px",
order: upMd ? 2 : 1,
}}
>
{currencyFormatter.format(priceAfterDiscounts / 100)}
</Typography>
</Box>
{notEnoughMoneyAmount > 0 &&
<Alert
severity="error"
variant="filled"
>
Нехватает {currencyFormatter.format(notEnoughMoneyAmount / 100)}
</Alert>
}
<CustomButton
variant="contained"
onClick={notEnoughMoneyAmount === 0 ? handlePayClick : handleReplenishWallet}
sx={{
mt: "10px",
backgroundColor: theme.palette.brightPurple.main,
}}
>
{notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}
</CustomButton>
</Box>
</Box> </Box>
); {notEnoughMoneyAmount > 0 && (
<Alert severity="error" variant="filled">
Нехватает {currencyFormatter.format(notEnoughMoneyAmount / 100)}
</Alert>
)}
<CustomButton
variant="contained"
onClick={notEnoughMoneyAmount === 0 ? handlePayClick : handleReplenishWallet}
sx={{
mt: "10px",
backgroundColor: theme.palette.brightPurple.main,
}}
>
{notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}
</CustomButton>
</Box>
</Box>
);
} }

@ -145,6 +145,8 @@ export default function CustomWrapper({ serviceData }: Props) {
serviceData.tariffs.map((tariff) => { serviceData.tariffs.map((tariff) => {
const privilege = tariff.privileges[0]; const privilege = tariff.privileges[0];
console.log(tariff);
return tariff.privileges.length > 1 ? ( return tariff.privileges.length > 1 ? (
<CustomTariffAccordion key={tariff.id} tariffCartData={tariff} /> <CustomTariffAccordion key={tariff.id} tariffCartData={tariff} />
) : ( ) : (

@ -4,19 +4,16 @@ import { enqueueSnackbar } from "notistack";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import CustomButton from "@root/components/CustomButton"; import CustomButton from "@root/components/CustomButton";
import { createAndSendTariff } from "@root/stores/customTariffs";
import { updateTariffs } from "@root/stores/tariffs";
import { addTariffToCart } from "@root/stores/user"; import { addTariffToCart } from "@root/stores/user";
import CustomAccordion from "@components/CustomAccordion";
import { cardShadow } from "@root/utils/themes/shadow"; import { cardShadow } from "@root/utils/themes/shadow";
import { PrivilegeCartData, getMessageFromFetchError } from "@frontend/kitui"; import { TariffCartData, getMessageFromFetchError } from "@frontend/kitui";
import CustomSaveAccordion from "../../components/CustomSaveAccordion";
interface Props { interface Props {
content: PrivilegeCartData[]; content: TariffCartData[];
name: string;
} }
const SaveWrapper: FC<Props> = ({ content, name }) => { const SaveWrapper: FC<Props> = ({ content }) => {
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"));
@ -46,12 +43,10 @@ const SaveWrapper: FC<Props> = ({ content, name }) => {
boxShadow: cardShadow, boxShadow: cardShadow,
}} }}
> >
{content.map(({ price, description, privilegeId }, index) => ( {content.map(({ name, id, price, isCustom, privileges }) => (
<CustomAccordion <CustomSaveAccordion
key={index} key={id}
divide divide
text={description}
price={price}
header={ header={
<Box <Box
sx={{ sx={{
@ -101,10 +96,13 @@ const SaveWrapper: FC<Props> = ({ content, name }) => {
px: 0, px: 0,
}} }}
> >
{new Intl.NumberFormat("ru-RU").format(price)} руб. {new Intl.NumberFormat("ru-RU").format(price / 100)} руб.
</Typography> </Typography>
<CustomButton <CustomButton
onClick={() => handleTariffItemClick(privilegeId)} onClick={(event) => {
event.stopPropagation();
handleTariffItemClick(id);
}}
variant="contained" variant="contained"
sx={{ sx={{
mr: "25px", mr: "25px",
@ -117,12 +115,39 @@ const SaveWrapper: FC<Props> = ({ content, name }) => {
":hover": { ":hover": {
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
}, },
zIndex: "100",
}} }}
> >
Купить Купить
</CustomButton> </CustomButton>
</Box> </Box>
} }
privilege={privileges.map(({ description, price, privilegeId }) => (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
px: "20px",
py: upMd ? "25px" : undefined,
pt: upMd ? undefined : "15px",
pb: upMd ? undefined : "25px",
backgroundColor: "#F1F2F6",
}}
>
<Typography
sx={{
fontSize: upMd ? undefined : "16px",
lineHeight: upMd ? undefined : "19px",
color: theme.palette.grey3.main,
}}
>
{description}
</Typography>
<Typography sx={{ display: price ? "block" : "none", fontSize: "18px", mr: "120px" }}>
{price / 100} руб.
</Typography>
</Box>
))}
/> />
))} ))}
</Box> </Box>

@ -1,19 +1,21 @@
import { IconButton, Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { IconButton, Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "../../components/SectionWrapper"; import SectionWrapper from "../../components/SectionWrapper";
import AccordionWrapper from "./AccordionWrapper";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import { useCart } from "@root/utils/hooks/useCart"; import { useCart } from "@root/utils/hooks/useCart";
import SaveWrapper from "./SaveWrapper"; import SaveWrapper from "./SaveWrapper";
import { useTariffStore } from "@root/stores/tariffs";
export default function Faq() { export default function Faq() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const cart = useCart(); const cart = useCart();
const handleCustomBackNavigation = useHistoryTracker(); const tariffs = useTariffStore((state) => state.tariffs);
console.log(cart); console.log(tariffs);
const handleCustomBackNavigation = useHistoryTracker();
return ( return (
<SectionWrapper <SectionWrapper
@ -40,9 +42,7 @@ export default function Faq() {
</Box> </Box>
<Box mt={upMd ? "27px" : "10px"}> <Box mt={upMd ? "27px" : "10px"}>
{cart.services.map(({ serviceKey, tariffs }) => {cart.services.map(({ serviceKey, tariffs }) =>
serviceKey === "custom" serviceKey === "custom" ? <SaveWrapper content={tariffs} /> : null
? tariffs.map(({ privileges, name }) => <SaveWrapper name={name} content={privileges} />)
: null
)} )}
</Box> </Box>
</SectionWrapper> </SectionWrapper>

@ -1,124 +1,112 @@
import { import { Box, Typography, Tooltip, SxProps, Theme, useTheme } from "@mui/material";
Box,
Typography,
Tooltip,
SxProps,
Theme,
useTheme,
} from "@mui/material";
import CustomButton from "@components/CustomButton"; import CustomButton from "@components/CustomButton";
import { MouseEventHandler, ReactNode } from "react"; import { MouseEventHandler, ReactNode } from "react";
import { cardShadow } from "@root/utils/themes/shadow"; import { cardShadow } from "@root/utils/themes/shadow";
interface Props { interface Props {
icon: ReactNode; icon: ReactNode;
headerText: string; headerText: string;
text: string | string[]; text: string | string[];
sx?: SxProps<Theme>;
buttonProps?: {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
buttonProps?: { onClick?: MouseEventHandler<HTMLButtonElement>;
sx?: SxProps<Theme>; text?: string;
onClick?: MouseEventHandler<HTMLButtonElement>; };
text?: string; price?: ReactNode;
};
price?: ReactNode;
} }
export default function TariffCard({ export default function TariffCard({ icon, headerText, text, sx, price, buttonProps }: Props) {
icon, const theme = useTheme();
headerText,
text,
sx,
price,
buttonProps,
}: Props) {
const theme = useTheme();
text = Array.isArray(text) ? text : [text]; text = Array.isArray(text) ? text : [text];
return ( return (
<Box <Box
sx={{
width: "100%",
minHeight: "250px",
bgcolor: "white",
borderRadius: "12px",
display: "flex",
flexDirection: "column",
alignItems: "start",
p: "20px",
boxShadow: cardShadow,
...sx,
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "16px",
}}
>
{icon}
{price && (
<Box
sx={{ sx={{
width: "100%", display: "flex",
minHeight: "250px", alignItems: "baseline",
bgcolor: "white", flexWrap: "wrap",
borderRadius: "12px", columnGap: "10px",
display: "flex", rowGap: 0,
flexDirection: "column",
alignItems: "start",
p: "20px",
boxShadow: cardShadow,
...sx,
}} }}
>
{price}
</Box>
)}
</Box>
<Tooltip title={<Typography>{headerText}</Typography>} placement="top">
<Typography
variant="h5"
sx={{
mt: "14px",
mb: "10px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
}}
> >
<Box {headerText}
sx={{ </Typography>
width: "100%", </Tooltip>
display: "flex", <Tooltip
justifyContent: "space-between", title={text.map((line, index) => (
alignItems: "center", <Typography key={index}>{line}</Typography>
gap: "16px", ))}
}} placement="top"
> >
{icon} <Box
{price && ( sx={{
<Box overflow: "hidden",
sx={{ textOverflow: "clip",
display: "flex", mb: "auto",
alignItems: "baseline", }}
flexWrap: "wrap", >
columnGap: "10px", {text.map((line, index) => (
rowGap: 0, <Typography key={index}>{line}</Typography>
}} ))}
>
{price}
</Box>
)}
</Box>
<Tooltip title={<Typography>{headerText}</Typography>} placement="top">
<Typography
variant="h5"
sx={{
mt: "14px",
mb: "10px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
}}
>
{headerText}
</Typography>
</Tooltip>
<Tooltip
title={text.map((line, index) => (
<Typography key={index}>{line}</Typography>
))}
placement="top"
>
<Box sx={{
overflow: "hidden",
textOverflow: "clip",
mb: "auto",
}}>
{text.map((line, index) => (
<Typography key={index}>{line}</Typography>
))}
</Box>
</Tooltip>
{buttonProps && (
<CustomButton
onClick={buttonProps.onClick}
variant="outlined"
sx={{
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
mt: "10px",
...buttonProps.sx,
}}
>
{buttonProps.text}
</CustomButton>
)}
</Box> </Box>
); </Tooltip>
{buttonProps && (
<CustomButton
onClick={buttonProps.onClick}
variant="outlined"
sx={{
color: theme.palette.brightPurple.main,
borderColor: theme.palette.brightPurple.main,
mt: "10px",
...buttonProps.sx,
}}
>
{buttonProps.text}
</CustomButton>
)}
</Box>
);
} }

@ -39,6 +39,8 @@ export default function TariffPage() {
const unit: string = String(location.pathname).slice(9); const unit: string = String(location.pathname).slice(9);
const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object"); const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
console.log(currentTariffs);
useAllTariffsFetcher({ useAllTariffsFetcher({
onSuccess: updateTariffs, onSuccess: updateTariffs,
onError: (error) => { onError: (error) => {
@ -74,6 +76,8 @@ export default function TariffPage() {
); );
}); });
console.log(isCustomTariffs);
const createTariffElements = (filteredTariffs: Tariff[]) => { const createTariffElements = (filteredTariffs: Tariff[]) => {
const tariffElements = filteredTariffs const tariffElements = filteredTariffs
.filter((tariff) => tariff.privilegies.length > 0) .filter((tariff) => tariff.privilegies.length > 0)

@ -7,50 +7,59 @@ import { addCartTariffs, removeMissingCartTariffs, setCartTariffStatus, useCartS
import { isAxiosError } from "axios"; import { isAxiosError } from "axios";
import { useDiscountStore } from "@root/stores/discounts"; import { useDiscountStore } from "@root/stores/discounts";
export function useCart() { export function useCart() {
const tariffs = useTariffStore(state => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const cartTariffMap = useCartStore(state => state.cartTariffMap); const cartTariffMap = useCartStore((state) => state.cartTariffMap);
const cartTariffIds = useUserStore(state => state.userAccount?.cart); const cartTariffIds = useUserStore((state) => state.userAccount?.cart);
const cart = useCartStore(state => state.cart); const cart = useCartStore((state) => state.cart);
const discounts = useDiscountStore(state => state.discounts); const discounts = useDiscountStore((state) => state.discounts);
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0; const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
useEffect(function addTariffsToCart() { useEffect(
const knownTariffs: Tariff[] = []; function addTariffsToCart() {
const knownTariffs: Tariff[] = [];
cartTariffIds?.forEach(tariffId => { cartTariffIds?.forEach((tariffId) => {
if (typeof cartTariffMap[tariffId] === "object") return; if (typeof cartTariffMap[tariffId] === "object") return;
const tariff = tariffs.find(tariff => tariff._id === tariffId); const tariff = tariffs.find((tariff) => tariff._id === tariffId);
if (tariff) return knownTariffs.push(tariff); if (tariff) return knownTariffs.push(tariff);
if (!cartTariffMap[tariffId]) { if (!cartTariffMap[tariffId]) {
setCartTariffStatus(tariffId, "loading"); setCartTariffStatus(tariffId, "loading");
getTariffById(tariffId).then(tariff => { getTariffById(tariffId)
devlog("Unknown tariff", tariff); .then((tariff) => {
addCartTariffs([tariff], discounts, purchasesAmount); devlog("Unknown tariff", tariff);
}).catch(error => { addCartTariffs([tariff], discounts, purchasesAmount);
devlog(`Error fetching unknown tariff ${tariffId}`, error); })
setCartTariffStatus(tariffId, "not found"); .catch((error) => {
if (isAxiosError(error) && error.response?.status === 404) { devlog(`Error fetching unknown tariff ${tariffId}`, error);
removeTariffFromCart(tariffId).then(() => { setCartTariffStatus(tariffId, "not found");
devlog(`Unexistant tariff with id ${tariffId} deleted from cart`); if (isAxiosError(error) && error.response?.status === 404) {
}).catch(error => { removeTariffFromCart(tariffId)
devlog("Error deleting unexistant tariff from cart", error); .then(() => {
}); devlog(`Unexistant tariff with id ${tariffId} deleted from cart`);
} })
}); .catch((error) => {
} devlog("Error deleting unexistant tariff from cart", error);
}); });
}
});
}
});
if (knownTariffs.length > 0) addCartTariffs(knownTariffs, discounts, purchasesAmount); if (knownTariffs.length > 0) addCartTariffs(knownTariffs, discounts, purchasesAmount);
}, [cartTariffIds, cartTariffMap, discounts, purchasesAmount, tariffs]); },
[cartTariffIds, cartTariffMap, discounts, purchasesAmount, tariffs]
);
useEffect(function cleanUpCart() { useEffect(
if (cartTariffIds) removeMissingCartTariffs(cartTariffIds, discounts, purchasesAmount); function cleanUpCart() {
}, [cartTariffIds, discounts, purchasesAmount]); if (cartTariffIds) removeMissingCartTariffs(cartTariffIds, discounts, purchasesAmount);
},
[cartTariffIds, discounts, purchasesAmount]
);
return cart; return cart;
} }