diff --git a/package.json b/package.json index 38c4ac5..abed30b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@date-io/dayjs": "^2.15.0", "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", - "@frontend/kitui": "^1.0.59", + "@frontend/kitui": "^1.0.77", "@material-ui/pickers": "^3.3.10", "@mui/icons-material": "^5.10.3", "@mui/material": "^5.10.5", @@ -36,6 +36,7 @@ "numeral": "^2.0.6", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.13", "react-numeral": "^1.1.1", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", diff --git a/src/api/discounts.ts b/src/api/discounts.ts index 068f8f0..93316c4 100644 --- a/src/api/discounts.ts +++ b/src/api/discounts.ts @@ -8,6 +8,8 @@ import type { DiscountType, GetDiscountResponse, } from "@root/model/discount"; +import useSWR from "swr"; +import { enqueueSnackbar } from "notistack"; const baseUrl = process.env.REACT_APP_DOMAIN + "/price" @@ -213,3 +215,33 @@ export const requestDiscounts = async (): Promise< return [null, `Ошибка получения скидок. ${error}`]; } }; + +async function getDiscounts() { + try { + const discountsResponse = await makeRequest({ + url: baseUrl + "/discounts", + method: "get", + useToken: true, + }); + + return discountsResponse.Discounts.filter((discount) => !discount.Deprecated); + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + throw new Error(`Ошибка получения списка скидок. ${error}`); + } +} + +export function useDiscounts() { + const { data } = useSWR("discounts", getDiscounts, { + keepPreviousData: true, + suspense: true, + onError: (error) => { + if (!(error instanceof Error)) return; + + enqueueSnackbar(error.message, { variant: "error" }); + } + }); + + return data; +} diff --git a/src/api/privilegies.ts b/src/api/privilegies.ts index cc87faa..65e70a7 100644 --- a/src/api/privilegies.ts +++ b/src/api/privilegies.ts @@ -1,13 +1,13 @@ -import { makeRequest } from "@frontend/kitui"; +import { CustomPrivilege, makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; -import { PrivilegeWithAmount } from "@frontend/kitui"; +import { Privilege } from "@frontend/kitui"; import type { TMockData } from "./roles"; type SeverPrivilegesResponse = { - templategen: PrivilegeWithAmount[]; - squiz: PrivilegeWithAmount[]; + templategen: CustomPrivilege[]; + squiz: CustomPrivilege[]; }; const baseUrl = process.env.REACT_APP_DOMAIN + "/strator" @@ -28,11 +28,11 @@ export const getRoles = async (): Promise<[TMockData | null, string?]> => { }; export const putPrivilege = async ( - body: Omit + body: Omit ): Promise<[unknown, string?]> => { try { const putedPrivilege = await makeRequest< - Omit, + Omit, unknown >({ url: baseUrl + "/privilege", @@ -70,9 +70,9 @@ export const requestServicePrivileges = async (): Promise< export const requestPrivileges = async ( signal: AbortSignal | undefined -): Promise<[PrivilegeWithAmount[], string?]> => { +): Promise<[CustomPrivilege[], string?]> => { try { - const privilegesResponse = await makeRequest( + const privilegesResponse = await makeRequest( { url: baseUrl + "/privilege", method: "get", diff --git a/src/api/promocode/requests.ts b/src/api/promocode/requests.ts index f28ee26..d807d41 100644 --- a/src/api/promocode/requests.ts +++ b/src/api/promocode/requests.ts @@ -1,63 +1,143 @@ import { makeRequest } from "@frontend/kitui"; -import { CreatePromocodeBody, GetPromocodeListBody, Promocode, PromocodeList } from "@root/model/promocodes"; + +import type { + CreatePromocodeBody, + GetPromocodeListBody, + Promocode, + PromocodeList, + PromocodeStatistics, +} from "@root/model/promocodes"; import { parseAxiosError } from "@root/utils/parse-error"; +import { isAxiosError } from "axios"; const baseUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode"; const getPromocodeList = async (body: GetPromocodeListBody) => { - try { - const promocodeListResponse = await makeRequest< - GetPromocodeListBody, - PromocodeList - >({ - url: baseUrl + "/getList", - method: "POST", - body, - useToken: false, - }); + try { + const promocodeListResponse = await makeRequest< + GetPromocodeListBody, + PromocodeList + >({ + url: baseUrl + "/getList", + method: "POST", + body, + useToken: false, + }); - return promocodeListResponse; - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - throw new Error(`Ошибка при получении списка промокодов. ${error}`); + return promocodeListResponse; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка при получении списка промокодов. ${error}`); + } +}; +const createFastlink = async (id: string) => { + try { + return await makeRequest<{ id: string }, { fastlink: string }>({ + url: baseUrl + "/fastlink", + method: "POST", + body: { id }, + }); + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка при создании фастлинка. ${error}`); + } +}; + +export const getAllPromocodes = async () => { + try { + const promocodes: Promocode[] = []; + + let page = 0; + while (true) { + const promocodeList = await getPromocodeList({ + limit: 2, + filter: { + active: true, + }, + page, + }); + + if (promocodeList.items.length === 0) break; + + promocodes.push(...promocodeList.items); + page++; } + + return promocodes; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка при получении списка промокодов. ${error}`); + } }; const createPromocode = async (body: CreatePromocodeBody) => { - try { - const createPromocodeResponse = await makeRequest< - CreatePromocodeBody, - Promocode - >({ - url: baseUrl + "/create", - method: "POST", - body, - useToken: false, - }); + try { + const createPromocodeResponse = await makeRequest< + CreatePromocodeBody, + Promocode + >({ + url: baseUrl + "/create", + method: "POST", + body, + useToken: false, + }); - return createPromocodeResponse; - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - throw new Error(`Ошибка создания промокода. ${error}`); + return createPromocodeResponse; + } catch (nativeError) { + if ( + isAxiosError(nativeError) && + nativeError.response?.data.error === "Duplicate Codeword" + ) { + throw new Error(`Промокод уже существует`); } + + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка создания промокода. ${error}`); + } }; const deletePromocode = async (id: string): Promise => { - try { - await makeRequest({ - url: `${baseUrl}/${id}`, - method: "DELETE", - useToken: false, - }); - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - throw new Error(`Ошибка удаления промокода. ${error}`); - } + try { + await makeRequest({ + url: `${baseUrl}/${id}`, + method: "DELETE", + useToken: false, + }); + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка удаления промокода. ${error}`); + } +}; + +const getPromocodeStatistics = async (id: string, from: number, to: number) => { + try { + const promocodeStatisticsResponse = await makeRequest< + unknown, + PromocodeStatistics + >({ + url: baseUrl + `/stats`, + body: { + id: id, + from: from, + to: to, + }, + method: "POST", + useToken: false, + }); + console.log(promocodeStatisticsResponse); + return promocodeStatisticsResponse; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка при получении статистики промокода. ${error}`); + } }; export const promocodeApi = { - getPromocodeList, - createPromocode, - deletePromocode, + getPromocodeList, + createPromocode, + deletePromocode, + getAllPromocodes, + getPromocodeStatistics, + createFastlink, }; diff --git a/src/api/promocode/swr.ts b/src/api/promocode/swr.ts index 064d41a..ce1126e 100644 --- a/src/api/promocode/swr.ts +++ b/src/api/promocode/swr.ts @@ -1,81 +1,146 @@ -import { CreatePromocodeBody, PromocodeList } from "@root/model/promocodes"; -import { enqueueSnackbar } from "notistack"; import { useCallback, useRef } from "react"; import useSwr, { mutate } from "swr"; +import { enqueueSnackbar } from "notistack"; import { promocodeApi } from "./requests"; +import type { + CreatePromocodeBody, + PromocodeList, +} from "@root/model/promocodes"; -export function usePromocodes(page: number, pageSize: number) { - const promocodesCountRef = useRef(0); - const swrResponse = useSwr( - ["promocodes", page, pageSize], - async (key) => { - const result = await promocodeApi.getPromocodeList({ - limit: key[2], - filter: { - active: true, - }, - page: key[1], - }); - - promocodesCountRef.current = result.count; - return result; +export function usePromocodes( + page: number, + pageSize: number, + promocodeId: string, + to: number, + from: number +) { + const promocodesCountRef = useRef(0); + const swrResponse = useSwr( + ["promocodes", page, pageSize], + async (key) => { + const result = await promocodeApi.getPromocodeList({ + limit: key[2], + filter: { + active: true, }, - { - onError(err) { - console.log("Error fetching promocodes", err); - enqueueSnackbar(err.message, { variant: "error" }); + page: key[1], + }); + + promocodesCountRef.current = result.count; + return result; + }, + { + onError(err) { + console.log("Error fetching promocodes", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + focusThrottleInterval: 60e3, + keepPreviousData: true, + } + ); + + const createPromocode = useCallback( + async function (body: CreatePromocodeBody) { + try { + await promocodeApi.createPromocode(body); + mutate(["promocodes", page, pageSize]); + } catch (error) { + console.log("Error creating promocode", error); + if (error instanceof Error) + enqueueSnackbar(error.message, { variant: "error" }); + } + }, + [page, pageSize] + ); + + const deletePromocode = useCallback( + async function (id: string) { + try { + await mutate( + ["promocodes", page, pageSize], + promocodeApi.deletePromocode(id), + { + optimisticData(currentData, displayedData) { + if (!displayedData) return; + + return { + count: displayedData.count - 1, + items: displayedData.items.filter((item) => item.id !== id), + }; }, - focusThrottleInterval: 60e3, - keepPreviousData: true, - } - ); + rollbackOnError: true, + populateCache(result, currentData) { + if (!currentData) return; - const createPromocode = useCallback(async function (body: CreatePromocodeBody) { - try { - await promocodeApi.createPromocode(body); - mutate(["promocodes", page, pageSize]); - } catch (error) { - console.log("Error creating promocode", error); - if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" }); - } - }, [page, pageSize]); + return { + count: currentData.count - 1, + items: currentData.items.filter((item) => item.id !== id), + }; + }, + } + ); + } catch (error) { + console.log("Error deleting promocode", error); + if (error instanceof Error) + enqueueSnackbar(error.message, { variant: "error" }); + } + }, + [page, pageSize] + ); - const deletePromocode = useCallback(async function (id: string) { - try { - await mutate( - ["promocodes", page, pageSize], - promocodeApi.deletePromocode(id), - { - optimisticData(currentData, displayedData) { - if (!displayedData) return; + const promocodeStatistics = useSwr( + ["promocodeStatistics", promocodeId, from, to], + async ([_, id, from, to]) => { + if (!id) { + return null; + } - return { - count: displayedData.count - 1, - items: displayedData.items.filter((item) => item.id !== id), - }; - }, - rollbackOnError: true, - populateCache(result, currentData) { - if (!currentData) return; + const promocodeStatisticsResponse = + await promocodeApi.getPromocodeStatistics(id, from, to); - return { - count: currentData.count - 1, - items: currentData.items.filter((item) => item.id !== id), - }; - }, - } - ); - } catch (error) { - console.log("Error deleting promocode", error); - if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" }); - } - }, [page, pageSize]); + return promocodeStatisticsResponse; + }, + { + onError(err) { + console.log("Error fetching promocode statistics", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + focusThrottleInterval: 60e3, + keepPreviousData: true, + } + ); - return { - ...swrResponse, - createPromocode, - deletePromocode, - promocodesCount: promocodesCountRef.current, - }; -} + const createFastLink = useCallback(async function (id: string) { + try { + await promocodeApi.createFastlink(id); + mutate(["promocodes", page, pageSize]); + } catch (error) { + console.log("Error creating fast link", error); + if (error instanceof Error) + enqueueSnackbar(error.message, { variant: "error" }); + } + }, []); + + return { + ...swrResponse, + createPromocode, + deletePromocode, + createFastLink, + promocodeStatistics: promocodeStatistics.data, + promocodesCount: promocodesCountRef.current, + }; +} + +export function useAllPromocodes() { + const swrResponse = useSwr("allPromocodes", promocodeApi.getAllPromocodes, { + keepPreviousData: true, + suspense: true, + onError(err) { + console.log("Error fetching all promocodes", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + }); + + return swrResponse.data; +} diff --git a/src/api/quizStatistic.ts b/src/api/quizStatistic.ts new file mode 100644 index 0000000..2cc5f48 --- /dev/null +++ b/src/api/quizStatistic.ts @@ -0,0 +1,28 @@ +import { makeRequest } from "@frontend/kitui"; + +export type QuizStatisticResponse = { + Registrations: number; + Quizes: number; + Results: number +}; + +type TRequest = { + to: number; + from: number; +}; + +export const getStatistic = async ( + to: number, + from: number, +): Promise => { + try { + const generalResponse = await makeRequest({ + url: `${process.env.REACT_APP_DOMAIN}/squiz/statistic`, + body: { to, from } + }) + return generalResponse; + } catch (nativeError) { + + return { Registrations: 0, Quizes: 0, Results: 0 }; + } +}; \ No newline at end of file diff --git a/src/api/tariffs.ts b/src/api/tariffs.ts index 475b783..9ef9a3f 100644 --- a/src/api/tariffs.ts +++ b/src/api/tariffs.ts @@ -2,7 +2,7 @@ import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; -import type { PrivilegeWithAmount } from "@frontend/kitui"; +import type { Privilege } from "@frontend/kitui"; import type { Tariff } from "@frontend/kitui"; import type { EditTariffRequestBody } from "@root/model/tariff"; @@ -12,7 +12,7 @@ type CreateTariffBackendRequest = { order: number; price: number; isCustom: boolean; - privileges: Omit[]; + privileges: Omit[]; }; type GetTariffsResponse = { @@ -52,7 +52,7 @@ export const putTariff = async (tariff: Tariff): Promise<[null, string?]> => { price: tariff.price ?? 0, isCustom: false, order: tariff.order || 1, - description: tariff.description, + description: tariff.description ?? "", privileges: tariff.privileges, }, }); diff --git a/src/index.tsx b/src/index.tsx index 0223de9..31299ff 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,9 +24,11 @@ import { PromocodeManagement } from "@root/pages/dashboard/Content/PromocodeMana import { SettingRoles } from "@pages/Setting/SettingRoles"; import Support from "@pages/dashboard/Content/Support/Support"; import ChatImageNewWindow from "@pages/dashboard/Content/Support/ChatImageNewWindow"; +import QuizStatistic from "@pages/dashboard/Content/QuizStatistic"; import theme from "./theme"; import "./index.css"; +import { makeRequest } from "@frontend/kitui"; const componentsArray = [ ["/users", ], @@ -104,6 +106,14 @@ root.render( } /> + + + + } + /> {componentsArray.map((element) => ( state.discounts); + const discounts = useDiscounts(); + const promocodes = useAllPromocodes(); const cartData = useCartStore((store) => store.cartData); const tariffs = useTariffStore(state => state.tariffs); const [couponField, setCouponField] = useState(""); @@ -39,10 +41,6 @@ export default function Cart() { const [isNonCommercial, setIsNonCommercial] = useState(false); const selectedTariffIds = useTariffStore(state => state.selectedTariffIds); - const cartDiscounts = [cartData?.appliedCartPurchasesDiscount, cartData?.appliedLoyaltyDiscount].filter((d): d is Discount => !!d); - - const cartDiscountsResultFactor = findDiscountFactor(cartData?.appliedCartPurchasesDiscount) * findDiscountFactor(cartData?.appliedLoyaltyDiscount); - async function handleCalcCartClick() { await requestPrivileges(); await requestDiscounts(); @@ -53,8 +51,21 @@ export default function Cart() { if (!isFinite(loyaltyValue)) loyaltyValue = 0; + const promocode = promocodes.find(promocode => { + if (promocode.dueTo < (Date.now() / 1000)) return false; + + return promocode.codeword === couponField.trim(); + }); + + const userId = crypto.randomUUID(); + + const discountsWithPromocodeDiscount = promocode ? [ + ...discounts, + createDiscountFromPromocode(promocode, userId), + ] : discounts; + try { - const cartData = calcCart(cartTariffs, discounts, loyaltyValue, couponField); + const cartData = calcCart(cartTariffs, discountsWithPromocodeDiscount, loyaltyValue, userId); setErrorMessage(null); setCartData(cartData); @@ -224,45 +235,22 @@ export default function Cart() { - {cartData.services.flatMap(service => service.tariffs.map(taroffCartData => ( - - )))} + {cartData.services.flatMap(service => service.tariffs.map(tariffCartData => { + const appliedDiscounts = tariffCartData.privileges.flatMap( + privilege => Array.from(privilege.appliedDiscounts) + ).sort((a, b) => a.Layer - b.Layer); + + return ( + + ); + }))} - - Скидки корзины: - {cartDiscounts && ( - - {cartDiscounts?.map((discount, index, arr) => ( - - - {index < arr.length - 1 && } - - ))} -   - {cartDiscountsResultFactor && `= ${formatDiscountFactor(cartDiscountsResultFactor)}`} - - )} - - !!d); - useEffect(() => { if (tariffCartData.privileges.length > 1) { console.warn(`Количество привилегий в тарифе ${tariffCartData.name}(${tariffCartData.id}) больше одного`); @@ -39,22 +37,17 @@ export default function CartItemRow({ tariffCartData, appliedServiceDiscount }: {tariffCartData.privileges[0].description} - {envolvedDiscounts.map((discount, index, arr) => ( + {appliedDiscounts.map((discount, index, arr) => ( - {index < arr.length - (appliedServiceDiscount ? 0 : 1) && ( + {index < arr.length - 1 && ( )} ))} - {appliedServiceDiscount && ( - - - - )} - {currencyFormatter.format(tariffCartData.price)} + {currencyFormatter.format(tariffCartData.price / 100)} ); diff --git a/src/kitUI/Cart/DiscountTooltip.tsx b/src/kitUI/Cart/DiscountTooltip.tsx index 5b56dc9..2badb49 100644 --- a/src/kitUI/Cart/DiscountTooltip.tsx +++ b/src/kitUI/Cart/DiscountTooltip.tsx @@ -1,27 +1,28 @@ -import { Tooltip, Typography } from "@mui/material"; -import { Discount, findDiscountFactor } from "@frontend/kitui"; -import { formatDiscountFactor } from "@root/utils/calcCart/calcCart"; - - -interface Props { - discount: Discount; -} - -export function DiscountTooltip({ discount }: Props) { - const discountText = formatDiscountFactor(findDiscountFactor(discount)); - - return discountText ? ( - - Скидка: {discount?.Name} - {discount?.Description} - - } - > - {discountText} - - ) : ( - Ошибка поиска значения скидки - ); -} +import { Tooltip, Typography } from "@mui/material"; +import { Discount, findDiscountFactor } from "@frontend/kitui"; +import { formatDiscountFactor } from "@root/utils/formatDiscountFactor"; + + +interface Props { + discount: Discount; +} + +export function DiscountTooltip({ discount }: Props) { + const discountText = formatDiscountFactor(findDiscountFactor(discount)); + + return discountText ? ( + + Слой: {discount.Layer} + Название: {discount.Name} + Описание: {discount.Description} + + } + > + {discountText} + + ) : ( + Ошибка поиска значения скидки + ); +} diff --git a/src/model/cart.ts b/src/model/cart.ts deleted file mode 100644 index e5263b3..0000000 --- a/src/model/cart.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Promocode { - id: string; - name: string; - endless: boolean; - from: string; - dueTo: string; - privileges: string[]; -} diff --git a/src/model/promocodes.ts b/src/model/promocodes.ts index 1a6ff14..599508c 100644 --- a/src/model/promocodes.ts +++ b/src/model/promocodes.ts @@ -1,41 +1,48 @@ export type CreatePromocodeBody = { - codeword: string; - description: string; - greetings: string; - dueTo: number; - activationCount: number; - bonus: { - privilege: { - privilegeID: string; - amount: number; - }; - discount: { - layer: number; - factor: number; - target: string; - threshold: number; - }; + codeword: string; + description: string; + greetings: string; + dueTo: number; + activationCount: number; + bonus: { + privilege: { + privilegeID: string; + amount: number; }; + discount: { + layer: number; + factor: number; + target: string; + threshold: number; + }; + }; }; export type GetPromocodeListBody = { - page: number; - limit: number; - filter: { - active: boolean; - text?: string; - }; + page: number; + limit: number; + filter: { + active: boolean; + text?: string; + }; }; export type Promocode = CreatePromocodeBody & { - id: string; - outdated: boolean; - offLimit: boolean; - delete: boolean; - createdAt: string; + id: string; + outdated: boolean; + offLimit: boolean; + delete: boolean; + createdAt: string; + fastLinks: string[]; }; export type PromocodeList = { - count: number; - items: Promocode[]; + count: number; + items: Promocode[]; +}; + +export type PromocodeStatistics = { + id: string; + usageCount: number; + usageMap: Record; }; diff --git a/src/model/tariff.ts b/src/model/tariff.ts index d03fd32..68535dc 100644 --- a/src/model/tariff.ts +++ b/src/model/tariff.ts @@ -1,4 +1,4 @@ -import { PrivilegeWithAmount } from "@frontend/kitui"; +import { Privilege } from "@frontend/kitui"; export const SERVICE_LIST = [ { @@ -25,5 +25,5 @@ export type EditTariffRequestBody = { order: number; price: number; isCustom: boolean; - privileges: Omit[]; + privileges: Omit[]; }; diff --git a/src/pages/Setting/CardPrivilegie.tsx b/src/pages/Setting/CardPrivilegie.tsx index 9fe3aad..f4de843 100644 --- a/src/pages/Setting/CardPrivilegie.tsx +++ b/src/pages/Setting/CardPrivilegie.tsx @@ -2,14 +2,14 @@ import { KeyboardEvent, useRef, useState } from "react"; import { enqueueSnackbar } from "notistack"; import {Box, IconButton, TextField, Tooltip, Typography, useMediaQuery, useTheme} from "@mui/material"; import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined"; -import { PrivilegeWithAmount } from "@frontend/kitui"; +import { CustomPrivilege } from "@frontend/kitui"; import { putPrivilege } from "@root/api/privilegies"; import SaveIcon from '@mui/icons-material/Save'; import { currencyFormatter } from "@root/utils/currencyFormatter"; interface CardPrivilege { - privilege: PrivilegeWithAmount; + privilege: CustomPrivilege; } export const СardPrivilege = ({ privilege }: CardPrivilege) => { @@ -27,7 +27,7 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => { const putPrivileges = async () => { - const [_, putedPrivilegeError] = await putPrivilege({ + const [, putedPrivilegeError] = await putPrivilege({ name: privilege.name, privilegeId: privilege.privilegeId, serviceKey: privilege.serviceKey, diff --git a/src/pages/dashboard/Content/DiscountManagement/ControlPanel.tsx b/src/pages/dashboard/Content/DiscountManagement/ControlPanel.tsx index 9a40332..65c7c63 100644 --- a/src/pages/dashboard/Content/DiscountManagement/ControlPanel.tsx +++ b/src/pages/dashboard/Content/DiscountManagement/ControlPanel.tsx @@ -6,6 +6,7 @@ import { changeDiscount } from "@root/api/discounts"; import { findDiscountsById } from "@root/stores/discounts"; import { requestDiscounts } from "@root/services/discounts.service"; +import { mutate } from "swr"; interface Props { selectedRows: GridSelectionModel; @@ -24,7 +25,7 @@ export default function DiscountDataGrid({ selectedRows }: Props) { return enqueueSnackbar("Скидка не найдена"); } - const [_, changedDiscountError] = await changeDiscount(String(id), { + const [, changedDiscountError] = await changeDiscount(String(id), { ...discount, Deprecated: isActive, }); @@ -34,6 +35,7 @@ export default function DiscountDataGrid({ selectedRows }: Props) { } else { fatal += 1; } + mutate("discounts"); } await requestDiscounts(); diff --git a/src/pages/dashboard/Content/DiscountManagement/CreateDiscount.tsx b/src/pages/dashboard/Content/DiscountManagement/CreateDiscount.tsx index 525e7e4..5eabd34 100644 --- a/src/pages/dashboard/Content/DiscountManagement/CreateDiscount.tsx +++ b/src/pages/dashboard/Content/DiscountManagement/CreateDiscount.tsx @@ -23,6 +23,7 @@ import { DiscountType, discountTypes } from "@root/model/discount"; import { createDiscount } from "@root/api/discounts"; import usePrivileges from "@root/utils/hooks/usePrivileges"; import { Formik, Field, Form, FormikHelpers } from "formik"; +import { mutate } from "swr"; interface Values { discountNameField: string, @@ -92,6 +93,7 @@ export default function CreateDiscount() { } if (createdDiscountResponse) { + mutate("discounts"); addDiscount(createdDiscountResponse); } } diff --git a/src/pages/dashboard/Content/DiscountManagement/DiscountDataGrid.tsx b/src/pages/dashboard/Content/DiscountManagement/DiscountDataGrid.tsx index b4e1ef4..5dcb364 100644 --- a/src/pages/dashboard/Content/DiscountManagement/DiscountDataGrid.tsx +++ b/src/pages/dashboard/Content/DiscountManagement/DiscountDataGrid.tsx @@ -18,7 +18,8 @@ import { deleteDiscount } from "@root/api/discounts"; import { GridSelectionModel } from "@mui/x-data-grid"; import { requestDiscounts } from "@root/services/discounts.service"; import AutorenewIcon from "@mui/icons-material/Autorenew"; -import { formatDiscountFactor } from "@root/utils/calcCart/calcCart"; +import { formatDiscountFactor } from "@root/utils/formatDiscountFactor"; +import { mutate } from "swr"; const columns: GridColDef[] = [ // { @@ -93,6 +94,7 @@ const columns: GridColDef[] = [ disabled={row.deleted} onClick={() => { deleteDiscount(row.id).then(([discount]) => { + mutate("discounts"); if (discount) { updateDiscount(discount); } diff --git a/src/pages/dashboard/Content/DiscountManagement/EditDiscountDialog.tsx b/src/pages/dashboard/Content/DiscountManagement/EditDiscountDialog.tsx index 47d1293..06fab07 100644 --- a/src/pages/dashboard/Content/DiscountManagement/EditDiscountDialog.tsx +++ b/src/pages/dashboard/Content/DiscountManagement/EditDiscountDialog.tsx @@ -31,6 +31,7 @@ import { getDiscountTypeFromLayer } from "@root/utils/discount"; import usePrivileges from "@root/utils/hooks/usePrivileges"; import { enqueueSnackbar } from "notistack"; import { useEffect, useState } from "react"; +import { mutate } from "swr"; export default function EditDiscountDialog() { const theme = useTheme(); @@ -59,17 +60,17 @@ export default function EditDiscountDialog() { function setDiscountFields() { if (!discount) return; - setServiceType(discount.Condition.Group); + setServiceType(discount.Condition.Group ?? ""); setDiscountType(getDiscountTypeFromLayer(discount.Layer)); setDiscountNameField(discount.Name); setDiscountDescriptionField(discount.Description); - setPrivilegeIdField(discount.Condition.Product); + setPrivilegeIdField(discount.Condition.Product ?? ""); setDiscountFactorField(((1 - discount.Target.Factor) * 100).toFixed(2)); - setPurchasesAmountField(discount.Condition.PurchasesAmount.toString()); + setPurchasesAmountField(discount.Condition.PurchasesAmount ?? ""); setCartPurchasesAmountField( - discount.Condition.CartPurchasesAmount.toString() + discount.Condition.CartPurchasesAmount ?? "" ); - setDiscountMinValueField(discount.Condition.PriceFrom.toString()); + setDiscountMinValueField(discount.Condition.PriceFrom ?? ""); }, [discount] ); @@ -137,6 +138,7 @@ export default function EditDiscountDialog() { } if (patchedDiscountResponse) { + mutate("discounts"); updateDiscount(patchedDiscountResponse); closeEditDiscountDialog(); } diff --git a/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx index 9ce1870..8b99a10 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/CreatePromocodeForm.tsx @@ -10,7 +10,6 @@ import { } from "@mui/material"; import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker"; import { Field, Form, Formik } from "formik"; -import moment from "moment"; import { useEffect, useState } from "react"; import { requestPrivileges } from "@root/services/privilegies.service"; @@ -135,10 +134,8 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => { as={DesktopDatePicker} inputFormat="DD/MM/YYYY" value={values.dueTo ? new Date(Number(values.dueTo) * 1000) : null} - onChange={(date: Date | null) => { - if (date) { - setFieldValue("dueTo", moment(date).unix() || null); - } + onChange={(event: any) => { + setFieldValue("dueTo", event.$d.getTime() / 1000 || null); }} renderInput={(params: TextFieldProps) => } InputProps={{ diff --git a/src/pages/dashboard/Content/PromocodeManagement/DeleteModal.tsx b/src/pages/dashboard/Content/PromocodeManagement/DeleteModal.tsx new file mode 100644 index 0000000..5302a00 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/DeleteModal.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Modal from '@mui/material/Modal'; +import Button from '@mui/material/Button'; +import { Typography } from '@mui/material'; + +const style = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: '#c1c1c1', + border: '2px solid #000', + boxShadow: 24, + pt: 2, + px: 4, + pb: 3, +}; + +interface Props { + id: string; + setModal: (id: string) => void; + deletePromocode: (id: string) => Promise; +} + +export default function ({ + id, + setModal, + deletePromocode +}: Props) { + return ( + setModal("")} + > + + Точно удалить промокод? + + + + + + + ); +} diff --git a/src/pages/dashboard/Content/PromocodeManagement/StatisticsModal.tsx b/src/pages/dashboard/Content/PromocodeManagement/StatisticsModal.tsx new file mode 100644 index 0000000..301e3f9 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/StatisticsModal.tsx @@ -0,0 +1,285 @@ +import { useEffect, useState } from "react"; +import { + Box, + Button, + Typography, + Modal, + TextField, + useTheme, + useMediaQuery, + IconButton, +} from "@mui/material"; +import { DataGrid, GridLoadingOverlay, GridToolbar } from "@mui/x-data-grid"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; + +import { fadeIn } from "@root/utils/style/keyframes"; + +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; + +import type { GridColDef } from "@mui/x-data-grid"; +import type { Promocode, PromocodeStatistics } from "@root/model/promocodes"; + +type StatisticsModalProps = { + id: string; + to: number; + from: number; + setId: (id: string) => void; + setTo: (date: number) => void; + setFrom: (date: number) => void; + promocodes: Promocode[]; + promocodeStatistics: PromocodeStatistics | null | undefined; + createFastLink: (id: string) => Promise; +}; + +type Row = { + id: number; + link: string; + useCount: number; +}; + +const COLUMNS: GridColDef[] = [ + { + field: "copy", + headerName: "копировать", + width: 50, + sortable: false, + valueGetter: ({ row }) => String(row.useCount), + renderCell: (params) => { + return ( + navigator.clipboard.writeText(params.row.link)} + > + + + ); + }, + }, + { + field: "link", + headerName: "Ссылка", + width: 320, + sortable: false, + valueGetter: ({ row }) => row.link, + renderCell: ({ value }) => + value?.split("|").map((link) => {link}), + }, + { + field: "useCount", + headerName: "Использований", + width: 120, + sortable: false, + valueGetter: ({ row }) => String(row.useCount), + }, + { + field: "purchasesCount", + headerName: "Покупок", + width: 70, + sortable: false, + valueGetter: ({ row }) => String(0), + }, +]; + +export const StatisticsModal = ({ + id, + setId, + setFrom, + from, + to, + setTo, + promocodeStatistics, + promocodes, + createFastLink, +}: StatisticsModalProps) => { + const [startDate, setStartDate] = useState(new Date()); + const [endDate, setEndDate] = useState(new Date()); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(550)); + const [rows, setRows] = useState([]); + const createFastlink = async () => { + await createFastLink(id); + + getParseData(); + }; + + const getParseData = async () => { + const rows = promocodes + .find((promocode) => promocode.id === id) + ?.fastLinks?.map((link, index) => ({ + link, + id: index, + useCount: promocodeStatistics?.usageMap[link] ?? 0, + })) as Row[]; + + setRows(rows); + }; + + useEffect(() => { + if (id.length > 0) { + getParseData(); + } + + if (!id) { + setRows([]); + } + }, [id]); + + // const formatTo = to === null ? 0 : moment(to).unix() + // const formatFrom = from === null ? 0 : moment(from).unix() + // useEffect(() => { + // (async () => { + // const gottenGeneral = await promocodeStatistics(id, startDate, endDate) + // setGeneral(gottenGeneral[0]) + // })() + // }, [to, from]); + return ( + { + setId(""); + setStartDate(new Date()); + setEndDate(new Date()); + }} + sx={{ "& > .MuiBox-root": { outline: "none" } }} + > + + + + + + + + + + от + + date && setStartDate(date)} + renderInput={(params) => ( + + )} + InputProps={{ + sx: { + height: "40px", + color: theme.palette.secondary.main, + border: "1px solid", + borderColor: theme.palette.secondary.main, + "& .MuiSvgIcon-root": { + color: theme.palette.secondary.main, + }, + }, + }} + /> + + + + до + + date && setEndDate(date)} + renderInput={(params) => ( + + )} + InputProps={{ + sx: { + height: "40px", + color: theme.palette.secondary.main, + border: "1px solid", + borderColor: theme.palette.secondary.main, + "& .MuiSvgIcon-root": { + color: theme.palette.secondary.main, + }, + }, + }} + /> + + + + + + + ); +}; diff --git a/src/pages/dashboard/Content/PromocodeManagement/index.tsx b/src/pages/dashboard/Content/PromocodeManagement/index.tsx index bdd7be5..c2335e9 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/index.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/index.tsx @@ -1,78 +1,115 @@ +import { useState } from "react"; import { Box, Typography, useTheme } from "@mui/material"; import { DataGrid, GridLoadingOverlay, GridToolbar } from "@mui/x-data-grid"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { usePromocodes } from "@root/api/promocode/swr"; import { fadeIn } from "@root/utils/style/keyframes"; -import { useState } from "react"; import { CreatePromocodeForm } from "./CreatePromocodeForm"; import { usePromocodeGridColDef } from "./usePromocodeGridColDef"; - +import { StatisticsModal } from "./StatisticsModal"; +import DeleteModal from "./DeleteModal"; export const PromocodeManagement = () => { - const theme = useTheme(); - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(10); - const { data, error, isValidating, promocodesCount, deletePromocode, createPromocode } = usePromocodes(page, pageSize); - const columns = usePromocodeGridColDef(deletePromocode); + const theme = useTheme(); - if (error) return Ошибка загрузки промокодов; + const [deleteModal, setDeleteModal] = useState(""); + const deleteModalHC = (id: string) => setDeleteModal(id); - return ( - - - Создание промокода - - - - - - - ); + const [showStatisticsModalId, setShowStatisticsModalId] = + useState(""); + const [page, setPage] = useState(0); + const [to, setTo] = useState(0); + const [from, setFrom] = useState(0); + const [pageSize, setPageSize] = useState(10); + const { + data, + error, + isValidating, + promocodesCount, + promocodeStatistics, + deletePromocode, + createPromocode, + createFastLink, + } = usePromocodes(page, pageSize, showStatisticsModalId, to, from); + const columns = usePromocodeGridColDef( + setShowStatisticsModalId, + deleteModalHC + ); + console.log(showStatisticsModalId); + if (error) return Ошибка загрузки промокодов; + + return ( + + + Создание промокода + + + + + + + + + ); }; diff --git a/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx b/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx index a897d00..2bd6da9 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx @@ -1,59 +1,85 @@ -import DeleteIcon from '@mui/icons-material/Delete'; import { IconButton } from "@mui/material"; import { GridColDef } from "@mui/x-data-grid"; import { Promocode } from "@root/model/promocodes"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; -export function usePromocodeGridColDef(deletePromocode: (id: string) => void) { - return useMemo[]>(() => [ - { - field: "id", - headerName: "ID", - width: 30, - sortable: false, - valueGetter: ({ row }) => row.id, +import { BarChart, Delete } from "@mui/icons-material"; +import {promocodeApi} from "@root/api/promocode/requests"; + +export function usePromocodeGridColDef( + setStatistics: (id: string) => void, + deletePromocode: (id: string) => void +) { + const validity = (value:string|number) => {if(value===0){return "неоганичен"}else {return new Date(value).toLocaleString()}} + return useMemo[]>( + () => [ + { + field: "id", + headerName: "ID", + width: 30, + sortable: false, + valueGetter: ({ row }) => row.id, + }, + { + field: "codeword", + headerName: "Кодовое слово", + width: 160, + sortable: false, + valueGetter: ({ row }) => row.codeword, + }, + { + field: "factor", + headerName: "Коэф. скидки", + width: 120, + sortable: false, + valueGetter: ({ row }) => + Math.round(row.bonus.discount.factor * 1000) / 1000, + }, + { + field: "activationCount", + headerName: "Кол-во активаций", + width: 140, + sortable: false, + valueGetter: ({ row }) => row.activationCount, + }, + { + field: "dueTo", + headerName: "Истекает", + width: 160, + sortable: false, + valueGetter: ({ row }) => row.dueTo * 1000, + valueFormatter: ({ value }) => `${validity(value)}`, + }, + { + field: "settings", + headerName: "", + width: 60, + sortable: false, + renderCell: (params) => { + return ( + { + setStatistics(params.row.id,) + promocodeApi.getPromocodeStatistics(params.row.id, 0, 0) + }}> + + + ); }, - { - field: "codeword", - headerName: "Кодовое слово", - width: 160, - sortable: false, - valueGetter: ({ row }) => row.codeword, + }, + { + field: "delete", + headerName: "", + width: 60, + sortable: false, + renderCell: (params) => { + return ( + deletePromocode(params.row.id)}> + + + ); }, - { - field: "factor", - headerName: "Коэф. скидки", - width: 120, - sortable: false, - valueGetter: ({ row }) => row.bonus.discount.factor, - }, - { - field: "activationCount", - headerName: "Кол-во активаций", - width: 140, - sortable: false, - valueGetter: ({ row }) => row.activationCount, - }, - { - field: "dueTo", - headerName: "Истекает", - width: 160, - sortable: false, - valueGetter: ({ row }) => row.dueTo * 1000, - valueFormatter: ({ value }) => new Date(value).toLocaleString(), - }, - { - field: "delete", - headerName: "", - width: 60, - sortable: false, - renderCell: (params) => { - return ( - deletePromocode(params.row.id)}> - - - ); - }, - }, - ], [deletePromocode]); + }, + ], + [deletePromocode, setStatistics] + ); } diff --git a/src/pages/dashboard/Content/QuizStatistic/index.tsx b/src/pages/dashboard/Content/QuizStatistic/index.tsx new file mode 100644 index 0000000..4c2b5e2 --- /dev/null +++ b/src/pages/dashboard/Content/QuizStatistic/index.tsx @@ -0,0 +1,169 @@ +import { Table, TableBody, TableCell, TableHead, TableRow, useTheme, Typography, Box, TextField, Button } from '@mui/material'; +import { useState } from 'react'; +import moment from "moment"; +import type { Moment } from "moment"; +import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { useQuizStatistic } from '@root/utils/hooks/useQuizStatistic'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment' + +export default () => { + const theme = useTheme() + + const [isOpen, setOpen] = useState(false); + const [isOpenEnd, setOpenEnd] = useState(false); + + const [from, setFrom] = useState(null); + const [to, setTo] = useState(moment(Date.now())); + + + const { Registrations, Quizes, Results } = useQuizStatistic({ + from, + to, + }); + + const resetTime = () => { + setFrom(moment(0)); + setTo(moment(Date.now())); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleOpen = () => { + setOpen(true); + }; + + const onAdornmentClick = () => { + setOpen((old) => !old); + if (isOpenEnd) { + handleCloseEnd(); + } + }; + + const handleCloseEnd = () => { + setOpenEnd(false); + }; + + const handleOpenEnd = () => { + setOpenEnd(true); + }; + + const onAdornmentClickEnd = () => { + setOpenEnd((old) => !old); + if (isOpen) { + handleClose(); + } + }; + + + return <> + + + + Дата начала + + date && setFrom(date)} + renderInput={(params) => ( + + )} + InputProps={{ + sx: { + height: "40px", + color: theme.palette.secondary.main, + border: "1px solid", + borderColor: theme.palette.secondary.main, + "& .MuiSvgIcon-root": { + color: theme.palette.secondary.main, + }, + }, + }} + /> + + + + Дата окончания + + date && setTo(date)} + renderInput={(params) => ( + + )} + InputProps={{ + sx: { + height: "40px", + color: theme.palette.secondary.main, + border: "1px solid", + borderColor: theme.palette.secondary.main, + "& .MuiSvgIcon-root": { + color: theme.palette.secondary.main, + }, + }, + }} + /> + + + + + + + Регистраций + Quiz + Результаты + + + + {Registrations} + {Quizes} + {Results} + +
+
+ +} \ No newline at end of file diff --git a/src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx b/src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx index f180a67..c0ba536 100644 --- a/src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx +++ b/src/pages/dashboard/Content/Support/Chat/ChatDocument.tsx @@ -24,6 +24,7 @@ export default function ChatDocument({ display: "flex", gap: "9px", padding: isSelf ? "0 8px 0 0" : "0 0 0 8px", + justifyContent: isSelf ? "end" : "start", }} > ); - +console.log(isSelf) return ( ) => { if (values.privilege !== null) { - const [_, createdTariffError] = await createTariff({ + const [, createdTariffError] = await createTariff({ name: values.nameField, price: Number(values.customPriceField) * 100, order: values.orderField, @@ -185,7 +185,7 @@ export default function CreateTariff() { {privileges.map((privilege) => ( diff --git a/src/pages/dashboard/Content/Tariffs/TariffsInfo.tsx b/src/pages/dashboard/Content/Tariffs/TariffsInfo.tsx index 04b8734..cc4be27 100644 --- a/src/pages/dashboard/Content/Tariffs/TariffsInfo.tsx +++ b/src/pages/dashboard/Content/Tariffs/TariffsInfo.tsx @@ -1,17 +1,23 @@ -import { Typography } from "@mui/material"; -import Cart from "@root/kitUI/Cart/Cart"; -import TariffsDG from "./tariffsDG"; - - -export default function TariffsInfo() { - - return ( - <> - - Список тарифов - - - - - ); -} +import { CircularProgress, Typography } from "@mui/material"; +import Cart from "@root/kitUI/Cart/Cart"; +import TariffsDG from "./tariffsDG"; +import { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + + +export default function TariffsInfo() { + + return ( + <> + + Список тарифов + + + Что-то пошло не так}> + }> + + + + + ); +} diff --git a/src/pages/dashboard/Content/Tariffs/tariffsDG.tsx b/src/pages/dashboard/Content/Tariffs/tariffsDG.tsx index a5018e4..ac5fc23 100644 --- a/src/pages/dashboard/Content/Tariffs/tariffsDG.tsx +++ b/src/pages/dashboard/Content/Tariffs/tariffsDG.tsx @@ -34,7 +34,7 @@ const columns: GridColDef[] = [ { field: "type", headerName: "Единица", width: 100, valueGetter: ({ row }) => row.privileges[0].type }, { field: "pricePerUnit", headerName: "Цена за ед.", width: 100, valueGetter: ({ row }) => currencyFormatter.format(row.privileges[0].price / 100) }, { field: "isCustom", headerName: "Кастомная цена", width: 130, valueGetter: ({ row }) => row.isCustom ? "Да" : "Нет" }, - { field: "total", headerName: "Сумма", width: 60, valueGetter: ({ row }) => currencyFormatter.format(getTariffPrice(row) / 100) }, + { field: "total", headerName: "Сумма", width: 100, valueGetter: ({ row }) => currencyFormatter.format(getTariffPrice(row) / 100) }, { field: "delete", headerName: "Удаление", diff --git a/src/pages/dashboard/Content/Users.tsx b/src/pages/dashboard/Content/Users.tsx index dfc4a89..3ea33aa 100644 --- a/src/pages/dashboard/Content/Users.tsx +++ b/src/pages/dashboard/Content/Users.tsx @@ -131,7 +131,7 @@ const Users: React.FC = () => { ); return ( - + */} prop !== "open" })); const links: { path: string; element: JSX.Element; title: string; className: string }[] = [ + { path: "/quizStatistic", element: <>📝, title: "Статистика Quiz", className: "menu" }, { path: "/users", element: , title: "Информация о проекте", className: "menu" }, { path: "/entities", element: , title: "Юридические лица", className: "menu" }, { path: "/tariffs", element: , title: "Тарифы", className: "menu" }, diff --git a/src/pages/dashboard/ModalUser/QuizTab.tsx b/src/pages/dashboard/ModalUser/QuizTab.tsx new file mode 100644 index 0000000..0781d50 --- /dev/null +++ b/src/pages/dashboard/ModalUser/QuizTab.tsx @@ -0,0 +1,48 @@ +import {Box, Button, TextField, Typography} from "@mui/material"; +import {ChangeEvent, useState} from "react"; +import {makeRequest} from "@frontend/kitui"; + +type QuizTabProps = { + userId: string; +}; + +export default function QuizTab({ userId }: QuizTabProps) { + const [quizId, setQuizId] = useState("") + console.log(quizId) + return( + + + Передача Квиза + + + )=>{ + setQuizId(event.target.value.split("link/")[1]) + + }} + /> + + + + + ) +} \ No newline at end of file diff --git a/src/pages/dashboard/ModalUser/index.tsx b/src/pages/dashboard/ModalUser/index.tsx index 63014df..da0686e 100644 --- a/src/pages/dashboard/ModalUser/index.tsx +++ b/src/pages/dashboard/ModalUser/index.tsx @@ -21,10 +21,11 @@ import { ReactComponent as PackageIcon } from "@root/assets/icons/package.svg"; import { ReactComponent as TransactionsIcon } from "@root/assets/icons/transactions.svg"; import { ReactComponent as CheckIcon } from "@root/assets/icons/check.svg"; import { ReactComponent as CloseIcon } from "@root/assets/icons/close.svg"; - +import QuizIcon from '@mui/icons-material/Quiz'; import forwardIcon from "@root/assets/icons/forward.svg"; import type { SyntheticEvent } from "react"; +import QuizTab from "@pages/dashboard/ModalUser/QuizTab"; const TABS = [ { name: "Пользователь", icon: UserIcon, activeStyles: { fill: "#7E2AEA" } }, @@ -39,6 +40,7 @@ const TABS = [ activeStyles: { stroke: "#7E2AEA" }, }, { name: "Верификация", icon: CheckIcon, activeStyles: { stroke: "#7E2AEA" } }, + { name: "Квизы", icon: QuizIcon, activeStyles: { stroke: "#7E2AEA" } }, ]; type ModalUserProps = { @@ -194,6 +196,7 @@ const ModalUser = ({ open, onClose, userId }: ModalUserProps) => { {value === 1 && } {value === 2 && } {value === 3 && } + {value === 4 && } diff --git a/src/services/privilegies.service.ts b/src/services/privilegies.service.ts index d2acf83..89ecd32 100644 --- a/src/services/privilegies.service.ts +++ b/src/services/privilegies.service.ts @@ -1,10 +1,10 @@ import { resetPrivilegeArray } from "@root/stores/privilegesStore"; import { requestServicePrivileges } from "@root/api/privilegies"; -import type { PrivilegeWithAmount } from "@frontend/kitui"; +import type { CustomPrivilege } from "@frontend/kitui"; -const mutatePrivileges = (privileges: PrivilegeWithAmount[]) => { - let extracted: PrivilegeWithAmount[] = []; +const mutatePrivileges = (privileges: CustomPrivilege[]) => { + let extracted: CustomPrivilege[] = []; for (let serviceKey in privileges) { //Приходит объект. В его значениях массивы привилегий для разных сервисов. Высыпаем в общую кучу и обновляем стор extracted = extracted.concat(privileges[serviceKey]); diff --git a/src/stores/privilegesStore.ts b/src/stores/privilegesStore.ts index 0e31994..f0fd652 100644 --- a/src/stores/privilegesStore.ts +++ b/src/stores/privilegesStore.ts @@ -1,26 +1,26 @@ -import { create } from "zustand"; -import { devtools } from "zustand/middleware"; -import { PrivilegeWithAmount } from "@frontend/kitui"; - - -interface PrivilegeStore { - privileges: PrivilegeWithAmount[]; -} - -export const usePrivilegeStore = create()( - devtools( - (set, get) => ({ - privileges: [], - }), - { - name: "Privileges", - enabled: process.env.NODE_ENV === "development", - } - ) -); - -export const resetPrivilegeArray = (privileges: PrivilegeStore["privileges"]) => usePrivilegeStore.setState({ privileges }); - -export const findPrivilegeById = (privilegeId: string) => { - return usePrivilegeStore.getState().privileges.find((privilege) => privilege._id === privilegeId || privilege.privilegeId === privilegeId) ?? null; -}; +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; +import { CustomPrivilege } from "@frontend/kitui"; + + +interface PrivilegeStore { + privileges: CustomPrivilege[]; +} + +export const usePrivilegeStore = create()( + devtools( + (set, get) => ({ + privileges: [], + }), + { + name: "Privileges", + enabled: process.env.NODE_ENV === "development", + } + ) +); + +export const resetPrivilegeArray = (privileges: PrivilegeStore["privileges"]) => usePrivilegeStore.setState({ privileges }); + +export const findPrivilegeById = (privilegeId: string) => { + return usePrivilegeStore.getState().privileges.find((privilege) => privilege._id === privilegeId || privilege.privilegeId === privilegeId) ?? null; +}; diff --git a/src/utils/calcCart/calcCart.test.ts b/src/utils/calcCart/calcCart.test.ts deleted file mode 100644 index 04ac79c..0000000 --- a/src/utils/calcCart/calcCart.test.ts +++ /dev/null @@ -1,1204 +0,0 @@ -/// -import { CartData, Discount, Tariff } from "@frontend/kitui"; -import { calcCart } from "./calcCart"; - - -describe("Cart calculations", () => { - describe("without discounts", () => { - it("calculates cart with 1 item", () => { - const cart = calcCart([templategenTariff1], [], 0); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100, - isCustom: false, - privileges: [ - { - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100, - amount: 100, - appliedPrivilegeDiscount: null, - }, - ], - } - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("calculates cart with 3 items", () => { - const cart = calcCart([templategenTariff1, squizTariff, reducerTariff], [], 0); - - const expectedCart: CartData = { - itemCount: 3, - priceAfterDiscounts: 100 * 100 + 200 * 200 + 300 * 300, - priceBeforeDiscounts: 100 * 100 + 200 * 200 + 300 * 300, - services: [ - { - serviceKey: "templategen", - price: 100 * 100, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100, - isCustom: false, - privileges: [ - { - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100, - amount: 100, - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - { - serviceKey: "squiz", - price: 200 * 200, - tariffs: [ - { - id: "t2", - name: "squizTariff", - price: 200 * 200, - isCustom: false, - privileges: [ - { - description: "d2", - price: 200 * 200, - privilegeId: "p2", - serviceKey: "squiz", - amount: 100, - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - { - serviceKey: "reducer", - price: 300 * 300, - tariffs: [ - { - id: "t3", - name: "reducerTariff", - price: 300 * 300, - isCustom: false, - privileges: [ - { - description: "d3", - amount: 100, - price: 300 * 300, - privilegeId: "p3", - serviceKey: "reducer", - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("calculates cart with items for the same service", () => { - const cart = calcCart([templategenTariff1, templategenTariff2, reducerTariff], [], 0); - - const expectedCart: CartData = { - itemCount: 3, - priceAfterDiscounts: 100 * 100 + 600 * 600 + 300 * 300, - priceBeforeDiscounts: 100 * 100 + 600 * 600 + 300 * 300, - services: [ - { - serviceKey: "templategen", - price: 100 * 100 + 600 * 600, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100, - isCustom: false, - privileges: [ - { - privilegeId: "p1", - amount: 100, - serviceKey: "templategen", - description: "d1", - price: 100 * 100, - appliedPrivilegeDiscount: null, - }, - ], - }, - { - id: "t5", - name: "templategenTariff2", - price: 600 * 600, - isCustom: false, - privileges: [ - { - amount: 100, - description: "d5", - price: 600 * 600, - privilegeId: "p5", - serviceKey: "templategen", - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - { - serviceKey: "reducer", - price: 300 * 300, - tariffs: [ - { - id: "t3", - name: "reducerTariff", - price: 300 * 300, - isCustom: false, - privileges: [ - { - amount: 100, - description: "d3", - price: 300 * 300, - privilegeId: "p3", - serviceKey: "reducer", - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("returns blank cart when no tariffs", () => { - const cart = calcCart([], [], 0); - - const expectedCart: CartData = { - itemCount: 0, - priceAfterDiscounts: 0, - priceBeforeDiscounts: 0, - services: [], - allAppliedDiscounts: [], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("throw when tariffs are incompatible", () => { - expect(() => { - calcCart([customTemplategenTariff, templategenTariff1], [], 0); - }).toThrow(); - }); - }); - - describe("with single discount", () => { - it("applies privilege discount to tariff 1", () => { - const cart = calcCart([templategenTariff1], [templategenP1PrivilegeDiscount], 0); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100 * 0.9, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100 * 0.9, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100 * 0.9, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100 * 0.9, - appliedPrivilegeDiscount: templategenP1PrivilegeDiscount, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [templategenP1PrivilegeDiscount], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies privilege discount to tariff 2", () => { - const cart = calcCart([reducerTariff], [reducerPrivilegeDiscount], 0); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 300 * 300 * 0.95, - priceBeforeDiscounts: 300 * 300, - services: [ - { - serviceKey: "reducer", - price: 300 * 300 * 0.95, - tariffs: [ - { - id: "t3", - name: "reducerTariff", - price: 300 * 300 * 0.95, - isCustom: false, - privileges: [ - { - amount: 100, - description: "d3", - price: 300 * 300 * 0.95, - privilegeId: "p3", - serviceKey: "reducer", - appliedPrivilegeDiscount: reducerPrivilegeDiscount, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [reducerPrivilegeDiscount], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies service discount to tariffs", () => { - const cart = calcCart([templategenTariff1, templategenTariff2], [templategenServiceDiscount], 0); - - const expectedCart: CartData = { - itemCount: 2, - priceAfterDiscounts: (100 * 100 + 600 * 600) * 0.8, - priceBeforeDiscounts: 100 * 100 + 600 * 600, - services: [ - { - serviceKey: "templategen", - price: (100 * 100 + 600 * 600) * 0.8, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100 * 0.8, - isCustom: false, - privileges: [ - { - privilegeId: "p1", - amount: 100, - serviceKey: "templategen", - description: "d1", - price: 100 * 100 * 0.8, - appliedPrivilegeDiscount: null, - }, - ], - }, - { - id: "t5", - name: "templategenTariff2", - price: 600 * 600 * 0.8, - isCustom: false, - privileges: [ - { - description: "d5", - amount: 100, - price: 600 * 600 * 0.8, - privilegeId: "p5", - serviceKey: "templategen", - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: templategenServiceDiscount, - }, - ], - allAppliedDiscounts: [templategenServiceDiscount], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies privilege discount to 2 tariffs", () => { - const cart = calcCart([templategenTariff1, templategenTariff2], [templategenP1PrivilegeDiscount], 0); - - const expectedCart: CartData = { - itemCount: 2, - priceAfterDiscounts: 100 * 100 * 0.9 + 600 * 600, - priceBeforeDiscounts: 100 * 100 + 600 * 600, - services: [ - { - serviceKey: "templategen", - price: 100 * 100 * 0.9 + 600 * 600, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100 * 0.9, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100 * 0.9, - appliedPrivilegeDiscount: templategenP1PrivilegeDiscount, - }, - ], - }, - { - id: "t5", - name: "templategenTariff2", - price: 600 * 600, - isCustom: false, - privileges: [ - { - description: "d5", - price: 600 * 600, - amount: 100, - privilegeId: "p5", - serviceKey: "templategen", - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [templategenP1PrivilegeDiscount], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies cart purchases discount to tariff", () => { - const cart = calcCart([templategenTariff1], [cartPurchasesDiscount], 0); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100 * 0.7, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100, - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [cartPurchasesDiscount], - appliedCartPurchasesDiscount: cartPurchasesDiscount, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("doesn't apply cart purchases discount when cartPurchasesAmount is not enough", () => { - const cart = calcCart([templategenTariff1], [highAmountCartPurchasesDiscount], 0); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100, - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies loyalty discount to tariff", () => { - const cart = calcCart([templategenTariff1], [loyaltyDiscount], 1001); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100 * 0.6, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100, - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [loyaltyDiscount], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: loyaltyDiscount, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("doesn't apply loyalty discount when purchasesAmount is not enough", () => { - const cart = calcCart([templategenTariff1], [loyaltyDiscount], 0); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100, - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - }); - - describe("with multiple discounts", () => { - it("applies privilege and service discounts to tariff", () => { - const cart = calcCart([templategenTariff1], [templategenP1PrivilegeDiscount, templategenServiceDiscount], 0); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100 * 0.9 * 0.8, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100 * 0.9 * 0.8, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100 * 0.9 * 0.8, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100 * 0.9 * 0.8, - appliedPrivilegeDiscount: templategenP1PrivilegeDiscount, - }, - ], - }, - ], - appliedServiceDiscount: templategenServiceDiscount, - }, - ], - allAppliedDiscounts: [templategenP1PrivilegeDiscount, templategenServiceDiscount], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies all types of discounts to tariff", () => { - const cart = calcCart( - [templategenTariff1], - [templategenP1PrivilegeDiscount, templategenServiceDiscount, cartPurchasesDiscount, loyaltyDiscount], - 1001 - ); - - const expectedCart: CartData = { - itemCount: 1, - priceAfterDiscounts: 100 * 100 * 0.9 * 0.8 * 0.7 * 0.6, - priceBeforeDiscounts: 100 * 100, - services: [ - { - serviceKey: "templategen", - price: 100 * 100 * 0.9 * 0.8, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100 * 0.9 * 0.8, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100 * 0.9 * 0.8, - appliedPrivilegeDiscount: templategenP1PrivilegeDiscount, - }, - ], - }, - ], - appliedServiceDiscount: templategenServiceDiscount, - }, - ], - allAppliedDiscounts: [templategenP1PrivilegeDiscount, templategenServiceDiscount, cartPurchasesDiscount, loyaltyDiscount], - appliedCartPurchasesDiscount: cartPurchasesDiscount, - appliedLoyaltyDiscount: loyaltyDiscount, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies different discounts to different tariffs", () => { - const cart = calcCart( - [templategenTariff1, reducerTariff], - [templategenP1PrivilegeDiscount, reducerPrivilegeDiscount], - 1001 - ); - - const expectedCart: CartData = { - itemCount: 2, - priceAfterDiscounts: 100 * 100 * 0.9 + 300 * 300 * 0.95, - priceBeforeDiscounts: 100 * 100 + 300 * 300, - services: [ - { - serviceKey: "templategen", - price: 100 * 100 * 0.9, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100 * 0.9, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100 * 0.9, - appliedPrivilegeDiscount: templategenP1PrivilegeDiscount, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - { - serviceKey: "reducer", - price: 300 * 300 * 0.95, - tariffs: [ - { - id: "t3", - name: "reducerTariff", - price: 300 * 300 * 0.95, - isCustom: false, - privileges: [ - { - amount: 100, - description: "d3", - price: 300 * 300 * 0.95, - privilegeId: "p3", - serviceKey: "reducer", - appliedPrivilegeDiscount: reducerPrivilegeDiscount, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [templategenP1PrivilegeDiscount, reducerPrivilegeDiscount], - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - }; - expect(cart).toStrictEqual(expectedCart); - }); - - it("applies all types of discounts to multiple tariffs", () => { - const cart = calcCart( - [templategenTariff1, templategenTariff2, squizTariff, reducerTariff], - [templategenP1PrivilegeDiscount, reducerPrivilegeDiscount, templategenServiceDiscount, cartPurchasesDiscount, loyaltyDiscount], - 1001 - ); - - const expectedCart: CartData = { - itemCount: 4, - priceAfterDiscounts: ((100 * 100 * 0.9 + 600 * 600) * 0.8 + 200 * 200 + 300 * 300 * 0.95) * 0.7 * 0.6, - priceBeforeDiscounts: 100 * 100 + 600 * 600 + 200 * 200 + 300 * 300, - services: [ - { - serviceKey: "templategen", - price: (100 * 100 * 0.9 + 600 * 600) * 0.8, - tariffs: [ - { - id: "t1", - name: "templategenTariff1", - price: 100 * 100 * 0.9 * 0.8, - isCustom: false, - privileges: [ - { - amount: 100, - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - price: 100 * 100 * 0.9 * 0.8, - appliedPrivilegeDiscount: templategenP1PrivilegeDiscount, - }, - ], - }, - { - id: "t5", - name: "templategenTariff2", - price: 600 * 600 * 0.8, - isCustom: false, - privileges: [ - { - amount: 100, - description: "d5", - price: 600 * 600 * 0.8, - privilegeId: "p5", - serviceKey: "templategen", - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: templategenServiceDiscount, - }, - { - serviceKey: "squiz", - price: 200 * 200, - tariffs: [ - { - id: "t2", - name: "squizTariff", - price: 200 * 200, - isCustom: false, - privileges: [ - { - amount: 100, - description: "d2", - price: 200 * 200, - privilegeId: "p2", - serviceKey: "squiz", - appliedPrivilegeDiscount: null, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - { - serviceKey: "reducer", - price: 300 * 300 * 0.95, - tariffs: [ - { - id: "t3", - name: "reducerTariff", - price: 300 * 300 * 0.95, - isCustom: false, - privileges: [ - { - amount: 100, - description: "d3", - price: 300 * 300 * 0.95, - privilegeId: "p3", - serviceKey: "reducer", - appliedPrivilegeDiscount: reducerPrivilegeDiscount, - }, - ], - }, - ], - appliedServiceDiscount: null, - }, - ], - allAppliedDiscounts: [templategenP1PrivilegeDiscount, reducerPrivilegeDiscount, templategenServiceDiscount, cartPurchasesDiscount, loyaltyDiscount], - appliedCartPurchasesDiscount: cartPurchasesDiscount, - appliedLoyaltyDiscount: loyaltyDiscount, - }; - expect(cart).toStrictEqual(expectedCart); - }); - }); -}); - -const templategenTariff1: Tariff = { - _id: "t1", - name: "templategenTariff1", - price: 0, - description: "test", - isCustom: false, - privileges: [ - { - _id: "p1", - name: "n1", - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - type: "count", - value: "МБ", - price: 100, - amount: 100, - }, - ], - isDeleted: false, - createdAt: "", - updatedAt: "" -}; - -const templategenTariff2: Tariff = { - description: "test", - _id: "t5", - name: "templategenTariff2", - price: 0, - isCustom: false, - privileges: [ - { - _id: "p5", - name: "n5", - privilegeId: "p5", - serviceKey: "templategen", - description: "d5", - type: "count", - value: "МБ", - price: 600, - amount: 600, - }, - ], - isDeleted: false, - createdAt: "", - updatedAt: "" -}; - -const customTemplategenTariff: Tariff = { - description: "test", - _id: "t1", - name: "templategenTariff3", - price: 0, - isCustom: true, - privileges: [ - { - _id: "p1", - name: "n1", - privilegeId: "p1", - serviceKey: "templategen", - description: "d1", - type: "count", - value: "МБ", - price: 100, - amount: 100, - }, - ], - isDeleted: false, - createdAt: "", - updatedAt: "" -}; - -const squizTariff: Tariff = { - description: "test", - _id: "t2", - name: "squizTariff", - price: 0, - isCustom: false, - privileges: [ - { - _id: "p2", - name: "n2", - privilegeId: "p2", - serviceKey: "squiz", - description: "d2", - type: "count", - value: "МБ", - price: 200, - amount: 200, - }, - ], - isDeleted: false, - createdAt: "", - updatedAt: "" -}; - -const reducerTariff: Tariff = { - description: "test", - _id: "t3", - name: "reducerTariff", - price: 0, - isCustom: false, - privileges: [ - { - _id: "p3", - name: "n3", - privilegeId: "p3", - serviceKey: "reducer", - description: "d3", - type: "count", - value: "МБ", - price: 300, - amount: 300, - }, - ], - isDeleted: false, - createdAt: "", - updatedAt: "" -}; - -const templategenP1PrivilegeDiscount: Discount = { - ID: "id1", - Name: "n1", - Layer: 1, - Description: "d1", - Condition: { - Period: { - From: "", - To: "" - }, - User: "", - UserType: "", - Coupon: "", - PurchasesAmount: 0, - CartPurchasesAmount: 0, - Product: "p1", - Term: "1000", - Usage: "0", - PriceFrom: 0, - Group: "templategen" - }, - Target: { - Products: [ - { - ID: "p1", - Factor: 0.9, - Overhelm: false - } - ], - Factor: 0.9, - TargetScope: "Sum", - TargetGroup: "templategen", - Overhelm: false - }, - Audit: { - UpdatedAt: "", - CreatedAt: "", - Deleted: false - }, - Deprecated: false -}; - -const templategenServiceDiscount: Discount = { - ID: "id2", - Name: "n2", - Layer: 2, - Description: "d2", - Condition: { - Period: { - From: "", - To: "" - }, - User: "", - UserType: "", - Coupon: "", - PurchasesAmount: 0, - CartPurchasesAmount: 0, - Product: "", - Term: "1000", - Usage: "0", - PriceFrom: 0, - Group: "templategen" - }, - Target: { - Products: [ - { - ID: "", - Factor: 0.8, - Overhelm: false - } - ], - Factor: 0.8, - TargetScope: "Sum", - TargetGroup: "templategen", - Overhelm: false - }, - Audit: { - UpdatedAt: "", - CreatedAt: "", - Deleted: false - }, - Deprecated: false -}; - -const reducerPrivilegeDiscount: Discount = { - ID: "id11", - Name: "n11", - Layer: 1, - Description: "d11", - Condition: { - Period: { - From: "", - To: "" - }, - User: "", - UserType: "", - Coupon: "", - PurchasesAmount: 0, - CartPurchasesAmount: 0, - Product: "p3", - Term: "1000", - Usage: "0", - PriceFrom: 0, - Group: "reducer" - }, - Target: { - Products: [ - { - ID: "p1", - Factor: 0.95, - Overhelm: false - } - ], - Factor: 0.95, - TargetScope: "Sum", - TargetGroup: "reducer", - Overhelm: false - }, - Audit: { - UpdatedAt: "", - CreatedAt: "", - Deleted: false - }, - Deprecated: false -}; - -const cartPurchasesDiscount: Discount = { - ID: "id3", - Name: "n3", - Layer: 3, - Description: "d3", - Condition: { - Period: { - From: "", - To: "" - }, - User: "", - UserType: "", - Coupon: "", - PurchasesAmount: 0, - CartPurchasesAmount: 1000, - Product: "", - Term: "0", - Usage: "0", - PriceFrom: 0, - Group: "templategen" - }, - Target: { - Products: [ - { - ID: "p1", - Factor: 0.7, - Overhelm: false - } - ], - Factor: 0.7, - TargetScope: "Sum", - TargetGroup: "templategen", - Overhelm: false - }, - Audit: { - UpdatedAt: "", - CreatedAt: "", - Deleted: false - }, - Deprecated: false -}; - -const highAmountCartPurchasesDiscount: Discount = { - ID: "id3", - Name: "n3", - Layer: 3, - Description: "d3", - Condition: { - Period: { - From: "", - To: "" - }, - User: "", - UserType: "", - Coupon: "", - PurchasesAmount: 0, - CartPurchasesAmount: 1000000, - Product: "", - Term: "0", - Usage: "0", - PriceFrom: 0, - Group: "templategen" - }, - Target: { - Products: [ - { - ID: "p1", - Factor: 0.7, - Overhelm: false - } - ], - Factor: 0.7, - TargetScope: "Sum", - TargetGroup: "templategen", - Overhelm: false - }, - Audit: { - UpdatedAt: "", - CreatedAt: "", - Deleted: false - }, - Deprecated: false -}; - -const loyaltyDiscount: Discount = { - ID: "id3", - Name: "n3", - Layer: 4, - Description: "d3", - Condition: { - Period: { - From: "", - To: "" - }, - User: "", - UserType: "", - Coupon: "", - PurchasesAmount: 1000, - CartPurchasesAmount: 0, - Product: "", - Term: "0", - Usage: "0", - PriceFrom: 0, - Group: "templategen" - }, - Target: { - Products: [ - { - ID: "p1", - Factor: 0.6, - Overhelm: false - } - ], - Factor: 0.6, - TargetScope: "Sum", - TargetGroup: "templategen", - Overhelm: false - }, - Audit: { - UpdatedAt: "", - CreatedAt: "", - Deleted: false - }, - Deprecated: false -}; diff --git a/src/utils/calcCart/calcCart.ts b/src/utils/calcCart/calcCart.ts deleted file mode 100644 index 8275e76..0000000 --- a/src/utils/calcCart/calcCart.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { CartData, Discount, PrivilegeCartData, Tariff, TariffCartData, applyCartDiscount, applyLoyaltyDiscount, applyPrivilegeDiscounts, applyServiceDiscounts } from "@frontend/kitui"; - - -export function calcCart( - tariffs: Tariff[], - discounts: Discount[], - purchasesAmount: number, - coupon?: string, -): CartData { - const cartData: CartData = { - services: [], - priceBeforeDiscounts: 0, - priceAfterDiscounts: 0, - itemCount: 0, - appliedCartPurchasesDiscount: null, - appliedLoyaltyDiscount: null, - allAppliedDiscounts: [], - }; - - const serviceTariffType: Record = {}; - - tariffs.forEach(tariff => { - let serviceData = cartData.services.find(service => service.serviceKey === tariff.privileges[0].serviceKey); - if (!serviceData) { - serviceData = { - serviceKey: 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 => { - serviceTariffType[privilege.serviceKey] ??= +tariff.isCustom; - const isIncompatibleTariffs = serviceTariffType[privilege.serviceKey] ^ +tariff.isCustom; - if (isIncompatibleTariffs) throw new Error("Если взят готовый тариф, то кастомный на этот сервис сделать уже нельзя"); - - const privilegePrice = privilege.amount * privilege.price; - - if (!tariff.price) tariffCartData.price += privilegePrice; - - const privilegeCartData: PrivilegeCartData = { - serviceKey: privilege.serviceKey, - amount: privilege.amount, - privilegeId: privilege.privilegeId, - description: privilege.description, - price: privilegePrice, - appliedPrivilegeDiscount: null, - }; - - tariffCartData.privileges.push(privilegeCartData); - cartData.priceAfterDiscounts += privilegePrice; - cartData.itemCount++; - }); - - cartData.priceBeforeDiscounts += tariffCartData.price; - serviceData.price += tariffCartData.price; - }); - - applyPrivilegeDiscounts(cartData, discounts); - applyServiceDiscounts(cartData, discounts); - applyCartDiscount(cartData, discounts); - applyLoyaltyDiscount(cartData, discounts, purchasesAmount); - - cartData.allAppliedDiscounts = Array.from(new Set(cartData.allAppliedDiscounts)); - - return cartData; -} - -export function formatDiscountFactor(factor: number): string { - return `${((1 - factor) * 100).toFixed(1)}%`; -} diff --git a/src/utils/createDiscountFromPromocode.ts b/src/utils/createDiscountFromPromocode.ts new file mode 100644 index 0000000..392b9e4 --- /dev/null +++ b/src/utils/createDiscountFromPromocode.ts @@ -0,0 +1,42 @@ +import { Promocode } from "@root/model/promocodes"; + + +export function createDiscountFromPromocode(promocode: Promocode, userId: string) { + return { + "ID": crypto.randomUUID(), + "Name": promocode.codeword, + "Layer": promocode.bonus.discount.layer, + "Description": "", + "Condition": { + "User": userId, + "UserType": "", + "Coupon": promocode.codeword, + "PurchasesAmount": "0", + "CartPurchasesAmount": "0", + "Product": promocode.bonus.discount.target, + "Term": "0", + "Usage": "0", + "PriceFrom": "0", + "Group": promocode.bonus.discount.target + }, + "Target": { + "Products": promocode.bonus.discount.layer === 1 ? [ + { + "ID": promocode.bonus.discount.target, + "Factor": promocode.bonus.discount.factor, + "Overhelm": false + } + ] : [], + "Factor": promocode.bonus.discount.layer === 2 ? promocode.bonus.discount.factor : 0, + "TargetScope": "Sum", + "TargetGroup": promocode.bonus.discount.target, + "Overhelm": true + }, + "Audit": { + "UpdatedAt": "", + "CreatedAt": "", + "Deleted": false + }, + "Deprecated": false + }; +} diff --git a/src/utils/formatDiscountFactor.ts b/src/utils/formatDiscountFactor.ts new file mode 100644 index 0000000..6ed645e --- /dev/null +++ b/src/utils/formatDiscountFactor.ts @@ -0,0 +1,3 @@ +export function formatDiscountFactor(factor: number): string { + return `${((1 - factor) * 100).toFixed(1)}%`; +} diff --git a/src/utils/hooks/usePrivileges.ts b/src/utils/hooks/usePrivileges.ts index 81d8489..8969731 100644 --- a/src/utils/hooks/usePrivileges.ts +++ b/src/utils/hooks/usePrivileges.ts @@ -1,29 +1,28 @@ -import { useEffect } from "react"; - -import { requestPrivileges } from "@root/api/privilegies"; - -import type { PrivilegeWithAmount } from "@frontend/kitui"; - -export default function usePrivileges({ - onError, - onNewPrivileges, -}: { - onNewPrivileges: (response: PrivilegeWithAmount[]) => void; - onError?: (error: any) => void; -}) { - useEffect(() => { - const controller = new AbortController(); - - requestPrivileges(controller.signal).then( - ([privilegesResponse, privilegesError]) => { - if (privilegesError) { - return onError?.(privilegesError); - } - - onNewPrivileges(privilegesResponse); - } - ); - - return () => controller.abort(); - }, [onError, onNewPrivileges]); -} +import { useEffect } from "react"; + +import { requestPrivileges } from "@root/api/privilegies"; + +import type { CustomPrivilege } from "@frontend/kitui"; + +export default function usePrivileges({ + onError, + onNewPrivileges, +}: { + onNewPrivileges: (response: CustomPrivilege[]) => void; + onError?: (error: any) => void; +}) { + useEffect(() => { + const controller = new AbortController(); + + requestPrivileges(controller.signal).then( + ([privilegesResponse, privilegesError]) => { + if (privilegesError) { + return onError?.(privilegesError); + } + onNewPrivileges(privilegesResponse); + } + ); + + return () => controller.abort(); + }, [onError, onNewPrivileges]); +} diff --git a/src/utils/hooks/useQuizStatistic.ts b/src/utils/hooks/useQuizStatistic.ts new file mode 100644 index 0000000..f4554f1 --- /dev/null +++ b/src/utils/hooks/useQuizStatistic.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { + QuizStatisticResponse, + getStatistic +} from "@root/api/quizStatistic"; + +import type { Moment } from "moment"; + +interface useQuizStatisticProps { + to: Moment | null; + from: Moment | null; +} + +export function useQuizStatistic({ to, from }: useQuizStatisticProps) { + const formatTo = to?.unix(); + const formatFrom = from?.unix(); + + const [data, setData] = useState({ Registrations: 0, Quizes: 0, Results: 0 }); + + useEffect(() => { + + const requestStatistics = async () => { + console.log("работаю раз") + console.log("работаю два") + + const gottenData = await getStatistic(Number(formatTo), Number(formatFrom)); + setData(gottenData) + } + + requestStatistics(); + }, [to, from]); + + return { ...data }; +} diff --git a/yarn.lock b/yarn.lock index e38adc7..f88ce4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1433,10 +1433,10 @@ lodash.isundefined "^3.0.1" lodash.uniq "^4.5.0" -"@frontend/kitui@^1.0.59": - version "1.0.59" - resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.59.tgz#c4584506bb5cab4fc1df35f5b1d0d66ec379a9a1" - integrity sha1-xFhFBrtcq0/B3zX1sdDWbsN5qaE= +"@frontend/kitui@^1.0.77": + version "1.0.77" + resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.77.tgz#a749dee0e7622b4c4509e8354ace47299b0606f7" + integrity sha1-p0ne4OdiK0xFCeg1Ss5HKZsGBvc= dependencies: immer "^10.0.2" reconnecting-eventsource "^1.6.2" @@ -10472,6 +10472,13 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-error-boundary@^4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.13.tgz#80386b7b27b1131c5fbb7368b8c0d983354c7947" + integrity sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"