This commit is contained in:
Nastya 2023-10-28 02:43:29 +03:00
commit 38d3e3b535
18 changed files with 351 additions and 200 deletions

@ -14,7 +14,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@frontend/kitui": "1.0.53", "@frontend/kitui": "1.0.54",
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
@ -29,6 +29,7 @@
"pdfjs-dist": "3.6.172", "pdfjs-dist": "3.6.172",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-pdf": "^7.1.2", "react-pdf": "^7.1.2",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"react-slick": "^0.29.0", "react-slick": "^0.29.0",

@ -30,8 +30,11 @@ import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg";
import { payCart } from "@root/api/cart"; import { payCart } from "@root/api/cart";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
export default function Drawers() { function Drawers() {
const [openNotificationsModal, setOpenNotificationsModal] = const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false); useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@ -57,12 +60,15 @@ export default function Drawers() {
const [payCartResponse, payCartError] = await payCart(); const [payCartResponse, payCartError] = await payCart();
if (payCartError) { if (payCartError) {
const notEnoughMoneyAmount = parseInt( if (payCartError.includes("insufficient funds: ")) {
payCartError.replace("insufficient funds: ", "") const notEnoughMoneyAmount = parseInt(
); payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
);
setNotEnoughMoneyAmount(notEnoughMoneyAmount); setNotEnoughMoneyAmount(notEnoughMoneyAmount);
setLoading(false); }
setLoading(false);
return enqueueSnackbar(payCartError); return enqueueSnackbar(payCartError);
} }
@ -72,6 +78,7 @@ export default function Drawers() {
} }
setLoading(false); setLoading(false);
closeCartDrawer();
} }
function handleReplenishWallet() { function handleReplenishWallet() {
@ -303,8 +310,6 @@ export default function Drawers() {
</Box> </Box>
<Button <Button
variant="pena-contained-dark" variant="pena-contained-dark"
component={Link}
to="/cart"
onClick={() => onClick={() =>
notEnoughMoneyAmount === 0 notEnoughMoneyAmount === 0
? !loading && handlePayClick() ? !loading && handlePayClick()
@ -322,3 +327,15 @@ export default function Drawers() {
</Box> </Box>
); );
} }
export default withErrorBoundary(Drawers, {
fallback: (
<Box sx={{
display: "flex",
alignItems: "center",
}}>
<ErrorOutlineIcon color="error" />
</Box>
),
onError: handleComponentError,
})

@ -38,12 +38,15 @@ export default function TotalPrice({
const [payCartResponse, payCartError] = await payCart(); const [payCartResponse, payCartError] = await payCart();
if (payCartError) { if (payCartError) {
const notEnoughMoneyAmount = parseInt( if (payCartError.includes("insufficient funds: ")) {
payCartError.replace("insufficient funds: ", "") const notEnoughMoneyAmount = parseInt(
); payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")
);
setNotEnoughMoneyAmount(notEnoughMoneyAmount); setNotEnoughMoneyAmount(notEnoughMoneyAmount);
setLoading(false); }
setLoading(false);
return enqueueSnackbar(payCartError); return enqueueSnackbar(payCartError);
} }

@ -20,10 +20,10 @@ export default function CustomIcon() {
> >
<svg width="24" height="24" viewBox="0 0 24 24" stroke="inherit" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" stroke="inherit" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3.2002" y="0.75" width="17.8" height="22.25" rx="4" stroke-width="1.5"/> <rect x="3.2002" y="0.75" width="17.8" height="22.25" rx="4" strokeWidth="1.5"/>
<path d="M7.65039 6.3125H16.5504" stroke-width="1.5" stroke-linecap="round"/> <path d="M7.65039 6.3125H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M7.65039 11.875H16.5504" stroke-width="1.5" stroke-linecap="round"/> <path d="M7.65039 11.875H16.5504" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M7.65039 17.4375H12.1004" stroke-width="1.5" stroke-linecap="round"/> <path d="M7.65039 17.4375H12.1004" strokeWidth="1.5" strokeLinecap="round"/>
</svg> </svg>
</Box> </Box>
); );

@ -1,7 +1,9 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Box, Button, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
import SectionWrapper from "@components/SectionWrapper"; import InputTextfield from "@components/InputTextfield";
import PasswordInput from "@components/passwordInput";
import UserFields from "./UserFields"; import UserFields from "./UserFields";
import SectionWrapper from "@components/SectionWrapper";
import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user"; import { openDocumentsDialog, sendUserData, setSettingsField, useUserStore } from "@root/stores/user";
import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon"; import UnderlinedButtonWithIcon from "@root/components/UnderlinedButtonWithIcon";
import UploadIcon from "@root/components/icons/UploadIcon"; import UploadIcon from "@root/components/icons/UploadIcon";
@ -12,157 +14,167 @@ import { getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { VerificationStatus } from "@root/model/account"; import { VerificationStatus } from "@root/model/account";
import { verify } from "./helper"; import { verify } from "./helper";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
export default function AccountSettings() { function AccountSettings() {
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 isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const fields = useUserStore((state) => state.settingsFields); const fields = useUserStore((state) => state.settingsFields);
const verificationStatus = useUserStore((state) => state.verificationStatus); const verificationStatus = useUserStore((state) => state.verificationStatus);
const verificationType = useUserStore((state) => state.verificationType); const verificationType = useUserStore((state) => state.verificationType);
const comment = useUserStore((state) => state.comment); const comment = useUserStore((state) => state.comment);
const userId = useUserStore((state) => state.userId) ?? ""; const userId = useUserStore((state) => state.userId) ?? "";
useEffect(() => { useEffect(() => {
verify(userId); verify(userId);
}, []); }, []);
const textFieldProps = {
gap: upMd ? "16px" : "10px",
color: "#F2F3F7",
bold: true,
};
const verificationStatusData: Record<VerificationStatus, { text: string; color: string }> = { function handleSendDataClick() {
sendUserData()
.then(() => {
enqueueSnackbar("Информация обновлена");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
px: isTablet ? (isMobile ? "18px" : "40px") : "20px",
}}
>
<DocumentsDialog />
<Typography variant="h4" mt="20px">
Настройки аккаунта
</Typography>
<Box
sx={{
mt: "40px",
mb: "40px",
backgroundColor: "white",
display: "flex",
flexDirection: "column",
borderRadius: "12px",
p: "20px",
gap: "40px",
boxShadow: cardShadow,
}}
>
<Box
sx={{
display: "flex",
gap: "31px",
justifyContent: "space-between",
flexDirection: upMd ? "row" : "column",
}}
>
<UserFields/>
<Box
sx={{
maxWidth: "246px",
}}
>
<Typography variant="p1">Статус</Typography>
<VerificationIndicator verificationStatus={verificationStatus} sx={{ mt: "16px", p: "14px 7.5px" }} />
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("juridical"),
}}
>
Загрузить документы для юр лиц
</UnderlinedButtonWithIcon>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "15px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("nko"),
}}
>
Загрузить документы для НКО
</UnderlinedButtonWithIcon>
</>
)}
{verificationStatus === VerificationStatus.VERIFICATED && (
<UnderlinedButtonWithIcon
icon={<EyeIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog(verificationType),
}}
>
Посмотреть свою верификацию
</UnderlinedButtonWithIcon>
)}
{comment && <p>{comment}</p>}
</Box>
</Box>
<Button
variant="pena-contained-dark"
onClick={handleSendDataClick}
disabled={fields.hasError}
sx={{ alignSelf: "end" }}
>
Сохранить
</Button>
</Box>
</SectionWrapper>
);
}
export default withErrorBoundary(AccountSettings, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении настроек аккаунта</Typography>,
onError: handleComponentError,
})
const verificationStatusData: Record<VerificationStatus, { text: string; color: string; }> = {
verificated: { text: "Верификация пройдена", color: "#0D9F00" }, verificated: { text: "Верификация пройдена", color: "#0D9F00" },
waiting: { text: "В ожидании верификации", color: "#F18956" }, waiting: { text: "В ожидании верификации", color: "#F18956" },
notVerificated: { text: "Не верифицирован", color: "#E02C2C" }, notVerificated: { text: "Не верифицирован", color: "#E02C2C" },
}; };
function handleSendDataClick() { function VerificationIndicator({
sendUserData()
.then(() => {
enqueueSnackbar("Информация обновлена");
})
.catch((error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
});
}
function VerificationIndicator({
verificationStatus, verificationStatus,
sx, sx,
}: { }: {
verificationStatus: VerificationStatus; verificationStatus: VerificationStatus;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
}) { }) {
return ( return (
<Box
sx={{
py: "14px",
px: "8.5px",
borderWidth: "1px",
borderStyle: "solid",
color: verificationStatusData[verificationStatus].color,
borderColor: verificationStatusData[verificationStatus].color,
borderRadius: "8px",
textAlign: "center",
...sx,
}}
>
<Typography lineHeight="100%">{verificationStatusData[verificationStatus].text}</Typography>
</Box>
);
}
console.log("компонент настройки юзера")
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
px: isTablet ? (isMobile ? "18px" : "40px") : "20px",
}}
>
<DocumentsDialog />
<Typography variant="h4" mt="20px">
Настройки аккаунта
</Typography>
<Box
sx={{
mt: "40px",
mb: "40px",
backgroundColor: "white",
display: "flex",
flexDirection: "column",
borderRadius: "12px",
p: "20px",
gap: "40px",
boxShadow: cardShadow,
}}
>
<Box <Box
sx={{
display: "flex",
gap: "31px",
justifyContent: "space-between",
flexDirection: upMd ? "row" : "column",
}}
>
<UserFields/>
<Box
sx={{ sx={{
maxWidth: "246px", py: "14px",
px: "8.5px",
borderWidth: "1px",
borderStyle: "solid",
color: verificationStatusData[verificationStatus].color,
borderColor: verificationStatusData[verificationStatus].color,
borderRadius: "8px",
textAlign: "center",
...sx,
}} }}
>
<Typography variant="p1">Статус</Typography>
<VerificationIndicator verificationStatus={verificationStatus} sx={{ mt: "16px", p: "14px 7.5px" }} />
{verificationStatus === VerificationStatus.NOT_VERIFICATED && (
<>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("juridical"),
}}
>
Загрузить документы для юр лиц
</UnderlinedButtonWithIcon>
<UnderlinedButtonWithIcon
icon={<UploadIcon />}
sx={{ mt: "15px" }}
ButtonProps={{
onClick: () => openDocumentsDialog("nko"),
}}
>
Загрузить документы для НКО
</UnderlinedButtonWithIcon>
</>
)}
{verificationStatus === VerificationStatus.VERIFICATED && (
<UnderlinedButtonWithIcon
icon={<EyeIcon />}
sx={{ mt: "55px" }}
ButtonProps={{
onClick: () => openDocumentsDialog(verificationType),
}}
>
Посмотреть свою верификацию
</UnderlinedButtonWithIcon>
)}
{comment && <p>{comment}</p>}
</Box>
</Box>
<Button
variant="pena-contained-dark"
onClick={handleSendDataClick}
disabled={fields.hasError}
sx={{ alignSelf: "end" }}
> >
Сохранить <Typography lineHeight="100%">{verificationStatusData[verificationStatus].text}</Typography>
</Button> </Box>
</Box> );
</SectionWrapper> }
);
}

@ -6,8 +6,10 @@ import CustomWrapper from "./CustomWrapper";
import { useCart } from "@root/utils/hooks/useCart"; import { useCart } from "@root/utils/hooks/useCart";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"; import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
import { handleComponentError } from "@root/utils/handleComponentError";
import { withErrorBoundary } from "react-error-boundary";
export default function Cart() { function Cart() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(550)); const isMobile = useMediaQuery(theme.breakpoints.down(550));
@ -71,3 +73,8 @@ export default function Cart() {
</SectionWrapper> </SectionWrapper>
); );
} }
export default withErrorBoundary(Cart, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении корзины</Typography>,
onError: handleComponentError,
})

@ -11,6 +11,8 @@ import { HISTORY } from "./historyMocks";
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import { useHistoryData } from "@root/utils/hooks/useHistoryData"; import { useHistoryData } from "@root/utils/hooks/useHistoryData";
import { isArray } from "cypress/types/lodash"; import { isArray } from "cypress/types/lodash";
import { ErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]; const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"];
@ -67,23 +69,30 @@ export default function History() {
) : ( ) : (
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} /> <Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
)} )}
{historyData?.records <ErrorBoundary
.filter((e) => { fallback={
e.createdAt = extractDateFromString(e.createdAt) <Typography mt="8px">Ошибка загрузки истории</Typography>
return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value) }
)}) onError={handleComponentError}
.map(( e, index) => { >
return ( {historyData?.records
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}> .filter((e) => {
<AccordionWrapper e.createdAt = extractDateFromString(e.createdAt)
first={index === 0} return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value)
last={index === historyData?.records.length - 1} )})
content={e.rawDetails} .map(( e, index) => {
key={e.id} return (
createdAt={e.createdAt} <Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
/> <AccordionWrapper
</Box> first={index === 0}
)})} last={index === historyData?.records.length - 1}
content={e.rawDetails}
key={e.id}
createdAt={e.createdAt}
/>
</Box>
)})}
</ErrorBoundary>
</SectionWrapper> </SectionWrapper>
); );
} }

@ -5,8 +5,10 @@ import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import SaveWrapper from "./SaveWrapper"; import SaveWrapper from "./SaveWrapper";
import { useTariffStore } from "@root/stores/tariffs"; import { useTariffStore } from "@root/stores/tariffs";
import { type Tariff } from "@frontend/kitui"; import { type Tariff } from "@frontend/kitui";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
export default function SavedTariffs() { function SavedTariffs() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(550)); const isMobile = useMediaQuery(theme.breakpoints.down(550));
@ -65,3 +67,8 @@ export default function SavedTariffs() {
</SectionWrapper> </SectionWrapper>
); );
} }
export default withErrorBoundary(SavedTariffs, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении сохраненных тарифов</Typography>,
onError: handleComponentError,
})

@ -35,8 +35,10 @@ import {
useTicketMessages, useTicketMessages,
} from "@frontend/kitui"; } from "@frontend/kitui";
import { shownMessage, sendTicketMessage } from "@root/api/ticket"; import { shownMessage, sendTicketMessage } from "@root/api/ticket";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
export default function SupportChat() { function SupportChat() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.up(460)); const isMobile = useMediaQuery(theme.breakpoints.up(460));
@ -128,7 +130,7 @@ export default function SupportChat() {
async function handleSendMessage() { async function handleSendMessage() {
if (!ticket || !messageField) return; if (!ticket || !messageField) return;
const [_, sendTicketMessageError] = await sendTicketMessage( const [, sendTicketMessageError] = await sendTicketMessage(
ticket.id, ticket.id,
messageField messageField
); );
@ -318,3 +320,8 @@ export default function SupportChat() {
</Box> </Box>
); );
} }
export default withErrorBoundary(SupportChat, {
fallback: <Typography mt="8px" textAlign="center">Не удалось отобразить чат</Typography>,
onError: handleComponentError,
})

@ -5,13 +5,16 @@ import {
Box, Box,
useTheme, useTheme,
Pagination, Pagination,
Typography,
} from "@mui/material"; } from "@mui/material";
import TicketCard from "./TicketCard"; import TicketCard from "./TicketCard";
import { setTicketApiPage, useTicketStore } from "@root/stores/tickets"; import { setTicketApiPage, useTicketStore } from "@root/stores/tickets";
import { Ticket } from "@frontend/kitui"; import { Ticket } from "@frontend/kitui";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
export default function TicketList() { function TicketList() {
const theme = useTheme(); const theme = useTheme();
const tickets = useTicketStore((state) => state.tickets); const tickets = useTicketStore((state) => state.tickets);
const ticketCount = useTicketStore((state) => state.ticketCount); const ticketCount = useTicketStore((state) => state.ticketCount);
@ -87,3 +90,8 @@ function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) {
const date2 = new Date(ticket2.updated_at).getTime(); const date2 = new Date(ticket2.updated_at).getTime();
return date2 - date1; return date2 - date1;
} }
export default withErrorBoundary(TicketList, {
fallback: <Typography mt="8px" textAlign="center">Ошибка загрузки тикетов</Typography>,
onError: handleComponentError,
});

@ -1,4 +1,4 @@
import { Box, IconButton, useMediaQuery, useTheme } from "@mui/material"; import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import { useCustomTariffsStore } from "@root/stores/customTariffs"; import { useCustomTariffsStore } from "@root/stores/customTariffs";
@ -8,8 +8,10 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import TotalPrice from "@root/components/TotalPrice"; import TotalPrice from "@root/components/TotalPrice";
import { serviceNameByKey } from "@root/utils/serviceKeys"; import { serviceNameByKey } from "@root/utils/serviceKeys";
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
export default function TariffConstructor() { function TariffConstructor() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
@ -84,3 +86,8 @@ export default function TariffConstructor() {
</SectionWrapper> </SectionWrapper>
); );
} }
export default withErrorBoundary(TariffConstructor, {
fallback: <Typography mt="8px" textAlign="center">Ошибка при отображении кастомных тарифов</Typography>,
onError: handleComponentError,
})

@ -28,6 +28,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
const discounts = useDiscountStore((state) => state.discounts); const discounts = useDiscountStore((state) => state.discounts);
const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts); const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts);
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0; const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0;
const isUserNko = useUserStore(state => state.userAccount?.status) === "nko";
const [value, setValue] = useState<number>(userValue); const [value, setValue] = useState<number>(userValue);
const throttledValue = useThrottle(value, 200); const throttledValue = useThrottle(value, 200);
@ -39,10 +40,11 @@ export default function TariffPrivilegeSlider({ privilege }: Props) {
throttledValue, throttledValue,
discounts, discounts,
currentCartTotal, currentCartTotal,
purchasesAmount purchasesAmount,
isUserNko,
); );
}, },
[currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue] [currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue, isUserNko]
); );
function handleSliderChange(value: number | number[]) { function handleSliderChange(value: number | number[]) {

@ -18,6 +18,8 @@ import { Slider } from "./slider";
import { useCartStore } from "@root/stores/cart"; import { useCartStore } from "@root/stores/cart";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"; import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"]; const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
@ -26,7 +28,7 @@ const StepperText: Record<string, string> = {
time: "Тарифы на время", time: "Тарифы на время",
}; };
export default function TariffPage() { function TariffPage() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -178,3 +180,8 @@ export default function TariffPage() {
</SectionWrapper> </SectionWrapper>
); );
} }
export default withErrorBoundary(TariffPage, {
fallback: <Typography mt="8px" textAlign="center">Ошибка загрузки тарифов</Typography>,
onError: handleComponentError,
})

@ -5,6 +5,7 @@ import { produce } from "immer";
import { create } from "zustand"; import { create } from "zustand";
import { devtools, persist } from "zustand/middleware"; import { devtools, persist } from "zustand/middleware";
import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
import { findNkoDiscount } from "@root/utils/calcCart/calcCart";
interface CustomTariffsStore { interface CustomTariffsStore {
@ -50,6 +51,7 @@ export const setCustomTariffsUserValue = (
discounts: Discount[], discounts: Discount[],
currentCartTotal: number, currentCartTotal: number,
purchasesAmount: number, purchasesAmount: number,
isUserNko: boolean,
) => useCustomTariffsStore.setState( ) => useCustomTariffsStore.setState(
produce<CustomTariffsStore>(state => { produce<CustomTariffsStore>(state => {
state.userValuesMap[serviceKey] ??= {}; state.userValuesMap[serviceKey] ??= {};
@ -58,22 +60,33 @@ export const setCustomTariffsUserValue = (
let priceWithoutDiscounts = 0; let priceWithoutDiscounts = 0;
let priceAfterDiscounts = 0; let priceAfterDiscounts = 0;
state.privilegeByService[serviceKey].forEach(privilege => { const nkoDiscount = findNkoDiscount(discounts);
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
priceWithoutDiscounts += privilege.price * amount;
const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts); if (isUserNko && nkoDiscount) {
priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount); state.privilegeByService[serviceKey].forEach(privilege => {
}); const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
priceWithoutDiscounts += privilege.price * amount;
});
const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts); priceAfterDiscounts = priceWithoutDiscounts * findDiscountFactor(nkoDiscount);
priceAfterDiscounts *= findDiscountFactor(serviceDiscount); } else {
state.privilegeByService[serviceKey].forEach(privilege => {
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
priceWithoutDiscounts += privilege.price * amount;
const cartDiscount = findCartDiscount(currentCartTotal, discounts); const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts);
priceAfterDiscounts *= findDiscountFactor(cartDiscount); priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount);
});
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts); const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts);
priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount); priceAfterDiscounts *= findDiscountFactor(serviceDiscount);
const cartDiscount = findCartDiscount(currentCartTotal, discounts);
priceAfterDiscounts *= findDiscountFactor(cartDiscount);
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount);
}
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts; state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts; state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;

@ -279,7 +279,7 @@ export const sendUserData = async () => {
export const addTariffToCart = async (tariffId: string) => { export const addTariffToCart = async (tariffId: string) => {
const [patchCartResponse, patchCartError] = await patchCart(tariffId); const [patchCartResponse, patchCartError] = await patchCart(tariffId);
if (patchCartError !== undefined) { if (patchCartError === undefined) {
setCart(patchCartResponse); setCart(patchCartResponse);
} }
return({patchCartResponse, patchCartError}) return({patchCartResponse, patchCartError})

@ -75,7 +75,7 @@ function applyNkoDiscount(cartData: CartData, discount: Discount) {
cartData.allAppliedDiscounts.push(discount); cartData.allAppliedDiscounts.push(discount);
} }
function findNkoDiscount(discounts: Discount[]): Discount | null { export function findNkoDiscount(discounts: Discount[]): Discount | null {
const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko"); const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko");
if (!applicableDiscounts.length) return null; if (!applicableDiscounts.length) return null;

@ -0,0 +1,44 @@
import { makeRequest } from "@frontend/kitui";
import { ErrorInfo } from "react";
interface ComponentError {
timestamp: number;
message: string;
callStack: string | undefined;
componentStack: string;
}
export function handleComponentError(error: Error, info: ErrorInfo) {
const componentError: ComponentError = {
timestamp: Math.floor(Date.now() / 1000),
message: error.message,
callStack: error.stack,
componentStack: info.componentStack,
};
queueErrorRequest(componentError);
}
let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout>;
function queueErrorRequest(error: ComponentError) {
errorsQueue.push(error);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
sendErrorsToServer();
}, 1000);
}
async function sendErrorsToServer() {
// makeRequest({
// url: "",
// method: "POST",
// body: errorsQueue,
// useToken: true,
// });
console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
errorsQueue = [];
}

@ -1532,10 +1532,10 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@frontend/kitui@1.0.53": "@frontend/kitui@1.0.54":
version "1.0.53" version "1.0.54"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.53.tgz#a663052d300b9e3c588346c646f276c9aec7de5d" resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.54.tgz#0235d5a8effb9b92351471c3c7775f28cb2839f6"
integrity sha1-pmMFLTALnjxYg0bGRvJ2ya7H3l0= integrity sha1-AjXVqO/7m5I1FHHDx3dfKMsoOfY=
dependencies: dependencies:
immer "^10.0.2" immer "^10.0.2"
reconnecting-eventsource "^1.6.2" reconnecting-eventsource "^1.6.2"
@ -9448,6 +9448,13 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-error-boundary@^4.0.11:
version "4.0.11"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c"
integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-overlay@^6.0.11: react-error-overlay@^6.0.11:
version "6.0.11" version "6.0.11"
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"