Merge branch 'dev' into 'main'
Dev See merge request frontend/marketplace!35
This commit is contained in:
commit
de0f243e62
@ -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.17",
|
"@frontend/kitui": "1.0.21",
|
||||||
"@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",
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Tariff, makeRequest } from "@frontend/kitui";
|
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) {
|
export function createTariff(tariff: CreateTariffBody) {
|
||||||
return makeRequest<CreateTariffBody, CustomTariff>({
|
return makeRequest<CreateTariffBody, Tariff>({
|
||||||
url: `https://admin.pena.digital/strator/tariff`,
|
url: `${apiUrl}/tariff`,
|
||||||
method: "post",
|
method: "post",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
body: tariff,
|
body: tariff,
|
||||||
@ -13,7 +15,7 @@ export function createTariff(tariff: CreateTariffBody) {
|
|||||||
|
|
||||||
export function getTariffById(tariffId:string){
|
export function getTariffById(tariffId:string){
|
||||||
return makeRequest<never, Tariff>({
|
return makeRequest<never, Tariff>({
|
||||||
url: `https://admin.pena.digital/strator/tariff/${tariffId}`,
|
url: `${apiUrl}/tariff/${tariffId}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
});
|
});
|
||||||
|
3
src/assets/Icons/cross.svg
Normal file
3
src/assets/Icons/cross.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="black" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.400391 0.399902L19.6004 19.5999M0.400391 19.5999L19.6004 0.399902" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 200 B |
159
src/components/CustomTariffAccordion.tsx
Normal file
159
src/components/CustomTariffAccordion.tsx
Normal file
@ -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<boolean>(false);
|
||||||
|
|
||||||
|
function handleDeleteClick() {
|
||||||
|
removeTariffFromCart(tariffCartData.tariffId)
|
||||||
|
.then(() => {
|
||||||
|
enqueueSnackbar("Тариф удален");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message = getMessageFromFetchError(error);
|
||||||
|
if (message) enqueueSnackbar(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
|
sx={{
|
||||||
|
height: "72px",
|
||||||
|
pr: "20px",
|
||||||
|
pl: "30px",
|
||||||
|
display: "flex",
|
||||||
|
gap: "15px",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
cursor: "pointer",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "50px",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandIcon isExpanded={isExpanded} />
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: upMd ? "20px" : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
px: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Кастомный тариф
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
height: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: upSm ? "111px" : "17px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: upSm ? "20px" : "16px",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(tariffCartData.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleDeleteClick}
|
||||||
|
sx={{ padding: 0, height: "22px", width: "22px" }}
|
||||||
|
>
|
||||||
|
<CrossIcon style={{ height: "22 px", width: "22px" }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
{isExpanded && tariffCartData.privileges.map((privilege) => (
|
||||||
|
<Box
|
||||||
|
key={privilege.privilegeId}
|
||||||
|
sx={{
|
||||||
|
px: "50px",
|
||||||
|
py: upMd ? "15px" : undefined,
|
||||||
|
pt: upMd ? undefined : "15px",
|
||||||
|
pb: upMd ? undefined : "25px",
|
||||||
|
backgroundColor: "#F1F2F6",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: upMd ? undefined : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{privilege.description}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: upSm ? "140px" : "123px",
|
||||||
|
marginRight: upSm ? "65px" : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: upSm ? "20px" : "16px",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(privilege.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -1,14 +1,30 @@
|
|||||||
import { useState } from "react";
|
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 ClearIcon from "@mui/icons-material/Clear";
|
||||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
import { removeTariffFromCart } from "@root/stores/user";
|
import { removeTariffFromCart } from "@root/stores/user";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
|
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<string, string> = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" };
|
import type { MouseEvent } from "react";
|
||||||
|
import CustomTariffAccordion from "@root/components/CustomTariffAccordion";
|
||||||
|
|
||||||
|
const name: Record<string, string> = {
|
||||||
|
templategen: "Шаблонизатор",
|
||||||
|
squiz: "Опросник",
|
||||||
|
reducer: "Скоращатель ссылок",
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceData: ServiceCartData;
|
serviceData: ServiceCartData;
|
||||||
@ -21,19 +37,36 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
|
|||||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
function handleItemDeleteClick(tariffId: string) {
|
function handleItemDeleteClick(tariffId: string) {
|
||||||
removeTariffFromCart(tariffId).then(() => {
|
removeTariffFromCart(tariffId)
|
||||||
enqueueSnackbar("Тариф удален");
|
.then(() => {
|
||||||
}).catch(error => {
|
enqueueSnackbar("Тариф удален");
|
||||||
const message = getMessageFromFetchError(error);
|
})
|
||||||
if (message) enqueueSnackbar(message);
|
.catch((error) => {
|
||||||
});
|
const message = getMessageFromFetchError(error);
|
||||||
|
if (message) enqueueSnackbar(message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
setIsExpanded(false);
|
||||||
|
|
||||||
|
for (const { tariffId } of serviceData.tariffs) {
|
||||||
|
try {
|
||||||
|
await removeTariffFromCart(tariffId);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueSnackbar("Тарифы удален");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@ -56,101 +89,132 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
|
|||||||
onClick={() => setIsExpanded((prev) => !prev)}
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
sx={{
|
sx={{
|
||||||
height: "72px",
|
height: "72px",
|
||||||
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<ExpandIcon isExpanded={isExpanded} />
|
||||||
sx={{
|
|
||||||
fontSize: upMd ? "20px" : "16px",
|
|
||||||
lineHeight: upMd ? undefined : "19px",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
px: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name[serviceData.serviceKey]}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "space-between",
|
||||||
height: "100%",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{ pr: "11px", color: theme.palette.grey3.main, fontSize: upSm ? "20px" : "16px", fontWeight: 500 }}
|
sx={{
|
||||||
|
fontSize: upMd ? "20px" : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
px: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{currencyFormatter.format(serviceData.price / 100)}
|
{name[serviceData.serviceKey]}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
paddingLeft: upSm ? "24px" : 0,
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
></Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{isExpanded &&
|
|
||||||
serviceData.privileges.map(privilege => (
|
|
||||||
<Box
|
|
||||||
key={privilege.tariffId + privilege.privilegeId}
|
|
||||||
sx={{
|
|
||||||
py: upMd ? "10px" : undefined,
|
|
||||||
pt: upMd ? undefined : "15px",
|
|
||||||
pb: upMd ? undefined : "20px",
|
|
||||||
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "15px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
width: "200px",
|
|
||||||
fontSize: upMd ? undefined : "16px",
|
|
||||||
lineHeight: upMd ? undefined : "19px",
|
|
||||||
color: theme.palette.grey3.main,
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: upSm ? "20px" : "16px",
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{privilege.description}
|
{currencyFormatter.format(serviceData.price / 100)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
onClick={deleteService}
|
||||||
|
sx={{
|
||||||
|
padding: "3px",
|
||||||
|
height: "30px",
|
||||||
|
width: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CrossIcon
|
||||||
|
style={{
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
stroke: "#7E2AEA",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
{isExpanded &&
|
||||||
|
serviceData.tariffs.map(tariff => {
|
||||||
|
const privilege = tariff.privileges[0];
|
||||||
|
|
||||||
|
return tariff.privileges.length > 1 ? (
|
||||||
|
<CustomTariffAccordion
|
||||||
|
key={privilege.tariffId + privilege.privilegeId}
|
||||||
|
tariffCartData={tariff}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Box
|
<Box
|
||||||
|
key={privilege.tariffId + privilege.privilegeId}
|
||||||
sx={{
|
sx={{
|
||||||
|
py: upMd ? "10px" : undefined,
|
||||||
|
pt: upMd ? undefined : "15px",
|
||||||
|
pb: upMd ? undefined : "20px",
|
||||||
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
gap: "10px",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
|
width: "200px",
|
||||||
|
fontSize: upMd ? undefined : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
color: theme.palette.grey3.main,
|
color: theme.palette.grey3.main,
|
||||||
fontSize: "20px",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currencyFormatter.format(privilege.price / 100)}
|
{privilege.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Box
|
||||||
<SvgIcon
|
sx={{
|
||||||
sx={{ cursor: "pointer", color: "#7E2AEA" }}
|
display: "flex",
|
||||||
onClick={() => handleItemDeleteClick(privilege.tariffId)}
|
justifyContent: "space-between",
|
||||||
component={ClearIcon}
|
gap: "10px",
|
||||||
/>
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(privilege.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
<SvgIcon
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
width: "30px",
|
||||||
|
color: "#7E2AEA",
|
||||||
|
}}
|
||||||
|
onClick={() => handleItemDeleteClick(privilege.tariffId)}
|
||||||
|
component={ClearIcon}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
);
|
||||||
))}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -6,11 +6,9 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
SvgIcon,
|
|
||||||
Badge,
|
Badge,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
|
||||||
import { useTickets } from "@frontend/kitui";
|
import { useTickets } from "@frontend/kitui";
|
||||||
import SectionWrapper from "./SectionWrapper";
|
import SectionWrapper from "./SectionWrapper";
|
||||||
import CustomWrapperDrawer from "./CustomWrapperDrawer";
|
import CustomWrapperDrawer from "./CustomWrapperDrawer";
|
||||||
@ -24,7 +22,6 @@ import {
|
|||||||
openCartDrawer,
|
openCartDrawer,
|
||||||
useCartStore,
|
useCartStore,
|
||||||
} from "@root/stores/cart";
|
} from "@root/stores/cart";
|
||||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
|
||||||
import { useUserStore } from "@root/stores/user";
|
import { useUserStore } from "@root/stores/user";
|
||||||
import {
|
import {
|
||||||
updateTickets,
|
updateTickets,
|
||||||
@ -34,6 +31,7 @@ import {
|
|||||||
|
|
||||||
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg";
|
import { ReactComponent as BellIcon } from "@root/assets/Icons/bell.svg";
|
||||||
import { ReactComponent as CartIcon } from "@root/assets/Icons/cart.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() {
|
export default function Drawers() {
|
||||||
const [openNotificationsModal, setOpenNotificationsModal] =
|
const [openNotificationsModal, setOpenNotificationsModal] =
|
||||||
@ -44,12 +42,6 @@ export default function Drawers() {
|
|||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const isDrawerOpen = useCartStore((state) => state.isDrawerOpen);
|
const isDrawerOpen = useCartStore((state) => state.isDrawerOpen);
|
||||||
const cart = useCart();
|
const cart = useCart();
|
||||||
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(
|
|
||||||
(state) => state.summaryPriceBeforeDiscountsMap
|
|
||||||
);
|
|
||||||
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(
|
|
||||||
(state) => state.summaryPriceAfterDiscountsMap
|
|
||||||
);
|
|
||||||
const userAccount = useUserStore((state) => state.userAccount);
|
const userAccount = useUserStore((state) => state.userAccount);
|
||||||
const { tickets, apiPage, ticketsPerPage } = useTicketStore((state) => state);
|
const { tickets, apiPage, ticketsPerPage } = useTicketStore((state) => state);
|
||||||
|
|
||||||
@ -64,21 +56,12 @@ export default function Drawers() {
|
|||||||
onError: () => {},
|
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(
|
const notificationsCount = tickets.filter(
|
||||||
({ user, top_message }) => user !== top_message.user_id
|
({ user, top_message }) => user !== top_message.user_id
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice;
|
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts;
|
||||||
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice;
|
const totalPriceAfterDiscounts = cart.priceAfterDiscounts;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", gap: "20px" }}>
|
<Box sx={{ display: "flex", gap: "20px" }}>
|
||||||
@ -227,15 +210,23 @@ export default function Drawers() {
|
|||||||
Корзина
|
Корзина
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={closeCartDrawer} sx={{ p: 0 }}>
|
<IconButton onClick={closeCartDrawer} sx={{ p: 0 }}>
|
||||||
<SvgIcon component={ClearIcon} />
|
<CrossIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||||
{cart.services.map((serviceData) => (
|
{cart.services.map((serviceData) => (
|
||||||
<CustomWrapperDrawer
|
<Box
|
||||||
key={serviceData.serviceKey}
|
key={serviceData.serviceKey}
|
||||||
serviceData={serviceData}
|
sx={{
|
||||||
/>
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CustomWrapperDrawer
|
||||||
|
key={serviceData.serviceKey}
|
||||||
|
serviceData={serviceData}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
))}
|
))}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -14,7 +14,6 @@ export default function Menu() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const color = location.pathname === "/" ? "white" : "black";
|
const color = location.pathname === "/" ? "white" : "black";
|
||||||
console.log(location)
|
|
||||||
|
|
||||||
const arrayMenu: MenuItem[] = [
|
const arrayMenu: MenuItem[] = [
|
||||||
{
|
{
|
||||||
@ -42,49 +41,50 @@ export default function Menu() {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{location.pathname !== "/" ? arrayMenu.map(({ name, url, subMenu = [] }) => (
|
{location.pathname !== "/"
|
||||||
<Link
|
? arrayMenu.map(({ name, url, subMenu = [] }) => (
|
||||||
key={name}
|
<Link
|
||||||
style={{
|
key={name}
|
||||||
textDecoration: "none",
|
style={{
|
||||||
height: "100%",
|
textDecoration: "none",
|
||||||
display: "flex",
|
height: "100%",
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
}}
|
alignItems: "center",
|
||||||
to={url}
|
}}
|
||||||
onMouseEnter={() => setActiveSubMenu(subMenu)}
|
to={url}
|
||||||
>
|
onMouseEnter={() => setActiveSubMenu(subMenu)}
|
||||||
<Typography
|
state={{ previousUrl: location.pathname }}
|
||||||
color={
|
>
|
||||||
location.pathname === url
|
<Typography
|
||||||
? theme.palette.brightPurple.main
|
color={
|
||||||
: color
|
location.pathname === url
|
||||||
}
|
? theme.palette.brightPurple.main
|
||||||
variant="body2"
|
: color
|
||||||
sx={{
|
}
|
||||||
whiteSpace: "nowrap",
|
variant="body2"
|
||||||
}}
|
sx={{
|
||||||
>
|
whiteSpace: "nowrap",
|
||||||
{name}
|
}}
|
||||||
</Typography>
|
>
|
||||||
</Link>
|
{name}
|
||||||
))
|
</Typography>
|
||||||
:arrayMenu.map(({ name, url, subMenu = [] }) => (
|
</Link>
|
||||||
<Typography
|
))
|
||||||
color={
|
: arrayMenu.map(({ name, url, subMenu = [] }) => (
|
||||||
location.pathname === url
|
<Typography
|
||||||
? theme.palette.brightPurple.main
|
color={
|
||||||
: color
|
location.pathname === url
|
||||||
}
|
? theme.palette.brightPurple.main
|
||||||
variant="body2"
|
: color
|
||||||
sx={{
|
}
|
||||||
whiteSpace: "nowrap",
|
variant="body2"
|
||||||
}}
|
sx={{
|
||||||
>
|
whiteSpace: "nowrap",
|
||||||
{name}
|
}}
|
||||||
</Typography>
|
>
|
||||||
))
|
{name}
|
||||||
}
|
</Typography>
|
||||||
|
))}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: "10",
|
zIndex: "10",
|
||||||
@ -96,29 +96,30 @@ export default function Menu() {
|
|||||||
}}
|
}}
|
||||||
onMouseLeave={() => setActiveSubMenu([])}
|
onMouseLeave={() => setActiveSubMenu([])}
|
||||||
>
|
>
|
||||||
{location.pathname !== "/" && activeSubMenu.map(({ name, url }) => (
|
{location.pathname !== "/" &&
|
||||||
<Link key={name} style={{ textDecoration: "none" }} to={url}>
|
activeSubMenu.map(({ name, url }) => (
|
||||||
<Typography
|
<Link key={name} style={{ textDecoration: "none" }} to={url}>
|
||||||
color={
|
<Typography
|
||||||
location.pathname === url
|
color={
|
||||||
? theme.palette.brightPurple.main
|
location.pathname === url
|
||||||
: color
|
? theme.palette.brightPurple.main
|
||||||
}
|
: color
|
||||||
variant="body2"
|
}
|
||||||
sx={{
|
variant="body2"
|
||||||
padding: "15px",
|
sx={{
|
||||||
whiteSpace: "nowrap",
|
padding: "15px",
|
||||||
paddingLeft: "185px",
|
whiteSpace: "nowrap",
|
||||||
"&:hover": {
|
paddingLeft: "185px",
|
||||||
color: theme.palette.brightPurple.main,
|
"&:hover": {
|
||||||
background: theme.palette.background.default,
|
color: theme.palette.brightPurple.main,
|
||||||
},
|
background: theme.palette.background.default,
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
{name}
|
>
|
||||||
</Typography>
|
{name}
|
||||||
</Link>
|
</Typography>
|
||||||
))}
|
</Link>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -85,7 +85,7 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) {
|
|||||||
key={index}
|
key={index}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={url}
|
to={url}
|
||||||
state={user ? undefined : { backgroundLocation: location }}
|
state={{ previousUrl: location.pathname }}
|
||||||
disableRipple
|
disableRipple
|
||||||
variant="text"
|
variant="text"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -134,6 +134,7 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) {
|
|||||||
}}
|
}}
|
||||||
to={url}
|
to={url}
|
||||||
onClick={closeDialogMenu}
|
onClick={closeDialogMenu}
|
||||||
|
state={{ previousUrl: location.pathname }}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
|
@ -58,6 +58,7 @@ export default function NavbarFull({ isLoggedIn, children }: Props) {
|
|||||||
disableGutters
|
disableGutters
|
||||||
maxWidth={false}
|
maxWidth={false}
|
||||||
sx={{
|
sx={{
|
||||||
|
zIndex: 1,
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
top: "0",
|
top: "0",
|
||||||
px: "16px",
|
px: "16px",
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
import { useTheme } from "@mui/material";
|
import { useTheme , Box} from "@mui/material";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
export default function ExpandIcon({ isExpanded }: Props) {
|
export default function ExpandIcon({ isExpanded }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg style={{ transform: isExpanded ? "rotate(180deg)" : undefined }} xmlns="http://www.w3.org/2000/svg" width="32" height="33" viewBox="0 0 32 33" fill="none">
|
<Box sx={{
|
||||||
|
width: "33px",
|
||||||
|
height: "33px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
<svg style={{ transform: isExpanded ? "rotate(180deg)" : undefined }} xmlns="http://www.w3.org/2000/svg" width="32" height="33" viewBox="0 0 32 33" fill="none">
|
||||||
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z" strokeWidth="2" strokeMiterlimit="10" />
|
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z" strokeWidth="2" strokeMiterlimit="10" />
|
||||||
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M20.5 15.2949L16 20.2949L11.5 15.2949" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
<path stroke={isExpanded ? theme.palette.orange.main : theme.palette.brightPurple.main} d="M20.5 15.2949L16 20.2949L11.5 15.2949" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
</Box>
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ import Tariffs from "./pages/Tariffs/Tariffs";
|
|||||||
import SigninDialog from "./pages/auth/Signin";
|
import SigninDialog from "./pages/auth/Signin";
|
||||||
import SignupDialog from "./pages/auth/Signup";
|
import SignupDialog from "./pages/auth/Signup";
|
||||||
import History from "./pages/History";
|
import History from "./pages/History";
|
||||||
import Basket from "./pages/Basket/Basket";
|
import Cart from "./pages/Cart/Cart";
|
||||||
import TariffPage from "./pages/Tariffs/TariffsPage";
|
import TariffPage from "./pages/Tariffs/TariffsPage";
|
||||||
import SavedTariffs from "./pages/SavedTariffs";
|
import SavedTariffs from "./pages/SavedTariffs";
|
||||||
import lightTheme from "@utils/themes/light";
|
import lightTheme from "@utils/themes/light";
|
||||||
@ -24,13 +24,14 @@ import Layout from "./components/Layout";
|
|||||||
import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user";
|
import { clearUserData, setUser, setUserAccount, useUserStore } from "./stores/user";
|
||||||
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
|
import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
|
||||||
import { useUser } from "./utils/hooks/useUser";
|
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 { useUserAccount } from "./utils/hooks/useUserAccount";
|
||||||
import { setCustomTariffs } from "@root/stores/customTariffs";
|
import { setCustomTariffs } from "@root/stores/customTariffs";
|
||||||
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
|
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
|
||||||
import { useDiscounts } from "./utils/hooks/useDiscounts";
|
import { useDiscounts } from "./utils/hooks/useDiscounts";
|
||||||
import { setDiscounts } from "./stores/discounts";
|
import { setDiscounts } from "./stores/discounts";
|
||||||
import { pdfjs } from "react-pdf";
|
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();
|
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 <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
|
if (location.state?.redirectTo) return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -108,7 +116,7 @@ const App = () => {
|
|||||||
<Route path="/support" element={<Support />} />
|
<Route path="/support" element={<Support />} />
|
||||||
<Route path="/support/:ticketId" element={<Support />} />
|
<Route path="/support/:ticketId" element={<Support />} />
|
||||||
<Route path="/tariffconstructor" element={<TariffConstructor />} />
|
<Route path="/tariffconstructor" element={<TariffConstructor />} />
|
||||||
<Route path="/cart" element={<Basket />} />
|
<Route path="/cart" element={<Cart />} />
|
||||||
<Route path="/wallet" element={<Wallet />} />
|
<Route path="/wallet" element={<Wallet />} />
|
||||||
<Route path="/payment" element={<Payment />} />
|
<Route path="/payment" element={<Payment />} />
|
||||||
<Route path="/settings" element={<AccountSettings />} />
|
<Route path="/settings" element={<AccountSettings />} />
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { PrivilegeWithAmount, PrivilegeWithoutPrice } from "./privilege";
|
import { PrivilegeWithAmount } from "@frontend/kitui";
|
||||||
|
import { PrivilegeWithoutPrice } from "./privilege";
|
||||||
|
|
||||||
|
|
||||||
type ServiceKey = string;
|
type ServiceKey = string;
|
||||||
@ -9,14 +10,9 @@ export type CustomTariffUserValuesMap = Record<ServiceKey, CustomTariffUserValue
|
|||||||
|
|
||||||
export type ServiceKeyToPriceMap = Record<ServiceKey, number>;
|
export type ServiceKeyToPriceMap = Record<ServiceKey, number>;
|
||||||
|
|
||||||
export interface CustomTariff {
|
export interface CreateTariffBody {
|
||||||
name: string;
|
name: string;
|
||||||
price?: number;
|
price?: number;
|
||||||
isCustom: boolean;
|
isCustom: boolean;
|
||||||
privilegies: PrivilegeWithAmount[];
|
privilegies: PrivilegeWithoutPrice[];
|
||||||
updatedAt?: string;
|
|
||||||
isDeleted?: boolean;
|
|
||||||
createdAt?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };
|
|
||||||
|
@ -1,21 +1,6 @@
|
|||||||
export interface Privilege {
|
import { Privilege, PrivilegeWithAmount } from "@frontend/kitui";
|
||||||
_id: string;
|
|
||||||
name: string;
|
|
||||||
privilegeId: string;
|
|
||||||
serviceKey: string;
|
|
||||||
description: string;
|
|
||||||
type: "day" | "count";
|
|
||||||
value: PrivilegeValueType;
|
|
||||||
price: number;
|
|
||||||
updatedAt?: string;
|
|
||||||
isDeleted?: boolean;
|
|
||||||
createdAt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;
|
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;
|
||||||
|
|
||||||
export type PrivilegeValueType = "шаблон" | "день" | "МБ";
|
|
||||||
|
|
||||||
export type PrivilegeWithAmount = Omit<Privilege, "_id"> & { amount: number; };
|
|
||||||
|
|
||||||
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;
|
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;
|
||||||
|
@ -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 (
|
|
||||||
<SectionWrapper
|
|
||||||
maxWidth="lg"
|
|
||||||
sx={{
|
|
||||||
mt: upMd ? "25px" : "20px",
|
|
||||||
mb: upMd ? "70px" : "37px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: "20px",
|
|
||||||
mb: upMd ? "40px" : "20px",
|
|
||||||
display: "flex",
|
|
||||||
gap: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!upMd && (
|
|
||||||
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
|
||||||
<ArrowBackIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
<Typography component="h4" variant="h4">
|
|
||||||
Корзина
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{
|
|
||||||
mt: upMd ? "27px" : "10px",
|
|
||||||
}}>
|
|
||||||
{cart.services.map(serviceData =>
|
|
||||||
<CustomWrapper
|
|
||||||
key={serviceData.serviceKey}
|
|
||||||
serviceData={serviceData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} />
|
|
||||||
</SectionWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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<string, string> = {
|
|
||||||
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<boolean>(false);
|
|
||||||
|
|
||||||
function handleItemDeleteClick(tariffId: string) {
|
|
||||||
removeTariffFromCart(tariffId)
|
|
||||||
.then(() => {
|
|
||||||
enqueueSnackbar("Тариф удален");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const message = getMessageFromFetchError(error);
|
|
||||||
if (message) enqueueSnackbar(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "12px",
|
|
||||||
boxShadow: cardShadow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "white",
|
|
||||||
"&:first-of-type": {
|
|
||||||
borderTopLeftRadius: "12px",
|
|
||||||
borderTopRightRadius: "12px",
|
|
||||||
},
|
|
||||||
"&:last-of-type": {
|
|
||||||
borderBottomLeftRadius: "12px",
|
|
||||||
borderBottomRightRadius: "12px",
|
|
||||||
},
|
|
||||||
"&:not(:last-of-type)": {
|
|
||||||
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
onClick={() => setIsExpanded((prev) => !prev)}
|
|
||||||
sx={{
|
|
||||||
height: "72px",
|
|
||||||
px: "20px",
|
|
||||||
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
cursor: "pointer",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: upMd ? "20px" : "16px",
|
|
||||||
lineHeight: upMd ? undefined : "19px",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
px: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name[serviceData.serviceKey]}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
height: "100%",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: upSm ? "111px" : "17px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.grey3.main,
|
|
||||||
fontSize: upSm ? "20px" : "16px",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currencyFormatter.format(serviceData.price / 100)}
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
borderLeft: upSm ? "1px solid #9A9AAF" : "none",
|
|
||||||
paddingLeft: upSm ? "24px" : 0,
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExpandIcon isExpanded={isExpanded} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{isExpanded &&
|
|
||||||
serviceData.privileges.map((privilege) => (
|
|
||||||
<Box
|
|
||||||
key={privilege.tariffId + privilege.privilegeId}
|
|
||||||
sx={{
|
|
||||||
px: "20px",
|
|
||||||
py: upMd ? "25px" : undefined,
|
|
||||||
pt: upMd ? undefined : "15px",
|
|
||||||
pb: upMd ? undefined : "25px",
|
|
||||||
backgroundColor: "#F1F2F6",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "15px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: upMd ? undefined : "16px",
|
|
||||||
lineHeight: upMd ? undefined : "19px",
|
|
||||||
color: theme.palette.grey3.main,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{privilege.description}
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: "10px",
|
|
||||||
alignItems: "center",
|
|
||||||
width: upSm ? "195px" : "123px",
|
|
||||||
marginRight: upSm ? "65px" : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.grey3.main,
|
|
||||||
fontSize: upSm ? "20px" : "16px",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currencyFormatter.format(privilege.price / 100)}
|
|
||||||
</Typography>
|
|
||||||
{upSm ? (
|
|
||||||
<Typography
|
|
||||||
component="div"
|
|
||||||
onClick={() => handleItemDeleteClick(privilege.tariffId)}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
borderBottom: `1px solid ${theme.palette.text.secondary}`,
|
|
||||||
width: "max-content",
|
|
||||||
lineHeight: "19px",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Удалить
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<SvgIcon
|
|
||||||
onClick={() => handleItemDeleteClick(privilege.tariffId)}
|
|
||||||
component={ClearIcon}
|
|
||||||
sx={{ fill: "#7E2AEA" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
67
src/pages/Cart/Cart.tsx
Normal file
67
src/pages/Cart/Cart.tsx
Normal file
@ -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 (
|
||||||
|
<SectionWrapper
|
||||||
|
maxWidth="lg"
|
||||||
|
sx={{
|
||||||
|
mt: upMd ? "25px" : "20px",
|
||||||
|
mb: upMd ? "70px" : "37px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: "20px",
|
||||||
|
mb: upMd ? "40px" : "20px",
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!upMd && (
|
||||||
|
<IconButton
|
||||||
|
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Typography component="h4" variant="h4">
|
||||||
|
Корзина
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: upMd ? "27px" : "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cart.services.map((serviceData) => (
|
||||||
|
<CustomWrapper
|
||||||
|
key={serviceData.serviceKey}
|
||||||
|
serviceData={serviceData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<TotalPrice
|
||||||
|
priceBeforeDiscounts={totalPriceBeforeDiscounts}
|
||||||
|
priceAfterDiscounts={totalPriceAfterDiscounts}
|
||||||
|
/>
|
||||||
|
</SectionWrapper>
|
||||||
|
);
|
||||||
|
}
|
220
src/pages/Cart/CustomWrapper.tsx
Normal file
220
src/pages/Cart/CustomWrapper.tsx
Normal file
@ -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<string, string> = {
|
||||||
|
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<boolean>(false);
|
||||||
|
|
||||||
|
function handleItemDeleteClick(tariffId: string) {
|
||||||
|
removeTariffFromCart(tariffId)
|
||||||
|
.then(() => {
|
||||||
|
enqueueSnackbar("Тариф удален");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message = getMessageFromFetchError(error);
|
||||||
|
if (message) enqueueSnackbar(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteService = async (event: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
setIsExpanded(false);
|
||||||
|
|
||||||
|
for (const { tariffId } of serviceData.tariffs) {
|
||||||
|
try {
|
||||||
|
await removeTariffFromCart(tariffId);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueSnackbar("Тарифы удален");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: cardShadow,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
"&:first-of-type": {
|
||||||
|
borderTopLeftRadius: "12px",
|
||||||
|
borderTopRightRadius: "12px",
|
||||||
|
},
|
||||||
|
"&:last-of-type": {
|
||||||
|
borderBottomLeftRadius: "12px",
|
||||||
|
borderBottomRightRadius: "12px",
|
||||||
|
},
|
||||||
|
"&:not(:last-of-type)": {
|
||||||
|
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
|
sx={{
|
||||||
|
height: "72px",
|
||||||
|
px: "20px",
|
||||||
|
display: "flex",
|
||||||
|
gap: "15px",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
cursor: "pointer",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "50px",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandIcon isExpanded={isExpanded} />
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: upMd ? "20px" : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
px: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name[serviceData.serviceKey]}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
height: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: upSm ? "111px" : "17px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: upSm ? "20px" : "16px",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(serviceData.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
onClick={deleteService}
|
||||||
|
sx={{ padding: 0, height: "22px", width: "22px" }}
|
||||||
|
>
|
||||||
|
<CrossIcon style={{ height: "22 px", width: "22px" }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
{isExpanded &&
|
||||||
|
serviceData.tariffs.map(tariff => {
|
||||||
|
const privilege = tariff.privileges[0];
|
||||||
|
|
||||||
|
return tariff.privileges.length > 1 ? (
|
||||||
|
<CustomTariffAccordion
|
||||||
|
key={privilege.tariffId + privilege.privilegeId}
|
||||||
|
tariffCartData={tariff}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
key={privilege.tariffId + privilege.privilegeId}
|
||||||
|
sx={{
|
||||||
|
px: "20px",
|
||||||
|
py: upMd ? "25px" : undefined,
|
||||||
|
pt: upMd ? undefined : "15px",
|
||||||
|
pb: upMd ? undefined : "25px",
|
||||||
|
backgroundColor: "#F1F2F6",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: upMd ? undefined : "16px",
|
||||||
|
lineHeight: upMd ? undefined : "19px",
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{privilege.description}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: upSm ? "140px" : "123px",
|
||||||
|
marginRight: upSm ? "65px" : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
fontSize: upSm ? "20px" : "16px",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currencyFormatter.format(tariff.price / 100)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
width: "35px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
onClick={() => handleItemDeleteClick(privilege.tariffId)}
|
||||||
|
>
|
||||||
|
<SvgIcon component={ClearIcon} sx={{ fill: "#7E2AEA" }} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -6,12 +6,12 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
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 SectionWrapper from "@components/SectionWrapper";
|
||||||
import SupportChat from "./SupportChat";
|
import SupportChat from "./SupportChat";
|
||||||
import CreateTicket from "./CreateTicket";
|
import CreateTicket from "./CreateTicket";
|
||||||
import TicketList from "./TicketList/TicketList";
|
import TicketList from "./TicketList/TicketList";
|
||||||
import { useCallback } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui";
|
import { Ticket, getMessageFromFetchError, useToken } from "@frontend/kitui";
|
||||||
import {
|
import {
|
||||||
updateTickets,
|
updateTickets,
|
||||||
@ -23,12 +23,14 @@ import { enqueueSnackbar } from "notistack";
|
|||||||
import { useSSESubscription, useTickets } from "@frontend/kitui";
|
import { useSSESubscription, useTickets } from "@frontend/kitui";
|
||||||
|
|
||||||
export default function Support() {
|
export default function Support() {
|
||||||
|
const [previousPage, setPreviousPage] = useState<string>("");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const ticketId = useParams().ticketId;
|
const ticketId = useParams().ticketId;
|
||||||
const ticketApiPage = useTicketStore((state) => state.apiPage);
|
const ticketApiPage = useTicketStore((state) => state.apiPage);
|
||||||
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
|
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
|
||||||
const token = useToken();
|
const token = useToken();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const fetchState = useTickets({
|
const fetchState = useTickets({
|
||||||
url: "https://hub.pena.digital/heruvym/getTickets",
|
url: "https://hub.pena.digital/heruvym/getTickets",
|
||||||
@ -44,6 +46,10 @@ export default function Support() {
|
|||||||
}, []),
|
}, []),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPreviousPage(location.state?.previousUrl || "/");
|
||||||
|
}, []);
|
||||||
|
|
||||||
useSSESubscription<Ticket>({
|
useSSESubscription<Ticket>({
|
||||||
enabled: Boolean(token),
|
enabled: Boolean(token),
|
||||||
url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`,
|
url: `https://admin.pena.digital/heruvym/subscribe?Authorization=${token}`,
|
||||||
@ -73,7 +79,7 @@ export default function Support() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to="/support"
|
to={ticketId ? "/support" : previousPage}
|
||||||
style={{
|
style={{
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -1,144 +1,145 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Divider,
|
Divider,
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import CustomButton from "../../components/CustomButton";
|
import CustomButton from "../../components/CustomButton";
|
||||||
import { Privilege } from "@root/model/privilege";
|
|
||||||
import TariffPrivilegeSlider from "./TariffItem";
|
import TariffPrivilegeSlider from "./TariffItem";
|
||||||
import {
|
import {
|
||||||
createAndSendTariff,
|
createAndSendTariff,
|
||||||
useCustomTariffsStore,
|
useCustomTariffsStore,
|
||||||
} from "@root/stores/customTariffs";
|
} from "@root/stores/customTariffs";
|
||||||
import { cardShadow } from "@root/utils/themes/shadow";
|
import { cardShadow } from "@root/utils/themes/shadow";
|
||||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
import { devlog, getMessageFromFetchError } from "@frontend/kitui";
|
import { Privilege, getMessageFromFetchError } from "@frontend/kitui";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { updateTariffs } from "@root/stores/tariffs";
|
||||||
|
import { addTariffToCart } from "@root/stores/user";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceKey: string;
|
serviceKey: string;
|
||||||
privileges: Privilege[];
|
privileges: Privilege[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomTariffCard({ serviceKey, privileges }: Props) {
|
export default function CustomTariffCard({ serviceKey, privileges }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const summaryPriceBeforeDiscounts = useCustomTariffsStore(
|
const summaryPriceBeforeDiscounts = useCustomTariffsStore(
|
||||||
(state) => state.summaryPriceBeforeDiscountsMap
|
(state) => state.summaryPriceBeforeDiscountsMap
|
||||||
);
|
);
|
||||||
const summaryPriceAfterDiscounts = useCustomTariffsStore(
|
const summaryPriceAfterDiscounts = useCustomTariffsStore(
|
||||||
(state) => state.summaryPriceAfterDiscountsMap
|
(state) => state.summaryPriceAfterDiscountsMap
|
||||||
);
|
);
|
||||||
|
|
||||||
const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0;
|
const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0;
|
||||||
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
|
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
|
||||||
|
|
||||||
async function handleConfirmClick() {
|
async function handleConfirmClick() {
|
||||||
createAndSendTariff(serviceKey)
|
try {
|
||||||
.then((result) => {
|
const tariff = await createAndSendTariff(serviceKey);
|
||||||
devlog(result);
|
updateTariffs([tariff]);
|
||||||
enqueueSnackbar("Тариф создан");
|
await addTariffToCart(tariff._id);
|
||||||
})
|
enqueueSnackbar("Тариф добавлен в корзину");
|
||||||
.catch((error) => {
|
} catch (error) {
|
||||||
const message = getMessageFromFetchError(
|
const message = getMessageFromFetchError(
|
||||||
error,
|
error,
|
||||||
"Не удалось создать тариф"
|
"Не удалось создать тариф"
|
||||||
);
|
);
|
||||||
if (message) enqueueSnackbar(message);
|
if (message) enqueueSnackbar(message);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "white",
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: upMd ? "row" : "column",
|
|
||||||
borderRadius: "12px",
|
|
||||||
boxShadow: cardShadow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: "20px",
|
|
||||||
pr: upMd ? "35px" : undefined,
|
|
||||||
display: "flex",
|
|
||||||
flexBasis: 0,
|
|
||||||
flexGrow: 2.37,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "25px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{privileges.map((privilege) => (
|
|
||||||
<TariffPrivilegeSlider key={privilege._id} privilege={privilege} />
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
{!upMd && (
|
|
||||||
<Divider
|
|
||||||
sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexBasis: 0,
|
|
||||||
flexGrow: 1,
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "start",
|
|
||||||
color: theme.palette.grey3.main,
|
|
||||||
p: "20px",
|
|
||||||
pl: upMd ? "33px" : undefined,
|
|
||||||
borderLeft: upMd
|
|
||||||
? `1px solid ${theme.palette.grey2.main}`
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
backgroundColor: "white",
|
||||||
justifyContent: "space-between",
|
width: "100%",
|
||||||
gap: "15%",
|
display: "flex",
|
||||||
mb: "auto",
|
flexDirection: upMd ? "row" : "column",
|
||||||
width: "100%",
|
borderRadius: "12px",
|
||||||
}}
|
boxShadow: cardShadow,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Typography>
|
<Box
|
||||||
Чем больше пакеты, тем дешевле подписки и опции{" "}
|
sx={{
|
||||||
</Typography>
|
p: "20px",
|
||||||
|
pr: upMd ? "35px" : undefined,
|
||||||
|
display: "flex",
|
||||||
|
flexBasis: 0,
|
||||||
|
flexGrow: 2.37,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "25px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{privileges.map((privilege) => (
|
||||||
|
<TariffPrivilegeSlider key={privilege._id} privilege={privilege} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
{!upMd && (
|
||||||
|
<Divider
|
||||||
|
sx={{ mx: "20px", my: "10px", borderColor: theme.palette.grey2.main }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexBasis: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "start",
|
||||||
|
color: theme.palette.grey3.main,
|
||||||
|
p: "20px",
|
||||||
|
pl: upMd ? "33px" : undefined,
|
||||||
|
borderLeft: upMd
|
||||||
|
? `1px solid ${theme.palette.grey2.main}`
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "15%",
|
||||||
|
mb: "auto",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>
|
||||||
|
Чем больше пакеты, тем дешевле подписки и опции{" "}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography mb="20px" mt="18px">
|
||||||
|
Сумма с учетом скидки
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "20px",
|
||||||
|
mb: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="price">
|
||||||
|
{currencyFormatter.format(priceAfterDiscounts / 100)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="oldPrice" pt="3px">
|
||||||
|
{currencyFormatter.format(priceBeforeDiscounts / 100)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<CustomButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleConfirmClick}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.brightPurple.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выбрать
|
||||||
|
</CustomButton>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography mb="20px" mt="18px">
|
);
|
||||||
Сумма с учетом скидки
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "20px",
|
|
||||||
mb: "30px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="price">
|
|
||||||
{currencyFormatter.format(priceAfterDiscounts / 100)}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="oldPrice" pt="3px">
|
|
||||||
{currencyFormatter.format(priceBeforeDiscounts / 100)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<CustomButton
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleConfirmClick}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.brightPurple.main,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Выбрать
|
|
||||||
</CustomButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,16 @@ import CustomTariffCard from "./CustomTariffCard";
|
|||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
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 { 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() {
|
export default function TariffConstructor() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const customTariffs = useCustomTariffsStore(
|
const customTariffs = useCustomTariffsStore(
|
||||||
(state) => state.customTariffsMap
|
(state) => state.privilegeByService
|
||||||
);
|
);
|
||||||
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(
|
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(
|
||||||
(state) => state.summaryPriceBeforeDiscountsMap
|
(state) => state.summaryPriceBeforeDiscountsMap
|
||||||
@ -30,6 +34,14 @@ export default function TariffConstructor() {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useAllTariffsFetcher({
|
||||||
|
onSuccess: updateTariffs,
|
||||||
|
onError: (error) => {
|
||||||
|
const errorMessage = getMessageFromFetchError(error);
|
||||||
|
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWrapper
|
<SectionWrapper
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useThrottle } from "@frontend/kitui";
|
import { Privilege, PrivilegeValueType, useThrottle } from "@frontend/kitui";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
SliderProps,
|
SliderProps,
|
||||||
@ -10,7 +10,6 @@ import { CustomSlider } from "@root/components/CustomSlider";
|
|||||||
import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment";
|
import NumberInputWithUnitAdornment from "@root/components/NumberInputWithUnitAdornment";
|
||||||
import CalendarIcon from "@root/components/icons/CalendarIcon";
|
import CalendarIcon from "@root/components/icons/CalendarIcon";
|
||||||
import PieChartIcon from "@root/components/icons/PieChartIcon";
|
import PieChartIcon from "@root/components/icons/PieChartIcon";
|
||||||
import { Privilege, PrivilegeValueType } from "@root/model/privilege";
|
|
||||||
import { useCartStore } from "@root/stores/cart";
|
import { useCartStore } from "@root/stores/cart";
|
||||||
import {
|
import {
|
||||||
setCustomTariffsUserValue,
|
setCustomTariffsUserValue,
|
||||||
|
@ -18,7 +18,13 @@ export default function FreeTariffCard() {
|
|||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#7E2AEA",
|
backgroundColor: "#7E2AEA",
|
||||||
color: "white",
|
color: "white",
|
||||||
minHeight: "206px",
|
}}
|
||||||
|
buttonProps={{
|
||||||
|
text: "Выбрать",
|
||||||
|
sx: {
|
||||||
|
color: "white",
|
||||||
|
borderColor: "white",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,148 +1,124 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
SxProps,
|
SxProps,
|
||||||
Theme,
|
Theme,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import CustomButton from "@components/CustomButton";
|
import CustomButton from "@components/CustomButton";
|
||||||
import { MouseEventHandler, ReactNode } from "react";
|
import { MouseEventHandler, ReactNode } from "react";
|
||||||
import { cardShadow } from "@root/utils/themes/shadow";
|
import { cardShadow } from "@root/utils/themes/shadow";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
headerText: string;
|
headerText: string;
|
||||||
text: string | string[];
|
text: string | string[];
|
||||||
sx?: SxProps<Theme>;
|
|
||||||
buttonProps?: {
|
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
buttonProps?: {
|
||||||
text?: string;
|
sx?: SxProps<Theme>;
|
||||||
};
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
price?: ReactNode;
|
text?: string;
|
||||||
|
};
|
||||||
|
price?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TariffCard({
|
export default function TariffCard({
|
||||||
icon,
|
icon,
|
||||||
headerText,
|
headerText,
|
||||||
text,
|
text,
|
||||||
sx,
|
sx,
|
||||||
price,
|
price,
|
||||||
buttonProps,
|
buttonProps,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
text = Array.isArray(text) ? text : [text];
|
||||||
<Box
|
|
||||||
sx={{
|
return (
|
||||||
width: "100%",
|
|
||||||
bgcolor: "white",
|
|
||||||
borderRadius: "12px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "start",
|
|
||||||
p: "20px",
|
|
||||||
boxShadow: cardShadow,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "16px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{price && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "baseline",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
columnGap: "10px",
|
|
||||||
rowGap: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{price}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Tooltip title={headerText} placement="top">
|
|
||||||
<Typography
|
|
||||||
variant="h5"
|
|
||||||
sx={{
|
|
||||||
mt: "14px",
|
|
||||||
mb: "10px",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{headerText}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
{Array.isArray(text) ? (
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
|
||||||
marginBottom: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text.map((line, index) => (
|
|
||||||
<Tooltip key={index} title={line} placement="top">
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
height: "65px",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
display: "-webkit-box",
|
|
||||||
WebkitBoxOrient: "vertical",
|
|
||||||
MozBoxOrient: "vertical",
|
|
||||||
WebkitLineClamp: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{line}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={text} placement="top">
|
|
||||||
<Typography
|
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: "calc(1.185*2em)",
|
width: "100%",
|
||||||
marginBottom: "auto",
|
minHeight: "250px",
|
||||||
height: "65px",
|
bgcolor: "white",
|
||||||
overflow: "hidden",
|
borderRadius: "12px",
|
||||||
textOverflow: "ellipsis",
|
display: "flex",
|
||||||
display: "-webkit-box",
|
flexDirection: "column",
|
||||||
WebkitBoxOrient: "vertical",
|
alignItems: "start",
|
||||||
MozBoxOrient: "vertical",
|
p: "20px",
|
||||||
WebkitLineClamp: 3,
|
boxShadow: cardShadow,
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{buttonProps && (
|
|
||||||
<CustomButton
|
|
||||||
onClick={buttonProps.onClick}
|
|
||||||
variant="outlined"
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
borderColor: theme.palette.brightPurple.main,
|
|
||||||
mt: "10px",
|
|
||||||
...buttonProps.sx,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{buttonProps.text}
|
<Box
|
||||||
</CustomButton>
|
sx={{
|
||||||
)}
|
width: "100%",
|
||||||
</Box>
|
display: "flex",
|
||||||
);
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{price && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
columnGap: "10px",
|
||||||
|
rowGap: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{price}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Tooltip title={<Typography>{headerText}</Typography>} placement="top">
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{
|
||||||
|
mt: "14px",
|
||||||
|
mb: "10px",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{headerText}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
title={text.map((line, index) => (
|
||||||
|
<Typography key={index}>{line}</Typography>
|
||||||
|
))}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Box sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "clip",
|
||||||
|
mb: "auto",
|
||||||
|
}}>
|
||||||
|
{text.map((line, index) => (
|
||||||
|
<Typography key={index}>{line}</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
{buttonProps && (
|
||||||
|
<CustomButton
|
||||||
|
onClick={buttonProps.onClick}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
borderColor: theme.palette.brightPurple.main,
|
||||||
|
mt: "10px",
|
||||||
|
...buttonProps.sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{buttonProps.text}
|
||||||
|
</CustomButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { useState } from "react";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import SectionWrapper from "@components/SectionWrapper";
|
import SectionWrapper from "@components/SectionWrapper";
|
||||||
import { useTariffs } from "@root/utils/hooks/useTariffs";
|
|
||||||
import { updateTariffs, useTariffStore } from "@root/stores/tariffs";
|
import { updateTariffs, useTariffStore } from "@root/stores/tariffs";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { Select } from "@root/components/Select";
|
import { Select } from "@root/components/Select";
|
||||||
@ -11,154 +10,147 @@ import TariffCard from "./TariffCard";
|
|||||||
import NumberIcon from "@root/components/NumberIcon";
|
import NumberIcon from "@root/components/NumberIcon";
|
||||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||||
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
|
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
|
||||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
import { Tariff, getMessageFromFetchError } from "@frontend/kitui";
|
||||||
import FreeTariffCard from "./FreeTariffCard";
|
import FreeTariffCard from "./FreeTariffCard";
|
||||||
import { addTariffToCart, useUserStore } from "@root/stores/user";
|
import { addTariffToCart, useUserStore } from "@root/stores/user";
|
||||||
import { useDiscountStore } from "@root/stores/discounts";
|
import { useDiscountStore } from "@root/stores/discounts";
|
||||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
|
||||||
import { Slider } from "./slider";
|
import { Slider } from "./slider";
|
||||||
import { useCartStore } from "@root/stores/cart";
|
import { useCartStore } from "@root/stores/cart";
|
||||||
|
import { useAllTariffsFetcher } from "@root/utils/hooks/useAllTariffsFetcher";
|
||||||
|
|
||||||
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
|
const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"];
|
||||||
|
|
||||||
export default function TariffPage() {
|
const StepperText: Record<string, string> = {
|
||||||
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<number>(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<string, string> = {
|
|
||||||
volume: "Тарифы на объём",
|
volume: "Тарифы на объём",
|
||||||
time: "Тарифы на время",
|
time: "Тарифы на время",
|
||||||
};
|
};
|
||||||
|
|
||||||
useTariffs({
|
export default function TariffPage() {
|
||||||
apiPage: 0,
|
const theme = useTheme();
|
||||||
tariffsPerPage: 100,
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
onNewTariffs: updateTariffs,
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
onError: (error) => {
|
const location = useLocation();
|
||||||
const errorMessage = getMessageFromFetchError(error);
|
const tariffs = useTariffStore((state) => state.tariffs);
|
||||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
const [selectedItem, setSelectedItem] = useState<number>(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) {
|
const unit: string = String(location.pathname).slice(9);
|
||||||
addTariffToCart(tariffId)
|
const currentTariffs = Object.values(cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
|
||||||
.then(() => {
|
|
||||||
enqueueSnackbar("Тариф добавлен в корзину");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const message = getMessageFromFetchError(error);
|
|
||||||
if (message) enqueueSnackbar(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredTariffs = tariffs.filter((tariff) => {
|
useAllTariffsFetcher({
|
||||||
return (
|
onSuccess: updateTariffs,
|
||||||
tariff.privilegies.map((p) => p.type).includes("day") ===
|
onError: (error) => {
|
||||||
(unit === "time") && !tariff.isDeleted
|
const errorMessage = getMessageFromFetchError(error);
|
||||||
);
|
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const tariffElements = filteredTariffs.map((tariff, index) => {
|
function handleTariffItemClick(tariffId: string) {
|
||||||
const { price, tariffPriceAfterDiscounts } = calcIndividualTariffPrices(
|
addTariffToCart(tariffId)
|
||||||
tariff,
|
.then(() => {
|
||||||
discounts,
|
enqueueSnackbar("Тариф добавлен в корзину");
|
||||||
customTariffs,
|
})
|
||||||
purchasesAmount,
|
.catch((error) => {
|
||||||
cart
|
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 (
|
||||||
|
<TariffCard
|
||||||
|
key={tariff._id}
|
||||||
|
icon={
|
||||||
|
<NumberIcon
|
||||||
|
number={index + 1}
|
||||||
|
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
|
||||||
|
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
buttonProps={{
|
||||||
|
text: "Выбрать",
|
||||||
|
onClick: () => handleTariffItemClick(tariff._id),
|
||||||
|
}}
|
||||||
|
headerText={tariff.name}
|
||||||
|
text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)}
|
||||||
|
price={
|
||||||
|
<>
|
||||||
|
{priceBeforeDiscounts !== priceAfterDiscounts && (
|
||||||
|
<Typography variant="oldPrice">
|
||||||
|
{currencyFormatter.format(priceBeforeDiscounts / 100)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Typography variant="price">
|
||||||
|
{currencyFormatter.format(priceAfterDiscounts / 100)}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tariffElements.length < 6)
|
||||||
|
tariffElements.push(<FreeTariffCard key="free_tariff_card" />);
|
||||||
|
else tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TariffCard
|
<SectionWrapper
|
||||||
key={tariff._id}
|
maxWidth="lg"
|
||||||
icon={
|
sx={{
|
||||||
<NumberIcon
|
mt: "20px",
|
||||||
number={index + 1}
|
mb: upMd ? "90px" : "63px",
|
||||||
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
|
display: "flex",
|
||||||
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
|
flexDirection: "column",
|
||||||
/>
|
}}
|
||||||
}
|
>
|
||||||
buttonProps={{
|
<Typography variant="h4" sx={{ marginBottom: "23px", mt: "20px" }}>
|
||||||
text: "Выбрать",
|
{StepperText[unit]}
|
||||||
onClick: () => handleTariffItemClick(tariff._id),
|
</Typography>
|
||||||
}}
|
{isMobile ? (
|
||||||
headerText={tariff.name}
|
<Select
|
||||||
text={tariff.privilegies.map((p) => `${p.name} - ${p.amount}`)}
|
items={subPages}
|
||||||
price={
|
selectedItem={selectedItem}
|
||||||
<>
|
setSelectedItem={setSelectedItem}
|
||||||
{price !== undefined && price !== tariffPriceAfterDiscounts && (
|
/>
|
||||||
<Typography variant="oldPrice">
|
) : (
|
||||||
{currencyFormatter.format(price / 100)}
|
<Tabs
|
||||||
</Typography>
|
items={subPages}
|
||||||
|
selectedItem={selectedItem}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{tariffPriceAfterDiscounts !== undefined && (
|
<Box
|
||||||
<Typography variant="price">
|
sx={{
|
||||||
{currencyFormatter.format(tariffPriceAfterDiscounts / 100)}
|
justifyContent: "center",
|
||||||
</Typography>
|
mt: "40px",
|
||||||
)}
|
mb: "30px",
|
||||||
</>
|
display: "grid",
|
||||||
}
|
gap: "40px",
|
||||||
/>
|
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 360px))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tariffElements}
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h4" sx={{ mt: "40px" }}>
|
||||||
|
Ранее вы покупали
|
||||||
|
</Typography>
|
||||||
|
<Slider items={tariffElements} />
|
||||||
|
</SectionWrapper>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (tariffElements.length < 6)
|
|
||||||
tariffElements.push(<FreeTariffCard key="free_tariff_card" />);
|
|
||||||
else tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionWrapper
|
|
||||||
maxWidth="lg"
|
|
||||||
sx={{
|
|
||||||
mt: "20px",
|
|
||||||
mb: upMd ? "90px" : "63px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h4" sx={{ marginBottom: "23px", mt: "20px" }}>
|
|
||||||
{StepperText[unit]}
|
|
||||||
</Typography>
|
|
||||||
{isMobile ? (
|
|
||||||
<Select
|
|
||||||
items={subPages}
|
|
||||||
selectedItem={selectedItem}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Tabs
|
|
||||||
items={subPages}
|
|
||||||
selectedItem={selectedItem}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
justifyContent: "center",
|
|
||||||
mt: "40px",
|
|
||||||
mb: "30px",
|
|
||||||
display: "grid",
|
|
||||||
gap: "40px",
|
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 360px))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tariffElements}
|
|
||||||
</Box>
|
|
||||||
<Typography variant="h4" sx={{ mt: "40px" }}>
|
|
||||||
Ранее вы покупали
|
|
||||||
</Typography>
|
|
||||||
<Slider items={tariffElements} />
|
|
||||||
</SectionWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import { createTariff } from "@root/api/tariff";
|
import { createTariff } from "@root/api/tariff";
|
||||||
import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs";
|
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 { 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, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
||||||
|
|
||||||
|
|
||||||
interface CustomTariffsStore {
|
interface CustomTariffsStore {
|
||||||
customTariffsMap: ServiceKeyToPrivilegesMap;
|
privilegeByService: ServiceKeyToPrivilegesMap;
|
||||||
userValuesMap: CustomTariffUserValuesMap;
|
userValuesMap: CustomTariffUserValuesMap;
|
||||||
summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap;
|
summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap;
|
||||||
summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap;
|
summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: CustomTariffsStore = {
|
const initialState: CustomTariffsStore = {
|
||||||
customTariffsMap: {},
|
privilegeByService: {},
|
||||||
userValuesMap: {},
|
userValuesMap: {},
|
||||||
summaryPriceBeforeDiscountsMap: {},
|
summaryPriceBeforeDiscountsMap: {},
|
||||||
summaryPriceAfterDiscountsMap: {},
|
summaryPriceAfterDiscountsMap: {},
|
||||||
@ -41,7 +41,7 @@ export const useCustomTariffsStore = create<CustomTariffsStore>()(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ customTariffsMap: customTariffs });
|
export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ privilegeByService: customTariffs });
|
||||||
|
|
||||||
export const setCustomTariffsUserValue = (
|
export const setCustomTariffsUserValue = (
|
||||||
serviceKey: string,
|
serviceKey: string,
|
||||||
@ -58,22 +58,22 @@ export const setCustomTariffsUserValue = (
|
|||||||
let priceWithoutDiscounts = 0;
|
let priceWithoutDiscounts = 0;
|
||||||
let priceAfterDiscounts = 0;
|
let priceAfterDiscounts = 0;
|
||||||
|
|
||||||
state.customTariffsMap[serviceKey].forEach(tariff => {
|
state.privilegeByService[serviceKey].forEach(privilege => {
|
||||||
const amount = state.userValuesMap[serviceKey]?.[tariff._id] ?? 0;
|
const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0;
|
||||||
priceWithoutDiscounts += tariff.price * amount;
|
priceWithoutDiscounts += privilege.price * amount;
|
||||||
|
|
||||||
const discount = findPrivilegeDiscount(tariff.privilegeId, tariff.price * amount, discounts);
|
const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts);
|
||||||
priceAfterDiscounts += tariff.price * amount * discount.factor;
|
priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount);
|
||||||
});
|
});
|
||||||
|
|
||||||
const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts);
|
const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts);
|
||||||
priceAfterDiscounts *= serviceDiscount.factor;
|
priceAfterDiscounts *= findDiscountFactor(serviceDiscount);
|
||||||
|
|
||||||
const cartDiscount = findCartDiscount(currentCartTotal, discounts);
|
const cartDiscount = findCartDiscount(currentCartTotal, discounts);
|
||||||
priceAfterDiscounts *= cartDiscount.factor;
|
priceAfterDiscounts *= findDiscountFactor(cartDiscount);
|
||||||
|
|
||||||
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
||||||
priceAfterDiscounts *= loyaltyDiscount.factor;
|
priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount);
|
||||||
|
|
||||||
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
|
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
|
||||||
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
|
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
|
||||||
@ -83,22 +83,21 @@ export const setCustomTariffsUserValue = (
|
|||||||
export const createAndSendTariff = (serviceKey: string) => {
|
export const createAndSendTariff = (serviceKey: string) => {
|
||||||
const state = useCustomTariffsStore.getState();
|
const state = useCustomTariffsStore.getState();
|
||||||
|
|
||||||
const privilegies: PrivilegeWithoutPrice[] = [];
|
const privilegies: PrivilegeWithAmount[] = [];
|
||||||
|
|
||||||
Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => {
|
Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => {
|
||||||
if (userValue === 0) return;
|
if (userValue === 0) return;
|
||||||
|
|
||||||
const privilege = state.customTariffsMap[serviceKey].find(p => p._id === privilegeId);
|
const privilegeWithoutAmount = state.privilegeByService[serviceKey].find(p => p._id === privilegeId);
|
||||||
if (!privilege) throw new Error(`Privilege not found: ${privilegeId}`);
|
if (!privilegeWithoutAmount) throw new Error(`Privilege not found: ${privilegeId}`);
|
||||||
|
|
||||||
const p2 = {
|
const privilege: PrivilegeWithAmount = {
|
||||||
...privilege,
|
...privilegeWithoutAmount,
|
||||||
privilegeId: privilege._id,
|
privilegeId: privilegeWithoutAmount._id,
|
||||||
amount: userValue,
|
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(", ");
|
const name = [...privilegies.map(p => p.name), new Date().toISOString()].join(", ");
|
||||||
|
24
src/stores/privileges.ts
Normal file
24
src/stores/privileges.ts
Normal file
@ -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<PrivilegeStore>()(
|
||||||
|
devtools(
|
||||||
|
(get, set) => initialState,
|
||||||
|
{
|
||||||
|
name: "Privileges",
|
||||||
|
enabled: process.env.NODE_ENV === "development",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setPrivileges = (privileges: PrivilegeStore["privileges"]) => usePrivilegeStore.setState({ privileges });
|
@ -33,7 +33,7 @@ export const updateTariffs = (tariffs: TariffStore["tariffs"]) => useTariffStore
|
|||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
type: "updateTariffs",
|
type: "updateTariffs",
|
||||||
tariffsLength: tariffs.length,
|
tariffs: tariffs,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// @ts-nocheck TODO fix tests
|
||||||
import { CartData, Discount, Tariff } from "@frontend/kitui";
|
import { CartData, Discount, Tariff } from "@frontend/kitui";
|
||||||
import { calcCart } from "./calcCart";
|
import { calcCart } from "./calcCart";
|
||||||
|
|
||||||
|
@ -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 {
|
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 => {
|
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 => {
|
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;
|
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,
|
tariffId: tariff._id,
|
||||||
serviceKey: privilege.serviceKey,
|
serviceKey: privilege.serviceKey,
|
||||||
privilegeId: privilege.privilegeId,
|
privilegeId: privilege.privilegeId,
|
||||||
@ -41,11 +47,13 @@ export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmou
|
|||||||
tariffName: tariff.name,
|
tariffName: tariff.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
serviceData.privileges.push(privilegeData);
|
tariffCartData.privileges.push(privilegeCartData);
|
||||||
serviceData.price += privilegePrice;
|
|
||||||
cartData.priceAfterDiscounts += privilegePrice;
|
cartData.priceAfterDiscounts += privilegePrice;
|
||||||
cartData.itemCount++;
|
cartData.itemCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cartData.priceBeforeDiscounts += tariffCartData.price;
|
||||||
|
serviceData.price += tariffCartData.price;
|
||||||
});
|
});
|
||||||
|
|
||||||
applyPrivilegeDiscounts(cartData, discounts);
|
applyPrivilegeDiscounts(cartData, discounts);
|
||||||
|
@ -1,47 +1,24 @@
|
|||||||
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
|
import { Discount, Tariff, findDiscountFactor } from "@frontend/kitui";
|
||||||
import { CartData, Discount, Tariff, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
import { calcCart } from "./calcCart/calcCart";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function calcIndividualTariffPrices(
|
export function calcIndividualTariffPrices(
|
||||||
tariff: Tariff,
|
tariff: Tariff,
|
||||||
discounts: Discount[],
|
discounts: Discount[],
|
||||||
privilegies: ServiceKeyToPrivilegesMap,
|
|
||||||
purchasesAmount: number,
|
purchasesAmount: number,
|
||||||
cart: CartData,
|
currentTariffs: Tariff[],
|
||||||
): {
|
): {
|
||||||
price: number | undefined;
|
priceBeforeDiscounts: number;
|
||||||
tariffPriceAfterDiscounts: number | undefined;
|
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) => {
|
const cart = calcCart([...currentTariffs, tariff], discounts, purchasesAmount);
|
||||||
let privilegePrice = privilege.amount * privilege.price;
|
|
||||||
|
|
||||||
let realprivilegie = privilegies[privilege.serviceKey]?.find(e => e._id === privilege.privilegeId);
|
cart.allAppliedDiscounts.forEach(discount => {
|
||||||
if (realprivilegie) privilege.privilegeId = realprivilegie.privilegeId;
|
priceAfterDiscounts *= findDiscountFactor(discount);
|
||||||
|
});
|
||||||
|
|
||||||
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * privilege.amount, discounts);
|
return { priceBeforeDiscounts, priceAfterDiscounts };
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -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]) {
|
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 "МБ":
|
case "МБ":
|
||||||
return "МБ";
|
return "МБ";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
55
src/utils/hooks/useAllTariffsFetcher.ts
Normal file
55
src/utils/hooks/useAllTariffsFetcher.ts
Normal file
@ -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<never, GetTariffsResponse>({
|
||||||
|
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]);
|
||||||
|
}
|
50
src/utils/hooks/useTariffFetcher.ts
Normal file
50
src/utils/hooks/useTariffFetcher.ts
Normal file
@ -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<never, GetTariffsResponse>({
|
||||||
|
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;
|
||||||
|
}
|
@ -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<never, GetTariffsResponse>({
|
|
||||||
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]);
|
|
||||||
}
|
|
@ -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.17":
|
"@frontend/kitui@1.0.21":
|
||||||
version "1.0.17"
|
version "1.0.21"
|
||||||
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.17.tgz#a5bddaaa18b168be0e1814d5cfbd86e4030d15af"
|
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.21.tgz#572b3e774ba42b65c4b946704aeb68237b4b3cd5"
|
||||||
integrity sha1-pb3aqhixaL4OGBTVz72G5AMNFa8=
|
integrity sha1-Vys+d0ukK2XEuUZwSutoI3tLPNU=
|
||||||
dependencies:
|
dependencies:
|
||||||
immer "^10.0.2"
|
immer "^10.0.2"
|
||||||
reconnecting-eventsource "^1.6.2"
|
reconnecting-eventsource "^1.6.2"
|
||||||
|
Loading…
Reference in New Issue
Block a user