diff --git a/package.json b/package.json index a53900d..cc8a55c 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.17", + "@frontend/kitui": "1.0.21", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", "@popperjs/core": "^2.11.8", diff --git a/src/api/tariff.ts b/src/api/tariff.ts index f03ad1b..8f0512d 100644 --- a/src/api/tariff.ts +++ b/src/api/tariff.ts @@ -1,10 +1,12 @@ import { Tariff, makeRequest } from "@frontend/kitui"; -import { CreateTariffBody, CustomTariff } from "@root/model/customTariffs"; +import { CreateTariffBody } from "@root/model/customTariffs"; +const apiUrl = process.env.NODE_ENV === "production" ? "/strator" : "https://hub.pena.digital/strator"; + export function createTariff(tariff: CreateTariffBody) { - return makeRequest({ - url: `https://admin.pena.digital/strator/tariff`, + return makeRequest({ + url: `${apiUrl}/tariff`, method: "post", useToken: true, body: tariff, @@ -13,7 +15,7 @@ export function createTariff(tariff: CreateTariffBody) { export function getTariffById(tariffId:string){ return makeRequest({ - url: `https://admin.pena.digital/strator/tariff/${tariffId}`, + url: `${apiUrl}/tariff/${tariffId}`, method: "get", useToken: true, }); diff --git a/src/assets/Icons/cross.svg b/src/assets/Icons/cross.svg new file mode 100644 index 0000000..a39e226 --- /dev/null +++ b/src/assets/Icons/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/CustomTariffAccordion.tsx b/src/components/CustomTariffAccordion.tsx new file mode 100644 index 0000000..4ff582e --- /dev/null +++ b/src/components/CustomTariffAccordion.tsx @@ -0,0 +1,159 @@ +import { useState } from "react"; +import { + Box, Typography, + IconButton, + useMediaQuery, + useTheme +} from "@mui/material"; +import ExpandIcon from "@components/icons/ExpandIcon"; +import { currencyFormatter } from "@root/utils/currencyFormatter"; +import { removeTariffFromCart } from "@root/stores/user"; +import { enqueueSnackbar } from "notistack"; +import { TariffCartData, getMessageFromFetchError } from "@frontend/kitui"; +import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"; + + +interface Props { + tariffCartData: TariffCartData; +} + +export default function CustomTariffAccordion({ tariffCartData }: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const upSm = useMediaQuery(theme.breakpoints.up("sm")); + const [isExpanded, setIsExpanded] = useState(false); + + function handleDeleteClick() { + removeTariffFromCart(tariffCartData.tariffId) + .then(() => { + enqueueSnackbar("Тариф удален"); + }) + .catch((error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }); + } + + return ( + + + setIsExpanded((prev) => !prev)} + sx={{ + height: "72px", + pr: "20px", + pl: "30px", + display: "flex", + gap: "15px", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + userSelect: "none", + }} + > + + + + + Кастомный тариф + + + + {currencyFormatter.format(tariffCartData.price / 100)} + + + + + + + {isExpanded && tariffCartData.privileges.map((privilege) => ( + + + {privilege.description} + + + + {currencyFormatter.format(privilege.price / 100)} + + + + ))} + + + ); +} diff --git a/src/components/CustomWrapperDrawer.tsx b/src/components/CustomWrapperDrawer.tsx index 4d9fa1c..fc79d11 100644 --- a/src/components/CustomWrapperDrawer.tsx +++ b/src/components/CustomWrapperDrawer.tsx @@ -1,14 +1,30 @@ import { useState } from "react"; -import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { + Box, + SvgIcon, + IconButton, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; import { currencyFormatter } from "@root/utils/currencyFormatter"; import { removeTariffFromCart } from "@root/stores/user"; import { enqueueSnackbar } from "notistack"; import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"; +import ExpandIcon from "@components/icons/ExpandIcon"; +import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"; -const name: Record = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" }; +import type { MouseEvent } from "react"; +import CustomTariffAccordion from "@root/components/CustomTariffAccordion"; + +const name: Record = { + templategen: "Шаблонизатор", + squiz: "Опросник", + reducer: "Скоращатель ссылок", +}; interface Props { serviceData: ServiceCartData; @@ -21,19 +37,36 @@ export default function CustomWrapperDrawer({ serviceData }: Props) { const [isExpanded, setIsExpanded] = useState(false); function handleItemDeleteClick(tariffId: string) { - removeTariffFromCart(tariffId).then(() => { - enqueueSnackbar("Тариф удален"); - }).catch(error => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }); + removeTariffFromCart(tariffId) + .then(() => { + enqueueSnackbar("Тариф удален"); + }) + .catch((error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }); } + const deleteService = async (event: MouseEvent) => { + event.stopPropagation(); + + setIsExpanded(false); + + for (const { tariffId } of serviceData.tariffs) { + try { + await removeTariffFromCart(tariffId); + } catch { } + } + + enqueueSnackbar("Тарифы удален"); + }; + return ( setIsExpanded((prev) => !prev)} sx={{ height: "72px", - display: "flex", + gap: "10px", alignItems: "center", justifyContent: "space-between", cursor: "pointer", userSelect: "none", }} > - - {name[serviceData.serviceKey]} - - + - {currencyFormatter.format(serviceData.price / 100)} + {name[serviceData.serviceKey]} - - - {isExpanded && - serviceData.privileges.map(privilege => ( - - {privilege.description} + {currencyFormatter.format(serviceData.price / 100)} + + + + + + + {isExpanded && + serviceData.tariffs.map(tariff => { + const privilege = tariff.privileges[0]; + + return tariff.privileges.length > 1 ? ( + + ) : ( - {currencyFormatter.format(privilege.price / 100)} + {privilege.description} - - handleItemDeleteClick(privilege.tariffId)} - component={ClearIcon} - /> + + + {currencyFormatter.format(privilege.price / 100)} + + handleItemDeleteClick(privilege.tariffId)} + component={ClearIcon} + /> + - - ))} + ); + })} ); diff --git a/src/components/Drawers.tsx b/src/components/Drawers.tsx index d314b14..9f9fd7d 100644 --- a/src/components/Drawers.tsx +++ b/src/components/Drawers.tsx @@ -6,11 +6,9 @@ import { useTheme, Box, IconButton, - SvgIcon, Badge, } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import ClearIcon from "@mui/icons-material/Clear"; import { useTickets } from "@frontend/kitui"; import SectionWrapper from "./SectionWrapper"; import CustomWrapperDrawer from "./CustomWrapperDrawer"; @@ -24,7 +22,6 @@ import { openCartDrawer, useCartStore, } from "@root/stores/cart"; -import { useCustomTariffsStore } from "@root/stores/customTariffs"; import { useUserStore } from "@root/stores/user"; import { updateTickets, @@ -34,6 +31,7 @@ import { import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg"; import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.svg"; +import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"; export default function Drawers() { const [openNotificationsModal, setOpenNotificationsModal] = @@ -44,12 +42,6 @@ export default function Drawers() { const upMd = useMediaQuery(theme.breakpoints.up("md")); const isDrawerOpen = useCartStore((state) => state.isDrawerOpen); const cart = useCart(); - const summaryPriceBeforeDiscountsMap = useCustomTariffsStore( - (state) => state.summaryPriceBeforeDiscountsMap - ); - const summaryPriceAfterDiscountsMap = useCustomTariffsStore( - (state) => state.summaryPriceAfterDiscountsMap - ); const userAccount = useUserStore((state) => state.userAccount); const { tickets, apiPage, ticketsPerPage } = useTicketStore((state) => state); @@ -64,21 +56,12 @@ export default function Drawers() { onError: () => {}, }); - const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce( - (a, e) => a + e, - 0 - ); - const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce( - (a, e) => a + e, - 0 - ); - const notificationsCount = tickets.filter( ({ user, top_message }) => user !== top_message.user_id ).length; - const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice; - const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice; + const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts; + const totalPriceAfterDiscounts = cart.priceAfterDiscounts; return ( @@ -227,15 +210,23 @@ export default function Drawers() { Корзина - + {cart.services.map((serviceData) => ( - + sx={{ + display: "flex", + alignItems: "center", + }} + > + + ))} - {location.pathname !== "/" ? arrayMenu.map(({ name, url, subMenu = [] }) => ( - setActiveSubMenu(subMenu)} - > - - {name} - - - )) - :arrayMenu.map(({ name, url, subMenu = [] }) => ( - - {name} - - )) - } + {location.pathname !== "/" + ? arrayMenu.map(({ name, url, subMenu = [] }) => ( + setActiveSubMenu(subMenu)} + state={{ previousUrl: location.pathname }} + > + + {name} + + + )) + : arrayMenu.map(({ name, url, subMenu = [] }) => ( + + {name} + + ))} setActiveSubMenu([])} > - {location.pathname !== "/" && activeSubMenu.map(({ name, url }) => ( - - - {name} - - - ))} + {location.pathname !== "/" && + activeSubMenu.map(({ name, url }) => ( + + + {name} + + + ))} ); diff --git a/src/components/Navbar/DialogMenu.tsx b/src/components/Navbar/DialogMenu.tsx index 007add9..4d69d83 100644 --- a/src/components/Navbar/DialogMenu.tsx +++ b/src/components/Navbar/DialogMenu.tsx @@ -85,7 +85,7 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) { key={index} component={Link} to={url} - state={user ? undefined : { backgroundLocation: location }} + state={{ previousUrl: location.pathname }} disableRipple variant="text" onClick={() => @@ -134,6 +134,7 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) { }} to={url} onClick={closeDialogMenu} + state={{ previousUrl: location.pathname }} > + + - ); -} \ No newline at end of file + + ) +} diff --git a/src/index.tsx b/src/index.tsx index 222fec9..a012a30 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,7 +12,7 @@ import Tariffs from "./pages/Tariffs/Tariffs"; import SigninDialog from "./pages/auth/Signin"; import SignupDialog from "./pages/auth/Signup"; import History from "./pages/History"; -import Basket from "./pages/Basket/Basket"; +import Cart from "./pages/Cart/Cart"; import TariffPage from "./pages/Tariffs/TariffsPage"; import SavedTariffs from "./pages/SavedTariffs"; import lightTheme from "@utils/themes/light"; @@ -24,13 +24,14 @@ import Layout from "./components/Layout"; import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user"; import TariffConstructor from "./pages/TariffConstructor/TariffConstructor"; import { useUser } from "./utils/hooks/useUser"; -import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui"; +import { clearAuthToken, getMessageFromFetchError, usePrivilegeFetcher } from "@frontend/kitui"; import { useUserAccount } from "./utils/hooks/useUserAccount"; import { setCustomTariffs } from "@root/stores/customTariffs"; import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs"; import { useDiscounts } from "./utils/hooks/useDiscounts"; import { setDiscounts } from "./stores/discounts"; import { pdfjs } from "react-pdf"; +import { setPrivileges } from "./stores/privileges"; pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString(); @@ -85,6 +86,13 @@ const App = () => { } }); + usePrivilegeFetcher({ + onSuccess: setPrivileges, + onError: error => { + console.log("usePrivilegeFetcher error :>> ", error); + } + }); + if (location.state?.redirectTo) return ; return ( @@ -108,7 +116,7 @@ const App = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/model/customTariffs.ts b/src/model/customTariffs.ts index 19fd7dd..a85631a 100644 --- a/src/model/customTariffs.ts +++ b/src/model/customTariffs.ts @@ -1,4 +1,5 @@ -import { PrivilegeWithAmount, PrivilegeWithoutPrice } from "./privilege"; +import { PrivilegeWithAmount } from "@frontend/kitui"; +import { PrivilegeWithoutPrice } from "./privilege"; type ServiceKey = string; @@ -9,14 +10,9 @@ export type CustomTariffUserValuesMap = Record; -export interface CustomTariff { +export interface CreateTariffBody { name: string; price?: number; isCustom: boolean; - privilegies: PrivilegeWithAmount[]; - updatedAt?: string; - isDeleted?: boolean; - createdAt?: string; + privilegies: PrivilegeWithoutPrice[]; } - -export type CreateTariffBody = Omit & { privilegies: PrivilegeWithoutPrice[]; }; diff --git a/src/model/privilege.ts b/src/model/privilege.ts index fdedde3..47aa509 100644 --- a/src/model/privilege.ts +++ b/src/model/privilege.ts @@ -1,21 +1,6 @@ -export interface Privilege { - _id: string; - name: string; - privilegeId: string; - serviceKey: string; - description: string; - type: "day" | "count"; - value: PrivilegeValueType; - price: number; - updatedAt?: string; - isDeleted?: boolean; - createdAt?: string; -}; +import { Privilege, PrivilegeWithAmount } from "@frontend/kitui"; + export type ServiceKeyToPrivilegesMap = Record; -export type PrivilegeValueType = "шаблон" | "день" | "МБ"; - -export type PrivilegeWithAmount = Omit & { amount: number; }; - export type PrivilegeWithoutPrice = Omit; diff --git a/src/pages/Basket/Basket.tsx b/src/pages/Basket/Basket.tsx deleted file mode 100644 index be7bd5e..0000000 --- a/src/pages/Basket/Basket.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"; -import SectionWrapper from "@components/SectionWrapper"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import TotalPrice from "@components/TotalPrice"; -import CustomWrapper from "./CustomWrapper"; -import { useCart } from "@root/utils/hooks/useCart"; -import { useCustomTariffsStore } from "@root/stores/customTariffs"; - - -export default function Basket() { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const cart = useCart(); - const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap); - const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap); - - const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0); - const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0); - - const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice; - const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice; - - return ( - - - {!upMd && ( - - - - )} - - Корзина - - - - {cart.services.map(serviceData => - - )} - - - - ); -} diff --git a/src/pages/Basket/CustomWrapper.tsx b/src/pages/Basket/CustomWrapper.tsx deleted file mode 100644 index d6f12f0..0000000 --- a/src/pages/Basket/CustomWrapper.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { useState } from "react"; -import { - Box, - SvgIcon, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; -import ExpandIcon from "@components/icons/ExpandIcon"; -import ClearIcon from "@mui/icons-material/Clear"; -import { cardShadow } from "@root/utils/themes/shadow"; -import { currencyFormatter } from "@root/utils/currencyFormatter"; -import { removeTariffFromCart } from "@root/stores/user"; -import { enqueueSnackbar } from "notistack"; -import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"; - -const name: Record = { - templategen: "Шаблонизатор", - squiz: "Опросник", - reducer: "Сокращатель ссылок", -}; - -interface Props { - serviceData: ServiceCartData; -} - -export default function CustomWrapper({ serviceData }: Props) { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const upSm = useMediaQuery(theme.breakpoints.up("sm")); - const [isExpanded, setIsExpanded] = useState(false); - - function handleItemDeleteClick(tariffId: string) { - removeTariffFromCart(tariffId) - .then(() => { - enqueueSnackbar("Тариф удален"); - }) - .catch((error) => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }); - } - - return ( - - - setIsExpanded((prev) => !prev)} - sx={{ - height: "72px", - px: "20px", - - display: "flex", - alignItems: "center", - justifyContent: "space-between", - cursor: "pointer", - userSelect: "none", - }} - > - - {name[serviceData.serviceKey]} - - - - - {currencyFormatter.format(serviceData.price / 100)} - - - - - - - {isExpanded && - serviceData.privileges.map((privilege) => ( - - - {privilege.description} - - - - {currencyFormatter.format(privilege.price / 100)} - - {upSm ? ( - handleItemDeleteClick(privilege.tariffId)} - sx={{ - color: theme.palette.text.secondary, - borderBottom: `1px solid ${theme.palette.text.secondary}`, - width: "max-content", - lineHeight: "19px", - cursor: "pointer", - }} - > - Удалить - - ) : ( - handleItemDeleteClick(privilege.tariffId)} - component={ClearIcon} - sx={{ fill: "#7E2AEA" }} - /> - )} - - - ))} - - - ); -} diff --git a/src/pages/Cart/Cart.tsx b/src/pages/Cart/Cart.tsx new file mode 100644 index 0000000..9c83d09 --- /dev/null +++ b/src/pages/Cart/Cart.tsx @@ -0,0 +1,67 @@ +import { + Box, + IconButton, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import SectionWrapper from "@components/SectionWrapper"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import TotalPrice from "@components/TotalPrice"; +import CustomWrapper from "./CustomWrapper"; +import { useCart } from "@root/utils/hooks/useCart"; + +export default function Cart() { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const cart = useCart(); + + const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts; + const totalPriceAfterDiscounts = cart.priceAfterDiscounts; + + return ( + + + {!upMd && ( + + + + )} + + Корзина + + + + {cart.services.map((serviceData) => ( + + ))} + + + + ); +} diff --git a/src/pages/Cart/CustomWrapper.tsx b/src/pages/Cart/CustomWrapper.tsx new file mode 100644 index 0000000..ba476e5 --- /dev/null +++ b/src/pages/Cart/CustomWrapper.tsx @@ -0,0 +1,220 @@ +import { useState } from "react"; +import { + Box, + SvgIcon, + Typography, + IconButton, + useMediaQuery, + useTheme, +} from "@mui/material"; +import ExpandIcon from "@components/icons/ExpandIcon"; +import ClearIcon from "@mui/icons-material/Clear"; +import { cardShadow } from "@root/utils/themes/shadow"; +import { currencyFormatter } from "@root/utils/currencyFormatter"; +import { removeTariffFromCart } from "@root/stores/user"; +import { enqueueSnackbar } from "notistack"; +import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui"; + +import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"; + +import type { MouseEvent } from "react"; +import CustomTariffAccordion from "@root/components/CustomTariffAccordion"; + +const name: Record = { + templategen: "Шаблонизатор", + squiz: "Опросник", + reducer: "Сокращатель ссылок", +}; + +interface Props { + serviceData: ServiceCartData; +} + +export default function CustomWrapper({ serviceData }: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const upSm = useMediaQuery(theme.breakpoints.up("sm")); + const [isExpanded, setIsExpanded] = useState(false); + + function handleItemDeleteClick(tariffId: string) { + removeTariffFromCart(tariffId) + .then(() => { + enqueueSnackbar("Тариф удален"); + }) + .catch((error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }); + } + + const deleteService = async (event: MouseEvent) => { + event.stopPropagation(); + + setIsExpanded(false); + + for (const { tariffId } of serviceData.tariffs) { + try { + await removeTariffFromCart(tariffId); + } catch { } + } + + enqueueSnackbar("Тарифы удален"); + }; + + return ( + + + setIsExpanded((prev) => !prev)} + sx={{ + height: "72px", + px: "20px", + display: "flex", + gap: "15px", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + userSelect: "none", + }} + > + + + + + {name[serviceData.serviceKey]} + + + + {currencyFormatter.format(serviceData.price / 100)} + + + + + + + {isExpanded && + serviceData.tariffs.map(tariff => { + const privilege = tariff.privileges[0]; + + return tariff.privileges.length > 1 ? ( + + ) : ( + + + {privilege.description} + + + + {currencyFormatter.format(tariff.price / 100)} + + + handleItemDeleteClick(privilege.tariffId)} + > + + + + ); + })} + + + ); +} diff --git a/src/pages/Support/Support.tsx b/src/pages/Support/Support.tsx index bb48b57..cfd9154 100644 --- a/src/pages/Support/Support.tsx +++ b/src/pages/Support/Support.tsx @@ -6,12 +6,12 @@ import { IconButton, } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { Link, useParams } from "react-router-dom"; +import { Link, useParams, useLocation } from "react-router-dom"; import SectionWrapper from "@components/SectionWrapper"; import SupportChat from "./SupportChat"; import CreateTicket from "./CreateTicket"; import TicketList from "./TicketList/TicketList"; -import { useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui"; import { updateTickets, @@ -23,12 +23,14 @@ import { enqueueSnackbar } from "notistack"; import { useSSESubscription, useTickets } from "@frontend/kitui"; export default function Support() { + const [previousPage, setPreviousPage] = useState(""); const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const ticketId = useParams().ticketId; const ticketApiPage = useTicketStore((state) => state.apiPage); const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage); const token = useToken(); + const location = useLocation(); const fetchState = useTickets({ url: "https://hub.pena.digital/heruvym/getTickets", @@ -44,6 +46,10 @@ export default function Support() { }, []), }); + useEffect(() => { + setPreviousPage(location.state?.previousUrl || "/"); + }, []); + useSSESubscription({ enabled: Boolean(token), url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`, @@ -73,7 +79,7 @@ export default function Support() { }} > state.summaryPriceBeforeDiscountsMap - ); - const summaryPriceAfterDiscounts = useCustomTariffsStore( - (state) => state.summaryPriceAfterDiscountsMap - ); + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const summaryPriceBeforeDiscounts = useCustomTariffsStore( + (state) => state.summaryPriceBeforeDiscountsMap + ); + const summaryPriceAfterDiscounts = useCustomTariffsStore( + (state) => state.summaryPriceAfterDiscountsMap + ); - const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0; - const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0; + const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0; + const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0; - async function handleConfirmClick() { - createAndSendTariff(serviceKey) - .then((result) => { - devlog(result); - enqueueSnackbar("Тариф создан"); - }) - .catch((error) => { - const message = getMessageFromFetchError( - error, - "Не удалось создать тариф" - ); - if (message) enqueueSnackbar(message); - }); - } + async function handleConfirmClick() { + try { + const tariff = await createAndSendTariff(serviceKey); + updateTariffs([tariff]); + await addTariffToCart(tariff._id); + enqueueSnackbar("Тариф добавлен в корзину"); + } catch (error) { + const message = getMessageFromFetchError( + error, + "Не удалось создать тариф" + ); + if (message) enqueueSnackbar(message); + } + } - return ( - - - {privileges.map((privilege) => ( - - ))} - - {!upMd && ( - - )} - + return ( - - Чем больше пакеты, тем дешевле подписки и опции{" "} - + + {privileges.map((privilege) => ( + + ))} + + {!upMd && ( + + )} + + + + Чем больше пакеты, тем дешевле подписки и опции{" "} + + + + Сумма с учетом скидки + + + + {currencyFormatter.format(priceAfterDiscounts / 100)} + + + {currencyFormatter.format(priceBeforeDiscounts / 100)} + + + + Выбрать + + - - Сумма с учетом скидки - - - - {currencyFormatter.format(priceAfterDiscounts / 100)} - - - {currencyFormatter.format(priceBeforeDiscounts / 100)} - - - - Выбрать - - - - ); + ); } diff --git a/src/pages/TariffConstructor/TariffConstructor.tsx b/src/pages/TariffConstructor/TariffConstructor.tsx index 9de21aa..a6f8624 100644 --- a/src/pages/TariffConstructor/TariffConstructor.tsx +++ b/src/pages/TariffConstructor/TariffConstructor.tsx @@ -7,12 +7,16 @@ import CustomTariffCard from "./CustomTariffCard"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import TotalPrice from "@root/components/TotalPrice"; import { serviceNameByKey } from "@root/utils/serviceKeys"; +import { useAllTariffsFetcher } from "@root/utils/hooks/useAllTariffsFetcher"; +import { updateTariffs } from "@root/stores/tariffs"; +import { getMessageFromFetchError } from "@frontend/kitui"; +import { enqueueSnackbar } from "notistack"; export default function TariffConstructor() { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const customTariffs = useCustomTariffsStore( - (state) => state.customTariffsMap + (state) => state.privilegeByService ); const summaryPriceBeforeDiscountsMap = useCustomTariffsStore( (state) => state.summaryPriceBeforeDiscountsMap @@ -30,6 +34,14 @@ export default function TariffConstructor() { 0 ); + useAllTariffsFetcher({ + onSuccess: updateTariffs, + onError: (error) => { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) enqueueSnackbar(errorMessage); + }, + }); + return ( ); diff --git a/src/pages/Tariffs/TariffCard.tsx b/src/pages/Tariffs/TariffCard.tsx index dbd7b80..223c1f1 100644 --- a/src/pages/Tariffs/TariffCard.tsx +++ b/src/pages/Tariffs/TariffCard.tsx @@ -1,148 +1,124 @@ import { - Box, - Typography, - Tooltip, - SxProps, - Theme, - useTheme, + Box, + Typography, + Tooltip, + SxProps, + Theme, + useTheme, } from "@mui/material"; import CustomButton from "@components/CustomButton"; import { MouseEventHandler, ReactNode } from "react"; import { cardShadow } from "@root/utils/themes/shadow"; interface Props { - icon: ReactNode; - headerText: string; - text: string | string[]; - sx?: SxProps; - buttonProps?: { + icon: ReactNode; + headerText: string; + text: string | string[]; sx?: SxProps; - onClick?: MouseEventHandler; - text?: string; - }; - price?: ReactNode; + buttonProps?: { + sx?: SxProps; + onClick?: MouseEventHandler; + text?: string; + }; + price?: ReactNode; } export default function TariffCard({ - icon, - headerText, - text, - sx, - price, - buttonProps, + icon, + headerText, + text, + sx, + price, + buttonProps, }: Props) { - const theme = useTheme(); + const theme = useTheme(); - return ( - - - {icon} - {price && ( - - {price} - - )} - - - - {headerText} - - - {Array.isArray(text) ? ( + text = Array.isArray(text) ? text : [text]; + + return ( - {text.map((line, index) => ( - - - {line} - - - ))} - - ) : ( - - - {text} - - - )} - {buttonProps && ( - - {buttonProps.text} - - )} - - ); + + {icon} + {price && ( + + {price} + + )} + + {headerText}} placement="top"> + + {headerText} + + + ( + {line} + ))} + placement="top" + > + + {text.map((line, index) => ( + {line} + ))} + + + {buttonProps && ( + + {buttonProps.text} + + )} + + ); } diff --git a/src/pages/Tariffs/TariffsPage.tsx b/src/pages/Tariffs/TariffsPage.tsx index 2d74ae1..b9a5b0b 100644 --- a/src/pages/Tariffs/TariffsPage.tsx +++ b/src/pages/Tariffs/TariffsPage.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { useLocation } from "react-router-dom"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import SectionWrapper from "@components/SectionWrapper"; -import { useTariffs } from "@root/utils/hooks/useTariffs"; import { updateTariffs, useTariffStore } from "@root/stores/tariffs"; import { enqueueSnackbar } from "notistack"; import { Select } from "@root/components/Select"; @@ -11,154 +10,147 @@ import TariffCard from "./TariffCard"; import NumberIcon from "@root/components/NumberIcon"; import { currencyFormatter } from "@root/utils/currencyFormatter"; import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices"; -import { getMessageFromFetchError } from "@frontend/kitui"; +import { Tariff, getMessageFromFetchError } from "@frontend/kitui"; import FreeTariffCard from "./FreeTariffCard"; import { addTariffToCart, useUserStore } from "@root/stores/user"; import { useDiscountStore } from "@root/stores/discounts"; -import { useCustomTariffsStore } from "@root/stores/customTariffs"; import { Slider } from "./slider"; import { useCartStore } from "@root/stores/cart"; +import { useAllTariffsFetcher } from "@root/utils/hooks/useAllTariffsFetcher"; const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"]; -export default function TariffPage() { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - const isMobile = useMediaQuery(theme.breakpoints.down(600)); - const location = useLocation(); - const tariffs = useTariffStore((state) => state.tariffs); - const [selectedItem, setSelectedItem] = useState(0); - const discounts = useDiscountStore((state) => state.discounts); - const customTariffs = useCustomTariffsStore( - (state) => state.customTariffsMap - ); - const purchasesAmount = - useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0; - const cart = useCartStore((state) => state.cart); - const unit: string = String(location.pathname).slice(9); - - const StepperText: Record = { +const StepperText: Record = { volume: "Тарифы на объём", time: "Тарифы на время", - }; +}; - useTariffs({ - apiPage: 0, - tariffsPerPage: 100, - onNewTariffs: updateTariffs, - onError: (error) => { - const errorMessage = getMessageFromFetchError(error); - if (errorMessage) enqueueSnackbar(errorMessage); - }, - }); +export default function TariffPage() { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const location = useLocation(); + const tariffs = useTariffStore((state) => state.tariffs); + const [selectedItem, setSelectedItem] = useState(0); + const discounts = useDiscountStore((state) => state.discounts); + const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0; + const cartTariffMap = useCartStore((state) => state.cartTariffMap); - function handleTariffItemClick(tariffId: string) { - addTariffToCart(tariffId) - .then(() => { - enqueueSnackbar("Тариф добавлен в корзину"); - }) - .catch((error) => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }); - } + const unit: string = String(location.pathname).slice(9); + const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object"); - const filteredTariffs = tariffs.filter((tariff) => { - return ( - tariff.privilegies.map((p) => p.type).includes("day") === - (unit === "time") && !tariff.isDeleted - ); - }); + useAllTariffsFetcher({ + onSuccess: updateTariffs, + onError: (error) => { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) enqueueSnackbar(errorMessage); + }, + }); - const tariffElements = filteredTariffs.map((tariff, index) => { - const { price, tariffPriceAfterDiscounts } = calcIndividualTariffPrices( - tariff, - discounts, - customTariffs, - purchasesAmount, - cart - ); + function handleTariffItemClick(tariffId: string) { + addTariffToCart(tariffId) + .then(() => { + enqueueSnackbar("Тариф добавлен в корзину"); + }) + .catch((error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }); + } + + const filteredTariffs = tariffs.filter((tariff) => { + return ( + tariff.privilegies.map((p) => p.type).includes("day") === + (unit === "time") && !tariff.isDeleted + ); + }); + + const tariffElements = filteredTariffs.filter((tariff)=>tariff.privilegies.length > 0).map((tariff, index) => { + const { priceBeforeDiscounts, priceAfterDiscounts } = calcIndividualTariffPrices( + tariff, + discounts, + purchasesAmount, + currentTariffs, + ); + + return ( + + } + buttonProps={{ + text: "Выбрать", + onClick: () => handleTariffItemClick(tariff._id), + }} + headerText={tariff.name} + text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)} + price={ + <> + {priceBeforeDiscounts !== priceAfterDiscounts && ( + + {currencyFormatter.format(priceBeforeDiscounts / 100)} + + )} + + {currencyFormatter.format(priceAfterDiscounts / 100)} + + + } + /> + ); + }); + + if (tariffElements.length < 6) + tariffElements.push(); + else tariffElements.splice(5, 0, ); return ( - - } - buttonProps={{ - text: "Выбрать", - onClick: () => handleTariffItemClick(tariff._id), - }} - headerText={tariff.name} - text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)} - price={ - <> - {price !== undefined && price !== tariffPriceAfterDiscounts && ( - - {currencyFormatter.format(price / 100)} - + + + {StepperText[unit]} + + {isMobile ? ( + - ) : ( - - )} - - {tariffElements} - - - Ранее вы покупали - - - - ); } diff --git a/src/stores/customTariffs.ts b/src/stores/customTariffs.ts index 05eb91a..b298d2a 100644 --- a/src/stores/customTariffs.ts +++ b/src/stores/customTariffs.ts @@ -1,21 +1,21 @@ import { createTariff } from "@root/api/tariff"; import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs"; -import { ServiceKeyToPrivilegesMap, PrivilegeWithoutPrice } from "@root/model/privilege"; +import { ServiceKeyToPrivilegesMap } from "@root/model/privilege"; import { produce } from "immer"; import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; -import { Discount, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; +import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; interface CustomTariffsStore { - customTariffsMap: ServiceKeyToPrivilegesMap; + privilegeByService: ServiceKeyToPrivilegesMap; userValuesMap: CustomTariffUserValuesMap; summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap; summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap; } const initialState: CustomTariffsStore = { - customTariffsMap: {}, + privilegeByService: {}, userValuesMap: {}, summaryPriceBeforeDiscountsMap: {}, summaryPriceAfterDiscountsMap: {}, @@ -41,7 +41,7 @@ export const useCustomTariffsStore = create()( ) ); -export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ customTariffsMap: customTariffs }); +export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ privilegeByService: customTariffs }); export const setCustomTariffsUserValue = ( serviceKey: string, @@ -58,22 +58,22 @@ export const setCustomTariffsUserValue = ( let priceWithoutDiscounts = 0; let priceAfterDiscounts = 0; - state.customTariffsMap[serviceKey].forEach(tariff => { - const amount = state.userValuesMap[serviceKey]?.[tariff._id] ?? 0; - priceWithoutDiscounts += tariff.price * amount; + state.privilegeByService[serviceKey].forEach(privilege => { + const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0; + priceWithoutDiscounts += privilege.price * amount; - const discount = findPrivilegeDiscount(tariff.privilegeId, tariff.price * amount, discounts); - priceAfterDiscounts += tariff.price * amount * discount.factor; + const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts); + priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount); }); const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts); - priceAfterDiscounts *= serviceDiscount.factor; + priceAfterDiscounts *= findDiscountFactor(serviceDiscount); const cartDiscount = findCartDiscount(currentCartTotal, discounts); - priceAfterDiscounts *= cartDiscount.factor; + priceAfterDiscounts *= findDiscountFactor(cartDiscount); const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts); - priceAfterDiscounts *= loyaltyDiscount.factor; + priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount); state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts; state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts; @@ -83,22 +83,21 @@ export const setCustomTariffsUserValue = ( export const createAndSendTariff = (serviceKey: string) => { const state = useCustomTariffsStore.getState(); - const privilegies: PrivilegeWithoutPrice[] = []; + const privilegies: PrivilegeWithAmount[] = []; Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => { if (userValue === 0) return; - const privilege = state.customTariffsMap[serviceKey].find(p => p._id === privilegeId); - if (!privilege) throw new Error(`Privilege not found: ${privilegeId}`); + const privilegeWithoutAmount = state.privilegeByService[serviceKey].find(p => p._id === privilegeId); + if (!privilegeWithoutAmount) throw new Error(`Privilege not found: ${privilegeId}`); - const p2 = { - ...privilege, - privilegeId: privilege._id, + const privilege: PrivilegeWithAmount = { + ...privilegeWithoutAmount, + privilegeId: privilegeWithoutAmount._id, amount: userValue, - } as PrivilegeWithoutPrice; - delete (p2 as any).price; + }; - privilegies.push(p2); + privilegies.push(privilege); }); const name = [...privilegies.map(p => p.name), new Date().toISOString()].join(", "); diff --git a/src/stores/privileges.ts b/src/stores/privileges.ts new file mode 100644 index 0000000..e0db849 --- /dev/null +++ b/src/stores/privileges.ts @@ -0,0 +1,24 @@ +import { PrivilegeWithAmount } from "@frontend/kitui"; +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + + +interface PrivilegeStore { + privileges: PrivilegeWithAmount[]; +} + +const initialState: PrivilegeStore = { + privileges: [], +}; + +const usePrivilegeStore = create()( + devtools( + (get, set) => initialState, + { + name: "Privileges", + enabled: process.env.NODE_ENV === "development", + } + ) +); + +export const setPrivileges = (privileges: PrivilegeStore["privileges"]) => usePrivilegeStore.setState({ privileges }); diff --git a/src/stores/tariffs.ts b/src/stores/tariffs.ts index 8fc68c2..9f8ad04 100644 --- a/src/stores/tariffs.ts +++ b/src/stores/tariffs.ts @@ -33,7 +33,7 @@ export const updateTariffs = (tariffs: TariffStore["tariffs"]) => useTariffStore false, { type: "updateTariffs", - tariffsLength: tariffs.length, + tariffs: tariffs, } ); diff --git a/src/utils/calcCart/calcCart.test.ts b/src/utils/calcCart/calcCart.test.ts index 559d6f0..c68c663 100644 --- a/src/utils/calcCart/calcCart.test.ts +++ b/src/utils/calcCart/calcCart.test.ts @@ -1,3 +1,4 @@ +// @ts-nocheck TODO fix tests import { CartData, Discount, Tariff } from "@frontend/kitui"; import { calcCart } from "./calcCart"; diff --git a/src/utils/calcCart/calcCart.ts b/src/utils/calcCart/calcCart.ts index 5136da8..1e7df16 100644 --- a/src/utils/calcCart/calcCart.ts +++ b/src/utils/calcCart/calcCart.ts @@ -1,4 +1,4 @@ -import { CartData, Discount, PrivilegeCartData, Tariff, applyCartDiscount, applyLoyaltyDiscount, applyPrivilegeDiscounts, applyServiceDiscounts } from "@frontend/kitui"; +import { CartData, Discount, PrivilegeCartData, Tariff, TariffCartData, applyCartDiscount, applyLoyaltyDiscount, applyPrivilegeDiscounts, applyServiceDiscounts } from "@frontend/kitui"; export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number): CartData { @@ -13,25 +13,31 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou }; tariffs.forEach(tariff => { - if (tariff.price && tariff.price > 0) cartData.priceBeforeDiscounts += tariff.price; + let serviceData = cartData.services.find(service => service.serviceKey === tariff.privilegies[0].serviceKey); + if (!serviceData) { + serviceData = { + serviceKey: tariff.privilegies[0].serviceKey, + tariffs: [], + price: 0, + appliedServiceDiscount: null, + }; + cartData.services.push(serviceData); + } + + const tariffCartData: TariffCartData = { + price: tariff.price ?? 0, + isCustom: tariff.isCustom, + privileges: [], + tariffId: tariff._id, + }; + serviceData.tariffs.push(tariffCartData); tariff.privilegies.forEach(privilege => { - let serviceData = cartData.services.find(service => service.serviceKey === privilege.serviceKey); - if (!serviceData) { - serviceData = { - serviceKey: privilege.serviceKey, - privileges: [], - price: 0, - appliedServiceDiscount: null, - }; - cartData.services.push(serviceData); - } - const privilegePrice = privilege.amount * privilege.price; - if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice; + if (!tariff.price) tariffCartData.price += privilegePrice; - const privilegeData: PrivilegeCartData = { + const privilegeCartData: PrivilegeCartData = { tariffId: tariff._id, serviceKey: privilege.serviceKey, privilegeId: privilege.privilegeId, @@ -41,11 +47,13 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou tariffName: tariff.name, }; - serviceData.privileges.push(privilegeData); - serviceData.price += privilegePrice; + tariffCartData.privileges.push(privilegeCartData); cartData.priceAfterDiscounts += privilegePrice; cartData.itemCount++; }); + + cartData.priceBeforeDiscounts += tariffCartData.price; + serviceData.price += tariffCartData.price; }); applyPrivilegeDiscounts(cartData, discounts); diff --git a/src/utils/calcTariffPrices.ts b/src/utils/calcTariffPrices.ts index 5c69512..d575612 100644 --- a/src/utils/calcTariffPrices.ts +++ b/src/utils/calcTariffPrices.ts @@ -1,47 +1,24 @@ -import { ServiceKeyToPrivilegesMap } from "@root/model/privilege"; -import { CartData, Discount, Tariff, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; - +import { Discount, Tariff, findDiscountFactor } from "@frontend/kitui"; +import { calcCart } from "./calcCart/calcCart"; export function calcIndividualTariffPrices( tariff: Tariff, discounts: Discount[], - privilegies: ServiceKeyToPrivilegesMap, purchasesAmount: number, - cart: CartData, + currentTariffs: Tariff[], ): { - price: number | undefined; - tariffPriceAfterDiscounts: number | undefined; + priceBeforeDiscounts: number; + priceAfterDiscounts: number; } { - let price = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0); + const priceBeforeDiscounts = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0); + let priceAfterDiscounts = priceBeforeDiscounts; - let tariffPriceAfterDiscounts = tariff.privilegies.reduce((sum, privilege) => { - let privilegePrice = privilege.amount * privilege.price; + const cart = calcCart([...currentTariffs, tariff], discounts, purchasesAmount); - let realprivilegie = privilegies[privilege.serviceKey]?.find(e => e._id === privilege.privilegeId); - if (realprivilegie) privilege.privilegeId = realprivilegie.privilegeId; + cart.allAppliedDiscounts.forEach(discount => { + priceAfterDiscounts *= findDiscountFactor(discount); + }); - const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * privilege.amount, discounts); - privilegePrice *= privilegeDiscount.factor; - - const serviceCartData = cart.services.find(e => e.serviceKey === privilege.serviceKey); - let serviceprice = 0; - if (serviceCartData) serviceprice = serviceCartData.price; - - const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice + serviceprice, discounts); - if (serviceDiscount) privilegePrice *= serviceDiscount.factor; - - return sum + privilegePrice; - }, 0); - - const cartDiscount = findCartDiscount(tariffPriceAfterDiscounts + cart.priceAfterDiscounts, discounts); - tariffPriceAfterDiscounts *= cartDiscount.factor; - - const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts); - tariffPriceAfterDiscounts *= loyalDiscount.factor; - - return { - price, - tariffPriceAfterDiscounts: tariffPriceAfterDiscounts, - }; + return { priceBeforeDiscounts, priceAfterDiscounts }; } diff --git a/src/utils/declension.ts b/src/utils/declension.ts index 01618e1..f99a4ad 100644 --- a/src/utils/declension.ts +++ b/src/utils/declension.ts @@ -1,4 +1,4 @@ -import { PrivilegeValueType } from "@root/model/privilege"; +import { PrivilegeValueType } from "@frontend/kitui"; function declension(number: number, declensions: string[], cases = [2, 0, 1, 1, 1, 2]) { @@ -22,4 +22,4 @@ export function getDeclension(number: number, word: PrivilegeValueType | "мес case "МБ": return "МБ"; } -}; \ No newline at end of file +}; diff --git a/src/utils/hooks/useAllTariffsFetcher.ts b/src/utils/hooks/useAllTariffsFetcher.ts new file mode 100644 index 0000000..f820598 --- /dev/null +++ b/src/utils/hooks/useAllTariffsFetcher.ts @@ -0,0 +1,55 @@ +import { GetTariffsResponse, Tariff, makeRequest } from "@frontend/kitui"; +import { useRef, useLayoutEffect, useEffect } from "react"; + + +export function useAllTariffsFetcher({ + baseUrl = process.env.NODE_ENV === "production" ? "/strator/tariff" : "https://hub.pena.digital/strator/tariff", + onSuccess, + onError, +}: { + baseUrl?: string; + onSuccess: (response: Tariff[]) => void; + onError?: (error: Error) => void; +}) { + const onNewTariffsRef = useRef(onSuccess); + const onErrorRef = useRef(onError); + + useLayoutEffect(() => { + onNewTariffsRef.current = onSuccess; + onErrorRef.current = onError; + }, [onError, onSuccess]); + + useEffect(() => { + const controller = new AbortController(); + + async function getPaginatedTariffs() { + let apiPage = 1; + const tariffsPerPage = 100; + let isDone = false; + + while (!isDone) { + try { + const result = await makeRequest({ + url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`, + method: "get", + useToken: true, + signal: controller.signal, + }); + if (result.tariffs.length > 0) { + onNewTariffsRef.current(result.tariffs); + apiPage++; + } else { + isDone = true; + } + } catch (error) { + onErrorRef.current?.(error as Error); + isDone = true; + } + } + } + + getPaginatedTariffs(); + + return () => controller.abort(); + }, [baseUrl]); +} diff --git a/src/utils/hooks/useTariffFetcher.ts b/src/utils/hooks/useTariffFetcher.ts new file mode 100644 index 0000000..fb4710c --- /dev/null +++ b/src/utils/hooks/useTariffFetcher.ts @@ -0,0 +1,50 @@ +import { Tariff, makeRequest } from "@frontend/kitui"; +import { GetTariffsResponse } from "@root/model/tariff"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; + + +export function useTariffFetcher({ + baseUrl = process.env.NODE_ENV === "production" ? "/strator/tariff" : "https://hub.pena.digital/strator/tariff", + tariffsPerPage, + apiPage, + onSuccess, + onError, +}: { + baseUrl?: string; + tariffsPerPage: number; + apiPage: number; + onSuccess: (response: Tariff[]) => void; + onError?: (error: Error) => void; +}) { + const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle"); + const onSuccessRef = useRef(onSuccess); + const onErrorRef = useRef(onError); + + useLayoutEffect(() => { + onSuccessRef.current = onSuccess; + onErrorRef.current = onError; + }, [onError, onSuccess]); + + useEffect(() => { + const controller = new AbortController(); + + setFetchState("fetching"); + makeRequest({ + url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`, + method: "get", + useToken: true, + signal: controller.signal, + }).then((result) => { + if (result.tariffs.length > 0) { + onSuccessRef.current(result.tariffs); + setFetchState("idle"); + } else setFetchState("all fetched"); + }).catch(error => { + onErrorRef.current?.(error); + }); + + return () => controller.abort(); + }, [apiPage, tariffsPerPage, baseUrl]); + + return fetchState; +} diff --git a/src/utils/hooks/useTariffs.ts b/src/utils/hooks/useTariffs.ts deleted file mode 100644 index 7875392..0000000 --- a/src/utils/hooks/useTariffs.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Tariff, devlog, makeRequest } from "@frontend/kitui"; -import { GetTariffsResponse } from "@root/model/tariff"; -import { useEffect, useLayoutEffect, useRef } from "react"; - - -export function useTariffs({ baseUrl = "https://admin.pena.digital/strator/tariff", tariffsPerPage, apiPage, onNewTariffs, onError }: { - baseUrl?: string; - tariffsPerPage: number; - apiPage: number; - onNewTariffs: (response: Tariff[]) => void; - onError: (error: Error) => void; -}) { - const onNewTariffsRef = useRef(onNewTariffs); - const onErrorRef = useRef(onError); - - useLayoutEffect(() => { - onNewTariffsRef.current = onNewTariffs; - onErrorRef.current = onError; - }, [onError, onNewTariffs]); - - useEffect(() => { - const controller = new AbortController(); - - makeRequest({ - url: baseUrl + `?page=${apiPage}&limit=${tariffsPerPage}`, - method: "get", - useToken: true, - signal: controller.signal, - }).then((result) => { - devlog("Tariffs", result); - if (result.tariffs.length > 0) { - onNewTariffsRef.current(result.tariffs); - } - }).catch(error => { - devlog("Error fetching tariffs", error); - onErrorRef.current(error); - }); - - return () => controller.abort(); - }, [apiPage, tariffsPerPage, baseUrl]); -} diff --git a/yarn.lock b/yarn.lock index 23ab222..6576da1 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.17": - version "1.0.17" - resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.17.tgz#a5bddaaa18b168be0e1814d5cfbd86e4030d15af" - integrity sha1-pb3aqhixaL4OGBTVz72G5AMNFa8= +"@frontend/kitui@1.0.21": + version "1.0.21" + resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.21.tgz#572b3e774ba42b65c4b946704aeb68237b4b3cd5" + integrity sha1-Vys+d0ukK2XEuUZwSutoI3tLPNU= dependencies: immer "^10.0.2" reconnecting-eventsource "^1.6.2"