diff --git a/src/App.tsx b/src/App.tsx index 35ccd8d3..0702b85f 100644 --- a/src/App.tsx +++ b/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, @@ -100,16 +101,20 @@ export function useUserAccountFetcher({ dayjs.locale("ru"); const routeslink = [ - { path: "/list", page: , header: false, sidebar: false }, { - path: "/questions/:quizId", - page: , + path: "/edit", + page: EditPage, header: true, sidebar: true, + footer: true, + }, + { + path: "/design", + page: DesignPage, + header: true, + sidebar: true, + footer: true, }, - { path: "/contacts", page: , header: true, sidebar: true }, - { path: "/result", page: , header: true, sidebar: true }, - { path: "/settings", page: , header: true, sidebar: true }, ] as const; export default function App() { @@ -185,21 +190,24 @@ export default function App() { } /> + } /> + } /> + } /> }> {routeslink.map((e, i) => ( +
} /> ))} - - } /> - } /> - } /> - } /> diff --git a/src/assets/icons/BackButtonIcon.tsx b/src/assets/icons/BackButtonIcon.tsx index 883173e9..daed12e3 100644 --- a/src/assets/icons/BackButtonIcon.tsx +++ b/src/assets/icons/BackButtonIcon.tsx @@ -9,9 +9,9 @@ export const BackButtonIcon = () => ( ); diff --git a/src/assets/icons/NumberIcon.tsx b/src/assets/icons/NumberIcon.tsx new file mode 100644 index 00000000..0402e39e --- /dev/null +++ b/src/assets/icons/NumberIcon.tsx @@ -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; +} + +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 ( + + + {circleSvg} + {number > 9 && firstDigitElement} + {secondDigitElement} + + + ); +} + +const circleSvg = ( + +); + +const digitSvgs: Record ReactElement> = { + 0: (translateX: number) => ( + + ), + 1: (translateX: number) => ( + + ), + 2: (translateX: number) => ( + + ), + 3: (translateX: number) => ( + + ), + 4: (translateX: number) => ( + <> + + + + ), + 5: (translateX: number) => ( + + ), + 6: (translateX: number) => ( + <> + + + + ), + 7: (translateX: number) => ( + + ), + 8: (translateX: number) => ( + <> + + + + ), + 9: (translateX: number) => ( + <> + + + + ), +}; diff --git a/src/model/privilege.ts b/src/model/privilege.ts new file mode 100644 index 00000000..2df7f763 --- /dev/null +++ b/src/model/privilege.ts @@ -0,0 +1,5 @@ +import { Privilege, PrivilegeWithAmount } from "@frontend/kitui"; + +export type ServiceKeyToPrivilegesMap = Record; + +export type PrivilegeWithoutPrice = Omit; diff --git a/src/model/tariff.ts b/src/model/tariff.ts new file mode 100644 index 00000000..39ebab6b --- /dev/null +++ b/src/model/tariff.ts @@ -0,0 +1,6 @@ +import { Tariff } from "@frontend/kitui"; + +export interface GetTariffsResponse { + totalPages: number; + tariffs: Tariff[]; +} diff --git a/src/pages/DesignPage/DesignFilling.tsx b/src/pages/DesignPage/DesignFilling.tsx index 6fde9e35..bd3ec613 100644 --- a/src/pages/DesignPage/DesignFilling.tsx +++ b/src/pages/DesignPage/DesignFilling.tsx @@ -138,20 +138,6 @@ export const DesignFilling = () => { ))} - - - - - ); }; diff --git a/src/pages/DesignPage/DesignPage.tsx b/src/pages/DesignPage/DesignPage.tsx index d35b5c51..94a2c716 100644 --- a/src/pages/DesignPage/DesignPage.tsx +++ b/src/pages/DesignPage/DesignPage.tsx @@ -6,7 +6,7 @@ import { useQuizStore } from "@root/quizes/store"; import Sidebar from "@ui_kit/Sidebar"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { SidebarMobile } from "../startPage/Sidebar/SidebarMobile"; +import { SidebarMobile } from "../../ui_kit/Sidebar/SidebarMobile"; import { cleanQuestions, setQuestions } from "@root/questions/actions"; import { updateModalInfoWhyCantCreate, @@ -17,7 +17,7 @@ import { questionApi } from "@api/question"; import { useUiTools } from "@root/uiTools/store"; import { ConfirmLeaveModal } from "../startPage/ConfirmLeaveModal"; -import { Header } from "../startPage/Header"; +import { Header } from "@ui_kit/Header/Header"; import { DesignFilling } from "./DesignFilling"; import { createPortal } from "react-dom"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; @@ -25,19 +25,6 @@ import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; export const DesignPage = () => { const quiz = useCurrentQuiz(); const { editQuizId } = useQuizStore(); - - useEffect(() => { - const getData = async () => { - const quizes = await quizApi.getList(); - setQuizes(quizes); - if (editQuizId) { - const questions = await questionApi.getList({ quiz_id: editQuizId }); - setQuestions(questions); - } - }; - getData(); - }, []); - const { showConfirmLeaveModal } = useUiTools(); const theme = useTheme(); const navigate = useNavigate(); @@ -82,13 +69,7 @@ export const DesignPage = () => { ); return ( <> -
- {isMobile ? ( - - ) : ( - - )} {createPortal(, document.body)} diff --git a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx index a62ff46f..6b6fc883 100644 --- a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx +++ b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx @@ -108,38 +108,38 @@ export const AnswerItem = ({ ), endAdornment: ( - - - - - - setQuestionVariantAnswer(e.target.value || " ") - } - onKeyDown={( - event: KeyboardEvent, - ) => event.stopPropagation()} - /> - + {/**/} + {/* */} + {/**/} + {/**/} + {/* */} + {/* setQuestionVariantAnswer(e.target.value || " ")*/} + {/* }*/} + {/* onKeyDown={(*/} + {/* event: KeyboardEvent,*/} + {/* ) => event.stopPropagation()}*/} + {/* />*/} + {/**/} diff --git a/src/pages/Questions/DataOptions/settingData.tsx b/src/pages/Questions/DataOptions/settingData.tsx index 8115e2e2..39a4fd7d 100644 --- a/src/pages/Questions/DataOptions/settingData.tsx +++ b/src/pages/Questions/DataOptions/settingData.tsx @@ -75,7 +75,7 @@ export default function SettingsData({ question }: SettingsDataProps) { */} */} setTitle(target.value)} sx={{ width: "100%", @@ -365,8 +365,6 @@ const IconAndrom = (questionType: QuestionType | null) => { ); default: - return ( - - ); + return null; } }; diff --git a/src/pages/Questions/OwnTextField/settingTextField.tsx b/src/pages/Questions/OwnTextField/settingTextField.tsx index 4534d176..bfd92290 100644 --- a/src/pages/Questions/OwnTextField/settingTextField.tsx +++ b/src/pages/Questions/OwnTextField/settingTextField.tsx @@ -126,7 +126,7 @@ export default function SettingTextField({ question }: SettingTextFieldProps) { */} (false); + + const openedModal = () => { + updateDesireToOpenABranchingModal(question.content.id); + }; + const SSHC = (data: string) => { setSwitchState(data); }; @@ -59,12 +86,105 @@ export default function PageOptions({ disableInput, question }: Props) { - - + + + {/* + + */} + copyQuestion(question.id, question.quizId)} + > + + + { + if (question.type === null) { + deleteQuestion(question.id); + } + if (question.content.rule.parentId.length !== 0) { + setOpenDelete(true); + } else { + deleteQuestionWithTimeout(question.id, () => + DeleteFunction(questions, question, quiz), + ); + } + }} + data-cy="delete-question" + > + + + setOpenDelete(false)}> + + + Вы удаляете вопрос, участвующий в ветвлении. Все его потомки + потеряют данные ветвления. Вы уверены, что хотите удалить + вопрос? + + + + + + + + + + + {/**/} + {/**/} ); } diff --git a/src/pages/Questions/QuestionsPage.tsx b/src/pages/Questions/QuestionsPage.tsx index e08a5fa2..df8d2a24 100755 --- a/src/pages/Questions/QuestionsPage.tsx +++ b/src/pages/Questions/QuestionsPage.tsx @@ -62,19 +62,21 @@ export default function QuestionsPage({ {quiz.name ? quiz.name : "Заголовок quiz"} - + {!openBranchingPage && ( + + )} - Настройки вопроса + + Настройки вопросов + {/* = ({ const theme = useTheme(); const dropZone = useRef(null); const [ready, setReady] = useState(false); - + const isMobile = useMediaQuery(theme.breakpoints.down(600)); const handleDragEnter = (event: DragEvent) => { event.preventDefault(); setReady(true); @@ -65,7 +66,7 @@ export const UploadImageModal: React.FC = ({ top: "50%", left: "50%", transform: "translate(-50%, -50%)", - maxWidth: "690px", + maxWidth: isMobile ? "300px" : "690px", bgcolor: "background.paper", borderRadius: "12px", boxShadow: 24, @@ -106,13 +107,14 @@ export const UploadImageModal: React.FC = ({ ref={dropZone} sx={{ width: "580px", - padding: "33px 10px 33px 55px", + padding: isMobile ? "33px" : "33px 10px 33px 55px", display: "flex", alignItems: "center", backgroundColor: theme.palette.background.default, border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`, borderRadius: "8px", gap: "55px", + flexDirection: isMobile ? "column" : undefined, }} onDragEnter={handleDragEnter} // Применяем обработчик onDragEnter напрямую > diff --git a/src/pages/Questions/answerOptions/responseSettings.tsx b/src/pages/Questions/answerOptions/responseSettings.tsx index 27b77271..51546395 100644 --- a/src/pages/Questions/answerOptions/responseSettings.tsx +++ b/src/pages/Questions/answerOptions/responseSettings.tsx @@ -98,7 +98,7 @@ export default function ResponseSettings({ question }: Props) { { + const get = async () => { + const user = await makeRequest({ + method: "GET", + url: "https://squiz.pena.digital/customer/account", + }); + const tariffs = await makeRequest({ + 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 ; + + 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); + } + //Добавляем желаемый тариф в корзину + await makeRequest({ + method: "PATCH", + url: `https://hub.pena.digital/customer/cart?id=${id}`, + }); + //Если нам хватает денежек - покупаем тариф + if (price <= user.wallet.cash) { + try { + await makeRequest({ + method: "POST", + url: "https://suiz.pena.digital/customer/cart/pay", + }); + } catch (e) { + enqueueSnackbar("Произошла ошибка. Попробуйте позже"); + } + //Развращаем товары в корзину + inCart(); + } else { + //Деняк не хватило + // history.pushState({}, null, "https://hub.pena.digital/wallet?action=squizpay"); + + var link = document.createElement("a"); + link.href = `https://hub.pena.digital/payment?action=squizpay&dif=${ + (price - Number(user.wallet.cash)) * 100 + }`; + document.body.appendChild(link); + // link.click(); + } + }; + + 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 ( + <> + + + {createTariffElements( + filteredTariffs, + true, + user, + discounts, + openModalHC, + )} + + 0} + onClose={() => setOpenModal({})} + > + + + Вы подтверждаете платёж в сумму {openModal.price} ₽ + + + + + + ); +} + +export const Tariffs = withErrorBoundary(TariffPage, { + fallback: ( + + Ошибка загрузки тарифов + + ), + onError: () => {}, +}); + +const LoadingPage = () => ( + + + {"Подождите, пожалуйста, идёт загрузка :)"} + + +); + +export 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("Я не смог удалить из корзины тариф :("); + } + }); +}; diff --git a/src/pages/Tariffs/tariffsUtils/FreeTariffCard.tsx b/src/pages/Tariffs/tariffsUtils/FreeTariffCard.tsx new file mode 100644 index 00000000..a5aaab30 --- /dev/null +++ b/src/pages/Tariffs/tariffsUtils/FreeTariffCard.tsx @@ -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 ( + } + discount={""} + headerText="бесплатно" + text="Первые 14 дней после регистрации, вы можете пользоваться полным функционалом сервиса совершенно бесплатно" + price={ + + 0 руб. + + } + sx={{ + backgroundColor: "#7e2aea", + color: "white", + }} + buttonProps={{ + text: "Выбрать", + sx: { + color: "white", + borderColor: "white", + }, + }} + /> + ); +} diff --git a/src/pages/Tariffs/tariffsUtils/TariffCard.tsx b/src/pages/Tariffs/tariffsUtils/TariffCard.tsx new file mode 100644 index 00000000..ae5a21cc --- /dev/null +++ b/src/pages/Tariffs/tariffsUtils/TariffCard.tsx @@ -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; + buttonProps?: { + sx?: SxProps; + onClick?: MouseEventHandler; + text?: string; + }; + price?: ReactNode; +} + +export default function TariffCard({ + icon, + headerText, + text, + sx, + price, + buttonProps, + discount, +}: Props) { + text = Array.isArray(text) ? text : [text]; + + return ( + + + {icon} + {price && ( + + {price} + + )} + + {discount && discount !== "0%" && ( + + -{discount} + + )} + + {headerText}} placement="top"> + + {headerText} + + + ( + {line} + ))} + placement="top" + > + + {text.map((line, index) => ( + + {line} + + ))} + + + {buttonProps && ( + + )} + + ); +} diff --git a/src/pages/Tariffs/tariffsUtils/calcCart.ts b/src/pages/Tariffs/tariffsUtils/calcCart.ts new file mode 100644 index 00000000..b2a3f74f --- /dev/null +++ b/src/pages/Tariffs/tariffsUtils/calcCart.ts @@ -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; +} diff --git a/src/pages/Tariffs/tariffsUtils/calcTariffPrices.ts b/src/pages/Tariffs/tariffsUtils/calcTariffPrices.ts new file mode 100644 index 00000000..42dd1d6b --- /dev/null +++ b/src/pages/Tariffs/tariffsUtils/calcTariffPrices.ts @@ -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 }; +} diff --git a/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx new file mode 100644 index 00000000..f552bb85 --- /dev/null +++ b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx @@ -0,0 +1,77 @@ +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 ( + + } + 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 && ( + + {currencyFormatter.format(priceBeforeDiscounts / 100)} + + )} + + {currencyFormatter.format(priceAfterDiscounts / 100)} + + + } + /> + ); + }); + + if (addFreeTariff) { + if (tariffElements.length < 6) + tariffElements.push(); + else tariffElements.splice(5, 0, ); + } + + return tariffElements; +}; diff --git a/src/pages/Tariffs/tariffsUtils/currencyFormatter.ts b/src/pages/Tariffs/tariffsUtils/currencyFormatter.ts new file mode 100644 index 00000000..5924c6bd --- /dev/null +++ b/src/pages/Tariffs/tariffsUtils/currencyFormatter.ts @@ -0,0 +1,6 @@ +export const currencyFormatter = new Intl.NumberFormat("ru", { + currency: "RUB", + style: "currency", + compactDisplay: "short", + minimumFractionDigits: 0, +}); diff --git a/src/pages/createQuize/MyQuizzesFull.tsx b/src/pages/createQuize/MyQuizzesFull.tsx index b0f15fe1..16a8895d 100644 --- a/src/pages/createQuize/MyQuizzesFull.tsx +++ b/src/pages/createQuize/MyQuizzesFull.tsx @@ -15,6 +15,7 @@ import { useNavigate } from "react-router-dom"; import { resetEditConfig } from "@root/quizes/actions"; import FirstQuiz from "./FirstQuiz"; import QuizCard from "./QuizCard"; +import HeaderFull from "@ui_kit/Header/HeaderFull"; interface Props { outerContainerSx?: SxProps; @@ -32,10 +33,14 @@ export default function MyQuizzesFull({ return ( <> + {quizes.length === 0 ? ( ) : ( - + { + const pay = async () => { + try { + await makeRequest({ + method: "POST", + url: "https://suiz.pena.digital/customer/cart/pay", + }); + inCart(); + } catch (e) { + enqueueSnackbar( + "Попробуйте снова купить тариф после зачисления средств", + ); + } + }; + const params = new URLSearchParams(window.location.search); + const fromSquiz = params.get("action"); + if (fromSquiz === "fromhub") { + window.history.replaceState(null, '', "/list") + pay(); + } + }, []); + return ( diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 311a6e8f..dc944572 100755 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,39 +1,255 @@ -import Header from "@ui_kit/Header/Header"; +import { Header } from "@ui_kit/Header/Header"; import Sidebar from "@ui_kit/Sidebar"; import Box from "@mui/material/Box"; -import { useTheme, useMediaQuery } from "@mui/material"; +import { useTheme, useMediaQuery, IconButton } from "@mui/material"; import HeaderFull from "@ui_kit/Header/HeaderFull"; +import { useEffect, useState } from "react"; +import { SidebarMobile } from "../ui_kit/Sidebar/SidebarMobile"; +import { setShowConfirmLeaveModal } from "@root/uiTools/actions"; +import { setCurrentStep, setQuizes } from "@root/quizes/actions"; +import { useQuizStore } from "@root/quizes/store"; +import { SmallSwitchQuestionListGraph } from "@ui_kit/Toolbars/SmallSwitchQuestionListGraph"; +import { PanelSwitchQuestionListGraph } from "@ui_kit/Toolbars/PanelSwitchQuestionListGraph"; +import { ButtonTestPublication } from "@ui_kit/Toolbars/ButtonTestPublication"; +import { ButtonRecallQuiz } from "@ui_kit/Toolbars/ButtonRecallQuiz"; +import { Link } from "react-router-dom"; +import { LinkSimple } from "@icons/LinkSimple"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { deleteTimeoutedQuestions } from "@utils/deleteTimeoutedQuestions"; +import { useQuestionsStore } from "@root/questions/store"; +import { quizApi } from "@api/quiz"; +import { questionApi } from "@api/question"; +import { createResult, setQuestions } from "@root/questions/actions"; +import { toggleQuizPreview } from "@root/quizPreview"; +import VisibilityIcon from "@mui/icons-material/Visibility"; interface Props { sidebar: boolean; header?: boolean; - page: JSX.Element; + footer?: boolean; + Page?: React.Component; } -export default function Main({ sidebar, header, page }: Props) { +export default function Main({ sidebar, header, footer, Page }: Props) { const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const quiz = useCurrentQuiz(); + const quizConfig = quiz?.config; + const { questions } = useQuestionsStore(); + const { editQuizId } = useQuizStore(); + const currentStep = useQuizStore((state) => state.currentStep); + + useEffect(() => { + const getData = async () => { + const quizes = await quizApi.getList(); + setQuizes(quizes); + + if (editQuizId) { + const questions = await questionApi.getList({ quiz_id: editQuizId }); + + setQuestions(questions); + //Всегда должен существовать хоть 1 резулт - "line" + if ( + !questions?.find( + (q) => + (q.type === "result" && q.content.includes(':"line"')) || + q.content.includes(":'line'"), + ) + ) { + createResult(quiz?.backendId, "line"); + console.log("Я не нашёл линейный резулт и собираюсь создать новый"); + } + } + }; + getData(); + }, []); + + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + const isMobileSm = useMediaQuery(theme.breakpoints.down(370)); + const isBranchingLogic = useMediaQuery(theme.breakpoints.down(1100)); + const isLinkButton = useMediaQuery(theme.breakpoints.down(708)); + const [mobileSidebar, setMobileSidebar] = useState(false); + const [nextStep, setNextStep] = useState(0); + const [openBranchingPage, setOpenBranchingPage] = useState(false); + + const openBranchingPageHC = () => { + if (!openBranchingPage) { + deleteTimeoutedQuestions(questions, quiz); + } + setOpenBranchingPage((old) => !old); + }; + + if (!quizConfig) return <>; + + const isConditionMet = + [1].includes(currentStep) && quizConfig.type !== "form"; + + const changePage = (index: number) => { + if (currentStep === 2) { + setNextStep(index); + setShowConfirmLeaveModal(true); + + return; + } + + setCurrentStep(index); + }; return ( <> - {header ?
: } +
- {sidebar ? : <>} + {sidebar ? ( + <> + {isMobile ? ( + + ) : ( + + )} + + ) : ( + <> + )} - {page} + + + + + {footer && ( + + {isConditionMet && + (isBranchingLogic ? ( + + ) : ( + + ))} + {/* Кнопка тестового просмотра */} + + {/* Кнопка отозвать */} + + {/* Ссылка */} + {quiz?.status === "start" && + (!isLinkButton ? ( + + https://hbpn.link/{quiz.qid} + + ) : ( + + + + ))} + {/* Маленькая кнопка ссылки */} + {isMobile && quiz?.status === "start" && ( + + + + )} + + + + + + + )} diff --git a/src/pages/startPage/EditPage.tsx b/src/pages/startPage/EditPage.tsx index ff8649ca..cb50dd5f 100755 --- a/src/pages/startPage/EditPage.tsx +++ b/src/pages/startPage/EditPage.tsx @@ -20,7 +20,7 @@ import SwitchStepPages from "@ui_kit/switchStepPages"; import { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useDebouncedCallback } from "use-debounce"; -import { SidebarMobile } from "./Sidebar/SidebarMobile"; +import { SidebarMobile } from "../../ui_kit/Sidebar/SidebarMobile"; import { cleanQuestions, createResult, @@ -32,7 +32,7 @@ import { setShowConfirmLeaveModal, updateSomeWorkBackend, } from "@root/uiTools/actions"; -import { Header } from "./Header"; +import { Header } from "@ui_kit/Header/Header"; import { useQuestionsStore } from "@root/questions/store"; import { questionApi } from "@api/question"; import { useUiTools } from "@root/uiTools/store"; @@ -41,64 +41,28 @@ import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { ModalInfoWhyCantCreate } from "./ModalInfoWhyCantCreate"; import { ConfirmLeaveModal } from "./ConfirmLeaveModal"; import { checkQuestionHint } from "@utils/checkQuestionHint"; -import { deleteTimeoutedQuestions } from "@utils/deleteTimeoutedQuestions"; -import { toggleQuizPreview } from "@root/quizPreview"; -import { LinkSimple } from "@icons/LinkSimple"; -import { SmallSwitchQuestionListGraph } from "@ui_kit/Toolbars/SmallSwitchQuestionListGraph"; -import { PanelSwitchQuestionListGraph } from "@ui_kit/Toolbars/PanelSwitchQuestionListGraph"; -import { ButtonTestPublication } from "@ui_kit/Toolbars/ButtonTestPublication"; -import { ButtonRecallQuiz } from "@ui_kit/Toolbars/ButtonRecallQuiz"; -export default function EditPage() { +interface Props { + openBranchingPage: boolean; + setOpenBranchingPage: (a: boolean) => void; +} +export default function EditPage({ + openBranchingPage, + setOpenBranchingPage, +}: Props) { const quiz = useCurrentQuiz(); const { editQuizId } = useQuizStore(); const { questions } = useQuestionsStore(); console.log(questions); - - useEffect(() => { - const getData = async () => { - const quizes = await quizApi.getList(); - setQuizes(quizes); - - if (editQuizId) { - const questions = await questionApi.getList({ quiz_id: editQuizId }); - - setQuestions(questions); - //Всегда должен существовать хоть 1 резулт - "line" - if ( - !questions?.find( - (q) => - (q.type === "result" && q.content.includes(':"line"')) || - q.content.includes(":'line'"), - ) - ) { - createResult(quiz?.backendId, "line"); - console.log("Я не нашёл линейный резулт и собираюсь создать новый"); - } - } - }; - getData(); - }, []); - const { whyCantCreatePublic, showConfirmLeaveModal } = useUiTools(); const theme = useTheme(); const navigate = useNavigate(); const currentStep = useQuizStore((state) => state.currentStep); const isBranchingLogic = useMediaQuery(theme.breakpoints.down(1100)); const isMobile = useMediaQuery(theme.breakpoints.down(660)); - const isLinkButton = useMediaQuery(theme.breakpoints.down(708)); - const isMobileSm = useMediaQuery(theme.breakpoints.down(370)); - const [mobileSidebar, setMobileSidebar] = useState(false); const [nextStep, setNextStep] = useState(0); const quizConfig = quiz?.config; - const [openBranchingPage, setOpenBranchingPage] = useState(false); - - const openBranchingPageHC = () => { - if (!openBranchingPage) { - deleteTimeoutedQuestions(questions, quiz); - } - setOpenBranchingPage((old) => !old); - }; + // const [openBranchingPage, setOpenBranchingPage] = useState(false); useEffect(() => { if (editQuizId === null) navigate("/list"); @@ -144,33 +108,14 @@ export default function EditPage() { const isConditionMet = [1].includes(currentStep) && quizConfig.type !== "form"; - const changePage = (index: number) => { - if (currentStep === 2) { - setNextStep(index); - setShowConfirmLeaveModal(true); - - return; - } - - setCurrentStep(index); - }; - return ( <> -
- - {isMobile ? ( - - ) : ( - - )} - )} - - - {isConditionMet && - (isBranchingLogic ? ( - - ) : ( - - ))} - {/* Кнопка тестового просмотра */} - - {/* Кнопка отозвать */} - - {/* Ссылка */} - {quiz?.status === "start" && - (!isLinkButton ? ( - - https://hbpn.link/{quiz.qid} - - ) : ( - - - - ))} - {/* Маленькая кнопка ссылки */} - {isMobile && quiz?.status === "start" && ( - - - - )} - diff --git a/src/pages/startPage/Header.tsx b/src/pages/startPage/Header.tsx deleted file mode 100644 index 4789ba97..00000000 --- a/src/pages/startPage/Header.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { LogoutButton } from "@ui_kit/LogoutButton"; -import BackArrowIcon from "@icons/BackArrowIcon"; -import { Burger } from "@icons/Burger"; - -import { - Box, - Container, - FormControl, - IconButton, - TextField, - useMediaQuery, - useTheme, -} from "@mui/material"; -import { updateQuiz } from "@root/quizes/actions"; -import { useCurrentQuiz } from "@root/quizes/hooks"; -import CustomAvatar from "@ui_kit/Header/Avatar"; -import NavMenuItem from "@ui_kit/Header/NavMenuItem"; - -import { enqueueSnackbar } from "notistack"; -import { useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; - -import Logotip from "../Landing/images/icons/QuizLogo"; -import { clearUserData } from "@root/user"; -import { clearAuthToken } from "@frontend/kitui"; -import { logout } from "@api/auth"; - -type HeaderProps = { - setMobileSidebar: (callback: (visible: boolean) => boolean) => void; -}; - -export const Header = ({ setMobileSidebar }: HeaderProps) => { - const quiz = useCurrentQuiz(); - const theme = useTheme(); - const navigate = useNavigate(); - const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - const isMobile = useMediaQuery(theme.breakpoints.down(660)); - - async function handleLogoutClick() { - const [, logoutError] = await logout(); - - if (logoutError) { - return enqueueSnackbar(logoutError); - } - - clearAuthToken(); - clearUserData(); - navigate("/"); - } - - return ( - - - {isMobile ? : } - - - - - - - - - - updateQuiz(quiz.id, (quiz) => { - quiz.name = e.target.value; - }) - } - fullWidth - id="project-name" - placeholder="Название проекта окно" - sx={{ - width: "270px", - "& .MuiInputBase-root": { - height: "34px", - borderRadius: "8px", - p: 0, - }, - }} - inputProps={{ - sx: { - height: "20px", - borderRadius: "8px", - fontSize: "16px", - lineHeight: "20px", - p: "7px", - color: "black", - "&::placeholder": { - opacity: 1, - }, - }, - }} - /> - - - {isTablet ? ( - - {isMobile ? ( - setMobileSidebar((visible: boolean) => !visible)} - style={{ fontSize: "30px", color: "white", cursor: "pointer" }} - /> - ) : ( - - {/* */} - - )} - - ) : ( - <> - - {/* */} - {/* - - - */} - - - {/* */} - - - - )} - - ); -}; diff --git a/src/ui_kit/Header/Header.tsx b/src/ui_kit/Header/Header.tsx old mode 100755 new mode 100644 index 8a891dd5..6e692666 --- a/src/ui_kit/Header/Header.tsx +++ b/src/ui_kit/Header/Header.tsx @@ -1,8 +1,9 @@ +import { LogoutButton } from "@ui_kit/LogoutButton"; import BackArrowIcon from "@icons/BackArrowIcon"; -import EyeIcon from "@icons/EyeIcon"; +import { Burger } from "@icons/Burger"; + import { Box, - Button, Container, FormControl, IconButton, @@ -10,15 +11,43 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import { decrementCurrentStep } from "@root/quizes/actions"; -import PenaLogo from "../PenaLogo"; -import CustomAvatar from "./Avatar"; -import NavMenuItem from "./NavMenuItem"; -import { Link } from "react-router-dom"; +import { updateQuiz } from "@root/quizes/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import CustomAvatar from "@ui_kit/Header/Avatar"; +import NavMenuItem from "@ui_kit/Header/NavMenuItem"; -export default function Header() { +import { enqueueSnackbar } from "notistack"; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; + +import Logotip from "../../pages/Landing/images/icons/QuizLogo"; +import { clearUserData } from "@root/user"; +import { clearAuthToken } from "@frontend/kitui"; +import { logout } from "@api/auth"; +import { ToTariffsButton } from "@ui_kit/Toolbars/ToTariffsButton"; + +type HeaderProps = { + setMobileSidebar: (callback: (visible: boolean) => boolean) => void; +}; + +export const Header = ({ setMobileSidebar }: HeaderProps) => { + const quiz = useCurrentQuiz(); const theme = useTheme(); + const navigate = useNavigate(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + + async function handleLogoutClick() { + const [, logoutError] = await logout(); + + if (logoutError) { + return enqueueSnackbar(logoutError); + } + + clearAuthToken(); + clearUserData(); + navigate("/"); + } return ( - - + + {isMobile ? ( + + ) : ( + + )} - - - + + + + + + updateQuiz(quiz.id, (quiz) => { + quiz.name = e.target.value; + }) + } fullWidth id="project-name" - placeholder="Название проекта окно" + placeholder="Название проекта" sx={{ width: "270px", "& .MuiInputBase-root": { @@ -77,36 +118,32 @@ export default function Header() { /> - - {isTablet ? null : ( - <> - - {/* */} - {/* - - - */} - - - {/* */} - - - )} + + {isMobile ? ( + setMobileSidebar((visible: boolean) => !visible)} + style={{ fontSize: "30px", color: "white", cursor: "pointer" }} + /> + ) : ( + <> + + + + + + )} + ); -} +}; diff --git a/src/ui_kit/Header/HeaderFull.tsx b/src/ui_kit/Header/HeaderFull.tsx index 6f81d324..e2074429 100644 --- a/src/ui_kit/Header/HeaderFull.tsx +++ b/src/ui_kit/Header/HeaderFull.tsx @@ -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(); @@ -47,18 +48,12 @@ export default function HeaderFull() { height: "80px", alignItems: "center", gap: isTablet ? "20px" : "60px", - flexDirection: isMobile ? "row-reverse" : "row", + flexDirection: "row", justifyContent: isMobile ? "space-between" : "center", bgcolor: "white", borderBottom: "1px solid #E3E3E3", }} > - {isTablet && ( - setMobileSidebar(!mobileSidebar)} - style={{ fontSize: "30px", color: "#000000", cursor: "pointer" }} - /> - )} @@ -69,57 +64,17 @@ export default function HeaderFull() { gap: "30px", overflow: "hidden", }} - > - {/* - - - - - - */} - + > )} - - {/* {!isTablet && ( - <> - - - - - - Мой баланс - - - 00.00 руб. - - - - )} */} - {!isMobile && ( - <> - {/* */} - - - )} + + + ); } diff --git a/src/ui_kit/Header/NavMenuItem.tsx b/src/ui_kit/Header/NavMenuItem.tsx index fe4dd953..d602c3c8 100755 --- a/src/ui_kit/Header/NavMenuItem.tsx +++ b/src/ui_kit/Header/NavMenuItem.tsx @@ -17,6 +17,7 @@ export default function NavMenuItem({ return ( + я есть навбар меню итем + я подписан как навбар коллапсед = ({ padding: "20px", borderRadius: "8px", width: isMobile ? "343px" : "620px", + height: isMobile ? "80vh" : undefined, + display: isMobile ? "flex" : undefined, + flexDirection: isMobile ? "column" : undefined, + justifyContent: isMobile ? "space-evenly" : undefined, }} > = ({ sx={[ styleSlider, { - width: isMobile ? "350px" : "250px", + width: isMobile ? undefined : "250px", }, ]} value={scale * 100} @@ -278,7 +282,7 @@ export const CropModal: FC = ({ sx={[ styleSlider, { - width: isMobile ? "350px" : "250px", + width: isMobile ? undefined : "250px", }, ]} value={darken} @@ -295,6 +299,7 @@ export const CropModal: FC = ({ width: "100%", display: "flex", gap: "10px", + flexWrap: isMobile ? "wrap" : undefined, }} > + + ); +};