fix: conflicts
This commit is contained in:
commit
55e9255f59
@ -5,16 +5,17 @@
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test --env=node",
|
||||
"test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
|
||||
"test:cart": "craco test src/utils/calcCart --watchAll=false --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
|
||||
"eject": "craco eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@frontend/kitui": "^1.0.8",
|
||||
"@frontend/kitui": "^1.0.12",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"@mui/material": "^5.10.14",
|
||||
"axios": "^1.3.4",
|
||||
"axios": "^1.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"formik": "^2.2.9",
|
||||
"immer": "^10.0.2",
|
||||
@ -27,7 +28,7 @@
|
||||
"react-router-dom": "^6.4.3",
|
||||
"web-vitals": "^2.1.0",
|
||||
"yup": "^1.1.1",
|
||||
"zustand": "^4.3.6"
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
|
@ -1,643 +0,0 @@
|
||||
import { AnyDiscount } from "../model/discount";
|
||||
|
||||
|
||||
export const mockDiscounts: AnyDiscount[] = [
|
||||
{
|
||||
_id: "id1",
|
||||
name: "Лояльность 1",
|
||||
description:
|
||||
"постоянная скидка для юзеров, внёсших на проект от 10 000 рублей. Применяется на итоговую сумму, после скидок за сумму в корзине",
|
||||
conditionType: "purchasesAmount",
|
||||
layer: 4, // "слой", т.е. этап применения скидки
|
||||
condition: {
|
||||
purchasesAmount: 10000,
|
||||
},
|
||||
factor: 0.99, // множитель, применяемый к сумме
|
||||
},
|
||||
{
|
||||
_id: "id2",
|
||||
name: "Лояльность 2",
|
||||
description:
|
||||
"постоянная скидка для юзеров, внёсших на проект от 25 000 рублей. Применяется на итоговую сумму, после скидок за сумму в корзине",
|
||||
conditionType: "purchasesAmount",
|
||||
layer: 4,
|
||||
condition: {
|
||||
purchasesAmount: 25000,
|
||||
},
|
||||
factor: 0.98,
|
||||
},
|
||||
{
|
||||
_id: "id3",
|
||||
name: "Лояльность 3",
|
||||
description:
|
||||
"постоянная скидка для юзеров, внёсших на проект от 50 000 рублей. Применяется на итоговую сумму, после скидок за сумму в корзине",
|
||||
conditionType: "purchasesAmount",
|
||||
layer: 4,
|
||||
condition: {
|
||||
purchasesAmount: 50000,
|
||||
},
|
||||
factor: 0.975,
|
||||
},
|
||||
{
|
||||
_id: "id4",
|
||||
name: "Корзина 1",
|
||||
description: "Скидка на размер корзины от 5 000 р. Применяется на итоговую сумму, после суммирования корзины",
|
||||
conditionType: "cartPurchasesAmount",
|
||||
layer: 3,
|
||||
condition: {
|
||||
cartPurchasesAmount: 5000,
|
||||
},
|
||||
factor: 0.985,
|
||||
},
|
||||
{
|
||||
_id: "id5",
|
||||
name: "Корзина 2",
|
||||
description: "Скидка на размер корзины от 50 000 р. Применяется на итоговую сумму, после суммирования корзины",
|
||||
conditionType: "cartPurchasesAmount",
|
||||
layer: 3,
|
||||
condition: {
|
||||
cartPurchasesAmount: 50000,
|
||||
},
|
||||
factor: 0.965,
|
||||
},
|
||||
{
|
||||
_id: "id6",
|
||||
name: "Анлим Шабло 1",
|
||||
description: "Скидка на количество безлимитных дней работы от 30 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
// если в condition условие для поиска, то вот я добавил в него условие для поиска по привилегии
|
||||
id: "p1", // айди привилегии
|
||||
value: 30, // скидка работает, если значние больше либо равно этому значению
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p1", // не знаю, стоит ли тут оставлять массив products, но на всякий случай оставлю. т.е. скидка, при срабатывании, применяется к этой привилегии в корзине, т.е. умножает её сумму на factor
|
||||
factor: 0.975,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id7",
|
||||
name: "Анлим Шабло 2",
|
||||
description: "Скидка на количество безлимитных дней работы от 90 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p1",
|
||||
value: 90,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p1",
|
||||
factor: 0.975,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id8.rev",
|
||||
name: "Анлим Шабло 3",
|
||||
description: "Скидка на количество безлимитных дней работы от 180 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p1",
|
||||
value: 180,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p1",
|
||||
factor: 0.93,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id8",
|
||||
name: "Генерации Шабло 1",
|
||||
description: "Скидка на количество генераций от 100 шт",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "templateCnt",
|
||||
value: 100,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "templateCnt",
|
||||
factor: 0.995,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id9",
|
||||
name: "Генерации Шабло 2",
|
||||
description: "Скидка на количество генераций от 350 шт",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "templateCnt",
|
||||
value: 350,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "templateCnt",
|
||||
factor: 0.98,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id10",
|
||||
name: "Генерации Шабло 3",
|
||||
description: "Скидка на количество генераций от 500 шт",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "templateCnt",
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "templateCnt",
|
||||
factor: 0.945,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id11",
|
||||
name: "Анлим Квиз 1",
|
||||
description: "Скидка на количество дней безлимитного использования опросника, от 30 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p3",
|
||||
value: 30,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p3",
|
||||
factor: 0.97,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id12",
|
||||
name: "Анлим Квиз 2",
|
||||
description: "Скидка на количество дней безлимитного использования опросника, от 90 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p3",
|
||||
value: 90,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p3",
|
||||
factor: 0.93,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id13",
|
||||
name: "Анлим Квиз 3",
|
||||
description: "Скидка на количество дней безлимитного использования опросника, от 180 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p3",
|
||||
value: 180,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p3",
|
||||
factor: 0.85,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id14",
|
||||
name: "Актив квиз 1",
|
||||
description: "Скидка на количество опросов от 100 шт",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p4",
|
||||
value: 100,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p4",
|
||||
factor: 0.98,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id15",
|
||||
name: "Актив квиз 2",
|
||||
description: "Скидка на количество опросов от 200 шт",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p4",
|
||||
value: 200,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p4",
|
||||
factor: 0.96,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id16",
|
||||
name: "Актив квиз 3",
|
||||
description: "Скидка на количество опросов от 350 шт",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p4",
|
||||
value: 350,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p4",
|
||||
factor: 0.9,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id17",
|
||||
name: "Анлим Сокращатель 1",
|
||||
description: "Скидка на безлимитное использование сокращателя от 30 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p5",
|
||||
value: 30,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p5",
|
||||
factor: 0.99,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id18",
|
||||
name: "Анлим Сокращатель 2",
|
||||
description: "Скидка на безлимитное использование сокращателя от 60 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p5",
|
||||
value: 60,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p5",
|
||||
factor: 0.98,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id19",
|
||||
name: "Анлим Сокращатель 3",
|
||||
description: "Скидка на безлимитное использование сокращателя от 90 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p5",
|
||||
value: 90,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p5",
|
||||
factor: 0.97,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id20",
|
||||
name: "АБ Сокращатель 1",
|
||||
description: "Скидка на количество АБ тестов, от 10 штук",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p6",
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p6",
|
||||
factor: 0.99,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id22",
|
||||
name: "АБ Сокращатель 2",
|
||||
description: "Скидка на количество АБ тестов, от 25 штук",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p6",
|
||||
value: 25,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p6",
|
||||
factor: 0.965,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id23",
|
||||
name: "АБ Сокращатель 3",
|
||||
description: "Скидка на количество АБ тестов, от 50 штук",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p6",
|
||||
value: 50,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p6",
|
||||
factor: 0.935,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id24.1",
|
||||
name: "Стата Сокращатель 1",
|
||||
description: "Скидка на дни сбора расширенной статистики от 30 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p7",
|
||||
value: 30,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p7",
|
||||
factor: 0.935,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id24.2",
|
||||
name: "Стата Сокращатель 2",
|
||||
description: "Скидка на дни сбора расширенной статистики от 90 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p7",
|
||||
value: 90,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p7",
|
||||
factor: 0.875,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id25",
|
||||
name: "Стата Сокращатель 3",
|
||||
description: "Скидка на дни сбора расширенной статистики от 180 дней",
|
||||
conditionType: "privilege",
|
||||
layer: 1,
|
||||
condition: {
|
||||
privilege: {
|
||||
id: "p7",
|
||||
value: 180,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p7",
|
||||
factor: 0.83,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id26",
|
||||
name: "Шаблонизатор 1",
|
||||
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису шаблонизации, от 1000 р",
|
||||
conditionType: "service",
|
||||
layer: 2,
|
||||
condition: {
|
||||
service: {
|
||||
id: "templategen",
|
||||
value: 1000,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
service: "templategen",
|
||||
factor: 0.996,
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id27",
|
||||
name: "Шаблонизатор 2",
|
||||
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису шаблонизации, от 5000 р",
|
||||
conditionType: "service",
|
||||
layer: 2,
|
||||
condition: {
|
||||
service: {
|
||||
id: "templategen",
|
||||
value: 5000,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
service: "templategen",
|
||||
factor: 0.983,
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id28",
|
||||
name: "Опросник 1",
|
||||
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису опросника, от 2000 р",
|
||||
conditionType: "service",
|
||||
layer: 2,
|
||||
condition: {
|
||||
service: {
|
||||
id: "squiz",
|
||||
value: 2000,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
service: "squiz",
|
||||
factor: 0.983,
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id29",
|
||||
name: "Опросник 2",
|
||||
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису опросника, от 6000 р",
|
||||
conditionType: "service",
|
||||
layer: 2,
|
||||
condition: {
|
||||
service: {
|
||||
id: "squiz",
|
||||
value: 6000,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
service: "squiz",
|
||||
factor: 0.969,
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id30",
|
||||
name: "Сокращатель 1",
|
||||
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису сокращателя, от 500 р",
|
||||
conditionType: "service",
|
||||
layer: 2,
|
||||
condition: {
|
||||
service: {
|
||||
id: "dwarfener",
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
service: "dwarfener",
|
||||
factor: 0.99,
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id31",
|
||||
name: "Сокращатель 2",
|
||||
description: "Скидка на сумму стоимостей товаров, принадлежащих сервису сокращателя, от 2500 р",
|
||||
conditionType: "service",
|
||||
layer: 2,
|
||||
condition: {
|
||||
service: {
|
||||
id: "dwarfener",
|
||||
value: 2500,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
service: "dwarfener",
|
||||
factor: 0.96,
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: "id32",
|
||||
name: "НКО",
|
||||
description:
|
||||
"Скидка всем подтвердившим статус НКО. Перекрывает ВСЕ остальные скидки. Если эта скидка срабатывает, остальные можно не вычислять. Т.е. если на уровне 0 находится какая-лидо скидка для выданных условий, просто суммируем корзину и применяем к сумме указанный множитель, после чего прекращаем процесс рассчёта",
|
||||
conditionType: "userType",
|
||||
layer: 0,
|
||||
condition: {
|
||||
userType: "nko",
|
||||
},
|
||||
target: {
|
||||
IsAllProducts: true,
|
||||
factor: 0.2,
|
||||
},
|
||||
overwhelm: true,
|
||||
},
|
||||
{
|
||||
_id: "id33",
|
||||
name: "Промокод На АБ тесты",
|
||||
description: "Скидка, полученная конкретным юзером, после введения промокода. Заменяет собой не промокодовую",
|
||||
conditionType: "user",
|
||||
layer: 1,
|
||||
condition: {
|
||||
coupon: "ABCD", // на мой вкус, стоит при активации промокода создавать скидку, привязанную к юзеру по айдишнику, и удалять после использования. т.е. кондишн не по coupon, а по
|
||||
user: "buddy",
|
||||
},
|
||||
target: {
|
||||
products: [
|
||||
{
|
||||
privilegeId: "p6",
|
||||
factor: 0.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
overwhelm: true,
|
||||
},
|
||||
];
|
@ -1,4 +1,5 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { UserAccount } from "@root/model/account";
|
||||
|
||||
|
||||
const apiUrl = process.env.NODE_ENV === "production" ? "/customer" : "https://hub.pena.digital/customer";
|
||||
@ -17,4 +18,23 @@ export function deleteCart(tariffId: string) {
|
||||
method: "DELETE",
|
||||
useToken: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function payCart() {
|
||||
return makeRequest<never, UserAccount>({
|
||||
url: apiUrl + "/cart/pay",
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchCurrency(currency: string) {
|
||||
return makeRequest<{ currency: string; }, UserAccount>({
|
||||
url: apiUrl + "/wallet",
|
||||
method: "PATCH",
|
||||
useToken: true,
|
||||
body: {
|
||||
currency
|
||||
},
|
||||
});
|
||||
}
|
32
src/api/wallet.ts
Normal file
32
src/api/wallet.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet";
|
||||
|
||||
|
||||
const apiUrl = process.env.NODE_ENV === "production" ? "/customer" : "https://hub.pena.digital/customer";
|
||||
|
||||
const testPaymentBody: SendPaymentRequest = {
|
||||
type: "bankCard",
|
||||
amount: 15020,
|
||||
currency: "RUB",
|
||||
bankCard: {
|
||||
number: "RUB",
|
||||
expiryYear: "2021",
|
||||
expiryMonth: "05",
|
||||
csc: "05",
|
||||
cardholder: "IVAN IVANOV",
|
||||
},
|
||||
phoneNumber: "79000000000",
|
||||
login: "login_test",
|
||||
returnUrl: "https://hub.pena.digital/wallet",
|
||||
};
|
||||
|
||||
export function sendPayment(body: SendPaymentRequest = testPaymentBody) {
|
||||
return makeRequest<SendPaymentRequest, SendPaymentResponse>({
|
||||
url: apiUrl + "/wallet",
|
||||
contentType: true,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
body
|
||||
});
|
||||
}
|
@ -3,11 +3,10 @@ import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material
|
||||
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
import { ServiceCartData } from "@root/model/cart";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
import { removeTariffFromCart } from "@root/stores/user";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
|
||||
|
||||
|
||||
const name: Record<string, string> = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Скоращатель ссылок" };
|
||||
@ -126,7 +125,7 @@ export default function CustomWrapperDrawer({ serviceData }: Props) {
|
||||
color: theme.palette.grey3.main,
|
||||
}}
|
||||
>
|
||||
{privilege.name}
|
||||
{privilege.description}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -10,6 +10,7 @@ import { useNavigate } from "react-router";
|
||||
import { useCart } from "@root/utils/hooks/useCart";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
import { closeCartDrawer, openCartDrawer, useCartStore } from "@root/stores/cart";
|
||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
|
||||
|
||||
export default function Drawers() {
|
||||
@ -18,6 +19,14 @@ export default function Drawers() {
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isDrawerOpen = useCartStore(state => state.isDrawerOpen);
|
||||
const cart = useCart();
|
||||
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
|
||||
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
|
||||
|
||||
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
|
||||
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice;
|
||||
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice;
|
||||
|
||||
return (
|
||||
<IconButton sx={{ p: 0 }}>
|
||||
@ -151,7 +160,7 @@ export default function Drawers() {
|
||||
order: upMd ? 1 : 2,
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(cart.priceBeforeDiscounts / 100)}
|
||||
{currencyFormatter.format(totalPriceBeforeDiscounts / 100)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="p1"
|
||||
@ -162,7 +171,7 @@ export default function Drawers() {
|
||||
order: upMd ? 2 : 1,
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(cart.priceAfterDiscounts / 100)}
|
||||
{currencyFormatter.format(totalPriceAfterDiscounts / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<CustomButton
|
||||
|
@ -1,96 +1,94 @@
|
||||
import {
|
||||
FormControl,
|
||||
InputLabel,
|
||||
SxProps,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
Theme,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
SxProps,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
Theme,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import "./text-input.css";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
label?: string;
|
||||
bold?: boolean;
|
||||
gap?: string;
|
||||
color?: string;
|
||||
FormInputSx?: SxProps<Theme>;
|
||||
TextfieldProps: TextFieldProps;
|
||||
onChange: (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => void;
|
||||
id: string;
|
||||
label?: string;
|
||||
bold?: boolean;
|
||||
gap?: string;
|
||||
color?: string;
|
||||
FormInputSx?: SxProps<Theme>;
|
||||
TextfieldProps: TextFieldProps;
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export default function InputTextfield({
|
||||
id,
|
||||
label,
|
||||
bold = false,
|
||||
gap = "10px",
|
||||
onChange,
|
||||
TextfieldProps,
|
||||
color,
|
||||
FormInputSx,
|
||||
id,
|
||||
label,
|
||||
bold = false,
|
||||
gap = "10px",
|
||||
onChange,
|
||||
TextfieldProps,
|
||||
color,
|
||||
FormInputSx,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
|
||||
const labelFont = upMd
|
||||
? bold
|
||||
? theme.typography.p1
|
||||
: { ...theme.typography.body1, fontWeight: 500 }
|
||||
: theme.typography.body2;
|
||||
const labelFont = upMd
|
||||
? bold
|
||||
? theme.typography.p1
|
||||
: { ...theme.typography.body1, fontWeight: 500 }
|
||||
: theme.typography.body2;
|
||||
|
||||
const placeholderFont = upMd
|
||||
? undefined
|
||||
: { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
|
||||
const placeholderFont = upMd ? undefined : { fontWeight: 400, fontSize: "16px", lineHeight: "19px" };
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="standard"
|
||||
sx={{
|
||||
gap,
|
||||
// mt: "10px",
|
||||
...FormInputSx,
|
||||
}}
|
||||
>
|
||||
<InputLabel
|
||||
shrink
|
||||
htmlFor={id}
|
||||
sx={{
|
||||
position: "inherit",
|
||||
color: "black",
|
||||
transform: "none",
|
||||
...labelFont,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<TextField
|
||||
{...TextfieldProps}
|
||||
fullWidth
|
||||
id={id}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
height: "48px",
|
||||
borderRadius: "8px",
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
backgroundColor: color,
|
||||
borderRadius: "8px",
|
||||
height: "48px",
|
||||
py: 0,
|
||||
color: "black",
|
||||
...placeholderFont,
|
||||
},
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="standard"
|
||||
sx={{
|
||||
gap,
|
||||
// mt: "10px",
|
||||
...FormInputSx,
|
||||
}}
|
||||
>
|
||||
{label &&
|
||||
<InputLabel
|
||||
shrink
|
||||
htmlFor={id}
|
||||
sx={{
|
||||
position: "inherit",
|
||||
color: "black",
|
||||
transform: "none",
|
||||
...labelFont,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
}
|
||||
<TextField
|
||||
{...TextfieldProps}
|
||||
fullWidth
|
||||
id={id}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
height: "48px",
|
||||
borderRadius: "8px",
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
sx: {
|
||||
backgroundColor: color,
|
||||
borderRadius: "8px",
|
||||
height: "48px",
|
||||
py: 0,
|
||||
color: "black",
|
||||
...placeholderFont,
|
||||
},
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
|
||||
const arrayMenu = [
|
||||
{ name: "Главная", url: "/" },
|
||||
@ -52,6 +53,7 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
|
||||
const location = useLocation();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const user = useUserStore((state) => state.user);
|
||||
const cash = useUserStore(state => state.userAccount?.wallet.cash) ?? 0;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -161,7 +163,7 @@ export default function DialogMenu({ open, handleClose }: DialogMenuProps) {
|
||||
Мой баланс
|
||||
</Typography>
|
||||
<Typography variant="body2" color={theme.palette.brightPurple.main}>
|
||||
00.00 руб.
|
||||
{currencyFormatter.format(cash / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -12,117 +12,122 @@ import { enqueueSnackbar } from "notistack";
|
||||
import { clearUserData, useUserStore } from "@root/stores/user";
|
||||
import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { clearCustomTariffs } from "@root/stores/customTariffs";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
|
||||
interface Props {
|
||||
isLoggedIn: boolean;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export default function NavbarFull({ isLoggedIn }: Props) {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const cash = useUserStore(state => state.userAccount?.wallet.cash) ?? 0;
|
||||
|
||||
async function handleLogoutClick() {
|
||||
try {
|
||||
await logout();
|
||||
clearAuthToken();
|
||||
clearUserData();
|
||||
clearCustomTariffs();
|
||||
navigate("/");
|
||||
} catch (error: any) {
|
||||
const message = getMessageFromFetchError(error, "Не удалось выйти");
|
||||
if (message) enqueueSnackbar(message);
|
||||
async function handleLogoutClick() {
|
||||
try {
|
||||
await logout();
|
||||
clearAuthToken();
|
||||
clearUserData();
|
||||
clearCustomTariffs();
|
||||
navigate("/");
|
||||
} catch (error: any) {
|
||||
const message = getMessageFromFetchError(error, "Не удалось выйти");
|
||||
if (message) enqueueSnackbar(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isLoggedIn ? (
|
||||
<Container
|
||||
component="nav"
|
||||
disableGutters
|
||||
maxWidth={false}
|
||||
sx={{
|
||||
px: "16px",
|
||||
display: "flex",
|
||||
height: "80px",
|
||||
alignItems: "center",
|
||||
gap: "60px",
|
||||
bgcolor: "white",
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
}}
|
||||
>
|
||||
<Link to="/"><PenaLogo width={124} /></Link>
|
||||
<Menu />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
ml: "auto",
|
||||
}}
|
||||
>
|
||||
<Drawers />
|
||||
<IconButton sx={{ p: 0, ml: "8px" }}>
|
||||
<WalletIcon color={theme.palette.grey2.main} bgcolor="#F2F3F7" />
|
||||
</IconButton>
|
||||
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
|
||||
<Typography
|
||||
return isLoggedIn ? (
|
||||
<Container
|
||||
component="nav"
|
||||
disableGutters
|
||||
maxWidth={false}
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: theme.palette.grey3.main,
|
||||
px: "16px",
|
||||
display: "flex",
|
||||
height: "80px",
|
||||
alignItems: "center",
|
||||
gap: "60px",
|
||||
bgcolor: "white",
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
}}
|
||||
>
|
||||
Мой баланс
|
||||
</Typography>
|
||||
<Typography variant="body2" color={theme.palette.brightPurple.main}>
|
||||
00.00 руб.
|
||||
</Typography>
|
||||
</Box>
|
||||
<CustomAvatar />
|
||||
<IconButton
|
||||
onClick={handleLogoutClick}
|
||||
sx={{ ml: "20px", bgcolor: "#F2F3F7", borderRadius: "6px", height: "36px", width: "36px" }}
|
||||
>
|
||||
<LogoutIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Container>
|
||||
) : (
|
||||
<>
|
||||
<SectionWrapper
|
||||
component="nav"
|
||||
maxWidth="lg"
|
||||
outerContainerSx={{
|
||||
backgroundColor: theme.palette.lightPurple.main,
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
}}
|
||||
sx={{
|
||||
px: "20px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
height: "80px",
|
||||
alignItems: "center",
|
||||
gap: "50px",
|
||||
}}
|
||||
>
|
||||
<PenaLogo width={150} />
|
||||
<Menu />
|
||||
<Button
|
||||
component={Link}
|
||||
to={user ? "/tariffs" : "/signin"}
|
||||
state={user ? undefined : { backgroundLocation: location }}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
px: "18px",
|
||||
py: "10px",
|
||||
borderColor: "white",
|
||||
borderRadius: "8px",
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: "180px",
|
||||
}}
|
||||
>
|
||||
Личный кабинет
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
</>
|
||||
);
|
||||
<Link to="/"><PenaLogo width={124} /></Link>
|
||||
<Menu />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
ml: "auto",
|
||||
}}
|
||||
>
|
||||
<Drawers />
|
||||
<IconButton
|
||||
sx={{ p: 0, ml: "8px" }}
|
||||
onClick={() => navigate("/wallet")}
|
||||
>
|
||||
<WalletIcon color={theme.palette.grey2.main} bgcolor="#F2F3F7" />
|
||||
</IconButton>
|
||||
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
lineHeight: "14px",
|
||||
color: theme.palette.grey3.main,
|
||||
}}
|
||||
>
|
||||
Мой баланс
|
||||
</Typography>
|
||||
<Typography variant="body2" color={theme.palette.brightPurple.main}>
|
||||
{currencyFormatter.format(cash / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<CustomAvatar />
|
||||
<IconButton
|
||||
onClick={handleLogoutClick}
|
||||
sx={{ ml: "20px", bgcolor: "#F2F3F7", borderRadius: "6px", height: "36px", width: "36px" }}
|
||||
>
|
||||
<LogoutIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Container>
|
||||
) : (
|
||||
<>
|
||||
<SectionWrapper
|
||||
component="nav"
|
||||
maxWidth="lg"
|
||||
outerContainerSx={{
|
||||
backgroundColor: theme.palette.lightPurple.main,
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
}}
|
||||
sx={{
|
||||
px: "20px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
height: "80px",
|
||||
alignItems: "center",
|
||||
gap: "50px",
|
||||
}}
|
||||
>
|
||||
<PenaLogo width={150} />
|
||||
<Menu />
|
||||
<Button
|
||||
component={Link}
|
||||
to={user ? "/tariffs" : "/signin"}
|
||||
state={user ? undefined : { backgroundLocation: location }}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
px: "18px",
|
||||
py: "10px",
|
||||
borderColor: "white",
|
||||
borderRadius: "8px",
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: "180px",
|
||||
}}
|
||||
>
|
||||
Личный кабинет
|
||||
</Button>
|
||||
</SectionWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -15,15 +15,15 @@ export default function NumberIcon({ number, backgroundColor = "rgb(0 0 0 / 0)",
|
||||
const firstDigit = Math.floor(number / 10);
|
||||
const secondDigit = number % 10;
|
||||
|
||||
const firstDigitTransformX = 6;
|
||||
const secondDigitTransformX = number < 10
|
||||
const firstDigitTranslateX = 6;
|
||||
const secondDigitTranslateX = number < 10
|
||||
? 9
|
||||
: number < 20
|
||||
? 11
|
||||
: 12;
|
||||
|
||||
const firstDigitElement = digitSvgs[firstDigit](firstDigitTransformX);
|
||||
const secondDigitElement = digitSvgs[secondDigit](secondDigitTransformX);
|
||||
const firstDigitElement = digitSvgs[firstDigit](firstDigitTranslateX);
|
||||
const secondDigitElement = digitSvgs[secondDigit](secondDigitTranslateX);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
@ -49,47 +49,47 @@ export default function NumberIcon({ number, backgroundColor = "rgb(0 0 0 / 0)",
|
||||
|
||||
const circleSvg = <path d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" />;
|
||||
|
||||
const digitSvgs: Record<number, (transformX: number) => ReactElement> = {
|
||||
0: (transformX: number) => (
|
||||
<path transform={`translate(${transformX} 7)`} d="M3 8.75C4.24264 8.75 5.25 7.07107 5.25 5C5.25 2.92893 4.24264 1.25 3 1.25C1.75736 1.25 0.75 2.92893 0.75 5C0.75 7.07107 1.75736 8.75 3 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
const digitSvgs: Record<number, (translateX: number) => ReactElement> = {
|
||||
0: (translateX: number) => (
|
||||
<path transform={`translate(${translateX} 7)`} d="M3 8.75C4.24264 8.75 5.25 7.07107 5.25 5C5.25 2.92893 4.24264 1.25 3 1.25C1.75736 1.25 0.75 2.92893 0.75 5C0.75 7.07107 1.75736 8.75 3 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
),
|
||||
1: (transformX: number) => (
|
||||
<path transform={`translate(${transformX} 7)`} d="M1.125 2.75L3.375 1.25V8.75015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
1: (translateX: number) => (
|
||||
<path transform={`translate(${translateX} 7)`} d="M1.125 2.75L3.375 1.25V8.75015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
),
|
||||
2: (transformX: number) => (
|
||||
<path transform={`translate(${transformX} 7)`} d="M1.27158 2.39455C1.44019 1.99638 1.74115 1.66868 2.12357 1.46688C2.50598 1.26507 2.94636 1.20155 3.37021 1.28705C3.79407 1.37256 4.17538 1.60185 4.44964 1.93613C4.7239 2.27041 4.87428 2.68916 4.87534 3.12156C4.87703 3.49512 4.76526 3.8604 4.55483 4.16907V4.16907L1.12305 8.75H4.87534" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
2: (translateX: number) => (
|
||||
<path transform={`translate(${translateX} 7)`} d="M1.27158 2.39455C1.44019 1.99638 1.74115 1.66868 2.12357 1.46688C2.50598 1.26507 2.94636 1.20155 3.37021 1.28705C3.79407 1.37256 4.17538 1.60185 4.44964 1.93613C4.7239 2.27041 4.87428 2.68916 4.87534 3.12156C4.87703 3.49512 4.76526 3.8604 4.55483 4.16907V4.16907L1.12305 8.75H4.87534" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
),
|
||||
3: (transformX: number) => (
|
||||
<path transform={`translate(${transformX} 7)`} d="M1.125 1.25H4.875L2.6875 4.375C3.04723 4.37503 3.40139 4.46376 3.71863 4.63336C4.03587 4.80295 4.30639 5.04816 4.50623 5.34727C4.70607 5.64637 4.82906 5.99015 4.86431 6.34814C4.89956 6.70614 4.84598 7.0673 4.70832 7.39964C4.57066 7.73198 4.35316 8.02525 4.07509 8.25345C3.79702 8.48166 3.46696 8.63777 3.11415 8.70796C2.76133 8.77815 2.39666 8.76024 2.05242 8.65583C1.70818 8.55142 1.395 8.36373 1.14062 8.10938" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
3: (translateX: number) => (
|
||||
<path transform={`translate(${translateX} 7)`} d="M1.125 1.25H4.875L2.6875 4.375C3.04723 4.37503 3.40139 4.46376 3.71863 4.63336C4.03587 4.80295 4.30639 5.04816 4.50623 5.34727C4.70607 5.64637 4.82906 5.99015 4.86431 6.34814C4.89956 6.70614 4.84598 7.0673 4.70832 7.39964C4.57066 7.73198 4.35316 8.02525 4.07509 8.25345C3.79702 8.48166 3.46696 8.63777 3.11415 8.70796C2.76133 8.77815 2.39666 8.76024 2.05242 8.65583C1.70818 8.55142 1.395 8.36373 1.14062 8.10938" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
),
|
||||
4: (transformX: number) => (
|
||||
4: (translateX: number) => (
|
||||
<>
|
||||
<path transform={`translate(${transformX} 7)`} d="M2.62508 1.07788L0.75 6.3906H4.50015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${transformX} 7)`} d="M4.5 3.89038V8.89058" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX} 7)`} d="M2.62508 1.07788L0.75 6.3906H4.50015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX} 7)`} d="M4.5 3.89038V8.89058" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
),
|
||||
5: (transformX: number) => (
|
||||
<path transform={`translate(${transformX} 7)`} d="M5.0625 1.25H1.92188L1.3125 5.01562C1.61844 4.70972 2.00821 4.5014 2.43254 4.41702C2.85687 4.33263 3.29669 4.37596 3.69639 4.54153C4.09609 4.70711 4.43772 4.98749 4.67807 5.34721C4.91843 5.70694 5.04672 6.12986 5.04672 6.5625C5.04672 6.99514 4.91843 7.41806 4.67807 7.77779C4.43772 8.13751 4.09609 8.41789 3.69639 8.58346C3.29669 8.74904 2.85687 8.79237 2.43254 8.70798C2.00821 8.6236 1.61844 8.41528 1.3125 8.10937" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
5: (translateX: number) => (
|
||||
<path transform={`translate(${translateX} 7)`} d="M5.0625 1.25H1.92188L1.3125 5.01562C1.61844 4.70972 2.00821 4.5014 2.43254 4.41702C2.85687 4.33263 3.29669 4.37596 3.69639 4.54153C4.09609 4.70711 4.43772 4.98749 4.67807 5.34721C4.91843 5.70694 5.04672 6.12986 5.04672 6.5625C5.04672 6.99514 4.91843 7.41806 4.67807 7.77779C4.43772 8.13751 4.09609 8.41789 3.69639 8.58346C3.29669 8.74904 2.85687 8.79237 2.43254 8.70798C2.00821 8.6236 1.61844 8.41528 1.3125 8.10937" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
),
|
||||
6: (transformX: number) => (
|
||||
6: (translateX: number) => (
|
||||
<>
|
||||
<path transform={`translate(${transformX - 1} 7)`} d="M2.00977 5.30469L4.65117 0.875" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${transformX - 1} 7)`} d="M3.99609 8.75C5.26462 8.75 6.29297 7.72165 6.29297 6.45312C6.29297 5.1846 5.26462 4.15625 3.99609 4.15625C2.72756 4.15625 1.69922 5.1846 1.69922 6.45312C1.69922 7.72165 2.72756 8.75 3.99609 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX - 1} 7)`} d="M2.00977 5.30469L4.65117 0.875" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX - 1} 7)`} d="M3.99609 8.75C5.26462 8.75 6.29297 7.72165 6.29297 6.45312C6.29297 5.1846 5.26462 4.15625 3.99609 4.15625C2.72756 4.15625 1.69922 5.1846 1.69922 6.45312C1.69922 7.72165 2.72756 8.75 3.99609 8.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
),
|
||||
7: (transformX: number) => (
|
||||
<path transform={`translate(${transformX} 7)`} d="M1.125 1.25H4.875L2.375 8.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
7: (translateX: number) => (
|
||||
<path transform={`translate(${translateX} 7)`} d="M1.125 1.25H4.875L2.375 8.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
),
|
||||
8: (transformX: number) => (
|
||||
8: (translateX: number) => (
|
||||
<>
|
||||
<path transform={`translate(${transformX} 7)`} d="M4.72779 2.97356C4.72547 3.36962 4.58601 3.75265 4.33315 4.05749C4.08029 4.36233 3.72963 4.57017 3.34082 4.64564C2.95201 4.72111 2.54906 4.65956 2.20051 4.47146C1.85196 4.28336 1.57934 3.98032 1.42901 3.6139C1.27868 3.24747 1.25993 2.84028 1.37595 2.46158C1.49196 2.08289 1.73559 1.75608 2.06537 1.53675C2.39516 1.31741 2.79075 1.21909 3.18485 1.25851C3.57895 1.29793 3.94722 1.47266 4.22703 1.75298C4.54727 2.07862 4.72705 2.51684 4.72779 2.97356Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${transformX} 7)`} d="M5.04125 6.72925C5.03995 7.19778 4.87634 7.65138 4.57827 8.01287C4.28019 8.37436 3.86607 8.62139 3.40637 8.71193C2.94667 8.80247 2.4698 8.73092 2.05691 8.50946C1.64403 8.288 1.32063 7.93031 1.14177 7.49727C0.962899 7.06422 0.939612 6.58258 1.07587 6.1343C1.21213 5.68602 1.49951 5.2988 1.8891 5.03854C2.2787 4.77829 2.74645 4.66107 3.21273 4.70684C3.67902 4.75261 4.11505 4.95854 4.44661 5.28958C4.63578 5.47847 4.78572 5.70292 4.88777 5.95C4.98983 6.19709 5.04199 6.46192 5.04125 6.72925Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX} 7)`} d="M4.72779 2.97356C4.72547 3.36962 4.58601 3.75265 4.33315 4.05749C4.08029 4.36233 3.72963 4.57017 3.34082 4.64564C2.95201 4.72111 2.54906 4.65956 2.20051 4.47146C1.85196 4.28336 1.57934 3.98032 1.42901 3.6139C1.27868 3.24747 1.25993 2.84028 1.37595 2.46158C1.49196 2.08289 1.73559 1.75608 2.06537 1.53675C2.39516 1.31741 2.79075 1.21909 3.18485 1.25851C3.57895 1.29793 3.94722 1.47266 4.22703 1.75298C4.54727 2.07862 4.72705 2.51684 4.72779 2.97356Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX} 7)`} d="M5.04125 6.72925C5.03995 7.19778 4.87634 7.65138 4.57827 8.01287C4.28019 8.37436 3.86607 8.62139 3.40637 8.71193C2.94667 8.80247 2.4698 8.73092 2.05691 8.50946C1.64403 8.288 1.32063 7.93031 1.14177 7.49727C0.962899 7.06422 0.939612 6.58258 1.07587 6.1343C1.21213 5.68602 1.49951 5.2988 1.8891 5.03854C2.2787 4.77829 2.74645 4.66107 3.21273 4.70684C3.67902 4.75261 4.11505 4.95854 4.44661 5.28958C4.63578 5.47847 4.78572 5.70292 4.88777 5.95C4.98983 6.19709 5.04199 6.46192 5.04125 6.72925Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
),
|
||||
9: (transformX: number) => (
|
||||
9: (translateX: number) => (
|
||||
<>
|
||||
<path transform={`translate(${transformX} 7)`} d="M5.03203 4.47046L2.39062 8.90015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${transformX} 7)`} d="M3.04688 5.6189C4.3154 5.6189 5.34375 4.59055 5.34375 3.32202C5.34375 2.05349 4.3154 1.02515 3.04688 1.02515C1.77835 1.02515 0.75 2.05349 0.75 3.32202C0.75 4.59055 1.77835 5.6189 3.04688 5.6189Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX} 7)`} d="M5.03203 4.47046L2.39062 8.90015" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path transform={`translate(${translateX} 7)`} d="M3.04688 5.6189C4.3154 5.6189 5.34375 4.59055 5.34375 3.32202C5.34375 2.05349 4.3154 1.02515 3.04688 1.02515C1.77835 1.02515 0.75 2.05349 0.75 3.32202C0.75 4.59055 1.77835 5.6189 3.04688 5.6189Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
@ -1,91 +1,118 @@
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
|
||||
import { Alert, Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import CustomButton from "./CustomButton";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
import { payCart } from "@root/api/cart";
|
||||
import { setUserAccount } from "@root/stores/user";
|
||||
import { isAxiosError } from "axios";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useCartStore } from "@root/stores/cart";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
||||
interface Props {
|
||||
price: number;
|
||||
priceWithDiscounts: number;
|
||||
priceBeforeDiscounts: number;
|
||||
priceAfterDiscounts: number;
|
||||
}
|
||||
|
||||
export default function TotalPrice({price,priceWithDiscounts}:Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts }: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
mt: upMd ? "80px" : "70px",
|
||||
pt: upMd ? "30px" : undefined,
|
||||
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "68.5%" : undefined,
|
||||
pr: upMd ? "15%" : undefined,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
|
||||
Итоговая цена
|
||||
</Typography>
|
||||
<Typography color={theme.palette.grey3.main}>
|
||||
Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет Текст-заполнитель —
|
||||
это текст, который имеет Текст-заполнитель — это текст, который имеет Текст-заполнитель
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.grey3.main,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
pl: upMd ? "33px" : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
function handlePayClick() {
|
||||
// payCart().then(result => {
|
||||
// setUserAccount(result);
|
||||
// }).catch(error => {
|
||||
// if (isAxiosError(error) && error.response?.status === 402) {
|
||||
// console.log("response", error.response);
|
||||
// // TODO
|
||||
// // setNotEnoughMoneyAmount(error.response.data?.???)
|
||||
// } else {
|
||||
// const message = getMessageFromFetchError(error);
|
||||
// if (message) enqueueSnackbar(message);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
function handleReplenishWallet() {
|
||||
navigate("/wallet");
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "column" : "row",
|
||||
alignItems: upMd ? "start" : "center",
|
||||
mt: upMd ? "10px" : "30px",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.orange.main}
|
||||
sx={{
|
||||
textDecoration: "line-through",
|
||||
order: upMd ? 1 : 2,
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(price / 100)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="p1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "26px",
|
||||
lineHeight: "31px",
|
||||
order: upMd ? 2 : 1,
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(priceWithDiscounts / 100)}
|
||||
</Typography>
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
mt: upMd ? "80px" : "70px",
|
||||
pt: upMd ? "30px" : undefined,
|
||||
borderTop: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: upMd ? "68.5%" : undefined,
|
||||
pr: upMd ? "15%" : undefined,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<Typography variant="h4" mb={upMd ? "18px" : "30px"}>
|
||||
Итоговая цена
|
||||
</Typography>
|
||||
<Typography color={theme.palette.grey3.main}>
|
||||
Текст-заполнитель — это текст, который имеет Текст-заполнитель — это текст, который имеет Текст-заполнитель —
|
||||
это текст, который имеет Текст-заполнитель — это текст, который имеет Текст-заполнитель
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
color: theme.palette.grey3.main,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
pl: upMd ? "33px" : undefined,
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "column" : "row",
|
||||
alignItems: upMd ? "start" : "center",
|
||||
mt: upMd ? "10px" : "30px",
|
||||
mb: "15px",
|
||||
gap: "15px",
|
||||
}}>
|
||||
<Typography
|
||||
variant="oldPrice"
|
||||
sx={{ order: upMd ? 1 : 2 }}
|
||||
>
|
||||
{currencyFormatter.format(priceBeforeDiscounts / 100)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="price"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "26px",
|
||||
lineHeight: "31px",
|
||||
order: upMd ? 2 : 1,
|
||||
}}
|
||||
>
|
||||
{currencyFormatter.format(priceAfterDiscounts / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{notEnoughMoneyAmount > 0 &&
|
||||
<Alert
|
||||
severity="error"
|
||||
variant="filled"
|
||||
>
|
||||
Нехватает {currencyFormatter.format(notEnoughMoneyAmount / 100)}
|
||||
</Alert>
|
||||
}
|
||||
<CustomButton
|
||||
variant="contained"
|
||||
onClick={notEnoughMoneyAmount === 0 ? handlePayClick : handleReplenishWallet}
|
||||
sx={{
|
||||
mt: "10px",
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
{notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<CustomButton
|
||||
variant="contained"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
Выбрать
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
@ -25,6 +25,10 @@ import TariffConstructor from "./pages/TariffConstructor/TariffConstructor";
|
||||
import { useUser } from "./utils/hooks/useUser";
|
||||
import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { useUserAccount } from "./utils/hooks/useUserAccount";
|
||||
import { setCustomTariffs } from "@root/stores/customTariffs";
|
||||
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
|
||||
import { useDiscounts } from "./utils/hooks/useDiscounts";
|
||||
import { setDiscounts } from "./stores/discounts";
|
||||
|
||||
|
||||
const App = () => {
|
||||
@ -32,6 +36,15 @@ const App = () => {
|
||||
const userId = useUserStore(state => state.userId);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useCustomTariffs({
|
||||
url: "https://admin.pena.digital/strator/privilege/service",
|
||||
onNewUser: setCustomTariffs,
|
||||
onError: error => {
|
||||
const errorMessage = getMessageFromFetchError(error, "Не удалось получить кастомные тарифы");
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
useUser({
|
||||
url: `https://hub.pena.digital/user/${userId}`,
|
||||
userId,
|
||||
@ -61,6 +74,14 @@ const App = () => {
|
||||
}
|
||||
});
|
||||
|
||||
useDiscounts({
|
||||
onNewDiscounts: setDiscounts,
|
||||
onError: error => {
|
||||
const message = getMessageFromFetchError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
}
|
||||
});
|
||||
|
||||
if (location.state?.redirectTo) return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
|
||||
|
||||
return (
|
||||
|
@ -1,19 +0,0 @@
|
||||
export type PrivilegeCartData = {
|
||||
tariffId: string;
|
||||
privilegeId: string;
|
||||
name: string;
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type ServiceCartData = {
|
||||
serviceKey: string;
|
||||
privileges: PrivilegeCartData[];
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type CartData = {
|
||||
services: ServiceCartData[];
|
||||
priceBeforeDiscounts: number;
|
||||
priceAfterDiscounts: number;
|
||||
itemCount: number;
|
||||
};
|
@ -1,11 +1,13 @@
|
||||
import { PrivilegeWithAmount, PrivilegeWithoutPrice } from "./privilege";
|
||||
|
||||
|
||||
type ServiceKey = string;
|
||||
|
||||
export type CustomTariffUserValues = Record<string, number>;
|
||||
|
||||
export type CustomTariffUserValuesMap = Record<string, CustomTariffUserValues>;
|
||||
export type CustomTariffUserValuesMap = Record<ServiceKey, CustomTariffUserValues>;
|
||||
|
||||
export type SummaryPriceMap = Record<string, number>;
|
||||
export type ServiceKeyToPriceMap = Record<ServiceKey, number>;
|
||||
|
||||
export interface CustomTariff {
|
||||
name: string;
|
||||
@ -17,4 +19,4 @@ export interface CustomTariff {
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };
|
||||
export type CreateTariffBody = Omit<CustomTariff, "privilegies"> & { privilegies: PrivilegeWithoutPrice[]; };
|
||||
|
@ -1,99 +1,23 @@
|
||||
export interface DiscountBase {
|
||||
_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** Этап применения скидки */
|
||||
layer?: number;
|
||||
disabled?: boolean;
|
||||
import { Discount } from "@frontend/kitui";
|
||||
|
||||
|
||||
export const SERVICE_LIST = [
|
||||
{
|
||||
serviceKey: "templategen",
|
||||
displayName: "Шаблонизатор документов",
|
||||
},
|
||||
{
|
||||
serviceKey: "squiz",
|
||||
displayName: "Опросник",
|
||||
},
|
||||
{
|
||||
serviceKey: "dwarfener",
|
||||
displayName: "Аналитика сокращателя",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];
|
||||
|
||||
export interface GetDiscountsResponse {
|
||||
Discounts: Discount[];
|
||||
}
|
||||
|
||||
export interface PurchasesAmountDiscount extends DiscountBase {
|
||||
conditionType: "purchasesAmount";
|
||||
condition: {
|
||||
purchasesAmount: number;
|
||||
};
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}
|
||||
|
||||
export interface CartPurchasesAmountDiscount extends DiscountBase {
|
||||
conditionType: "cartPurchasesAmount";
|
||||
condition: {
|
||||
cartPurchasesAmount: number;
|
||||
};
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}
|
||||
|
||||
export interface PrivilegeDiscount extends DiscountBase {
|
||||
conditionType: "privilege";
|
||||
condition: {
|
||||
privilege: {
|
||||
id: string;
|
||||
/** Скидка применяется, если значение больше или равно этому значению */
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
target: {
|
||||
products: Array<{
|
||||
privilegeId: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServiceDiscount extends DiscountBase {
|
||||
conditionType: "service";
|
||||
condition: {
|
||||
service: {
|
||||
id: string;
|
||||
/** Скидка применяется, если значение больше или равно этому значению */
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
target: {
|
||||
service: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserTypeDiscount extends DiscountBase {
|
||||
conditionType: "userType";
|
||||
condition: {
|
||||
userType: string;
|
||||
};
|
||||
target: {
|
||||
IsAllProducts: boolean;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
};
|
||||
overwhelm: boolean;
|
||||
}
|
||||
|
||||
export interface UserDiscount extends DiscountBase {
|
||||
conditionType: "user";
|
||||
condition: {
|
||||
coupon: string;
|
||||
user: string;
|
||||
};
|
||||
target: {
|
||||
products: Array<{
|
||||
privilegeId: string;
|
||||
/** Множитель, на который умножается сумма при применении скидки */
|
||||
factor: number;
|
||||
}>;
|
||||
};
|
||||
overwhelm: boolean;
|
||||
}
|
||||
|
||||
export type AnyDiscount =
|
||||
| PurchasesAmountDiscount
|
||||
| CartPurchasesAmountDiscount
|
||||
| PrivilegeDiscount
|
||||
| ServiceDiscount
|
||||
| UserTypeDiscount
|
||||
| UserDiscount;
|
||||
|
||||
export type DiscountConditionType = AnyDiscount["conditionType"];
|
||||
|
@ -12,10 +12,10 @@ export interface Privilege {
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
export type PrivilegeMap = Record<string, Privilege[]>;
|
||||
export type ServiceKeyToPrivilegesMap = Record<string, Privilege[]>;
|
||||
|
||||
export type PrivilegeValueType = "шаблон" | "день" | "МБ";
|
||||
|
||||
export type PrivilegeWithAmount = Omit<Privilege, "_id"> & { amount: number; };
|
||||
|
||||
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;
|
||||
export type PrivilegeWithoutPrice = Omit<PrivilegeWithAmount, "price">;
|
||||
|
19
src/model/wallet.ts
Normal file
19
src/model/wallet.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface SendPaymentRequest {
|
||||
type: "bankCard";
|
||||
currency: string;
|
||||
amount: number;
|
||||
bankCard: {
|
||||
number: string;
|
||||
expiryYear: string;
|
||||
expiryMonth: string;
|
||||
csc: string;
|
||||
cardholder: string;
|
||||
},
|
||||
phoneNumber: string;
|
||||
login: string;
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
export interface SendPaymentResponse {
|
||||
link: string;
|
||||
}
|
@ -5,12 +5,21 @@ import TotalPrice from "@components/TotalPrice";
|
||||
import CustomWrapper from "./CustomWrapper";
|
||||
import ComplexNavText from "@root/components/ComplexNavText";
|
||||
import { useCart } from "@root/utils/hooks/useCart";
|
||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
|
||||
|
||||
export default function Basket() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const cart = useCart();
|
||||
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
|
||||
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
|
||||
|
||||
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
|
||||
const totalPriceBeforeDiscounts = cart.priceBeforeDiscounts + basePrice;
|
||||
const totalPriceAfterDiscounts = cart.priceAfterDiscounts + discountedPrice;
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
@ -48,7 +57,7 @@ export default function Basket() {
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<TotalPrice price={cart.priceBeforeDiscounts} priceWithDiscounts={cart.priceAfterDiscounts} />
|
||||
<TotalPrice priceBeforeDiscounts={totalPriceBeforeDiscounts} priceAfterDiscounts={totalPriceAfterDiscounts} />
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -3,11 +3,10 @@ import { Box, SvgIcon, Typography, useMediaQuery, useTheme } from "@mui/material
|
||||
import ExpandIcon from "@components/icons/ExpandIcon";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
import { ServiceCartData } from "@root/model/cart";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
import { removeTariffFromCart } from "@root/stores/user";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { ServiceCartData, getMessageFromFetchError } from "@frontend/kitui";
|
||||
|
||||
|
||||
const name: Record<string, string> = { templategen: "Шаблонизатор", squiz: "Опросник", reducer: "Сокращатель ссылок" };
|
||||
@ -129,7 +128,7 @@ export default function CustomWrapper({ serviceData }: Props) {
|
||||
color: theme.palette.grey3.main,
|
||||
}}
|
||||
>
|
||||
{privilege.name}
|
||||
{privilege.description}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -1,124 +1,180 @@
|
||||
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
|
||||
import CustomButton from "@components/CustomButton";
|
||||
import SectionWrapper from "@components/SectionWrapper";
|
||||
import ComplexNavText from "@components/ComplexNavText";
|
||||
import PaymentMethodCard from "./PaymentMethodCard";
|
||||
|
||||
import mastercardLogo from "../../assets/bank-logo/logo-mastercard.png";
|
||||
import visaLogo from "../../assets/bank-logo/logo-visa.png";
|
||||
import qiwiLogo from "../../assets/bank-logo/logo-qiwi.png";
|
||||
import mirLogo from "../../assets/bank-logo/logo-mir.png";
|
||||
import tinkoffLogo from "../../assets/bank-logo/logo-tinkoff.png";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
import { useState } from "react";
|
||||
import InputTextfield from "@root/components/InputTextfield";
|
||||
import { sendPayment } from "@root/api/wallet";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
||||
const paymentMethods = [
|
||||
{ name: "Mastercard", image: mastercardLogo },
|
||||
{ name: "Visa", image: visaLogo },
|
||||
{ name: "QIWI Кошелек", image: qiwiLogo },
|
||||
{ name: "Мир", image: mirLogo },
|
||||
{ name: "Тинькофф", image: tinkoffLogo },
|
||||
] as const;
|
||||
|
||||
type PaymentMethod = typeof paymentMethods[number]["name"];
|
||||
|
||||
export default function Payment() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
const navigate = useNavigate();
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
|
||||
const [paymentValueField, setPaymentValueField] = useState<string>("0");
|
||||
const [paymentLink, setPaymentLink] = useState<string>("");
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
{upMd && <ComplexNavText text1="Все тарифы — " text2="Способ оплаты" />}
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "40px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{!upMd && (
|
||||
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography variant="h4">Способ оплаты</Typography>
|
||||
</Box>
|
||||
{!upMd && (
|
||||
<Typography variant="body2" mb="30px">
|
||||
Выберите способ оплаты
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: upMd ? "white" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
borderRadius: "12px",
|
||||
boxShadow: upMd
|
||||
? cardShadow
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "68.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upSm ? "row" : "column",
|
||||
flexWrap: "wrap",
|
||||
gap: upMd ? "14px" : "20px",
|
||||
}}
|
||||
>
|
||||
<PaymentMethodCard name="Mastercard" image={mastercardLogo} />
|
||||
<PaymentMethodCard name="Visa" image={visaLogo} />
|
||||
<PaymentMethodCard name="QIWI Кошелек" image={qiwiLogo} />
|
||||
<PaymentMethodCard name="Мир" image={mirLogo} />
|
||||
<PaymentMethodCard name="Тинькофф" image={tinkoffLogo} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "start",
|
||||
color: theme.palette.grey3.main,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
pl: upMd ? "33px" : undefined,
|
||||
mt: upMd ? undefined : "30px",
|
||||
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
function handleChoosePaymentClick() {
|
||||
sendPayment().then(result => {
|
||||
setPaymentLink(result.link);
|
||||
}).catch(error => {
|
||||
const message = getMessageFromFetchError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
});
|
||||
}
|
||||
|
||||
function handlePayClick() {
|
||||
navigate(paymentLink);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "85%",
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
|
||||
<Typography mb="20px">К оплате</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "20px",
|
||||
lineHeight: "24px",
|
||||
mb: "28px",
|
||||
}}
|
||||
>
|
||||
{upMd && <ComplexNavText text1="Все тарифы — " text2="Способ оплаты" />}
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "40px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
3 190 руб.
|
||||
</Typography>
|
||||
</Box>
|
||||
<CustomButton
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderColor: theme.palette.brightPurple.main,
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
Выбрать
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionWrapper>
|
||||
);
|
||||
{!upMd && (
|
||||
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography variant="h4">Способ оплаты</Typography>
|
||||
</Box>
|
||||
{!upMd && (
|
||||
<Typography variant="body2" mb="30px">
|
||||
Выберите способ оплаты
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: upMd ? "white" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
borderRadius: "12px",
|
||||
boxShadow: upMd
|
||||
? cardShadow
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "68.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: upSm ? "row" : "column",
|
||||
flexWrap: "wrap",
|
||||
gap: upMd ? "14px" : "20px",
|
||||
alignContent: "start",
|
||||
}}
|
||||
>
|
||||
{paymentMethods.map(method =>
|
||||
<PaymentMethodCard
|
||||
isSelected={selectedPaymentMethod === method.name}
|
||||
key={method.name}
|
||||
name={method.name}
|
||||
image={method.image}
|
||||
onClick={() => setSelectedPaymentMethod(method.name)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "start",
|
||||
color: theme.palette.grey3.main,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
p: upMd ? "20px" : undefined,
|
||||
pl: upMd ? "33px" : undefined,
|
||||
mt: upMd ? undefined : "30px",
|
||||
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "85%",
|
||||
}}
|
||||
>
|
||||
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
|
||||
<Typography mb="20px">К оплате</Typography>
|
||||
{paymentLink ?
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
fontSize: "20px",
|
||||
lineHeight: "48px",
|
||||
mb: "28px",
|
||||
}}
|
||||
>
|
||||
3 190 руб.
|
||||
</Typography>
|
||||
:
|
||||
<InputTextfield
|
||||
TextfieldProps={{
|
||||
placeholder: "К оплате",
|
||||
value: paymentValueField,
|
||||
type: "number",
|
||||
}}
|
||||
onChange={e => setPaymentValueField(e.target.value)}
|
||||
id="payment amount"
|
||||
gap={upMd ? "16px" : "10px"}
|
||||
color={"#F2F3F7"}
|
||||
FormInputSx={{
|
||||
mb: "28px",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
<CustomButton
|
||||
variant={paymentLink ? "contained" : "outlined"}
|
||||
onClick={paymentLink ? handlePayClick : handleChoosePaymentClick}
|
||||
sx={{
|
||||
borderColor: theme.palette.brightPurple.main,
|
||||
backgroundColor: paymentLink ? theme.palette.brightPurple.main : "",
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
Выбрать
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -1,31 +1,43 @@
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { Button, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
image: string;
|
||||
name: string;
|
||||
image: string;
|
||||
isSelected?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export default function PaymentMethodCard({ name, image }: Props) {
|
||||
const theme = useTheme();
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
export default function PaymentMethodCard({ name, image, isSelected, onClick }: Props) {
|
||||
const theme = useTheme();
|
||||
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: upSm ? "237px" : "100%",
|
||||
p: "20px",
|
||||
pr: "10px",
|
||||
display: "flex",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<img src={image} alt="payment method" />
|
||||
<Typography sx={{ color: theme.palette.grey3.main }}>{name}</Typography>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
sx={{
|
||||
width: upSm ? "237px" : "100%",
|
||||
p: "20px",
|
||||
pr: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: isSelected ? `1px solid ${theme.palette.brightPurple.main}` : `1px solid ${theme.palette.grey2.main}`,
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
boxShadow: isSelected ? `0 0 0 1.5px ${theme.palette.brightPurple.main};` : "none",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
border: `1px solid ${theme.palette.brightPurple.main}`,
|
||||
"& > p": {
|
||||
color: "white",
|
||||
}
|
||||
},
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img src={image} alt="payment method" />
|
||||
<Typography sx={{ color: theme.palette.grey3.main }}>{name}</Typography>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -17,9 +17,11 @@ interface Props {
|
||||
export default function CustomTariffCard({ serviceKey, tariffs }: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const summaryPrice = useCustomTariffsStore(state => state.summaryPrice);
|
||||
const summaryPriceBeforeDiscounts = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
|
||||
const summaryPriceAfterDiscounts = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
|
||||
|
||||
const tariffPrice = summaryPrice[serviceKey] ?? 0;
|
||||
const priceBeforeDiscounts = summaryPriceBeforeDiscounts[serviceKey] ?? 0;
|
||||
const priceAfterDiscounts = summaryPriceAfterDiscounts[serviceKey] ?? 0;
|
||||
|
||||
async function handleConfirmClick() {
|
||||
createAndSendTariff(serviceKey).then(result => {
|
||||
@ -98,8 +100,8 @@ export default function CustomTariffCard({ serviceKey, tariffs }: Props) {
|
||||
gap: "20px",
|
||||
mb: "30px",
|
||||
}}>
|
||||
<Typography variant="price">{currencyFormatter.format(tariffPrice / 100)}</Typography>
|
||||
<Typography variant="oldPrice" pt="3px">{currencyFormatter.format(10190)}</Typography>
|
||||
<Typography variant="price">{currencyFormatter.format(priceAfterDiscounts / 100)}</Typography>
|
||||
<Typography variant="oldPrice" pt="3px">{currencyFormatter.format(priceBeforeDiscounts / 100)}</Typography>
|
||||
</Box>
|
||||
<CustomButton
|
||||
variant="contained"
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import CustomButton from "@root/components/CustomButton";
|
||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
|
||||
|
||||
export default function Summary() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
|
||||
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
|
||||
|
||||
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
@ -45,7 +53,7 @@ export default function Summary() {
|
||||
variant="oldPrice"
|
||||
sx={{ order: upMd ? 1 : 2 }}
|
||||
>
|
||||
20 190 руб.
|
||||
{currencyFormatter.format(basePrice / 100)} руб.
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="price"
|
||||
@ -56,7 +64,7 @@ export default function Summary() {
|
||||
order: upMd ? 2 : 1,
|
||||
}}
|
||||
>
|
||||
6 380 руб.
|
||||
{currencyFormatter.format(discountedPrice / 100)} руб.
|
||||
</Typography>
|
||||
</Box>
|
||||
<CustomButton
|
||||
@ -71,4 +79,4 @@ export default function Summary() {
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,26 @@
|
||||
import { Box, IconButton, useMediaQuery, useTheme } from "@mui/material";
|
||||
import SectionWrapper from "@components/SectionWrapper";
|
||||
import ComplexNavText from "@root/components/ComplexNavText";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { setCustomTariffs, useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
import { useCallback } from "react";
|
||||
import Summary from "./Summary";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
import ComplexHeader from "@root/components/ComplexHeader";
|
||||
import CustomTariffCard from "./CustomTariffCard";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs";
|
||||
import TotalPrice from "@root/components/TotalPrice";
|
||||
|
||||
interface ServiceMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
const servicemap: ServiceMap = { "templategen": "Шаблонизатор" };
|
||||
|
||||
export default function TariffConstructor() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const customTariffs = useCustomTariffsStore(state => state.customTariffs);
|
||||
const customTariffs = useCustomTariffsStore(state => state.customTariffsMap);
|
||||
const summaryPriceBeforeDiscountsMap = useCustomTariffsStore(state => state.summaryPriceBeforeDiscountsMap);
|
||||
const summaryPriceAfterDiscountsMap = useCustomTariffsStore(state => state.summaryPriceAfterDiscountsMap);
|
||||
|
||||
useCustomTariffs({
|
||||
url: "https://admin.pena.digital/strator/privilege/service",
|
||||
onNewUser: setCustomTariffs,
|
||||
onError: useCallback(error => {
|
||||
const errorMessage = getMessageFromFetchError(error, "Не удалось получить кастомные тарифы");
|
||||
if (errorMessage) enqueueSnackbar(errorMessage);
|
||||
}, [])
|
||||
});
|
||||
const basePrice = Object.values(summaryPriceBeforeDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
const discountedPrice = Object.values(summaryPriceAfterDiscountsMap).reduce((a, e) => a + e, 0);
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
@ -60,14 +56,14 @@ export default function TariffConstructor() {
|
||||
)}
|
||||
<ComplexHeader
|
||||
text1="Кастомный тариф "
|
||||
text2={serviceKey}
|
||||
text2={servicemap[serviceKey]}
|
||||
/>
|
||||
</Box>
|
||||
<CustomTariffCard serviceKey={serviceKey} tariffs={tariffs} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Summary />
|
||||
<TotalPrice priceBeforeDiscounts={basePrice} priceAfterDiscounts={discountedPrice} />
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,10 @@ import CustomSlider from "@root/components/CustomSlider";
|
||||
import CalendarIcon from "@root/components/icons/CalendarIcon";
|
||||
import PieChartIcon from "@root/components/icons/PieChartIcon";
|
||||
import { Privilege, PrivilegeValueType } from "@root/model/privilege";
|
||||
import { useCartStore } from "@root/stores/cart";
|
||||
import { setCustomTariffsUserValue, useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
import { useDiscountStore } from "@root/stores/discounts";
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import { formatDateWithDeclention } from "@root/utils/date";
|
||||
import { getDeclension } from "@root/utils/declension";
|
||||
import { useEffect, useState } from "react";
|
||||
@ -32,7 +35,10 @@ interface Props {
|
||||
export default function TariffPrivilegeSlider({ tariff }: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const userValue = useCustomTariffsStore(state => state.userValues[tariff.serviceKey]?.[tariff._id]) ?? 0;
|
||||
const userValue = useCustomTariffsStore(state => state.userValuesMap[tariff.serviceKey]?.[tariff._id]) ?? 0;
|
||||
const discounts = useDiscountStore(state => state.discounts);
|
||||
const currentCartTotal = useCartStore(state => state.cart.priceAfterDiscounts);
|
||||
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0;
|
||||
const [value, setValue] = useState<number>(userValue);
|
||||
const throttledValue = useThrottle(value, 200);
|
||||
|
||||
@ -57,8 +63,16 @@ export default function TariffPrivilegeSlider({ tariff }: Props) {
|
||||
}
|
||||
|
||||
useEffect(function setStoreValue() {
|
||||
setCustomTariffsUserValue(tariff.serviceKey, tariff._id, throttledValue);
|
||||
}, [tariff._id, tariff.serviceKey, throttledValue]);
|
||||
console.log(currentCartTotal)
|
||||
setCustomTariffsUserValue(
|
||||
tariff.serviceKey,
|
||||
tariff._id,
|
||||
throttledValue,
|
||||
discounts,
|
||||
currentCartTotal,
|
||||
purchasesAmount
|
||||
);
|
||||
}, [currentCartTotal, discounts, purchasesAmount, tariff._id, tariff.serviceKey, throttledValue]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@ -96,4 +110,4 @@ export default function TariffPrivilegeSlider({ tariff }: Props) {
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,19 +12,13 @@ export default function FreeTariffCard() {
|
||||
color="#7E2AEA"
|
||||
backgroundColor="white"
|
||||
/>}
|
||||
buttonText="Выбрать"
|
||||
headerText="бесплатно"
|
||||
text="Текст-заполнитель — это текст, который имеет "
|
||||
onButtonClick={undefined}
|
||||
price={<Typography variant="price" color="white">0 руб.</Typography>}
|
||||
sx={{
|
||||
backgroundColor: "#7E2AEA",
|
||||
color: "white",
|
||||
}}
|
||||
buttonSx={{
|
||||
color: "white",
|
||||
borderColor: "white",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -10,13 +10,15 @@ interface Props {
|
||||
headerText: string;
|
||||
text: string | string[];
|
||||
sx?: SxProps<Theme>;
|
||||
buttonSx?: SxProps<Theme>;
|
||||
onButtonClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
buttonText: string;
|
||||
buttonProps?: {
|
||||
sx?: SxProps<Theme>;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
text?: string;
|
||||
};
|
||||
price?: ReactNode;
|
||||
}
|
||||
|
||||
export default function TariffCard({ icon, headerText, text, sx, buttonText, onButtonClick, price, buttonSx }: Props) {
|
||||
export default function TariffCard({ icon, headerText, text, sx, price, buttonProps }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@ -66,18 +68,20 @@ export default function TariffCard({ icon, headerText, text, sx, buttonText, onB
|
||||
mb: "10px",
|
||||
}}>{text}</Typography>
|
||||
}
|
||||
<CustomButton
|
||||
onClick={onButtonClick}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
color: theme.palette.brightPurple.main,
|
||||
borderColor: theme.palette.brightPurple.main,
|
||||
mt: "auto",
|
||||
...buttonSx,
|
||||
}}
|
||||
>
|
||||
{buttonText}
|
||||
</CustomButton>
|
||||
{buttonProps &&
|
||||
<CustomButton
|
||||
onClick={buttonProps.onClick}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
color: theme.palette.brightPurple.main,
|
||||
borderColor: theme.palette.brightPurple.main,
|
||||
mt: "auto",
|
||||
...buttonProps.sx,
|
||||
}}
|
||||
>
|
||||
{buttonProps.text}
|
||||
</CustomButton>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -44,24 +44,30 @@ export default function Tariffs() {
|
||||
icon={<CalendarIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
|
||||
headerText="Тарифы на время"
|
||||
text="безлимит на 1 месяц , 3 , 6 , 12"
|
||||
buttonText="Подробнее"
|
||||
onButtonClick={() => navigate("time")}
|
||||
buttonProps={{
|
||||
text: "Подробнее",
|
||||
onClick: () => navigate("time")
|
||||
}}
|
||||
sx={{ maxWidth: "360px" }}
|
||||
/>
|
||||
<TariffCard
|
||||
icon={<PieChartIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
|
||||
headerText="Тариф на объем"
|
||||
text="200 шаблонов, 1000 шаблонов, 5000 шаблонов, 10 000 шаблонов"
|
||||
buttonText="Подробнее"
|
||||
onButtonClick={() => navigate("volume")}
|
||||
buttonProps={{
|
||||
text: "Подробнее",
|
||||
onClick: () => navigate("volume")
|
||||
}}
|
||||
sx={{ maxWidth: "360px" }}
|
||||
/>
|
||||
<TariffCard
|
||||
icon={<CustomIcon color="white" bgcolor={theme.palette.brightPurple.main} />}
|
||||
headerText="Кастом"
|
||||
text="Текст-заполнитель — это текст, который имеет "
|
||||
buttonText="Подробнее"
|
||||
onButtonClick={() => navigate("/tariffconstructor")}
|
||||
buttonProps={{
|
||||
text: "Подробнее",
|
||||
onClick: () => navigate("/tariffconstructor")
|
||||
}}
|
||||
sx={{ maxWidth: "360px" }}
|
||||
/>
|
||||
</Box>
|
||||
@ -114,4 +120,4 @@ export default function Tariffs() {
|
||||
{/*</Box>*/}
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,10 +10,13 @@ import { CustomTab } from "@root/components/CustomTab";
|
||||
import TariffCard from "./TariffCard";
|
||||
import NumberIcon from "@root/components/NumberIcon";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
import { calcTariffPrices } from "@root/utils/calcTariffPrices";
|
||||
import { calcIndividualTariffPrices } from "@root/utils/calcTariffPrices";
|
||||
import { getMessageFromFetchError } from "@frontend/kitui";
|
||||
import FreeTariffCard from "./FreeTariffCard";
|
||||
import { addTariffToCart } from "@root/stores/user";
|
||||
import { addTariffToCart, useUserStore } from "@root/stores/user";
|
||||
import { useDiscountStore } from "@root/stores/discounts";
|
||||
import { useCustomTariffsStore } from "@root/stores/customTariffs";
|
||||
import { useCartStore } from "@root/stores/cart";
|
||||
|
||||
|
||||
export default function TariffPage() {
|
||||
@ -22,6 +25,10 @@ export default function TariffPage() {
|
||||
const location = useLocation();
|
||||
const tariffs = useTariffStore(state => state.tariffs);
|
||||
const [tabIndex, setTabIndex] = useState<number>(0);
|
||||
const discounts = useDiscountStore(state => state.discounts);
|
||||
const customTariffs = useCustomTariffsStore(state => state.customTariffsMap);
|
||||
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0;
|
||||
const cart = useCartStore(state => state.cart);
|
||||
|
||||
const unit: string = String(location.pathname).slice(9);
|
||||
|
||||
@ -47,11 +54,17 @@ export default function TariffPage() {
|
||||
}
|
||||
|
||||
const filteredTariffs = tariffs.filter(tariff => {
|
||||
return tariff.privilegies.map(p => p.type).includes("day") === (unit === "time");
|
||||
return tariff.privilegies.map(p => p.type).includes("day") === (unit === "time") && !tariff.isDeleted;
|
||||
});
|
||||
|
||||
const tariffElements = filteredTariffs.map((tariff, index) => {
|
||||
const { price, priceWithDiscounts } = calcTariffPrices(tariff);
|
||||
const { price, tariffPriceAfterDiscounts } = calcIndividualTariffPrices(
|
||||
tariff,
|
||||
discounts,
|
||||
customTariffs,
|
||||
purchasesAmount,
|
||||
cart,
|
||||
);
|
||||
|
||||
return (
|
||||
<TariffCard
|
||||
@ -61,16 +74,18 @@ export default function TariffPage() {
|
||||
color={unit === "time" ? "#7E2AEA" : "#FB5607"}
|
||||
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
|
||||
/>}
|
||||
buttonText="Выбрать"
|
||||
buttonProps={{
|
||||
text: "Выбрать",
|
||||
onClick: () => handleTariffItemClick(tariff._id),
|
||||
}}
|
||||
headerText={tariff.name}
|
||||
text={tariff.privilegies.map(p => `${p.name} - ${p.amount}`)}
|
||||
onButtonClick={() => handleTariffItemClick(tariff._id)}
|
||||
price={<>
|
||||
{price !== undefined && price !== priceWithDiscounts &&
|
||||
{price !== undefined && price !== tariffPriceAfterDiscounts &&
|
||||
<Typography variant="oldPrice">{currencyFormatter.format(price / 100)}</Typography>
|
||||
}
|
||||
{priceWithDiscounts !== undefined &&
|
||||
<Typography variant="price">{currencyFormatter.format(priceWithDiscounts / 100)}</Typography>
|
||||
{tariffPriceAfterDiscounts !== undefined &&
|
||||
<Typography variant="price">{currencyFormatter.format(tariffPriceAfterDiscounts / 100)}</Typography>
|
||||
}
|
||||
</>}
|
||||
/>
|
||||
|
@ -6,139 +6,147 @@ import WalletIcon from "@components/icons/WalletIcon";
|
||||
import SectionWrapper from "@components/SectionWrapper";
|
||||
import ComplexNavText from "@components/ComplexNavText";
|
||||
import { cardShadow } from "@root/utils/themes/shadow";
|
||||
import { currencyFormatter } from "@root/utils/currencyFormatter";
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function Wallet() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const navigate = useNavigate();
|
||||
const cash = useUserStore(state => state.userAccount?.wallet.cash) ?? 0;
|
||||
|
||||
const footnotes = (
|
||||
<Box
|
||||
component="ol"
|
||||
sx={{
|
||||
color: theme.palette.grey2.main,
|
||||
pt: "10px",
|
||||
pl: "25px",
|
||||
mt: 0,
|
||||
mb: upMd ? "3px" : "73px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="li"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Текст для сносок: текст-заполнитель — это текст, который имеет текст-заполнитель — это текст, который имеет
|
||||
</Typography>
|
||||
<Typography
|
||||
component="li"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Текст для сносок: тель — это текст, который имеет текст-заполнитель — это текст, который имеет
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
const footnotes = (
|
||||
<Box
|
||||
component="ol"
|
||||
sx={{
|
||||
color: theme.palette.grey2.main,
|
||||
pt: "10px",
|
||||
pl: "25px",
|
||||
mt: 0,
|
||||
mb: upMd ? "3px" : "73px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="li"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Текст для сносок: текст-заполнитель — это текст, который имеет текст-заполнитель — это текст, который имеет
|
||||
</Typography>
|
||||
<Typography
|
||||
component="li"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Текст для сносок: тель — это текст, который имеет текст-заполнитель — это текст, который имеет
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
{upMd && <ComplexNavText text1="Все тарифы — " text2="Мой кошелёк" />}
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "40px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{!upMd && (
|
||||
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography variant="h4">Мой кошелёк</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
gap: "9%",
|
||||
borderRadius: "12px",
|
||||
mb: "40px",
|
||||
boxShadow: cardShadow,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "59.5%" : undefined,
|
||||
p: "20px",
|
||||
pb: upMd ? undefined : "10px",
|
||||
}}
|
||||
return (
|
||||
<SectionWrapper
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: "25px",
|
||||
mb: "70px",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.grey3.main, mb: "30px" }}>Баланс 10.04.2022</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "22px",
|
||||
pb: "40px",
|
||||
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
||||
}}
|
||||
>
|
||||
<WalletIcon bgcolor="#FEDFD0" color={theme.palette.orange.main} />
|
||||
<Typography variant="h5">10 304 руб.</Typography>
|
||||
</Box>
|
||||
{upMd && footnotes}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
color: theme.palette.grey3.main,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
p: "20px",
|
||||
pl: upMd ? "33px" : undefined,
|
||||
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "22px",
|
||||
maxWidth: "85%",
|
||||
mb: "32px",
|
||||
}}
|
||||
>
|
||||
<Typography>Текст-заполнитель — это текст, который имеет</Typography>
|
||||
<Typography>Текст-заполнитель — это текст, который имеет</Typography>
|
||||
</Box>
|
||||
<CustomButton
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
textColor: "white",
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
Пополнить
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{!upMd && footnotes}
|
||||
</SectionWrapper>
|
||||
);
|
||||
{upMd && <ComplexNavText text1="Все тарифы — " text2="Мой кошелёк" />}
|
||||
<Box
|
||||
sx={{
|
||||
mt: "20px",
|
||||
mb: "40px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{!upMd && (
|
||||
<IconButton sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Typography variant="h4">Мой кошелёк</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
flexDirection: upMd ? "row" : "column",
|
||||
gap: "9%",
|
||||
borderRadius: "12px",
|
||||
mb: "40px",
|
||||
boxShadow: cardShadow,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: upMd ? "59.5%" : undefined,
|
||||
p: "20px",
|
||||
pb: upMd ? undefined : "10px",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.grey3.main, mb: "30px" }}>Баланс 10.04.2022</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "22px",
|
||||
pb: "40px",
|
||||
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
||||
}}
|
||||
>
|
||||
<WalletIcon bgcolor="#FEDFD0" color={theme.palette.orange.main} />
|
||||
<Typography variant="h5">
|
||||
{currencyFormatter.format(cash / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{upMd && footnotes}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
color: theme.palette.grey3.main,
|
||||
width: upMd ? "31.5%" : undefined,
|
||||
p: "20px",
|
||||
pl: upMd ? "33px" : undefined,
|
||||
borderLeft: upMd ? `1px solid ${theme.palette.grey2.main}` : undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "22px",
|
||||
maxWidth: "85%",
|
||||
mb: "32px",
|
||||
}}
|
||||
>
|
||||
<Typography>Текст-заполнитель — это текст, который имеет</Typography>
|
||||
<Typography>Текст-заполнитель — это текст, который имеет</Typography>
|
||||
</Box>
|
||||
<CustomButton
|
||||
variant="contained"
|
||||
onClick={() => navigate("/payment")}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
textColor: "white",
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
Пополнить
|
||||
</CustomButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{!upMd && footnotes}
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CartData } from "@root/model/cart";
|
||||
import { CartData, Discount } from "@frontend/kitui";
|
||||
import { Tariff } from "@root/model/tariff";
|
||||
import { calcCart } from "@root/utils/calcCart";
|
||||
import { calcCart } from "@root/utils/calcCart/calcCart";
|
||||
import { produce } from "immer";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
@ -10,6 +10,7 @@ interface CartStore {
|
||||
cartTariffMap: Record<string, Tariff | "loading" | "not found">;
|
||||
cart: CartData;
|
||||
isDrawerOpen: boolean;
|
||||
notEnoughMoneyAmount: number;
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartStore>()(
|
||||
@ -21,8 +22,10 @@ export const useCartStore = create<CartStore>()(
|
||||
priceBeforeDiscounts: 0,
|
||||
priceAfterDiscounts: 0,
|
||||
itemCount: 0,
|
||||
envolvedDiscounts: [],
|
||||
},
|
||||
isDrawerOpen: false,
|
||||
notEnoughMoneyAmount: 0,
|
||||
}),
|
||||
{
|
||||
name: "Cart",
|
||||
@ -45,36 +48,37 @@ export const setCartTariffStatus = (tariffId: string, status: "loading" | "not f
|
||||
}
|
||||
);
|
||||
|
||||
export const addCartTariffs = (tariffs: Tariff[]) => useCartStore.setState(
|
||||
export const addCartTariffs = (tariffs: Tariff[], discounts: Discount[], purchasesAmount: number) => useCartStore.setState(
|
||||
produce<CartStore>(state => {
|
||||
tariffs.forEach(tariff => {
|
||||
state.cartTariffMap[tariff._id] = tariff;
|
||||
});
|
||||
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
|
||||
state.cart = calcCart(cartTariffs);
|
||||
state.cart = calcCart(cartTariffs, discounts, purchasesAmount);
|
||||
}),
|
||||
false,
|
||||
{
|
||||
type: tariffs.length > 0 ? "addCartTariffs" : "rejected",
|
||||
tariffIds: tariffs.map(tariff => tariff._id),
|
||||
tariffs,
|
||||
}
|
||||
);
|
||||
|
||||
export const removeMissingCartTariffs = (tariffIds: string[]) => useCartStore.setState(
|
||||
export const removeMissingCartTariffs = (tariffIds: string[], discounts: Discount[], purchasesAmount: number) => useCartStore.setState(
|
||||
produce<CartStore>(state => {
|
||||
for (const key in state.cartTariffMap) {
|
||||
if (!tariffIds.includes(key)) delete state.cartTariffMap[key];
|
||||
}
|
||||
const cartTariffs = Object.values(state.cartTariffMap).filter((tariff): tariff is Tariff => typeof tariff === "object");
|
||||
state.cart = calcCart(cartTariffs);
|
||||
state.cart = calcCart(cartTariffs, discounts, purchasesAmount);
|
||||
}),
|
||||
false,
|
||||
{
|
||||
type: "removeMissingCartTariffs",
|
||||
tariffIds,
|
||||
type: "rejected"
|
||||
}
|
||||
);
|
||||
|
||||
export const openCartDrawer = () => useCartStore.setState({ isDrawerOpen: true });
|
||||
|
||||
export const closeCartDrawer = () => useCartStore.setState({ isDrawerOpen: false });
|
||||
|
||||
export const setNotEnoughMoneyAmount = (notEnoughMoneyAmount: number) => useCartStore.setState({ notEnoughMoneyAmount });
|
||||
|
@ -1,21 +1,24 @@
|
||||
import { createTariff } from "@root/api/tariff";
|
||||
import { CustomTariffUserValuesMap, SummaryPriceMap } from "@root/model/customTariffs";
|
||||
import { PrivilegeMap, PrivilegeWithoutPrice } from "@root/model/privilege";
|
||||
import { CustomTariffUserValuesMap, ServiceKeyToPriceMap } from "@root/model/customTariffs";
|
||||
import { ServiceKeyToPrivilegesMap, PrivilegeWithoutPrice } from "@root/model/privilege";
|
||||
import { produce } from "immer";
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
import { Discount, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
||||
|
||||
|
||||
interface CustomTariffsStore {
|
||||
customTariffs: PrivilegeMap;
|
||||
userValues: CustomTariffUserValuesMap;
|
||||
summaryPrice: SummaryPriceMap;
|
||||
customTariffsMap: ServiceKeyToPrivilegesMap;
|
||||
userValuesMap: CustomTariffUserValuesMap;
|
||||
summaryPriceBeforeDiscountsMap: ServiceKeyToPriceMap;
|
||||
summaryPriceAfterDiscountsMap: ServiceKeyToPriceMap;
|
||||
}
|
||||
|
||||
const initialState: CustomTariffsStore = {
|
||||
customTariffs: {},
|
||||
userValues: {},
|
||||
summaryPrice: {},
|
||||
customTariffsMap: {},
|
||||
userValuesMap: {},
|
||||
summaryPriceBeforeDiscountsMap: {},
|
||||
summaryPriceAfterDiscountsMap: {},
|
||||
};
|
||||
|
||||
export const useCustomTariffsStore = create<CustomTariffsStore>()(
|
||||
@ -29,31 +32,51 @@ export const useCustomTariffsStore = create<CustomTariffsStore>()(
|
||||
{
|
||||
name: "customTariffs",
|
||||
partialize: state => ({
|
||||
userValues: state.userValues,
|
||||
summaryPrice: state.summaryPrice,
|
||||
userValuesMap: state.userValuesMap,
|
||||
summaryPriceBeforeDiscountsMap: state.summaryPriceBeforeDiscountsMap,
|
||||
summaryPriceAfterDiscountsMap: state.summaryPriceAfterDiscountsMap,
|
||||
}),
|
||||
migrate: () => initialState,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const setCustomTariffs = (customTariffs: PrivilegeMap) => useCustomTariffsStore.setState({ customTariffs });
|
||||
export const setCustomTariffs = (customTariffs: ServiceKeyToPrivilegesMap) => useCustomTariffsStore.setState({ customTariffsMap: customTariffs });
|
||||
|
||||
export const setCustomTariffsUserValue = (
|
||||
serviceKey: string,
|
||||
privilegeId: string,
|
||||
value: number,
|
||||
discounts: Discount[],
|
||||
currentCartTotal: number,
|
||||
purchasesAmount: number,
|
||||
) => useCustomTariffsStore.setState(
|
||||
produce<CustomTariffsStore>(state => {
|
||||
state.userValues[serviceKey] ??= {};
|
||||
state.userValues[serviceKey][privilegeId] = value;
|
||||
state.userValuesMap[serviceKey] ??= {};
|
||||
state.userValuesMap[serviceKey][privilegeId] = value;
|
||||
|
||||
const sum = state.customTariffs[serviceKey].reduce((acc, tariff) => {
|
||||
const amount = state.userValues[serviceKey]?.[tariff._id] ?? 0;
|
||||
let priceWithoutDiscounts = 0;
|
||||
let priceAfterDiscounts = 0;
|
||||
|
||||
return acc + tariff.price * amount;
|
||||
}, 0);
|
||||
state.customTariffsMap[serviceKey].forEach(tariff => {
|
||||
const amount = state.userValuesMap[serviceKey]?.[tariff._id] ?? 0;
|
||||
priceWithoutDiscounts += tariff.price * amount;
|
||||
|
||||
state.summaryPrice[serviceKey] = sum;
|
||||
const discount = findPrivilegeDiscount(tariff.privilegeId, tariff.price * amount, discounts);
|
||||
priceAfterDiscounts += tariff.price * amount * discount.factor;
|
||||
});
|
||||
|
||||
const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts);
|
||||
priceAfterDiscounts *= serviceDiscount.factor;
|
||||
|
||||
const cartDiscount = findCartDiscount(currentCartTotal, discounts);
|
||||
priceAfterDiscounts *= cartDiscount.factor;
|
||||
|
||||
const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
||||
priceAfterDiscounts *= loyaltyDiscount.factor;
|
||||
|
||||
state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts;
|
||||
state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts;
|
||||
})
|
||||
);
|
||||
|
||||
@ -62,10 +85,10 @@ export const createAndSendTariff = (serviceKey: string) => {
|
||||
|
||||
const privilegies: PrivilegeWithoutPrice[] = [];
|
||||
|
||||
Object.entries(state.userValues[serviceKey]).forEach(([privilegeId, userValue]) => {
|
||||
Object.entries(state.userValuesMap[serviceKey]).forEach(([privilegeId, userValue]) => {
|
||||
if (userValue === 0) return;
|
||||
|
||||
const privilege = state.customTariffs[serviceKey].find(p => p._id === privilegeId);
|
||||
const privilege = state.customTariffsMap[serviceKey].find(p => p._id === privilegeId);
|
||||
if (!privilege) throw new Error(`Privilege not found: ${privilegeId}`);
|
||||
|
||||
const p2 = {
|
||||
|
@ -1,17 +1,16 @@
|
||||
import { mockDiscounts } from "@root/__mocks__/discounts";
|
||||
import { AnyDiscount } from "@root/model/discount";
|
||||
import { Discount } from "@frontend/kitui";
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
|
||||
interface MessageStore {
|
||||
discounts: AnyDiscount[];
|
||||
interface DiscountStore {
|
||||
discounts: Discount[];
|
||||
}
|
||||
|
||||
export const useMessageStore = create<MessageStore>()(
|
||||
export const useDiscountStore = create<DiscountStore>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
discounts: mockDiscounts
|
||||
discounts: []
|
||||
}),
|
||||
{
|
||||
name: "Discounts",
|
||||
@ -20,4 +19,4 @@ export const useMessageStore = create<MessageStore>()(
|
||||
)
|
||||
);
|
||||
|
||||
export const setDiscounts = (discounts: AnyDiscount[]) => useMessageStore.setState({ discounts });
|
||||
export const setDiscounts = (discounts: DiscountStore["discounts"]) => useDiscountStore.setState({ discounts });
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
VerificationStatus,
|
||||
} from "@root/model/account";
|
||||
import { patchUserAccount } from "@root/api/account";
|
||||
import { deleteCart, patchCart } from "@root/api/cart";
|
||||
import { patchCurrency, deleteCart, patchCart } from "@root/api/cart";
|
||||
|
||||
interface UserStore {
|
||||
userId: string | null;
|
||||
@ -297,17 +297,17 @@ export const removeTariffFromCart = async (tariffId: string) => {
|
||||
setCart(result);
|
||||
};
|
||||
|
||||
const validators: Record<UserSettingsField | keyof UserName, StringSchema> = {
|
||||
email: string().email("Неверный email"),
|
||||
phoneNumber: string()
|
||||
.matches(/^[+\d|\d]*$/, "Неверный номер телефона")
|
||||
.min(6, "Номер телефона должен содержать минимум 6 символов"),
|
||||
password: string()
|
||||
.min(8, "Минимум 8 символов")
|
||||
.matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы в пароле")
|
||||
.optional(),
|
||||
firstname: string(),
|
||||
secondname: string(),
|
||||
middlename: string(),
|
||||
orgname: string(),
|
||||
export const changeUserCurrency = async (currency: string) => {
|
||||
const result = await patchCurrency(currency);
|
||||
setUserAccount(result);
|
||||
};
|
||||
|
||||
const validators: Record<UserSettingsField | keyof UserName, StringSchema> = {
|
||||
email: string().email("Неверный email"),
|
||||
phoneNumber: string().matches(/^[+\d|\d]*$/, "Неверный номер телефона").min(6, "Номер телефона должен содержать минимум 6 символов"),
|
||||
password: string().min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы в пароле").optional(),
|
||||
firstname: string(),
|
||||
secondname: string(),
|
||||
middlename: string(),
|
||||
orgname: string(),
|
||||
};
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { mockDiscounts } from "@root/__mocks__/discounts";
|
||||
import { CartData, PrivilegeCartData } from "@root/model/cart";
|
||||
import { AnyDiscount } from "@root/model/discount";
|
||||
import { CartData, Discount, PrivilegeCartData, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
||||
import { Tariff } from "@root/model/tariff";
|
||||
import { findPrivilegeDiscount, findServiceDiscount } from "./calcTariffPrices";
|
||||
|
||||
|
||||
export function calcCart(tariffs: Tariff[], discounts: AnyDiscount[] = mockDiscounts): CartData {
|
||||
export function calcCart(tariffs: Tariff[], discounts: Discount[], purchasesAmount: number): CartData {
|
||||
const cartData: CartData = {
|
||||
services: [],
|
||||
priceBeforeDiscounts: 0,
|
||||
priceAfterDiscounts: 0,
|
||||
itemCount: 0,
|
||||
envolvedDiscounts: [],
|
||||
};
|
||||
|
||||
tariffs.forEach(tariff => {
|
||||
@ -31,16 +29,17 @@ export function calcCart(tariffs: Tariff[], discounts: AnyDiscount[] = mockDisco
|
||||
|
||||
if (!tariff.price) cartData.priceBeforeDiscounts += privilegePrice;
|
||||
|
||||
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
|
||||
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
|
||||
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * privilege.amount, discounts);
|
||||
privilegePrice *= privilegeDiscount.factor;
|
||||
|
||||
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts);
|
||||
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
|
||||
privilegePrice *= serviceDiscount.factor;
|
||||
|
||||
const privilegeData: PrivilegeCartData = {
|
||||
tariffId: tariff._id,
|
||||
serviceKey: privilege.serviceKey,
|
||||
privilegeId: privilege.privilegeId,
|
||||
name: privilege.description,
|
||||
description: privilege.description,
|
||||
price: privilegePrice,
|
||||
};
|
||||
|
||||
@ -51,5 +50,11 @@ export function calcCart(tariffs: Tariff[], discounts: AnyDiscount[] = mockDisco
|
||||
});
|
||||
});
|
||||
|
||||
const cartDiscount = findCartDiscount(cartData.priceAfterDiscounts, discounts);
|
||||
cartData.priceAfterDiscounts *= cartDiscount.factor;
|
||||
|
||||
const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
||||
cartData.priceAfterDiscounts *= loyalDiscount.factor;
|
||||
|
||||
return cartData;
|
||||
}
|
||||
}
|
@ -1,69 +1,47 @@
|
||||
import { Tariff } from "@root/model/tariff";
|
||||
import { mockDiscounts } from "../__mocks__/discounts";
|
||||
import { PrivilegeWithAmount } from "@root/model/privilege";
|
||||
import { AnyDiscount, PrivilegeDiscount, ServiceDiscount } from "../model/discount";
|
||||
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
|
||||
import { CartData, Discount, findCartDiscount, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui";
|
||||
|
||||
|
||||
export function calcTariffPrices(tariff: Tariff, discounts: AnyDiscount[] = mockDiscounts): {
|
||||
export function calcIndividualTariffPrices(
|
||||
tariff: Tariff,
|
||||
discounts: Discount[],
|
||||
privilegies: ServiceKeyToPrivilegesMap,
|
||||
purchasesAmount: number,
|
||||
cart: CartData,
|
||||
): {
|
||||
price: number | undefined;
|
||||
priceWithDiscounts: number | undefined;
|
||||
tariffPriceAfterDiscounts: number | undefined;
|
||||
} {
|
||||
let price = tariff.price || tariff.privilegies.reduce((sum, privilege) => sum + privilege.amount * privilege.price, 0);
|
||||
|
||||
const priceWithDiscounts = tariff.privilegies.reduce((sum, privilege) => {
|
||||
let tariffPriceAfterDiscounts = tariff.privilegies.reduce((sum, privilege) => {
|
||||
let privilegePrice = privilege.amount * privilege.price;
|
||||
|
||||
const privilegeDiscount = findPrivilegeDiscount(privilege, discounts);
|
||||
if (privilegeDiscount) privilegePrice *= privilegeDiscount.target.products[0].factor;
|
||||
let realprivilegie = privilegies[privilege.serviceKey].find(e => e._id === privilege.privilegeId);
|
||||
if (realprivilegie) privilege.privilegeId = realprivilegie.privilegeId;
|
||||
|
||||
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice, discounts);
|
||||
if (serviceDiscount) privilegePrice *= serviceDiscount.target.factor;
|
||||
const privilegeDiscount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * privilege.amount, discounts);
|
||||
privilegePrice *= privilegeDiscount.factor;
|
||||
|
||||
const serviceCartData = cart.services.find(e => e.serviceKey === privilege.serviceKey);
|
||||
let serviceprice = 0;
|
||||
if (serviceCartData) serviceprice = serviceCartData.price;
|
||||
|
||||
const serviceDiscount = findServiceDiscount(privilege.serviceKey, privilegePrice + serviceprice, discounts);
|
||||
if (serviceDiscount) privilegePrice *= serviceDiscount.factor;
|
||||
|
||||
return sum + privilegePrice;
|
||||
}, 0);
|
||||
|
||||
const cartDiscount = findCartDiscount(tariffPriceAfterDiscounts + cart.priceAfterDiscounts, discounts);
|
||||
tariffPriceAfterDiscounts *= cartDiscount.factor;
|
||||
|
||||
const loyalDiscount = findLoyaltyDiscount(purchasesAmount, discounts);
|
||||
tariffPriceAfterDiscounts *= loyalDiscount.factor;
|
||||
|
||||
return {
|
||||
price,
|
||||
priceWithDiscounts,
|
||||
tariffPriceAfterDiscounts: tariffPriceAfterDiscounts,
|
||||
};
|
||||
}
|
||||
|
||||
export function findPrivilegeDiscount(privilege: PrivilegeWithAmount, discounts: AnyDiscount[]): PrivilegeDiscount | null {
|
||||
const applicableDiscounts = discounts.filter((discount): discount is PrivilegeDiscount => {
|
||||
return (
|
||||
discount.conditionType === "privilege" &&
|
||||
privilege.privilegeId === discount.condition.privilege.id &&
|
||||
privilege.amount >= discount.condition.privilege.value
|
||||
);
|
||||
});
|
||||
|
||||
if (!applicableDiscounts.length) return null;
|
||||
|
||||
const maxValueDiscount = applicableDiscounts.reduce((prev, current) =>
|
||||
current.condition.privilege.value > prev.condition.privilege.value ? current : prev
|
||||
);
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
||||
export function findServiceDiscount(
|
||||
serviceKey: string,
|
||||
currentPrice: number,
|
||||
discounts: AnyDiscount[],
|
||||
): ServiceDiscount | null {
|
||||
const discountsForTariffService = discounts.filter((discount): discount is ServiceDiscount => {
|
||||
return (
|
||||
discount.conditionType === "service" &&
|
||||
discount.condition.service.id === serviceKey &&
|
||||
currentPrice >= discount.condition.service.value
|
||||
);
|
||||
});
|
||||
|
||||
if (!discountsForTariffService.length) return null;
|
||||
|
||||
const maxValueDiscount = discountsForTariffService.reduce((prev, current) => {
|
||||
return current.condition.service.value > prev.condition.service.value ? current : prev;
|
||||
});
|
||||
|
||||
return maxValueDiscount;
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import { getTariffById } from "@root/api/tariff";
|
||||
import { useTariffStore } from "@root/stores/tariffs";
|
||||
import { useUserStore } from "@root/stores/user";
|
||||
import { removeTariffFromCart, useUserStore } from "@root/stores/user";
|
||||
import { useEffect } from "react";
|
||||
import { addCartTariffs, removeMissingCartTariffs, setCartTariffStatus, useCartStore } from "@root/stores/cart";
|
||||
import { Tariff } from "@root/model/tariff";
|
||||
import { isAxiosError } from "axios";
|
||||
import { useDiscountStore } from "@root/stores/discounts";
|
||||
|
||||
|
||||
export function useCart() {
|
||||
@ -12,6 +14,8 @@ export function useCart() {
|
||||
const cartTariffMap = useCartStore(state => state.cartTariffMap);
|
||||
const cartTariffIds = useUserStore(state => state.userAccount?.cart);
|
||||
const cart = useCartStore(state => state.cart);
|
||||
const discounts = useDiscountStore(state => state.discounts);
|
||||
const purchasesAmount = useUserStore(state => state.userAccount?.wallet.purchasesAmount) ?? 0;
|
||||
|
||||
useEffect(function addTariffsToCart() {
|
||||
const knownTariffs: Tariff[] = [];
|
||||
@ -26,21 +30,28 @@ export function useCart() {
|
||||
setCartTariffStatus(tariffId, "loading");
|
||||
|
||||
getTariffById(tariffId).then(tariff => {
|
||||
devlog("Unlisted tariff", tariff);
|
||||
addCartTariffs([tariff]);
|
||||
devlog("Unknown tariff", tariff);
|
||||
addCartTariffs([tariff], discounts, purchasesAmount);
|
||||
}).catch(error => {
|
||||
devlog(`Error fetching unlisted tariff ${tariffId}`, error);
|
||||
devlog(`Error fetching unknown tariff ${tariffId}`, error);
|
||||
setCartTariffStatus(tariffId, "not found");
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
removeTariffFromCart(tariffId).then(() => {
|
||||
devlog(`Unexistant tariff with id ${tariffId} deleted from cart`);
|
||||
}).catch(error => {
|
||||
devlog("Error deleting unexistant tariff from cart", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (knownTariffs.length > 0) addCartTariffs(knownTariffs);
|
||||
}, [cartTariffIds, cartTariffMap, tariffs]);
|
||||
if (knownTariffs.length > 0) addCartTariffs(knownTariffs, discounts, purchasesAmount);
|
||||
}, [cartTariffIds, cartTariffMap, discounts, purchasesAmount, tariffs]);
|
||||
|
||||
useEffect(function cleanUpCart() {
|
||||
if (cartTariffIds) removeMissingCartTariffs(cartTariffIds);
|
||||
}, [cartTariffIds]);
|
||||
if (cartTariffIds) removeMissingCartTariffs(cartTariffIds, discounts, purchasesAmount);
|
||||
}, [cartTariffIds, discounts, purchasesAmount]);
|
||||
|
||||
return cart;
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,36 @@
|
||||
import { devlog, makeRequest } from "@frontend/kitui";
|
||||
import { PrivilegeMap } from "@root/model/privilege";
|
||||
import { useEffect } from "react";
|
||||
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
|
||||
|
||||
export function useCustomTariffs({ onError, onNewUser, url }: {
|
||||
url: string;
|
||||
onNewUser: (response: PrivilegeMap) => void;
|
||||
onNewUser: (response: ServiceKeyToPrivilegesMap) => void;
|
||||
onError: (error: any) => void;
|
||||
}) {
|
||||
const onNewUserRef = useRef(onNewUser);
|
||||
const onErrorRef = useRef(onError);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewUserRef.current = onNewUser;
|
||||
onErrorRef.current = onError;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
makeRequest<never, PrivilegeMap>({
|
||||
makeRequest<never, ServiceKeyToPrivilegesMap>({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
method: "get",
|
||||
useToken: true,
|
||||
}).then(result => {
|
||||
onNewUser(result);
|
||||
onNewUserRef.current(result);
|
||||
}).catch(error => {
|
||||
devlog("Error fetching custom tariffs", error);
|
||||
onError(error);
|
||||
onErrorRef.current(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [onError, onNewUser, url]);
|
||||
}
|
||||
}, [url]);
|
||||
}
|
||||
|
36
src/utils/hooks/useDiscounts.ts
Normal file
36
src/utils/hooks/useDiscounts.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Discount, devlog, makeRequest } from "@frontend/kitui";
|
||||
import { GetDiscountsResponse } from "@root/model/discount";
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
|
||||
|
||||
export function useDiscounts({ url = "https://admin.pena.digital/price/discounts", onNewDiscounts, onError }: {
|
||||
url?: string;
|
||||
onNewDiscounts: (response: Discount[]) => void;
|
||||
onError: (error: Error) => void;
|
||||
}) {
|
||||
const onNewTariffsRef = useRef(onNewDiscounts);
|
||||
const onErrorRef = useRef(onError);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
onNewTariffsRef.current = onNewDiscounts;
|
||||
onErrorRef.current = onError;
|
||||
}, [onError, onNewDiscounts]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
makeRequest<never, GetDiscountsResponse>({
|
||||
url,
|
||||
method: "get",
|
||||
useToken: true,
|
||||
signal: controller.signal,
|
||||
}).then((result) => {
|
||||
onNewTariffsRef.current(result.Discounts);
|
||||
}).catch(error => {
|
||||
devlog("Error fetching tariffs", error);
|
||||
onErrorRef.current(error);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [url]);
|
||||
}
|
@ -38,4 +38,4 @@ export function useTariffs({ baseUrl = "https://admin.pena.digital/strator/tarif
|
||||
|
||||
return () => controller.abort();
|
||||
}, [apiPage, tariffsPerPage, baseUrl]);
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,18 @@ const lightTheme = createTheme({
|
||||
navbarbg: {
|
||||
main: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
filledError: {
|
||||
backgroundColor: "#FB5607",
|
||||
},
|
||||
root: {
|
||||
borderRadius: "8px",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
25
yarn.lock
25
yarn.lock
@ -1450,11 +1450,12 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@frontend/kitui@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.8.tgz#9d0619a558db6b417250dceaee60ec61af18b3fd"
|
||||
integrity sha1-nQYZpVjba0FyUNzq7mDsYa8Ys/0=
|
||||
"@frontend/kitui@^1.0.12":
|
||||
version "1.0.12"
|
||||
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.12.tgz#1bf72c4f119357801e858f9f5c25d588a8bb8198"
|
||||
integrity sha1-G/csTxGTV4AehY+fXCXViKi7gZg=
|
||||
dependencies:
|
||||
immer "^10.0.2"
|
||||
reconnecting-eventsource "^1.6.2"
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.6":
|
||||
@ -3304,10 +3305,10 @@ axe-core@^4.4.3:
|
||||
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz"
|
||||
integrity sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==
|
||||
|
||||
axios@^1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz"
|
||||
integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==
|
||||
axios@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
|
||||
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
@ -10895,9 +10896,9 @@ yup@^1.1.1:
|
||||
toposort "^2.0.2"
|
||||
type-fest "^2.19.0"
|
||||
|
||||
zustand@^4.3.6:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.npmjs.org/zustand/-/zustand-4.3.6.tgz"
|
||||
integrity sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==
|
||||
zustand@^4.3.8:
|
||||
version "4.3.9"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.9.tgz#a7d4332bbd75dfd25c6848180b3df1407217f2ad"
|
||||
integrity sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==
|
||||
dependencies:
|
||||
use-sync-external-store "1.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user