все роуты в компоненте Main и это поломало отображение. Весело :) Страница тарифов умеет покупать тарифы если хватает шекелей
This commit is contained in:
parent
953ba0b5a3
commit
ea4c238813
10
src/App.tsx
10
src/App.tsx
@ -23,6 +23,7 @@ import { ResultSettings } from "./pages/ResultPage/ResultSettings";
|
||||
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
|
||||
import Main from "./pages/main";
|
||||
import EditPage from "./pages/startPage/EditPage";
|
||||
import Tariffs from "./pages/Tariffs/Tariffs";
|
||||
import {
|
||||
clearAuthToken,
|
||||
getMessageFromFetchError,
|
||||
@ -110,6 +111,10 @@ const routeslink = [
|
||||
{ path: "/contacts", page: <ContactFormPage />, header: true, sidebar: true },
|
||||
{ path: "/result", page: <Result />, header: true, sidebar: true },
|
||||
{ path: "/settings", page: <ResultSettings />, header: true, sidebar: true },
|
||||
{ path: "/tariffs", page: <Tariffs />, header: true, sidebar: false },
|
||||
{ path: "/edit", page: <EditPage />, header: true, sidebar: true },
|
||||
{ path: "/view", page: <ViewPage />, header: false, sidebar: false },
|
||||
{ path: "/design", page: <DesignPage />, header: true, sidebar: true },
|
||||
] as const;
|
||||
|
||||
export default function App() {
|
||||
@ -195,11 +200,6 @@ export default function App() {
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route path="edit" element={<EditPage />} />
|
||||
<Route path="crop" element={<ImageCrop />} />
|
||||
<Route path="/view" element={<ViewPage />} />
|
||||
<Route path="/design" element={<DesignPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</>
|
||||
|
208
src/assets/icons/NumberIcon.tsx
Normal file
208
src/assets/icons/NumberIcon.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import { Box, SxProps, Theme } from "@mui/material";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
interface Props {
|
||||
number: number;
|
||||
color: string;
|
||||
backgroundColor?: string;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export default function NumberIcon({
|
||||
number,
|
||||
backgroundColor = "rgb(0 0 0 / 0)",
|
||||
color,
|
||||
sx,
|
||||
}: Props) {
|
||||
number = number % 100;
|
||||
|
||||
const firstDigit = Math.floor(number / 10);
|
||||
const secondDigit = number % 10;
|
||||
|
||||
const firstDigitTranslateX = 6;
|
||||
const secondDigitTranslateX = number < 10 ? 9 : number < 20 ? 11 : 12;
|
||||
|
||||
const firstDigitElement = digitSvgs[firstDigit](firstDigitTranslateX);
|
||||
const secondDigitElement = digitSvgs[secondDigit](secondDigitTranslateX);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor,
|
||||
color,
|
||||
width: "36px",
|
||||
height: "36px",
|
||||
borderRadius: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{circleSvg}
|
||||
{number > 9 && firstDigitElement}
|
||||
{secondDigitElement}
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const circleSvg = (
|
||||
<path
|
||||
d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
);
|
||||
|
||||
const digitSvgs: Record<number, (translateX: number) => ReactElement> = {
|
||||
0: (translateX: number) => (
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M3 8.75C4.24264 8.75 5.25 7.07107 5.25 5C5.25 2.92893 4.24264 1.25 3 1.25C1.75736 1.25 0.75 2.92893 0.75 5C0.75 7.07107 1.75736 8.75 3 8.75Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
),
|
||||
1: (translateX: number) => (
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M1.125 2.75L3.375 1.25V8.75015"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
),
|
||||
2: (translateX: number) => (
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M1.27158 2.39455C1.44019 1.99638 1.74115 1.66868 2.12357 1.46688C2.50598 1.26507 2.94636 1.20155 3.37021 1.28705C3.79407 1.37256 4.17538 1.60185 4.44964 1.93613C4.7239 2.27041 4.87428 2.68916 4.87534 3.12156C4.87703 3.49512 4.76526 3.8604 4.55483 4.16907V4.16907L1.12305 8.75H4.87534"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
),
|
||||
3: (translateX: number) => (
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M1.125 1.25H4.875L2.6875 4.375C3.04723 4.37503 3.40139 4.46376 3.71863 4.63336C4.03587 4.80295 4.30639 5.04816 4.50623 5.34727C4.70607 5.64637 4.82906 5.99015 4.86431 6.34814C4.89956 6.70614 4.84598 7.0673 4.70832 7.39964C4.57066 7.73198 4.35316 8.02525 4.07509 8.25345C3.79702 8.48166 3.46696 8.63777 3.11415 8.70796C2.76133 8.77815 2.39666 8.76024 2.05242 8.65583C1.70818 8.55142 1.395 8.36373 1.14062 8.10938"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
),
|
||||
4: (translateX: number) => (
|
||||
<>
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M2.62508 1.07788L0.75 6.3906H4.50015"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M4.5 3.89038V8.89058"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
5: (translateX: number) => (
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M5.0625 1.25H1.92188L1.3125 5.01562C1.61844 4.70972 2.00821 4.5014 2.43254 4.41702C2.85687 4.33263 3.29669 4.37596 3.69639 4.54153C4.09609 4.70711 4.43772 4.98749 4.67807 5.34721C4.91843 5.70694 5.04672 6.12986 5.04672 6.5625C5.04672 6.99514 4.91843 7.41806 4.67807 7.77779C4.43772 8.13751 4.09609 8.41789 3.69639 8.58346C3.29669 8.74904 2.85687 8.79237 2.43254 8.70798C2.00821 8.6236 1.61844 8.41528 1.3125 8.10937"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
),
|
||||
6: (translateX: number) => (
|
||||
<>
|
||||
<path
|
||||
transform={`translate(${translateX - 1} 7)`}
|
||||
d="M2.00977 5.30469L4.65117 0.875"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
transform={`translate(${translateX - 1} 7)`}
|
||||
d="M3.99609 8.75C5.26462 8.75 6.29297 7.72165 6.29297 6.45312C6.29297 5.1846 5.26462 4.15625 3.99609 4.15625C2.72756 4.15625 1.69922 5.1846 1.69922 6.45312C1.69922 7.72165 2.72756 8.75 3.99609 8.75Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
7: (translateX: number) => (
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M1.125 1.25H4.875L2.375 8.75"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
),
|
||||
8: (translateX: number) => (
|
||||
<>
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M4.72779 2.97356C4.72547 3.36962 4.58601 3.75265 4.33315 4.05749C4.08029 4.36233 3.72963 4.57017 3.34082 4.64564C2.95201 4.72111 2.54906 4.65956 2.20051 4.47146C1.85196 4.28336 1.57934 3.98032 1.42901 3.6139C1.27868 3.24747 1.25993 2.84028 1.37595 2.46158C1.49196 2.08289 1.73559 1.75608 2.06537 1.53675C2.39516 1.31741 2.79075 1.21909 3.18485 1.25851C3.57895 1.29793 3.94722 1.47266 4.22703 1.75298C4.54727 2.07862 4.72705 2.51684 4.72779 2.97356Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M5.04125 6.72925C5.03995 7.19778 4.87634 7.65138 4.57827 8.01287C4.28019 8.37436 3.86607 8.62139 3.40637 8.71193C2.94667 8.80247 2.4698 8.73092 2.05691 8.50946C1.64403 8.288 1.32063 7.93031 1.14177 7.49727C0.962899 7.06422 0.939612 6.58258 1.07587 6.1343C1.21213 5.68602 1.49951 5.2988 1.8891 5.03854C2.2787 4.77829 2.74645 4.66107 3.21273 4.70684C3.67902 4.75261 4.11505 4.95854 4.44661 5.28958C4.63578 5.47847 4.78572 5.70292 4.88777 5.95C4.98983 6.19709 5.04199 6.46192 5.04125 6.72925Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
9: (translateX: number) => (
|
||||
<>
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M5.03203 4.47046L2.39062 8.90015"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
transform={`translate(${translateX} 7)`}
|
||||
d="M3.04688 5.6189C4.3154 5.6189 5.34375 4.59055 5.34375 3.32202C5.34375 2.05349 4.3154 1.02515 3.04688 1.02515C1.77835 1.02515 0.75 2.05349 0.75 3.32202C0.75 4.59055 1.77835 5.6189 3.04688 5.6189Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
5
src/model/privilege.ts
Normal file
5
src/model/privilege.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Privilege, PrivilegeWithAmount } from "@frontend/kitui";
|
||||
|
||||
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;
|
||||
|
||||
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;
|
6
src/model/tariff.ts
Normal file
6
src/model/tariff.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Tariff } from "@frontend/kitui";
|
||||
|
||||
export interface GetTariffsResponse {
|
||||
totalPages: number;
|
||||
tariffs: Tariff[];
|
||||
}
|
201
src/pages/Tariffs/Tariffs.tsx
Normal file
201
src/pages/Tariffs/Tariffs.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { GetTariffsResponse } from "@model/tariff";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
Paper,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { Tariff, getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { withErrorBoundary } from "react-error-boundary";
|
||||
import { createTariffElements } from "./tariffsUtils/createTariffElements";
|
||||
|
||||
function TariffPage() {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [tariffs, setTariffs] = useState();
|
||||
const [user, setUser] = useState();
|
||||
const [discounts, setDiscounts] = useState();
|
||||
const [cartTariffMap, setCartTariffMap] = useState();
|
||||
const [openModal, setOpenModal] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const get = async () => {
|
||||
const user = await makeRequest({
|
||||
method: "GET",
|
||||
url: "https://squiz.pena.digital/customer/account",
|
||||
});
|
||||
const tariffs = await makeRequest<never, GetTariffsResponse>({
|
||||
method: "GET",
|
||||
url: "https://squiz.pena.digital/strator/tariff?page=1&limit=100",
|
||||
});
|
||||
const discounts = await makeRequest({
|
||||
method: "GET",
|
||||
url: "https://squiz.pena.digital/price/discounts",
|
||||
});
|
||||
setUser(user);
|
||||
setTariffs(tariffs);
|
||||
setDiscounts(discounts.Discounts);
|
||||
};
|
||||
get();
|
||||
}, []);
|
||||
|
||||
if (!user || !tariffs || !discounts) return <LoadingPage />;
|
||||
|
||||
console.log("user ", user);
|
||||
console.log("tariffs ", tariffs);
|
||||
console.log("discounts ", discounts);
|
||||
|
||||
const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo);
|
||||
const tryBuy = async ({ id, price }: { id: string, price: number }) => {
|
||||
openModalHC({})
|
||||
//Если в корзине что-то было - выкладываем содержимое и запоминаем чо там лежало
|
||||
if (user.cart.length > 0) {
|
||||
outCart(user.cart)
|
||||
}
|
||||
//Если нам хватает денежек - покупаем тариф
|
||||
if (price <= user.wallet.cash) {
|
||||
try {
|
||||
await makeRequest({
|
||||
method: "POST",
|
||||
url: "https://suiz.pena.digital/customer/cart/pay"
|
||||
})
|
||||
} catch (e) {
|
||||
enqueueSnackbar("Произошла ошибка. Попробуйте позже")
|
||||
}
|
||||
//Развращаем товары в корзину
|
||||
inCart()
|
||||
} else {
|
||||
//Деняк не хватило
|
||||
navigate("https://hub.pena.digital/wallet?action=squizpay")
|
||||
}
|
||||
};
|
||||
|
||||
const purchasesAmount = user?.wallet.purchasesAmount ?? 0;
|
||||
const isUserNko = user?.status === "nko";
|
||||
const filteredTariffs = tariffs.tariffs.filter((tariff) => {
|
||||
return (
|
||||
tariff.privileges[0].serviceKey === "squiz" &&
|
||||
!tariff.isDeleted &&
|
||||
!tariff.isCustom
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
justifyContent: "left",
|
||||
mt: "40px",
|
||||
mb: "30px",
|
||||
display: "grid",
|
||||
gap: "40px",
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
|
||||
}))`,
|
||||
}}
|
||||
>
|
||||
{createTariffElements(
|
||||
filteredTariffs,
|
||||
true,
|
||||
user,
|
||||
discounts,
|
||||
openModalHC,
|
||||
)}
|
||||
</Box>
|
||||
<Modal
|
||||
open={Object.values(openModal).length > 0}
|
||||
onClose={() => setOpenModal({})}
|
||||
>
|
||||
<Paper
|
||||
sx={{
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
<Typography id="modal-modal-title" variant="h6" component="h2" mb="20px">
|
||||
Вы подтверждаете платёж в сумму {openModal.price} ₽
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={() => tryBuy(openModal)}>купить</Button>
|
||||
</Paper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withErrorBoundary(TariffPage, {
|
||||
fallback: (
|
||||
<Typography mt="8px" textAlign="center">
|
||||
Ошибка загрузки тарифов
|
||||
</Typography>
|
||||
),
|
||||
onError: () => { },
|
||||
});
|
||||
|
||||
const LoadingPage = () => (
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ textAlign: "center" }}>
|
||||
{"Подождите, пожалуйста, идёт загрузка :)"}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const inCart = () => {
|
||||
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]")
|
||||
saveCart.forEach(async (id: string) => {
|
||||
try {
|
||||
await makeRequest({
|
||||
method: "PATCH",
|
||||
url: `https://hub.pena.digital/customer/cart?id=${id}`
|
||||
})
|
||||
|
||||
let index = saveCart.indexOf('green');
|
||||
if (index !== -1) {
|
||||
saveCart.splice(index, 1);
|
||||
}
|
||||
localStorage.setItem("saveCart", JSON.stringify(saveCart))
|
||||
} catch (e) {
|
||||
console.log("Я не смог добавить тариф в корзину :( " + id)
|
||||
}
|
||||
})
|
||||
}
|
||||
const outCart = (cart: string[]) => {
|
||||
//Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально
|
||||
cart.forEach(async (id: string) => {
|
||||
try {
|
||||
await makeRequest({
|
||||
method: "DELETE",
|
||||
url: `https://suiz.pena.digital/customer/cart?id=${id}`
|
||||
})
|
||||
let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]")
|
||||
saveCart = saveCart.push(id)
|
||||
localStorage.setItem("saveCart", JSON.stringify(saveCart))
|
||||
} catch (e) {
|
||||
console.log("Я не смог удалить из корзины тариф :(")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
33
src/pages/Tariffs/tariffsUtils/FreeTariffCard.tsx
Normal file
33
src/pages/Tariffs/tariffsUtils/FreeTariffCard.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import TariffCard from "./TariffCard";
|
||||
import NumberIcon from "@icons/NumberIcon";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
export default function FreeTariffCard() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TariffCard
|
||||
icon={<NumberIcon number={0} color="#7e2aea" backgroundColor="white" />}
|
||||
discount={""}
|
||||
headerText="бесплатно"
|
||||
text="Первые 14 дней после регистрации, вы можете пользоваться полным функционалом сервиса совершенно бесплатно"
|
||||
price={
|
||||
<Typography variant="price" color="white">
|
||||
0 руб.
|
||||
</Typography>
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: "#7e2aea",
|
||||
color: "white",
|
||||
}}
|
||||
buttonProps={{
|
||||
text: "Выбрать",
|
||||
sx: {
|
||||
color: "white",
|
||||
borderColor: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
145
src/pages/Tariffs/tariffsUtils/TariffCard.tsx
Normal file
145
src/pages/Tariffs/tariffsUtils/TariffCard.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Tooltip,
|
||||
SxProps,
|
||||
Theme,
|
||||
Button,
|
||||
Badge,
|
||||
} from "@mui/material";
|
||||
import { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
icon: ReactNode;
|
||||
headerText: string;
|
||||
discount?: string;
|
||||
text: string | string[];
|
||||
sx?: SxProps<Theme>;
|
||||
buttonProps?: {
|
||||
sx?: SxProps<Theme>;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
text?: string;
|
||||
};
|
||||
price?: ReactNode;
|
||||
}
|
||||
|
||||
export default function TariffCard({
|
||||
icon,
|
||||
headerText,
|
||||
text,
|
||||
sx,
|
||||
price,
|
||||
buttonProps,
|
||||
discount,
|
||||
}: Props) {
|
||||
text = Array.isArray(text) ? text : [text];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
minHeight: "250px",
|
||||
bgcolor: "white",
|
||||
borderRadius: "12px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
p: "20px",
|
||||
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
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>
|
||||
)}
|
||||
|
||||
{discount && discount !== "0%" && (
|
||||
<Box
|
||||
sx={{
|
||||
padding: "5px 10px",
|
||||
position: "absolute",
|
||||
left: "50px",
|
||||
background: "#ff4904",
|
||||
borderRadius: "8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography color="white">-{discount}</Typography>
|
||||
</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={{
|
||||
width: "100%",
|
||||
mb: "auto",
|
||||
}}
|
||||
>
|
||||
{text.map((line, index) => (
|
||||
<Typography
|
||||
sx={{ overflow: "hidden", textOverflow: "ellipsis" }}
|
||||
key={index}
|
||||
>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{buttonProps && (
|
||||
<Button
|
||||
onClick={buttonProps.onClick}
|
||||
variant="pena-outlined-purple"
|
||||
sx={{
|
||||
mt: "10px",
|
||||
...buttonProps.sx,
|
||||
}}
|
||||
>
|
||||
{buttonProps.text}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
250
src/pages/Tariffs/tariffsUtils/calcCart.ts
Normal file
250
src/pages/Tariffs/tariffsUtils/calcCart.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import {
|
||||
CartData,
|
||||
Discount,
|
||||
PrivilegeCartData,
|
||||
Tariff,
|
||||
TariffCartData,
|
||||
findPrivilegeDiscount,
|
||||
findDiscountFactor,
|
||||
applyLoyaltyDiscount,
|
||||
} from "@frontend/kitui";
|
||||
|
||||
function applyPrivilegeDiscounts(cartData: CartData, discounts: Discount[]) {
|
||||
cartData.services.forEach((service) => {
|
||||
const privMap = new Map();
|
||||
service.tariffs.forEach((tariff) =>
|
||||
tariff.privileges.forEach((p) => {
|
||||
privMap.set(
|
||||
p.privilegeId,
|
||||
p.amount + (privMap.get(p.privilegeId) || 0),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
service.tariffs.forEach((tariff) => {
|
||||
tariff.privileges.forEach((privilege) => {
|
||||
const privilegeDiscount = findPrivilegeDiscount(
|
||||
privilege.privilegeId,
|
||||
privMap.get(privilege.privilegeId) || 0,
|
||||
discounts,
|
||||
);
|
||||
if (!privilegeDiscount) return;
|
||||
|
||||
const discountAmount =
|
||||
privilege.price * (1 - findDiscountFactor(privilegeDiscount));
|
||||
privilege.price -= discountAmount;
|
||||
cartData.allAppliedDiscounts.push(privilegeDiscount);
|
||||
privilege.appliedPrivilegeDiscount = privilegeDiscount;
|
||||
|
||||
tariff.price -= discountAmount;
|
||||
service.price -= discountAmount;
|
||||
cartData.priceAfterDiscounts -= discountAmount;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function findServiceDiscount(
|
||||
serviceKey: string,
|
||||
currentPrice: number,
|
||||
discounts: Discount[],
|
||||
): Discount | null {
|
||||
const applicableDiscounts = discounts.filter((discount) => {
|
||||
return (
|
||||
discount.Layer === 2 &&
|
||||
discount.Condition.Group === serviceKey &&
|
||||
currentPrice >= Number(discount.Condition.PriceFrom)
|
||||
);
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
|
||||
return Number(current.Condition.PriceFrom) >
|
||||
Number(prev.Condition.PriceFrom)
|
||||
? current
|
||||
: prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
function findCartDiscount(
|
||||
cartPurchasesAmount: number,
|
||||
discounts: Discount[],
|
||||
): Discount | null {
|
||||
const applicableDiscounts = discounts.filter((discount) => {
|
||||
return (
|
||||
discount.Layer === 3 &&
|
||||
cartPurchasesAmount >= Number(discount.Condition.CartPurchasesAmount)
|
||||
);
|
||||
});
|
||||
console.log("FCD", applicableDiscounts);
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
|
||||
return Number(current.Condition.CartPurchasesAmount) >
|
||||
Number(prev.Condition.CartPurchasesAmount)
|
||||
? current
|
||||
: prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
function applyCartDiscount(cartData: CartData, discounts: Discount[]) {
|
||||
const cartDiscount = findCartDiscount(
|
||||
cartData.priceAfterDiscounts,
|
||||
discounts,
|
||||
);
|
||||
if (!cartDiscount) return;
|
||||
|
||||
cartData.priceAfterDiscounts *= findDiscountFactor(cartDiscount);
|
||||
cartData.allAppliedDiscounts.push(cartDiscount);
|
||||
cartData.appliedCartPurchasesDiscount = cartDiscount;
|
||||
}
|
||||
|
||||
function applyServiceDiscounts(cartData: CartData, discounts: Discount[]) {
|
||||
const privMap = new Map();
|
||||
cartData.services.forEach((service) => {
|
||||
service.tariffs.forEach((tariff) =>
|
||||
tariff.privileges.forEach((p) => {
|
||||
privMap.set(p.serviceKey, p.price + (privMap.get(p.serviceKey) || 0));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
cartData.services.forEach((service) => {
|
||||
service.tariffs.map((tariff) => {
|
||||
tariff.privileges.forEach((privilege) => {
|
||||
const privilegeDiscount = findServiceDiscount(
|
||||
privilege.serviceKey,
|
||||
privMap.get(privilege.serviceKey),
|
||||
discounts,
|
||||
);
|
||||
if (!privilegeDiscount) return;
|
||||
|
||||
const discountAmount =
|
||||
privilege.price * (1 - findDiscountFactor(privilegeDiscount));
|
||||
privilege.price -= discountAmount;
|
||||
cartData.allAppliedDiscounts.push(privilegeDiscount);
|
||||
service.appliedServiceDiscount = privilegeDiscount;
|
||||
|
||||
tariff.price -= discountAmount;
|
||||
service.price -= discountAmount;
|
||||
cartData.priceAfterDiscounts -= discountAmount;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function calcCart(
|
||||
tariffs: Tariff[],
|
||||
discounts: Discount[],
|
||||
purchasesAmount: number,
|
||||
isUserNko?: boolean,
|
||||
): CartData {
|
||||
const cartData: CartData = {
|
||||
services: [],
|
||||
priceBeforeDiscounts: 0,
|
||||
priceAfterDiscounts: 0,
|
||||
itemCount: 0,
|
||||
appliedCartPurchasesDiscount: null,
|
||||
appliedLoyaltyDiscount: null,
|
||||
allAppliedDiscounts: [],
|
||||
};
|
||||
|
||||
tariffs.forEach((tariff) => {
|
||||
if (tariff.price !== undefined && tariff.privileges.length !== 1)
|
||||
throw new Error("Price is defined for tariff with several");
|
||||
|
||||
let serviceData = cartData.services.find(
|
||||
(service) => service.serviceKey === "custom" && tariff.isCustom,
|
||||
);
|
||||
if (!serviceData && !tariff.isCustom)
|
||||
serviceData = cartData.services.find(
|
||||
(service) => service.serviceKey === tariff.privileges[0].serviceKey,
|
||||
);
|
||||
|
||||
if (!serviceData) {
|
||||
serviceData = {
|
||||
serviceKey: tariff.isCustom
|
||||
? "custom"
|
||||
: tariff.privileges[0].serviceKey,
|
||||
tariffs: [],
|
||||
price: 0,
|
||||
appliedServiceDiscount: null,
|
||||
};
|
||||
cartData.services.push(serviceData);
|
||||
}
|
||||
|
||||
const tariffCartData: TariffCartData = {
|
||||
price: tariff.price ?? 0,
|
||||
isCustom: tariff.isCustom,
|
||||
privileges: [],
|
||||
id: tariff._id,
|
||||
name: tariff.name,
|
||||
};
|
||||
serviceData.tariffs.push(tariffCartData);
|
||||
|
||||
tariff.privileges.forEach((privilege) => {
|
||||
let privilegePrice = privilege.amount * privilege.price;
|
||||
if (!tariff.price) tariffCartData.price += privilegePrice;
|
||||
else privilegePrice = tariff.price;
|
||||
|
||||
const privilegeCartData: PrivilegeCartData = {
|
||||
serviceKey: privilege.serviceKey,
|
||||
privilegeId: privilege.privilegeId,
|
||||
description: privilege.description,
|
||||
price: privilegePrice,
|
||||
amount: privilege.amount,
|
||||
appliedPrivilegeDiscount: null,
|
||||
};
|
||||
|
||||
tariffCartData.privileges.push(privilegeCartData);
|
||||
cartData.priceAfterDiscounts += privilegePrice;
|
||||
cartData.itemCount++;
|
||||
});
|
||||
|
||||
cartData.priceBeforeDiscounts += tariffCartData.price;
|
||||
serviceData.price += tariffCartData.price;
|
||||
});
|
||||
|
||||
const nkoDiscount = findNkoDiscount(discounts);
|
||||
if (isUserNko && nkoDiscount) {
|
||||
applyNkoDiscount(cartData, nkoDiscount);
|
||||
} else {
|
||||
applyPrivilegeDiscounts(cartData, discounts);
|
||||
applyServiceDiscounts(cartData, discounts);
|
||||
applyCartDiscount(cartData, discounts);
|
||||
applyLoyaltyDiscount(cartData, discounts, purchasesAmount);
|
||||
}
|
||||
|
||||
cartData.allAppliedDiscounts = Array.from(
|
||||
new Set(cartData.allAppliedDiscounts),
|
||||
);
|
||||
|
||||
return cartData;
|
||||
}
|
||||
|
||||
function applyNkoDiscount(cartData: CartData, discount: Discount) {
|
||||
cartData.priceAfterDiscounts *= discount.Target.Factor;
|
||||
cartData.allAppliedDiscounts.push(discount);
|
||||
}
|
||||
|
||||
export function findNkoDiscount(discounts: Discount[]): Discount | null {
|
||||
const applicableDiscounts = discounts.filter(
|
||||
(discount) => discount.Condition.UserType === "nko",
|
||||
);
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce((prev, current) => {
|
||||
return current.Condition.CartPurchasesAmount >
|
||||
prev.Condition.CartPurchasesAmount
|
||||
? current
|
||||
: prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
54
src/pages/Tariffs/tariffsUtils/calcTariffPrices.ts
Normal file
54
src/pages/Tariffs/tariffsUtils/calcTariffPrices.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Discount, Tariff, findDiscountFactor } from "@frontend/kitui";
|
||||
import { calcCart } from "./calcCart";
|
||||
|
||||
export function calcIndividualTariffPrices(
|
||||
tariff: Tariff,
|
||||
discounts: Discount[],
|
||||
purchasesAmount: number,
|
||||
currentTariffs: Tariff[],
|
||||
isUserNko?: boolean,
|
||||
): {
|
||||
priceBeforeDiscounts: number;
|
||||
priceAfterDiscounts: number;
|
||||
} {
|
||||
const priceBeforeDiscounts =
|
||||
tariff.price ||
|
||||
tariff.privileges.reduce(
|
||||
(sum, privilege) => sum + privilege.amount * privilege.price,
|
||||
0,
|
||||
);
|
||||
let priceAfterDiscounts = 0;
|
||||
|
||||
const cart = calcCart(
|
||||
[...currentTariffs, tariff],
|
||||
discounts,
|
||||
purchasesAmount,
|
||||
isUserNko,
|
||||
);
|
||||
if (cart.allAppliedDiscounts[0]?.Target.Overhelm)
|
||||
return {
|
||||
priceBeforeDiscounts: priceBeforeDiscounts,
|
||||
priceAfterDiscounts:
|
||||
priceBeforeDiscounts * cart.allAppliedDiscounts[0].Target.Factor,
|
||||
};
|
||||
cart.services.forEach((s) => {
|
||||
if (s.serviceKey === tariff.privileges[0].serviceKey) {
|
||||
let processed = false;
|
||||
s.tariffs.forEach((t) => {
|
||||
if (t.id === tariff._id && !processed) {
|
||||
processed = true;
|
||||
t.privileges.forEach((p) => (priceAfterDiscounts += p.price));
|
||||
}
|
||||
});
|
||||
priceAfterDiscounts *= findDiscountFactor(s.appliedServiceDiscount);
|
||||
}
|
||||
});
|
||||
priceAfterDiscounts *= findDiscountFactor(cart.appliedLoyaltyDiscount);
|
||||
priceAfterDiscounts *= findDiscountFactor(cart.appliedCartPurchasesDiscount);
|
||||
|
||||
// cart.allAppliedDiscounts.forEach((discount) => {
|
||||
// priceAfterDiscounts *= findDiscountFactor(discount)
|
||||
// })
|
||||
//priceAfterDiscounts = cart.priceAfterDiscounts
|
||||
return { priceBeforeDiscounts, priceAfterDiscounts };
|
||||
}
|
76
src/pages/Tariffs/tariffsUtils/createTariffElements.tsx
Normal file
76
src/pages/Tariffs/tariffsUtils/createTariffElements.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Tariff } from "@frontend/kitui";
|
||||
import TariffCard from "./TariffCard";
|
||||
import NumberIcon from "@icons/NumberIcon";
|
||||
import { calcIndividualTariffPrices } from "./calcTariffPrices";
|
||||
import { currencyFormatter } from "./currencyFormatter";
|
||||
import FreeTariffCard from "./FreeTariffCard";
|
||||
import { Typography, useTheme } from "@mui/material";
|
||||
|
||||
export const createTariffElements = (
|
||||
filteredTariffs: Tariff[],
|
||||
addFreeTariff = false,
|
||||
user: any,
|
||||
discounts: any,
|
||||
onclick: any,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const tariffElements = filteredTariffs
|
||||
.filter((tariff) => tariff.privileges.length > 0)
|
||||
.map((tariff, index) => {
|
||||
const { priceBeforeDiscounts, priceAfterDiscounts } =
|
||||
calcIndividualTariffPrices(
|
||||
tariff,
|
||||
discounts,
|
||||
user.purchasesAmount,
|
||||
[],
|
||||
user.isUserNko,
|
||||
);
|
||||
|
||||
return (
|
||||
<TariffCard
|
||||
key={tariff._id}
|
||||
discount={
|
||||
priceBeforeDiscounts - priceAfterDiscounts
|
||||
? `${(
|
||||
(priceBeforeDiscounts - priceAfterDiscounts) /
|
||||
(priceBeforeDiscounts / 100)
|
||||
).toFixed(0)}%`
|
||||
: ""
|
||||
}
|
||||
icon={
|
||||
<NumberIcon
|
||||
number={index + 1}
|
||||
color={"#7e2aea"}
|
||||
backgroundColor={"#EEE4FC"}
|
||||
/>
|
||||
}
|
||||
buttonProps={{
|
||||
text: "Выбрать",
|
||||
onClick: () => onclick({id: tariff._id, price: priceBeforeDiscounts / 100}),
|
||||
}}
|
||||
headerText={tariff.name}
|
||||
text={tariff.privileges.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 (addFreeTariff) {
|
||||
if (tariffElements.length < 6)
|
||||
tariffElements.push(<FreeTariffCard key="free_tariff_card" />);
|
||||
else tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />);
|
||||
}
|
||||
|
||||
return tariffElements;
|
||||
};
|
6
src/pages/Tariffs/tariffsUtils/currencyFormatter.ts
Normal file
6
src/pages/Tariffs/tariffsUtils/currencyFormatter.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const currencyFormatter = new Intl.NumberFormat("ru", {
|
||||
currency: "RUB",
|
||||
style: "currency",
|
||||
compactDisplay: "short",
|
||||
minimumFractionDigits: 0,
|
||||
});
|
@ -94,6 +94,7 @@ export default function Header() {
|
||||
<NavMenuItem text="История" />
|
||||
<NavMenuItem text="Помощь" /> */}
|
||||
</Box>
|
||||
а я типа обычный хедер
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
@ -17,6 +17,7 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { clearUserData } from "@root/user";
|
||||
import { LogoutButton } from "@ui_kit/LogoutButton";
|
||||
import { ToTariffsButton } from "@ui_kit/Toolbars/ToTariffsButton";
|
||||
|
||||
export default function HeaderFull() {
|
||||
const theme = useTheme();
|
||||
@ -120,6 +121,7 @@ export default function HeaderFull() {
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<ToTariffsButton />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ export default function NavMenuItem({
|
||||
|
||||
return (
|
||||
<Link href={href} underline="none" onClick={onClick}>
|
||||
я есть навбар меню итем
|
||||
<Typography
|
||||
color={isActive ? theme.palette.brightPurple.main : undefined}
|
||||
variant="body2"
|
||||
|
@ -29,6 +29,7 @@ export default function NavbarCollapsed({ isLoggedIn }: Props) {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
я подписан как навбар коллапсед
|
||||
<PenaLogo width={100} />
|
||||
<IconButton
|
||||
sx={{
|
||||
|
10
src/ui_kit/Toolbars/ToTariffsButton.tsx
Normal file
10
src/ui_kit/Toolbars/ToTariffsButton.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const ToTariffsButton = () => {
|
||||
return (
|
||||
<Link to="/tariffs">
|
||||
<Button>Пополнить</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user