diff --git a/package.json b/package.json index d15dabb..14b55cd 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "@emotion/react": "^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/material": "^5.10.14", "@popperjs/core": "^2.11.8", @@ -29,6 +29,7 @@ "pdfjs-dist": "3.6.172", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", "react-pdf": "^7.1.2", "react-router-dom": "^6.15.0", "react-slick": "^0.29.0", diff --git a/src/api/verification.ts b/src/api/verification.ts index d607e52..458b085 100644 --- a/src/api/verification.ts +++ b/src/api/verification.ts @@ -35,7 +35,7 @@ export async function verification( export async function sendDocuments( documents: SendDocumentsArgs -): Promise<[Verification | null, string?]> { +): Promise<[Verification | "OK" | null, string?]> { try { const sendDocumentsResponse = await makeRequest({ url: apiUrl + "/verification", @@ -55,7 +55,7 @@ export async function sendDocuments( export async function updateDocuments( documents: UpdateDocumentsArgs -): Promise<[Verification | null, string?]> { +): Promise<[Verification | "OK" | null, string? ]> { try { const updateDocumentsResponse = await makeRequest({ url: apiUrl + "/verification/file", diff --git a/src/components/Drawers.tsx b/src/components/Drawers.tsx index 0896626..66ca6e0 100644 --- a/src/components/Drawers.tsx +++ b/src/components/Drawers.tsx @@ -30,8 +30,11 @@ import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"; import { payCart } from "@root/api/cart"; import { enqueueSnackbar } from "notistack"; 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] = useState(false); const [loading, setLoading] = useState(false); @@ -57,12 +60,15 @@ export default function Drawers() { const [payCartResponse, payCartError] = await payCart(); if (payCartError) { - const notEnoughMoneyAmount = parseInt( - payCartError.replace("insufficient funds: ", "") - ); + if (payCartError.includes("insufficient funds: ")) { + const notEnoughMoneyAmount = parseInt( + payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "") + ); - setNotEnoughMoneyAmount(notEnoughMoneyAmount); - setLoading(false); + setNotEnoughMoneyAmount(notEnoughMoneyAmount); + } + + setLoading(false); return enqueueSnackbar(payCartError); } @@ -72,6 +78,7 @@ export default function Drawers() { } setLoading(false); + closeCartDrawer(); } function handleReplenishWallet() { @@ -303,8 +310,6 @@ export default function Drawers() { + + + ); +} + +export default withErrorBoundary(AccountSettings, { + fallback: Ошибка при отображении настроек аккаунта, + onError: handleComponentError, +}) + +const verificationStatusData: Record = { verificated: { text: "Верификация пройдена", color: "#0D9F00" }, waiting: { text: "В ожидании верификации", color: "#F18956" }, notVerificated: { text: "Не верифицирован", color: "#E02C2C" }, - }; +}; - function handleSendDataClick() { - sendUserData() - .then(() => { - enqueueSnackbar("Информация обновлена"); - }) - .catch((error) => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }); - } - - function VerificationIndicator({ +function VerificationIndicator({ verificationStatus, sx, - }: { +}: { verificationStatus: VerificationStatus; sx?: SxProps; - }) { +}) { return ( - - {verificationStatusData[verificationStatus].text} - - ); - } - - return ( - - - - Настройки аккаунта - - - - setSettingsField("firstname", e.target.value)} - id="firstname" - label="Имя" - {...textFieldProps} - /> - setSettingsField("secondname", e.target.value)} - id="secondname" - label="Фамилия" - {...textFieldProps} - /> - setSettingsField("middlename", e.target.value)} - id="middlename" - label="Отчество" - {...textFieldProps} - /> - setSettingsField("orgname", e.target.value)} - id="orgname" - label="Название компании" - {...textFieldProps} - /> - setSettingsField("email", e.target.value)} - id="email" - label="E-mail" - {...textFieldProps} - /> - setSettingsField("phoneNumber", e.target.value)} - id="phoneNumber" - label="Телефон" - {...textFieldProps} - /> - setSettingsField("password", e.target.value)} - id="password" - label="Пароль" - {...textFieldProps} - /> - - - Статус - - {verificationStatus === VerificationStatus.NOT_VERIFICATED && ( - <> - } - sx={{ mt: "55px" }} - ButtonProps={{ - onClick: () => openDocumentsDialog("juridical"), - }} - > - Загрузить документы для юр лиц - - } - sx={{ mt: "15px" }} - ButtonProps={{ - onClick: () => openDocumentsDialog("nko"), - }} - > - Загрузить документы для НКО - - - )} - {verificationStatus === VerificationStatus.VERIFICATED && ( - } - sx={{ mt: "55px" }} - ButtonProps={{ - onClick: () => openDocumentsDialog(verificationType), - }} - > - Посмотреть свою верификацию - - )} - {comment &&

{comment}

} -
+ {verificationStatusData[verificationStatus].text}
- -
-
- ); -} + ); +} \ No newline at end of file diff --git a/src/pages/AccountSettings/DocumentsDialog/DocumentsDialog.tsx b/src/pages/AccountSettings/DocumentsDialog/DocumentsDialog.tsx index 8ad7209..6ada2f0 100644 --- a/src/pages/AccountSettings/DocumentsDialog/DocumentsDialog.tsx +++ b/src/pages/AccountSettings/DocumentsDialog/DocumentsDialog.tsx @@ -4,7 +4,14 @@ import JuridicalDocumentsDialog from "./JuridicalDocumentsDialog"; export default function DocumentsDialog() { - const type = useUserStore(state => state.dialogType); - - return type === "juridical" ? : + switch(useUserStore(state => state.dialogType)) { + case 'juridical': + return + + case "nko": + return + + default: + return <> + } } \ No newline at end of file diff --git a/src/pages/AccountSettings/DocumentsDialog/JuridicalDocumentsDialog.tsx b/src/pages/AccountSettings/DocumentsDialog/JuridicalDocumentsDialog.tsx index fa683bb..c649436 100644 --- a/src/pages/AccountSettings/DocumentsDialog/JuridicalDocumentsDialog.tsx +++ b/src/pages/AccountSettings/DocumentsDialog/JuridicalDocumentsDialog.tsx @@ -14,6 +14,7 @@ import { readFile } from "@root/utils/readFile"; import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys"; import { verify } from "../helper"; import { useState } from "react"; +import { theme } from "@root/utils/theme"; const dialogContainerStyle = { height: "100%", @@ -26,15 +27,15 @@ const dialogContainerStyle = { export default function JuridicalDocumentsDialog() { const isOpen = useUserStore((state) => state.isDocumentsDialogOpen); const verificationStatus = useUserStore((state) => state.verificationStatus); - const documents = useUserStore((state) => state.documents); - const documentsUrl = useUserStore((state) => state.documentsUrl); + const documents = useUserStore((state) => state.documents);//загруженные юзером файлы + const documentsUrl = useUserStore((state) => state.documentsUrl);//ссылки с бекенда const userId = useUserStore((state) => state.userId) ?? ""; - const [isLoading, setIsLoading] = useState(false); const sendUploadedDocuments = async () => { - setIsLoading(true); - if (documents["ИНН"].file && documents["Устав"].file) { + if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) { + closeDocumentsDialog(); + //Пользователь заполнил все поля и на беке пусто const inn = await readFile(documents["ИНН"].file, "binary"); const rule = await readFile(documents["Устав"].file, "binary"); @@ -46,42 +47,64 @@ export default function JuridicalDocumentsDialog() { if (sendDocumentsError) { enqueueSnackbar(sendDocumentsError); - return; } - } else { - const inn = documents["ИНН"].file - ? await readFile(documents["ИНН"].file, "binary") - : undefined; - const rule = documents["Устав"].file - ? await readFile(documents["Устав"].file, "binary") - : undefined; - - const [_, updateDocumentsError] = await updateDocuments( - deleteEmptyKeys({ - status: "org", - inn, - rule, - }) - ); - - if (updateDocumentsError) { - enqueueSnackbar(updateDocumentsError); - - return; + if (_ === "OK") { + enqueueSnackbar("Информация доставлена") } + + setDocument("ИНН", null); + setDocument("Устав", null); + + await verify(userId); + } else { //Пользователь заполнил не все, или на беке что-то есть + if ((documents["ИНН"].file || documents["Устав"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано + closeDocumentsDialog(); + const inn = documents["ИНН"].file + ? await readFile(documents["ИНН"].file, "binary") + : undefined; + const rule = documents["Устав"].file + ? await readFile(documents["Устав"].file, "binary") + : undefined; + + const [_, updateDocumentsError] = await updateDocuments( + deleteEmptyKeys({ + status: "org", + inn, + rule, + }) + ); + + if (updateDocumentsError) { + enqueueSnackbar(updateDocumentsError); + + return; + } + if (_ === "OK") { + enqueueSnackbar("Информация доставлена") + } + + setDocument("ИНН", null); + setDocument("Устав", null); + + await verify(userId); + } } - - setIsLoading(false); - closeDocumentsDialog(); - - setDocument("ИНН", null); - setDocument("Устав", null); - - await verify(userId); - if (!isLoading) closeDocumentsDialog(); }; + const disbutt = () => { + if (documents["ИНН"].file && documents["Устав"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"]) { //post + //все поля заполнены и на беке пусто + return false + } else {//patch + if (documents["ИНН"].file || documents["Устав"].file) { + //минимум одно поле заполнено + return false + } + return true + } + } + const documentElements = verificationStatus === VerificationStatus.VERIFICATED ? ( <> @@ -163,6 +186,18 @@ export default function JuridicalDocumentsDialog() { > для верификации юридических лиц в формате PDF + {Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"]) && + Пожалуйста, заполните все поля! + } + Отправить diff --git a/src/pages/AccountSettings/DocumentsDialog/NkoDocumentsDialog.tsx b/src/pages/AccountSettings/DocumentsDialog/NkoDocumentsDialog.tsx index 8b3f017..98ab7b0 100644 --- a/src/pages/AccountSettings/DocumentsDialog/NkoDocumentsDialog.tsx +++ b/src/pages/AccountSettings/DocumentsDialog/NkoDocumentsDialog.tsx @@ -14,6 +14,7 @@ import { sendDocuments, updateDocuments } from "@root/api/verification"; import { readFile } from "@root/utils/readFile"; import { deleteEmptyKeys } from "@root/utils/deleteEmptyKeys"; import { useState } from "react"; +import { theme } from "@root/utils/theme"; const dialogContainerStyle = { height: "100%", @@ -29,15 +30,16 @@ export default function NkoDocumentsDialog() { const documents = useUserStore((state) => state.documents); const documentsUrl = useUserStore((state) => state.documentsUrl); const userId = useUserStore((state) => state.userId) ?? ""; - const [isLoading, setIsLoading] = useState(false); const sendUploadedDocuments = async () => { - setIsLoading(true); if ( documents["ИНН"].file && documents["Устав"].file && documents["Свидетельство о регистрации НКО"].file + && !documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"] ) { + closeDocumentsDialog(); + //Пользователь заполнил все поля и на беке пусто const inn = await readFile(documents["ИНН"].file, "binary"); const rule = await readFile(documents["Устав"].file, "binary"); const certificate = await readFile( @@ -57,46 +59,72 @@ export default function NkoDocumentsDialog() { return; } - } else { - const inn = documents["ИНН"].file - ? await readFile(documents["ИНН"].file, "binary") - : undefined; - const rule = documents["Устав"].file - ? await readFile(documents["Устав"].file, "binary") - : undefined; - const certificate = documents["Свидетельство о регистрации НКО"].file - ? await readFile( - documents["Свидетельство о регистрации НКО"].file, - "binary" - ) - : undefined; + if (_ === "OK") { + enqueueSnackbar("Информация доставлена") + } - const [_, updateDocumentsError] = await updateDocuments( - deleteEmptyKeys({ - status: "org", - inn, - rule, - certificate, - }) - ); + setDocument("ИНН", null); + setDocument("Устав", null); + setDocument("Свидетельство о регистрации НКО", null); + + await verify(userId); + } else { //Пользователь заполнил не все, или на беке что-то есть + if ((documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file) && (documentsUrl["ИНН"] || documentsUrl["Устав"] || documentsUrl["Свидетельство о регистрации НКО"])) { //минимум 1 поле заполнено на фронте и минимум 1 поле на беке записано + closeDocumentsDialog(); + const inn = documents["ИНН"].file + ? await readFile(documents["ИНН"].file, "binary") + : undefined; + const rule = documents["Устав"].file + ? await readFile(documents["Устав"].file, "binary") + : undefined; + const certificate = documents["Свидетельство о регистрации НКО"].file + ? await readFile( + documents["Свидетельство о регистрации НКО"].file, + "binary" + ) + : undefined; - if (updateDocumentsError) { - enqueueSnackbar(updateDocumentsError); + const [_, updateDocumentsError] = await updateDocuments( + deleteEmptyKeys({ + status: "org", + inn, + rule, + certificate, + }) + ); - return; + if (updateDocumentsError) { + enqueueSnackbar(updateDocumentsError); + + return; + } + if (_ === "OK") { + enqueueSnackbar("Информация доставлена") + } + + setDocument("ИНН", null); + setDocument("Устав", null); + setDocument("Свидетельство о регистрации НКО", null); + + await verify(userId); } } - setIsLoading(false); - closeDocumentsDialog(); - - setDocument("ИНН", null); - setDocument("Устав", null); - setDocument("Свидетельство о регистрации НКО", null); - - await verify(userId); }; + const disbutt = () => { + if (documents["ИНН"].file && documents["Устав"].file && documents["Свидетельство о регистрации НКО"].file && !documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) { //post + //все поля заполнены и на беке пусто + return false + } else {//patch + if (documents["ИНН"].file || documents["Устав"].file || documents["Свидетельство о регистрации НКО"].file ) { + //минимум одно поле заполнено + return false + } + return true + } + } + const documentElements = verificationStatus === VerificationStatus.VERIFICATED ? ( <> @@ -194,6 +222,17 @@ export default function NkoDocumentsDialog() { > для верификации НКО в формате PDF + {Boolean(!documentsUrl["ИНН"] && !documentsUrl["Устав"] && !documentsUrl["Свидетельство о регистрации НКО"]) && + Пожалуйста, заполните все поля! + } Отправить diff --git a/src/pages/AccountSettings/UserFields.tsx b/src/pages/AccountSettings/UserFields.tsx new file mode 100644 index 0000000..2d29689 --- /dev/null +++ b/src/pages/AccountSettings/UserFields.tsx @@ -0,0 +1,121 @@ +import { Box, useMediaQuery, useTheme } from "@mui/material"; +import InputTextfield from "@components/InputTextfield"; +import PasswordInput from "@components/passwordInput"; +import { setSettingsField, useUserStore } from "@root/stores/user"; + + +export default function UserFields () { + const theme = useTheme(); + const upSm = useMediaQuery(theme.breakpoints.up("sm")); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + const fields = useUserStore((state) => state.settingsFields); + + console.log("fields") + + const textFieldProps = { + gap: upMd ? "16px" : "10px", + color: "#F2F3F7", + bold: true, + }; + + return( + + setSettingsField("firstname", e.target.value)} + id="firstname" + label="Имя" + {...textFieldProps} + /> + setSettingsField("secondname", e.target.value)} + id="secondname" + label="Фамилия" + {...textFieldProps} + /> + setSettingsField("middlename", e.target.value)} + id="middlename" + label="Отчество" + {...textFieldProps} + /> + setSettingsField("orgname", e.target.value)} + id="orgname" + label="Название компании" + {...textFieldProps} + /> + setSettingsField("email", e.target.value)} + id="email" + label="E-mail" + {...textFieldProps} + /> + setSettingsField("phoneNumber", e.target.value)} + id="phoneNumber" + label="Телефон" + {...textFieldProps} + /> + setSettingsField("password", e.target.value)} + id="password" + label="Пароль" + {...textFieldProps} + /> + + ) +} \ No newline at end of file diff --git a/src/pages/Cart/Cart.tsx b/src/pages/Cart/Cart.tsx index 84216c3..fb744bb 100644 --- a/src/pages/Cart/Cart.tsx +++ b/src/pages/Cart/Cart.tsx @@ -6,8 +6,10 @@ import CustomWrapper from "./CustomWrapper"; import { useCart } from "@root/utils/hooks/useCart"; import { useLocation } from "react-router-dom"; 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 upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(550)); @@ -71,3 +73,8 @@ export default function Cart() { ); } + +export default withErrorBoundary(Cart, { + fallback: Ошибка при отображении корзины, + onError: handleComponentError, +}) diff --git a/src/pages/History/index.tsx b/src/pages/History/index.tsx index 16ca86a..2f9f428 100644 --- a/src/pages/History/index.tsx +++ b/src/pages/History/index.tsx @@ -11,6 +11,8 @@ import { HISTORY } from "./historyMocks"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import { useHistoryData } from "@root/utils/hooks/useHistoryData"; import { isArray } from "cypress/types/lodash"; +import { ErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]; @@ -67,23 +69,30 @@ export default function History() { ) : ( )} - {historyData?.records - .filter((e) => { - e.createdAt = extractDateFromString(e.createdAt) - return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value) - )}) - .map(( e, index) => { - return ( - - - - )})} + Ошибка загрузки истории + } + onError={handleComponentError} + > + {historyData?.records + .filter((e) => { + e.createdAt = extractDateFromString(e.createdAt) + return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value) + )}) + .map(( e, index) => { + return ( + + + + )})} + ); } diff --git a/src/pages/SavedTariffs/index.tsx b/src/pages/SavedTariffs/index.tsx index 4b37a2b..fada753 100644 --- a/src/pages/SavedTariffs/index.tsx +++ b/src/pages/SavedTariffs/index.tsx @@ -5,8 +5,10 @@ import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import SaveWrapper from "./SaveWrapper"; import { useTariffStore } from "@root/stores/tariffs"; 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 upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(550)); @@ -65,3 +67,8 @@ export default function SavedTariffs() { ); } + +export default withErrorBoundary(SavedTariffs, { + fallback: Ошибка при отображении сохраненных тарифов, + onError: handleComponentError, +}) diff --git a/src/pages/Support/SupportChat.tsx b/src/pages/Support/SupportChat.tsx index d275a7b..88409a9 100644 --- a/src/pages/Support/SupportChat.tsx +++ b/src/pages/Support/SupportChat.tsx @@ -35,8 +35,10 @@ import { useTicketMessages, } from "@frontend/kitui"; 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 upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.up(460)); @@ -128,7 +130,7 @@ export default function SupportChat() { async function handleSendMessage() { if (!ticket || !messageField) return; - const [_, sendTicketMessageError] = await sendTicketMessage( + const [, sendTicketMessageError] = await sendTicketMessage( ticket.id, messageField ); @@ -318,3 +320,8 @@ export default function SupportChat() { ); } + +export default withErrorBoundary(SupportChat, { + fallback: Не удалось отобразить чат, + onError: handleComponentError, +}) diff --git a/src/pages/Support/TicketList/TicketList.tsx b/src/pages/Support/TicketList/TicketList.tsx index 22ad915..98035c3 100644 --- a/src/pages/Support/TicketList/TicketList.tsx +++ b/src/pages/Support/TicketList/TicketList.tsx @@ -5,13 +5,16 @@ import { Box, useTheme, Pagination, + Typography, } from "@mui/material"; import TicketCard from "./TicketCard"; import { setTicketApiPage, useTicketStore } from "@root/stores/tickets"; 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 tickets = useTicketStore((state) => state.tickets); const ticketCount = useTicketStore((state) => state.ticketCount); @@ -87,3 +90,8 @@ function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) { const date2 = new Date(ticket2.updated_at).getTime(); return date2 - date1; } + +export default withErrorBoundary(TicketList, { + fallback: Ошибка загрузки тикетов, + onError: handleComponentError, +}); diff --git a/src/pages/TariffConstructor/TariffConstructor.tsx b/src/pages/TariffConstructor/TariffConstructor.tsx index c007bbf..d36c667 100644 --- a/src/pages/TariffConstructor/TariffConstructor.tsx +++ b/src/pages/TariffConstructor/TariffConstructor.tsx @@ -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 SectionWrapper from "@components/SectionWrapper"; import { useCustomTariffsStore } from "@root/stores/customTariffs"; @@ -8,8 +8,10 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import TotalPrice from "@root/components/TotalPrice"; import { serviceNameByKey } from "@root/utils/serviceKeys"; 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 upMd = useMediaQuery(theme.breakpoints.up("md")); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); @@ -84,3 +86,8 @@ export default function TariffConstructor() { ); } + +export default withErrorBoundary(TariffConstructor, { + fallback: Ошибка при отображении кастомных тарифов, + onError: handleComponentError, +}) diff --git a/src/pages/TariffConstructor/TariffItem.tsx b/src/pages/TariffConstructor/TariffItem.tsx index aad11e2..1368e97 100644 --- a/src/pages/TariffConstructor/TariffItem.tsx +++ b/src/pages/TariffConstructor/TariffItem.tsx @@ -28,6 +28,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) { const discounts = useDiscountStore((state) => state.discounts); const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts); const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0; + const isUserNko = useUserStore(state => state.userAccount?.status) === "nko"; const [value, setValue] = useState(userValue); const throttledValue = useThrottle(value, 200); @@ -39,10 +40,11 @@ export default function TariffPrivilegeSlider({ privilege }: Props) { throttledValue, discounts, 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[]) { diff --git a/src/pages/Tariffs/TariffsPage.tsx b/src/pages/Tariffs/TariffsPage.tsx index 6248ec0..93b6832 100644 --- a/src/pages/Tariffs/TariffsPage.tsx +++ b/src/pages/Tariffs/TariffsPage.tsx @@ -18,6 +18,8 @@ import { Slider } from "./slider"; import { useCartStore } from "@root/stores/cart"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"]; @@ -26,7 +28,7 @@ const StepperText: Record = { time: "Тарифы на время", }; -export default function TariffPage() { +function TariffPage() { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -178,3 +180,8 @@ export default function TariffPage() { ); } + +export default withErrorBoundary(TariffPage, { + fallback: Ошибка загрузки тарифов, + onError: handleComponentError, +}) diff --git a/src/stores/customTariffs.ts b/src/stores/customTariffs.ts index 45610aa..1a6ca6d 100644 --- a/src/stores/customTariffs.ts +++ b/src/stores/customTariffs.ts @@ -5,6 +5,7 @@ import { produce } from "immer"; import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; +import { findNkoDiscount } from "@root/utils/calcCart/calcCart"; interface CustomTariffsStore { @@ -50,6 +51,7 @@ export const setCustomTariffsUserValue = ( discounts: Discount[], currentCartTotal: number, purchasesAmount: number, + isUserNko: boolean, ) => useCustomTariffsStore.setState( produce(state => { state.userValuesMap[serviceKey] ??= {}; @@ -58,22 +60,33 @@ export const setCustomTariffsUserValue = ( let priceWithoutDiscounts = 0; let priceAfterDiscounts = 0; - state.privilegeByService[serviceKey].forEach(privilege => { - const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0; - priceWithoutDiscounts += privilege.price * amount; + const nkoDiscount = findNkoDiscount(discounts); - const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts); - priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount); - }); + if (isUserNko && nkoDiscount) { + state.privilegeByService[serviceKey].forEach(privilege => { + const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0; + priceWithoutDiscounts += privilege.price * amount; + }); - const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts); - priceAfterDiscounts *= findDiscountFactor(serviceDiscount); + priceAfterDiscounts = priceWithoutDiscounts * findDiscountFactor(nkoDiscount); + } else { + state.privilegeByService[serviceKey].forEach(privilege => { + const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0; + priceWithoutDiscounts += privilege.price * amount; - const cartDiscount = findCartDiscount(currentCartTotal, discounts); - priceAfterDiscounts *= findDiscountFactor(cartDiscount); + const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts); + priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount); + }); - const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts); - priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount); + const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts); + priceAfterDiscounts *= findDiscountFactor(serviceDiscount); + + const cartDiscount = findCartDiscount(currentCartTotal, discounts); + priceAfterDiscounts *= findDiscountFactor(cartDiscount); + + const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts); + priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount); + } state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts; state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts; diff --git a/src/stores/user.ts b/src/stores/user.ts index e791c51..c023869 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -23,7 +23,7 @@ interface UserStore { verificationStatus: VerificationStatus; verificationType: "juridical" | "nko"; isDocumentsDialogOpen: boolean; - dialogType: "juridical" | "nko"; + dialogType: "juridical" | "nko" | ""; documents: UserDocuments; documentsUrl: UserDocumentsUrl; comment: string; @@ -61,7 +61,7 @@ const initialState: UserStore = { verificationStatus: VerificationStatus.NOT_VERIFICATED, verificationType: "juridical", isDocumentsDialogOpen: false, - dialogType: "juridical", + dialogType: "", comment: "", documents: { ИНН: { ...defaultDocument }, @@ -87,7 +87,7 @@ export const useUserStore = create()( version: 2, name: "user", storage: createJSONStorage(() => localStorage), - partialize: (state) => ({ + partialize: (state) => ({ // список полей для хранения в ЛС userId: state.userId, user: state.user, }), @@ -161,6 +161,7 @@ export const closeDocumentsDialog = () => useUserStore.setState( produce((state) => { state.isDocumentsDialogOpen = false; + state.dialogType = ""; }) ); @@ -214,7 +215,7 @@ export const setUploadedDocument = (type: UserDocumentTypes, fileName: string, u }) ); -export const setSettingsField = (fieldName: UserSettingsField | keyof UserName, value: string) => +export const setSettingsField = (fieldName: UserSettingsField | keyof UserName, value: string) => useUserStore.setState( produce((state) => { if (!state.settingsFields) return; @@ -278,7 +279,7 @@ export const sendUserData = async () => { export const addTariffToCart = async (tariffId: string) => { const [patchCartResponse, patchCartError] = await patchCart(tariffId); - if (patchCartError !== undefined) { + if (patchCartError === undefined) { setCart(patchCartResponse); } return({patchCartResponse, patchCartError}) diff --git a/src/utils/calcCart/calcCart.ts b/src/utils/calcCart/calcCart.ts index 3cf37db..92b62fe 100644 --- a/src/utils/calcCart/calcCart.ts +++ b/src/utils/calcCart/calcCart.ts @@ -75,7 +75,7 @@ function applyNkoDiscount(cartData: CartData, discount: 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"); if (!applicableDiscounts.length) return null; diff --git a/src/utils/handleComponentError.ts b/src/utils/handleComponentError.ts new file mode 100644 index 0000000..e6ae5bd --- /dev/null +++ b/src/utils/handleComponentError.ts @@ -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; + +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 = []; +} diff --git a/yarn.lock b/yarn.lock index 8217f37..0b0708c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1532,10 +1532,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@frontend/kitui@1.0.53": - version "1.0.53" - resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.53.tgz#a663052d300b9e3c588346c646f276c9aec7de5d" - integrity sha1-pmMFLTALnjxYg0bGRvJ2ya7H3l0= +"@frontend/kitui@1.0.54": + version "1.0.54" + resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.54.tgz#0235d5a8effb9b92351471c3c7775f28cb2839f6" + integrity sha1-AjXVqO/7m5I1FHHDx3dfKMsoOfY= dependencies: immer "^10.0.2" reconnecting-eventsource "^1.6.2" @@ -9448,6 +9448,13 @@ react-dom@^18.2.0: loose-envify "^1.1.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: version "6.0.11" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"