Merge branch 'user-modal' into 'main'

User modal

See merge request frontend/admin!34
This commit is contained in:
Nastya 2023-07-30 11:22:39 +00:00
commit b5f8b540da
47 changed files with 2612 additions and 945 deletions

11
cypress.config.ts Normal file

@ -0,0 +1,11 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
viewportWidth: 1200,
viewportHeight: 800,
fixturesFolder: "tests/e2e/fixtures",
supportFile: false,
defaultCommandTimeout: 100,
},
});

110
cypress/e2e/access.cy.ts Normal file

@ -0,0 +1,110 @@
describe("Форма Входа", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
});
it("должна успешно входить с правильными учетными данными", () => {
const email = "valid_user@example.com";
const password = "valid_password";
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('button[type="submit"]').click();
cy.url().should("include", "http://localhost:3000/users");
});
it("должна отображать сообщение об ошибке при неверном формате электронной почты", () => {
const invalidEmail = "invalid_email";
cy.get('input[name="email"]').type(invalidEmail);
cy.get('input[name="password"]').type("valid_password");
cy.get('button[type="submit"]').click();
cy.contains("Неверный формат эл. почты");
});
it("должна отображать сообщение об ошибке при отсутствии пароля", () => {
cy.get('input[name="email"]').type("valid_email@example.com");
cy.get('button[type="submit"]').click();
cy.contains("Введите пароль");
});
it("должна отображать сообщение об ошибке для недопустимого пароля", () => {
const invalidPassword = "short";
cy.get('input[name="email"]').type("valid_email@example.com");
cy.get('input[name="password"]').type(invalidPassword);
cy.get('button[type="submit"]').click();
cy.contains("Invalid password");
});
});
describe("Форма регистрации", () => {
beforeEach(() => {
cy.visit("http://localhost:3000/signup");
});
it("должна регистрировать нового пользователя с правильными данными", () => {
const email = Cypress._.random(1000) + "@example.com";
const password = "valid_password";
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('input[name="repeatPassword"]').type(password);
cy.get('button[type="submit"]').click();
cy.wait(5000);
cy.url().should("include", "http://localhost:3000/users");
});
it("должна отображать ошибку при неверном формате электронной почты", () => {
const invalidEmail = "invalid_email";
cy.get('input[name="email"]').type(invalidEmail);
cy.get('input[name="password"]').type("valid_password");
cy.get('input[name="repeatPassword"]').type("valid_password");
cy.get('button[type="submit"]').click();
cy.contains("Неверный формат эл. почты");
});
it("должна отображать ошибку при отсутствии пароля", () => {
cy.get('input[name="email"]').type("valid_email@example.com");
cy.get('input[name="repeatPassword"]').type("valid_password");
cy.get('button[type="submit"]').click();
cy.contains("Обязательное поле").should("have.length", 1);
});
it("должна отображать ошибку при несовпадении паролей", () => {
cy.get('input[name="email"]').type("valid_email@example.com");
cy.get('input[name="password"]').type("valid_password");
cy.get('input[name="repeatPassword"]').type("different_password");
cy.get('button[type="submit"]').click();
cy.contains("Пароли не совпадают");
});
it("попытка отправки запроса при уже зарегистрированном пользователе", () => {
const email = "users@gmail.com";
const password = "12344321";
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('input[name="repeatPassword"]').type(password);
cy.intercept("POST", "https://admin.pena.digital/auth/register").as("registerRequest");
cy.get('button[type="submit"]').click();
cy.wait("@registerRequest");
cy.wait(5000);
cy.contains("user with this login is exist");
});
});

195
cypress/e2e/tariffs.cy.ts Normal file

@ -0,0 +1,195 @@
describe("Форма Создания Тарифа", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.get('input[name="email"]').type("valid_user@example.com");
cy.get('input[name="password"]').type("valid_password");
cy.get('button[type="submit"]').click();
cy.wait(3000);
cy.url().should("include", "http://localhost:3000/users");
cy.visit("http://localhost:3000/tariffs");
});
it("должна отображать сообщение об ошибке при пустом названии тарифа", () => {
cy.get('input[id="tariff-amount"]').type("10");
// Выбрать первую привилегию с нужным текстом из выпадающего списка
cy.get("#privilege-select").click();
cy.get(`[data-cy = "select-option-Количество шаблонов, которые может сделать пользователь сервиса"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
cy.contains("Пустое название тарифа");
});
it("должна отображать сообщение об ошибке при отсутствии выбора привилегии", () => {
cy.get('input[id="tariff-name"]').type("Тестовый Тариф");
cy.get('input[id="tariff-amount"]').type("10");
cy.get(".btn_createTariffBackend").click({ force: true });
cy.contains("Не выбрана привилегия");
});
it("Создание трех тарифов", () => {
cy.get('input[id="tariff-name"]').type("Тестовый Тариф 1");
cy.get('input[id="tariff-amount"]').type("10");
cy.get("#privilege-select").click();
cy.get(`[data-cy="select-option-Количество шаблонов, которые может сделать пользователь сервиса"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
cy.get('input[id="tariff-name"]').scrollIntoView().clear({ force: true }).type("Тестовый Тариф 2");
cy.get('input[id="tariff-amount"]').scrollIntoView().clear({ force: true }).type("15");
cy.get("#privilege-select").click();
cy.wait(800);
cy.get(`[data-cy="select-option-Количество дней, в течении которых пользование сервисом безлимитно"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
cy.get('input[id="tariff-name"]').scrollIntoView().clear({ force: true }).type("Тестовый Тариф 3");
cy.get('input[id="tariff-amount"]').scrollIntoView().clear({ force: true }).type("20");
cy.get("#privilege-select").click();
cy.wait(800);
cy.get(`[data-cy="select-option-Обьём ПенаДиска для хранения шаблонов и результатов шаблонизации"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
});
});
describe("Форма Создания Тарифа", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.get('input[name="email"]').type("valid_user@example.com");
cy.get('input[name="password"]').type("valid_password");
cy.get('button[type="submit"]').click();
cy.wait(3000);
cy.url().should("include", "http://localhost:3000/users");
cy.visit("http://localhost:3000/tariffs");
});
it("Удаление тарифа единично, одного за другим ", () => {
const tariffNamesToFind = ["Тестовый Тариф 1", "Тестовый Тариф 2", "Тестовый Тариф 3"];
// Поиск каждого тарифа в DataGrid
cy.wait(3000);
tariffNamesToFind.forEach((tariffName) => {
cy.get(".tariffs-data-grid").scrollIntoView().contains(tariffName).should("be.visible");
});
const deleteTariffs = () => {
let tariffsFound = true;
cy.get(".tariffs-data-grid .MuiDataGrid-row").then(($rows) => {
const rowCount = $rows.length;
if (rowCount === 1) {
cy.log("Тарифы не найдены. Тест завершен.");
tariffsFound = false;
}
cy.wrap($rows).each(($row) => {
// Шаг 2: В каждом элементе найдите все дивы вложенные внутрь и выберите последний див
cy.wrap($row).find("div").last().scrollIntoView().get(".delete-tariff-button").last().click({ force: true });
});
cy.wait(2000);
cy.contains("Да")
.click()
.then(() => {
if (!tariffsFound) {
return;
}
cy.wait(5000);
deleteTariffs();
});
});
};
deleteTariffs();
// Проверяем что Дата грид тарифов пустой
cy.wait(2000);
cy.get(".tariffs-data-grid .MuiDataGrid-row").should("not.exist");
});
describe("Форма Создания Тарифа", () => {});
it("Удаление тарифов массово через DataGrid", () => {
// Добавляем 3 тариффа
cy.get('input[id="tariff-name"]').type("Тестовый Тариф 1");
cy.get('input[id="tariff-amount"]').type("10");
cy.get("#privilege-select").click();
cy.get(`[data-cy="select-option-Количество шаблонов, которые может сделать пользователь сервиса"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
cy.get('input[id="tariff-name"]').scrollIntoView().clear({ force: true }).type("Тестовый Тариф 2");
cy.get('input[id="tariff-amount"]').scrollIntoView().clear({ force: true }).type("15");
cy.get("#privilege-select").click();
cy.wait(800);
cy.get(`[data-cy="select-option-Количество дней, в течении которых пользование сервисом безлимитно"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
cy.get('input[id="tariff-name"]').scrollIntoView().clear({ force: true }).type("Тестовый Тариф 3");
cy.get('input[id="tariff-amount"]').scrollIntoView().clear({ force: true }).type("20");
cy.get("#privilege-select").click();
cy.wait(800);
cy.get(`[data-cy="select-option-Обьём ПенаДиска для хранения шаблонов и результатов шаблонизации"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
// Добавляем 3 тариффа
cy.wait(3000);
cy.get(".tariffs-data-grid .PrivateSwitchBase-input").first().click({ multiple: true });
// Проверить, что кнопка "Удалить" появилась
cy.wait(2000);
cy.contains("Удаление").should("be.visible");
// Нажать на кнопку "Удалить"
cy.contains("Удаление").click();
// Подтверждение удаления (если нужно)
cy.contains("Да").click();
// Проверяем что Дата грид тарифов пустой
cy.wait(2000);
cy.get(".tariffs-data-grid .MuiDataGrid-row").should("not.exist");
});
it("Добавление тарифом в корзину", () => {
// Добавляем 3 тариффа
cy.get('input[id="tariff-name"]').type("Тестовый Тариф 1");
cy.get('input[id="tariff-amount"]').type("10");
cy.get("#privilege-select").click();
cy.get(`[data-cy="select-option-Количество шаблонов, которые может сделать пользователь сервиса"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
cy.get('input[id="tariff-name"]').scrollIntoView().clear({ force: true }).type("Тестовый Тариф 2");
cy.get('input[id="tariff-amount"]').scrollIntoView().clear({ force: true }).type("15");
cy.get("#privilege-select").click();
cy.wait(800);
cy.get(`[data-cy="select-option-Количество дней, в течении которых пользование сервисом безлимитно"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
cy.get('input[id="tariff-name"]').scrollIntoView().clear({ force: true }).type("Тестовый Тариф 3");
cy.get('input[id="tariff-amount"]').scrollIntoView().clear({ force: true }).type("20");
cy.get("#privilege-select").click();
cy.wait(800);
cy.get(`[data-cy="select-option-Обьём ПенаДиска для хранения шаблонов и результатов шаблонизации"]`).click();
cy.get(".btn_createTariffBackend").click({ force: true });
// Добавляем 3 тариффа
cy.wait(3000);
cy.get(".tariffs-data-grid .PrivateSwitchBase-input").first().click({ multiple: true });
// Проверить, что кнопка "Удалить" появилась
cy.wait(2000);
cy.contains("рассчитать").should("be.visible");
// Нажать на кнопку "Удалить"
cy.contains("рассчитать").click();
// смотрим что в корзине ровно столько тарифом, сколько мы добовляли
cy.wait(5000);
cy.get(".MuiTable-root tbody tr").its("length").should("eq", 3);
});
});

@ -24,6 +24,7 @@
"@types/react-router-dom": "^5.3.3",
"axios": "^1.3.4",
"craco": "^0.0.3",
"cypress": "^12.17.2",
"dayjs": "^1.11.5",
"formik": "^2.2.9",
"immer": "^10.0.2",
@ -37,6 +38,7 @@
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"reconnecting-eventsource": "^1.6.2",
"start-server-and-test": "^2.0.0",
"styled-components": "^5.3.5",
"typescript": "^4.8.2",
"web-vitals": "^2.1.4",
@ -47,6 +49,8 @@
"build": "craco build",
"test": "craco test",
"test:cart": "craco test src/kitUI/Cart --watchAll=false",
"test:cypress": "start-server-and-test start http://localhost:3000 cypress",
"cypress": "cypress open",
"eject": "craco eject"
},
"eslintConfig": {

@ -21,7 +21,15 @@ export const MOCK_DATA_USERS = [
export type TMockData = typeof MOCK_DATA_USERS;
export type UsersType = { login: string; email: string; phoneNumber: string; isDeleted: boolean; createdAt: string }[];
export type UserType = {
_id: string;
login: string;
email: string;
phoneNumber: string;
isDeleted: boolean;
createdAt: string;
updatedAt: string;
};
export const getRoles_mock = (): Promise<TMockData> => {
return new Promise((resolve) => {

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="black">
<path d="M9.08717 1.85277C9.51062 1.18151 10.4894 1.18151 10.9128 1.85277C11.5762 2.90442 12.8955 3.33307 14.0503 2.87221C14.7875 2.57804 15.5793 3.15335 15.5273 3.9453C15.4459 5.18603 16.2612 6.30826 17.4664 6.61422C18.2357 6.8095 18.5381 7.74037 18.0306 8.35052C17.2354 9.30642 17.2354 10.6936 18.0306 11.6495C18.5381 12.2596 18.2357 13.1905 17.4664 13.3858C16.2612 13.6917 15.4459 14.814 15.5273 16.0547C15.5793 16.8467 14.7875 17.422 14.0503 17.1278C12.8955 16.6669 11.5762 17.0956 10.9128 18.1472C10.4894 18.8185 9.51062 18.8185 9.08717 18.1472C8.42377 17.0956 7.10451 16.6669 5.94967 17.1278C5.21254 17.422 4.4207 16.8467 4.47269 16.0547C4.55413 14.814 3.73878 13.6917 2.53361 13.3858C1.76435 13.1905 1.46189 12.2596 1.96945 11.6495C2.76462 10.6936 2.76462 9.30643 1.96945 8.35052C1.46189 7.74037 1.76435 6.8095 2.53361 6.61422C3.73878 6.30826 4.55413 5.18604 4.47269 3.9453C4.4207 3.15335 5.21254 2.57804 5.94968 2.87221C7.10451 3.33307 8.42377 2.90442 9.08717 1.85277Z" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M6.65234 10.5977L8.68495 12.5107L13.2284 7.84766" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.03641 9.47C4.92841 9.47 4.83841 9.44 4.76641 9.38C4.70641 9.308 4.67641 9.218 4.67641 9.11V8.39C4.67641 8.234 4.70041 8.114 4.74841 8.03C4.79641 7.934 4.88041 7.826 5.00041 7.706L7.70041 5.078L5.00041 2.45C4.88041 2.33 4.79641 2.228 4.74841 2.144C4.70041 2.048 4.67641 1.922 4.67641 1.766V1.046C4.67641 0.937999 4.70641 0.853999 4.76641 0.793999C4.83841 0.722 4.92841 0.686 5.03641 0.686C5.09641 0.686 5.16241 0.704 5.23441 0.739999C5.30641 0.763999 5.37241 0.806 5.43241 0.866L8.92441 4.25C9.03241 4.358 9.10441 4.46 9.14041 4.556C9.17641 4.652 9.20041 4.748 9.21241 4.844V5.312C9.20041 5.408 9.17641 5.504 9.14041 5.6C9.10441 5.696 9.03241 5.798 8.92441 5.906L5.43241 9.29C5.37241 9.35 5.30641 9.398 5.23441 9.434C5.16241 9.458 5.09641 9.47 5.03641 9.47ZM0.428414 9.47C0.320414 9.47 0.230414 9.44 0.158414 9.38C0.0864141 9.308 0.0504141 9.218 0.0504141 9.11V8.39C0.0504141 8.234 0.0744141 8.114 0.122414 8.03C0.170414 7.934 0.254414 7.826 0.374414 7.706L3.09241 5.078L0.374414 2.45C0.254414 2.33 0.170414 2.228 0.122414 2.144C0.0744141 2.048 0.0504141 1.922 0.0504141 1.766V1.046C0.0504141 0.937999 0.0864141 0.853999 0.158414 0.793999C0.230414 0.722 0.320414 0.686 0.428414 0.686C0.488414 0.686 0.548414 0.704 0.608414 0.739999C0.680414 0.763999 0.752414 0.806 0.824414 0.866L4.29841 4.25C4.40641 4.358 4.47841 4.46 4.51441 4.556C4.56241 4.652 4.59241 4.748 4.60441 4.844V5.312C4.59241 5.408 4.56241 5.504 4.51441 5.6C4.47841 5.696 4.40641 5.798 4.29841 5.906L0.824414 9.29C0.752414 9.35 0.680414 9.398 0.608414 9.434C0.548414 9.458 0.488414 9.47 0.428414 9.47Z" fill="#7E2AEA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="black">
<path d="M12 8C12 7.5 12 6.1 12 4.5C12 2.5 11 1 9 1C7 1 6 3 6 4.5C6 5.7 6 7.33333 6 8M2.8 5.5H15.2L16.5 16.5H1.5L2.8 5.5Z" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="black">
<path d="M1.66602 5.83268H16.666M13.3327 1.66602L17.4993 5.83268L13.3327 9.99935M18.3327 14.166H3.33268M6.66602 9.99935L2.49935 14.166L6.66602 18.3327" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 338 B

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" fill="black">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.4479 15.1701C2.62418 15.3572 2.8084 15.5368 3 15.7083C4.59227 17.1334 6.69494 18 9 18C11.3051 18 13.4077 17.1334 15 15.7083C15.1916 15.5368 15.3758 15.3572 15.5521 15.1701C17.0699 13.559 18 11.3881 18 9C18 4.02944 13.9706 0 9 0C4.02944 0 0 4.02944 0 9C0 11.3881 0.930136 13.559 2.4479 15.1701ZM4.40572 14.9287C5.67542 15.9144 7.26787 16.5 9 16.5C10.7321 16.5 12.3246 15.9144 13.5943 14.9287C12.6863 13.7089 11.2862 12.45 9 12.45C6.71379 12.45 5.31371 13.7089 4.40572 14.9287ZM14.6883 13.8884C13.6143 12.4984 11.8517 10.95 9 10.95C6.14827 10.95 4.38567 12.4984 3.31172 13.8884C2.18127 12.5747 1.5 10.8676 1.5 9C1.5 4.85786 4.85786 1.5 9 1.5C13.1421 1.5 16.5 4.85786 16.5 9C16.5 10.8676 15.8187 12.5747 14.6883 13.8884Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 9.1001C9.82843 9.1001 10.5 8.42852 10.5 7.6001C10.5 6.77167 9.82843 6.1001 9 6.1001C8.17157 6.1001 7.5 6.77167 7.5 7.6001C7.5 8.42852 8.17157 9.1001 9 9.1001ZM9 10.6001C10.6569 10.6001 12 9.25695 12 7.6001C12 5.94324 10.6569 4.6001 9 4.6001C7.34315 4.6001 6 5.94324 6 7.6001C6 9.25695 7.34315 10.6001 9 10.6001Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -1,30 +0,0 @@
import { authStore } from "@root/stores/auth";
import { setDiscounts } from "@root/stores/discounts";
import type { GetDiscountResponse } from "@root/model/discount";
const makeRequest = authStore.getState().makeRequest;
export const useDiscounts = () => {
const requestDiscounts = async () => {
const controller = new AbortController();
makeRequest<never, GetDiscountResponse>({
url: "https://admin.pena.digital/price/discounts",
method: "get",
useToken: true,
bearer: true,
signal: controller.signal,
})
.then((result) => {
setDiscounts(result.Discounts);
})
.catch((error) => {
console.log("Error fetching discounts", error);
});
return () => controller.abort();
};
return { requestDiscounts };
};

@ -1,88 +0,0 @@
import { useState, useEffect } from "react";
import { authStore } from "@root/stores/auth";
import { resetPrivilegeArray } from "@root/stores/privilegesStore";
import { exampleCartValues } from "@stores/mocks/exampleCartValues";
import type { RealPrivilege } from "@root/model/privilege";
export type Privilege = {
createdAt: string;
description: string;
isDeleted: boolean;
name: string;
price: number;
privilegeId: string;
serviceKey: string;
type: "count" | "day" | "mb";
updatedAt: string;
value: string;
_id: string;
};
type SeverPrivilegiesResponse = {
templategen: RealPrivilege[];
};
type UsePrivilegies = {
requestPrivilegies: () => Promise<void>;
isError: boolean;
isLoading: boolean;
errorMessage: string;
};
const baseUrl =
process.env.NODE_ENV === "production"
? "/strator"
: "https://admin.pena.digital/strator";
export const usePrivilegies = (): UsePrivilegies => {
const [privilegies, setPrivilegies] = useState<RealPrivilege[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const { makeRequest } = authStore.getState();
useEffect(() => {
let extracted: RealPrivilege[] = [];
for (let serviceKey in privilegies) {
//Приходит объект. В его значениях массивы привилегий для разных сервисов. Высыпаем в общую кучу и обновляем стор
extracted = extracted.concat(privilegies[serviceKey]);
}
let readyArray = extracted.map((privilege) => ({
serviceKey: privilege.serviceKey,
privilegeId: privilege.privilegeId,
name: privilege.name,
description: privilege.description,
type: privilege.type,
price: privilege.price,
value: privilege.value,
id: privilege._id,
}));
resetPrivilegeArray([...readyArray, ...exampleCartValues.privileges]);
}, [privilegies]);
const requestPrivilegies = async () => {
setIsLoading(true);
await makeRequest<never, SeverPrivilegiesResponse>({
url: baseUrl + "/privilege/service",
method: "get",
})
.then(({ templategen }) => setPrivilegies(templategen))
.catch(() => {
setIsError(true);
setErrorMessage("Ошибка при получении привилегий");
})
.finally(() => setIsLoading(false));
};
return {
requestPrivilegies,
isError,
isLoading,
errorMessage,
};
};

@ -1,77 +0,0 @@
import { useEffect, useState } from "react";
import { Tariff, Tariff_BACKEND } from "@root/model/tariff";
import { resetTariffsStore } from "@root/stores/tariffsStore";
import { authStore } from "@root/stores/auth";
type UseGetTariffs = {
requestTariffs: (page?: number, tariffs?: Tariff_BACKEND[]) => Promise<void>;
isLoading: boolean;
};
type GetTariffsResponse = {
totalPages: number;
tariffs: Tariff_BACKEND[];
};
const baseUrl =
process.env.NODE_ENV === "production"
? "/strator"
: "https://admin.pena.digital/strator";
export const useTariffs = (): UseGetTariffs => {
const [isLoading, setIsLoading] = useState(false);
const [tariffsList, setTariffsList] = useState<Tariff_BACKEND[]>([]);
const { makeRequest } = authStore.getState();
useEffect(() => {
const convertedTariffs: Record<string, Tariff> = {};
tariffsList
.filter(({ isDeleted }) => !isDeleted)
.forEach((tariff) => {
convertedTariffs[tariff._id] = {
id: tariff._id,
name: tariff.name,
isCustom: tariff.price ? true : false,
amount: tariff.privilegies[0].amount,
isFront: false,
privilegeId: tariff.privilegies[0].privilegeId,
price: tariff.privilegies[0].price,
isDeleted: tariff.isDeleted,
customPricePerUnit: tariff.price,
};
});
resetTariffsStore(convertedTariffs);
}, [tariffsList]);
const requestTariffs = async (
page: number = 1,
existingTariffs: Tariff_BACKEND[] = []
): Promise<void> => {
setIsLoading(true);
try {
const { tariffs, totalPages } = await makeRequest<
never,
GetTariffsResponse
>({
url: baseUrl + `/tariff/?page=${page}&limit=${100}`,
method: "get",
bearer: true,
});
if (page < totalPages) {
return requestTariffs(page + 1, [...existingTariffs, ...tariffs]);
}
setTariffsList([...existingTariffs, ...tariffs]);
} catch {
throw new Error("Ошибка при получении тарифов");
} finally {
setIsLoading(false);
}
};
return { requestTariffs, isLoading };
};

@ -17,6 +17,7 @@ import Sections from "@pages/Sections";
import Dashboard from "@pages/dashboard";
import Error404 from "@pages/Error404";
import Users from "@pages/dashboard/Content/Users";
import ModalUser from "@root/pages/dashboard/ModalUser";
import Entities from "@pages/dashboard/Content/Entities";
import Tariffs from "@pages/dashboard/Content/Tariffs";
import DiscountManagement from "@root/pages/dashboard/Content/DiscountManagement/DiscountManagement";
@ -29,6 +30,7 @@ import "./index.css";
const componentsArray = [
["/users", <Users />],
["/users/:userId", <ModalUser />],
["/entities", <Entities />],
["/tariffs", <Tariffs />],
["/discounts", <DiscountManagement />],

@ -25,20 +25,19 @@ import {
formatDiscountFactor,
} from "./calc";
import { AnyDiscount, CartItemTotal } from "@root/model/cart";
import { CartItemTotal } from "@root/model/cart";
import { Privilege } from "@root/model/tariff";
import { useDiscountStore, setDiscounts } from "@root/stores/discounts";
import { useCartStore } from "@root/stores/cart";
import { findPrivilegeById } from "@root/stores/privilegesStore";
import { testUser } from "@root/stores/mocks/user";
import { useTariffStore } from "@root/stores/tariffsStore";
import { Discount } from "@root/model/discount";
import { authStore } from "@root/stores/auth";
import { useDiscountStore } from "@root/stores/discounts";
import { requestPrivilegies } from "@root/services/privilegies.service";
import { requestDiscounts } from "@root/services/discounts.service";
interface Props {
requestPrivilegies: () => Promise<void>;
requestDiscounts: () => Promise<() => void>;
selectedTariffs: GridSelectionModel;
}
@ -58,19 +57,13 @@ interface MergedTariff {
isFront: boolean;
}
export default function Cart({
requestPrivilegies,
requestDiscounts,
selectedTariffs,
}: Props) {
export default function Cart({ selectedTariffs }: Props) {
let cartTariffs = Object.values(useTariffStore().tariffs);
// const discounts = useDiscountStore((store) => store.discounts);
const [discounts, setDiscounts] = useState<Discount[]>([]);
const discounts = useDiscountStore((store) => store.discounts);
const cartTotal = useCartStore((state) => state.cartTotal);
const setCartTotal = useCartStore((store) => store.setCartTotal);
const [couponField, setCouponField] = useState<string>("");
const [loyaltyField, setLoyaltyField] = useState<string>("");
const makeRequest = authStore.getState().makeRequest;
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isNonCommercial, setIsNonCommercial] = useState<boolean>(false);
@ -157,17 +150,6 @@ export default function Cart({
await requestPrivilegies();
await requestDiscounts();
//рассчитать
const dis = await makeRequest<unknown>({
url: "https://admin.pena.digital/price/discounts",
method: "get",
useToken: true,
bearer: true,
});
console.log(dis);
// @ts-ignore
setDiscounts(dis.Discounts);
const cartItems = cartTariffs
.filter((tariff) => selectedTariffs.includes(tariff.id))
.map((tariff) => createCartItem(tariff));
@ -178,15 +160,11 @@ export default function Cart({
if (!isFinite(loyaltyValue)) loyaltyValue = 0;
const activeDiscounts = discounts.filter(
(discount) => !discount.Deprecated
);
const cartData = calcCartData({
user: testUser,
purchasesAmount: loyaltyValue,
cartItems,
discounts: activeDiscounts,
discounts,
isNonCommercial,
coupon: couponField,
});

@ -1,14 +1,6 @@
import {
CartItem,
AnyDiscount,
CartTotal,
CartItemTotal,
PrivilegeDiscount,
CartPurchasesAmountDiscount,
PurchasesAmountDiscount,
ServiceToPriceMap,
ServiceDiscount,
UserDiscount,
CartItem, CartTotal,
CartItemTotal, ServiceToPriceMap
} from "@root/model/cart";
import { Discount } from "@root/model/discount";
import { User } from "../../model/user";

@ -1,15 +1,18 @@
import { styled } from "@mui/material/styles";
import { TextField} from "@mui/material";
import { TextField } from "@mui/material";
export default styled(TextField)(({ theme }) => ({
color: theme.palette.grayLight.main,
"& .MuiInputLabel-root": {
color: theme.palette.grayLight.main,
"& .MuiInputLabel-root": {
color: theme.palette.grayLight.main,
},
"& .MuiFilledInput-root": {
border: theme.palette.grayLight.main+" 1px solid",
borderRadius: "0",
backgroundColor: theme.palette.hover.main,
color: theme.palette.grayLight.main,
}
},
"& .MuiFilledInput-root": {
border: theme.palette.grayLight.main + " 1px solid",
borderRadius: "0",
backgroundColor: theme.palette.hover.main,
color: theme.palette.grayLight.main,
},
"& .MuiFilledInput-root.Mui-error": {
fontSize: "8px",
},
}));

@ -1,105 +1,6 @@
import { ServiceType, Privilege, Tariff } from "./tariff";
import { Discount } from "@root/model/discount";
interface DiscountBase {
_id: string;
name: string;
description: string;
/** Этап применения скидки */
layer?: number;
disabled?: boolean;
}
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: ServiceType;
/** Скидка применяется, если значение больше или равно этому значению */
value: number;
};
};
target: {
service: ServiceType;
/** Множитель, на который умножается сумма при применении скидки */
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"];
export interface Promocode {
id: string;

@ -19,11 +19,13 @@ function validate(values: Values) {
const errors = {} as any;
if (!values.email) {
errors.email = "Required";
errors.email = "Обязательное поле";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = "Неверный формат эл. почты";
}
if (!values.password) {
errors.password = "Required";
errors.password = "Введите пароль";
}
if (values.password && !/^[\S]{8,25}$/i.test(values.password)) {
@ -107,11 +109,34 @@ const SigninForm = () => {
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<EmailOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field as={OutlinedInput} name="email" variant="filled" label="Эл. почта" />
<Field
as={OutlinedInput}
name="email"
variant="filled"
label="Эл. почта"
error={props.touched.email && !!props.errors.email}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.email && props.errors.email}
</Typography>
}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field as={OutlinedInput} type="password" name="password" variant="filled" label="Пароль" />
<Field
as={OutlinedInput}
type="password"
name="password"
variant="filled"
label="Пароль"
error={props.touched.password && !!props.errors.password}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.password && props.errors.password}
</Typography>
}
/>
</Box>
<Box
component="article"

@ -1,4 +1,4 @@
import { enqueueSnackbar } from "notistack";
import { enqueueSnackbar, useSnackbar } from "notistack";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import { Formik, Field, Form } from "formik";
@ -10,142 +10,188 @@ import Logo from "@pages/Logo/index";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { authStore } from "@root/stores/auth";
interface Values {
email: string;
password: string;
repeatPassword: string;
email: string;
password: string;
repeatPassword: string;
}
function validate(values: Values) {
const errors = {} as any;
if (!values.email) {
errors.login = "Required";
}
if (!values.password) {
errors.password = "Required";
} else if (!/^[\S]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password";
}
if (values.password !== values.repeatPassword) {
errors.repeatPassword = "Passwords do not match";
}
return errors;
const errors: Partial<Values> = {};
if (!values.email) {
errors.email = "Обязательное поле";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = "Неверный формат эл. почты";
}
if (!values.password) {
errors.password = "Обязательное поле";
} else if (!/^[\S]{8,25}$/i.test(values.password)) {
errors.password = "Неверный пароль";
}
if (values.password !== values.repeatPassword) {
errors.repeatPassword = "Пароли не совпадают";
}
return errors;
}
const SignUp = () => {
const navigate = useNavigate();
const theme = useTheme();
const { makeRequest } = authStore();
const navigate = useNavigate();
const theme = useTheme();
const { makeRequest } = authStore();
const { enqueueSnackbar } = useSnackbar();
return (
<Formik
initialValues={{
email: "",
password: "",
repeatPassword: "",
return (
<Formik
initialValues={{
email: "",
password: "",
repeatPassword: "",
}}
validate={validate}
onSubmit={(values, formikHelpers) => {
formikHelpers.setSubmitting(true);
makeRequest({
url: "https://admin.pena.digital/auth/register",
body: {
login: values.email,
email: values.email,
password: values.repeatPassword,
phoneNumber: "--",
},
useToken: false,
})
.then((e) => {
navigate("/users");
})
.catch((e) => {
console.log(e);
enqueueSnackbar(
e.response && e.response.data && e.response.data.message ? e.response.data.message : `Unknown error`,
{ variant: "error" } // Устанавливаем вариант уведомления на "error"
);
})
.finally(() => {
formikHelpers.setSubmitting(false);
});
}}
>
{(props) => (
<Form>
<Box
component="section"
sx={{
minHeight: "100vh",
height: "100%",
width: "100%",
backgroundColor: theme.palette.content.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "15px 0",
}}
validate={validate}
onSubmit={(values, formikHelpers) => {
formikHelpers.setSubmitting(true);
makeRequest({
url: "https://admin.pena.digital/auth/register",
body: {
login: values.email,
email: values.email,
password: values.repeatPassword,
phoneNumber: "--",
},
useToken: false,
})
.then((e) => {
navigate("/users");
})
.catch((e) => {
console.log(e);
enqueueSnackbar(
e.response && e.response.data && e.response.data.message ? e.response.data.message : `Unknown error`
);
}).finally(() => {
formikHelpers.setSubmitting(false);
});
}}
>
{props =>
<Form>
<Box
component="section"
sx={{
minHeight: "100vh",
height: "100%",
width: "100%",
backgroundColor: theme.palette.content.main,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "15px 0",
}}
>
<Box
component="article"
sx={{
width: "350px",
backgroundColor: theme.palette.content.main,
display: "flex",
flexDirection: "column",
justifyContent: "center",
"> *": {
marginTop: "15px",
},
}}
>
<Typography variant="h6" color={theme.palette.secondary.main}>
Новый аккаунт
</Typography>
<Logo />
>
<Box
component="article"
sx={{
width: "350px",
backgroundColor: theme.palette.content.main,
display: "flex",
flexDirection: "column",
justifyContent: "center",
"> *": {
marginTop: "15px",
},
}}
>
<Typography variant="h6" color={theme.palette.secondary.main}>
Новый аккаунт
</Typography>
<Logo />
<Box>
<Typography variant="h5" color={theme.palette.secondary.main}>
Добро пожаловать
</Typography>
<Typography variant="h6" color={theme.palette.secondary.main}>
Мы рады что вы выбрали нас!
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<EmailOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field as={OutlinedInput} name="email" variant="filled" label="Эл. почта" />
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field as={OutlinedInput} type="password" name="password" variant="filled" label="Пароль" />
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field
as={OutlinedInput}
type="password"
name="repeatPassword"
variant="filled"
label="Повторите пароль"
/>
</Box>
<Button
type="submit"
disabled={props.isSubmitting}
sx={{
width: "250px",
margin: "15px auto",
padding: "20px 30px",
fontSize: 18,
}}
>Войти</Button>
<Link to="/signin" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}>У меня уже есть аккаунт</Typography>
</Link>
</Box>
</Box>
</Form>
}
</Formik>
);
<Box>
<Typography variant="h5" color={theme.palette.secondary.main}>
Добро пожаловать
</Typography>
<Typography variant="h6" color={theme.palette.secondary.main}>
Мы рады что вы выбрали нас!
</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<EmailOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field
as={OutlinedInput}
name="email"
variant="filled"
label="Эл. почта"
id="email"
error={props.touched.email && !!props.errors.email}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.email && props.errors.email}
</Typography>
}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field
sx={{}}
as={OutlinedInput}
type="password"
name="password"
variant="filled"
label="Пароль"
id="password"
error={props.touched.password && !!props.errors.password}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.password && props.errors.password}
</Typography>
}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field
as={OutlinedInput}
type="password"
name="repeatPassword"
variant="filled"
label="Повторите пароль"
id="repeatPassword"
error={props.touched.repeatPassword && !!props.errors.repeatPassword}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.repeatPassword && props.errors.repeatPassword}
</Typography>
}
/>
</Box>
<Button
type="submit"
disabled={props.isSubmitting}
sx={{
width: "250px",
margin: "15px auto",
padding: "20px 30px",
fontSize: 18,
}}
>
Войти
</Button>
<Link to="/signin" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}>У меня уже есть аккаунт</Typography>
</Link>
</Box>
</Box>
</Form>
)}
</Formik>
);
};
export default SignUp;

@ -1,16 +1,33 @@
import { Typography } from "@mui/material";
import { СardPrivilegie } from "./CardPrivilegie";
import { useEffect } from "react";
import { usePrivilegeStore } from "@root/stores/privilegesStore";
import { requestPrivilegies } from "@root/services/privilegies.service";
import { СardPrivilegie } from "./CardPrivilegie";
export default function ListPrivilegie() {
const privileges = usePrivilegeStore().privileges;
const privileges = usePrivilegeStore((state) => state.privileges);
useEffect(() => {
requestPrivilegies();
}, []);
return (
<>
{privileges.map(({ name, type, price, description, value, privilegeId, serviceKey, id, amount }) => (
{privileges.map(
({
name,
type,
price,
description,
value,
privilegeId,
serviceKey,
id,
amount,
}) => (
<СardPrivilegie
key={id}
key={privilegeId}
name={name}
type={type}
amount={1}
@ -20,8 +37,8 @@ export default function ListPrivilegie() {
serviceKey={serviceKey}
description={description}
/>
))
}
)
)}
</>
);
}

@ -1,47 +1,51 @@
import { Box, Button, IconButton, useTheme } from "@mui/material";
import { DataGrid, GridColDef, GridRowsProp, GridToolbar } from "@mui/x-data-grid";
import { formatDiscountFactor } from "@root/kitUI/Cart/calc";
import { openEditDiscountDialog, setDiscounts, setSelectedDiscountIds, updateDiscount, useDiscountStore } from "@root/stores/discounts";
import { enqueueSnackbar } from "notistack";
import { GridSelectionModel } from "@mui/x-data-grid";
import { Box, Button } from "@mui/material";
import { changeDiscount } from "@root/api/discounts";
import { findDiscountsById } from "@root/stores/discounts";
import { GridSelectionModel, GridRowId } from "@mui/x-data-grid";
import { enqueueSnackbar } from "notistack";
import { requestDiscounts } from "@root/services/discounts.service";
interface Props {
selectedRows: GridSelectionModel
selectedRows: GridSelectionModel;
}
export default function DiscountDataGrid({selectedRows}:Props) {
export default function DiscountDataGrid({ selectedRows }: Props) {
const changeData = async (isActive: boolean) => {
let done = 0;
let fatal = 0;
const changeData = (isActive:boolean) => {
let done = 0
let fatal = 0
selectedRows.forEach((id:GridRowId) => {
const discount = findDiscountsById(String(id))
if (discount) {
discount.Deprecated = isActive
changeDiscount(String(id), discount)
.then(() => {
done += 1
})
.catch(() => {
fatal += 1
})
.finally(() => {
if (done) enqueueSnackbar("Успешно изменён статус " + done + " скидок")
if (fatal) enqueueSnackbar(fatal + " скидок не изменили статус")
})
} else {
enqueueSnackbar("Скидка не найдена")
}
})
for (const id of selectedRows) {
const discount = findDiscountsById(String(id));
}
return (
<Box width="400px" display="flex" justifyContent="space-between">
<Button onClick={() => changeData(false)}>Активировать</Button>
<Button onClick={() => changeData(true)}>Деактивировать</Button>
</Box>
);
if (!discount) {
return enqueueSnackbar("Скидка не найдена");
}
try {
await changeDiscount(String(id), { ...discount, Deprecated: isActive });
done += 1;
} catch {
fatal += 1;
}
}
await requestDiscounts();
if (done) {
enqueueSnackbar("Успешно изменён статус " + done + " скидок");
}
if (fatal) {
enqueueSnackbar(fatal + " скидок не изменили статус");
}
};
return (
<Box width="400px" display="flex" justifyContent="space-between">
<Button onClick={() => changeData(false)}>Активировать</Button>
<Button onClick={() => changeData(true)}>Деактивировать</Button>
</Box>
);
}

@ -17,7 +17,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import { deleteDiscount } from "@root/api/discounts";
import { GridSelectionModel } from "@mui/x-data-grid";
import { useDiscounts } from "@root/hooks/useDiscounts.hook";
import { requestDiscounts } from "@root/services/discounts.service";
import AutorenewIcon from "@mui/icons-material/Autorenew";
const columns: GridColDef[] = [
@ -119,14 +119,13 @@ export default function DiscountDataGrid({ selectedRowsHC }: Props) {
);
const realDiscounts = useDiscountStore((state) => state.discounts);
const editDiscountId = useDiscountStore((state) => state.editDiscountId);
const { requestDiscounts } = useDiscounts();
useEffect(() => {
requestDiscounts();
}, []);
const rowBackDicounts: GridRowsProp = realDiscounts
.filter((e) => e.Layer > 0)
.filter(({ Layer }) => Layer > 0)
.map((discount) => {
console.log(
discount.Condition[

@ -1,9 +1,10 @@
import { GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
import { Skeleton } from "@mui/material";
import { useNavigate } from "react-router-dom";
import DataGrid from "@kitUI/datagrid";
import type { UsersType } from "@root/api/roles";
import type { UserType } from "@root/api/roles";
const columns: GridColDef[] = [
{ field: "login", headerName: "Логин", width: 100 },
@ -15,14 +16,17 @@ const columns: GridColDef[] = [
interface Props {
handleSelectionChange: (selectionModel: GridSelectionModel) => void;
users: any
users: UserType[];
}
export default function ServiceUsersDG({ handleSelectionChange, users }: Props) {
if (!users) {
return <Skeleton>Loading...</Skeleton>;
}
const gridData = users.users.map((user:any) => ({
export default function ServiceUsersDG({
handleSelectionChange,
users = [],
}: Props) {
const navigate = useNavigate();
const gridData = users.map((user) => ({
id: user._id,
login: user.login,
email: user.email,
phoneNumber: user.phoneNumber,
@ -31,14 +35,21 @@ export default function ServiceUsersDG({ handleSelectionChange, users }: Props)
}));
return (
<DataGrid
sx={{ maxWidth: "90%", mt: "30px" }}
getRowId={(users: any) => users.login}
checkboxSelection={true}
rows={gridData}
columns={columns}
components={{ Toolbar: GridToolbar }}
onSelectionModelChange={handleSelectionChange}
/>
<>
{users.length ? (
<DataGrid
sx={{ maxWidth: "90%", mt: "30px" }}
getRowId={(users: any) => users.login}
checkboxSelection={true}
rows={gridData}
columns={columns}
components={{ Toolbar: GridToolbar }}
onSelectionModelChange={handleSelectionChange}
onRowClick={({ row }) => navigate(row.id)}
/>
) : (
<Skeleton>Loading...</Skeleton>
)}
</>
);
}

@ -1,31 +1,15 @@
import { useState } from "react";
import {
Typography,
Container,
Button,
Select,
MenuItem,
FormControl,
InputLabel,
useTheme,
Box,
} from "@mui/material";
import { Typography, Container, Button, Select, MenuItem, FormControl, InputLabel, useTheme, Box } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { CustomTextField } from "@root/kitUI/CustomTextField";
import { requestTariffs } from "@root/services/tariffs.service";
import { authStore } from "@root/stores/auth";
import {
findPrivilegeById,
usePrivilegeStore,
} from "@root/stores/privilegesStore";
import { findPrivilegeById, usePrivilegeStore } from "@root/stores/privilegesStore";
import type { Privilege_BACKEND } from "@root/model/tariff";
type CreateTariffProps = {
requestTariffs: () => Promise<void>;
};
type CreateTariffBackendRequest = {
name: string;
price: number;
@ -33,12 +17,9 @@ type CreateTariffBackendRequest = {
privilegies: Omit<Privilege_BACKEND, "_id" | "updatedAt">[];
};
const baseUrl =
process.env.NODE_ENV === "production"
? "/strator"
: "https://admin.pena.digital/strator";
const baseUrl = process.env.NODE_ENV === "production" ? "/strator" : "https://admin.pena.digital/strator";
export default function CreateTariff({ requestTariffs }: CreateTariffProps) {
export default function CreateTariff() {
const { makeRequest } = authStore();
const theme = useTheme();
@ -173,7 +154,11 @@ export default function CreateTariff({ requestTariffs }: CreateTariffProps) {
inputProps={{ sx: { pt: "12px" } }}
>
{privileges.map((privilege) => (
<MenuItem key={privilege.description} value={privilege.id}>
<MenuItem
data-cy={`select-option-${privilege.description}`}
key={privilege.description}
value={privilege.id}
>
{privilege.description}
</MenuItem>
))}
@ -221,6 +206,8 @@ export default function CreateTariff({ requestTariffs }: CreateTariffProps) {
type="number"
/>
<Button
className="btn_createTariffBackend"
type="button"
onClick={() => {
createTariffBackend();
}}

@ -3,8 +3,8 @@ import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import Modal from "@mui/material/Modal";
import { GridSelectionModel } from "@mui/x-data-grid";
import { useTariffStore } from "@root/stores/tariffsStore";
import { requestTariffs } from "@root/services/tariffs.service";
import { enqueueSnackbar } from "notistack";
import { authStore } from "@root/stores/auth";
@ -12,7 +12,6 @@ type DeleteModalProps = {
open: boolean | string;
handleClose: () => void;
selectedTariffs: any;
requestTariffs: () => Promise<void>;
};
type DeleteTariffRequest = {
@ -28,7 +27,6 @@ export default function DeleteModal({
open,
handleClose,
selectedTariffs,
requestTariffs,
}: DeleteModalProps) {
const { makeRequest } = authStore();
const tariffs = useTariffStore((state) => state.tariffs);

@ -9,6 +9,7 @@ import { authStore } from "@root/stores/auth";
import { Privilege, Tariff } from "@root/model/tariff";
import { useTariffStore } from "@root/stores/tariffsStore";
import { findPrivilegeById } from "@root/stores/privilegesStore";
import { requestTariffs } from "@root/services/tariffs.service";
import { enqueueSnackbar } from "notistack";
interface EditProps {
@ -25,17 +26,9 @@ type EditTariffBackendRequest = {
privilegies: Omit<Privilege_BACKEND, "_id" | "updatedAt">[];
};
const baseUrl =
process.env.NODE_ENV === "production"
? "/strator"
: "https://admin.pena.digital/strator";
const baseUrl = process.env.NODE_ENV === "production" ? "/strator" : "https://admin.pena.digital/strator";
const editTariff = ({
tarifIid,
tariffName,
tariffPrice,
privilege,
}: EditProps): Promise<unknown> => {
const editTariff = ({ tarifIid, tariffName, tariffPrice, privilege }: EditProps): Promise<unknown> => {
const { makeRequest } = authStore.getState();
return makeRequest<EditTariffBackendRequest>({
@ -52,13 +45,10 @@ const editTariff = ({
};
interface Props {
tariff: Tariff;
requestTariffs: () => Promise<void>;
closeModal: () => void
}
export default function EditModal({
tariff = undefined,
requestTariffs,
}: Props) {
export default function EditModal({ tariff = undefined, closeModal }: Props) {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [price, setPrice] = useState("");
@ -72,7 +62,7 @@ export default function EditModal({
return (
<Modal
open={open}
onClose={() => setOpen(false)}
onClose={() => closeModal(undefined)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
@ -90,12 +80,7 @@ export default function EditModal({
p: 4,
}}
>
<Typography
id="modal-modal-title"
variant="h6"
component="h2"
sx={{ whiteSpace: "nowrap" }}
>
<Typography id="modal-modal-title" variant="h6" component="h2" sx={{ whiteSpace: "nowrap" }}>
Редактирование тариффа
</Typography>
@ -109,9 +94,7 @@ export default function EditModal({
value={name}
sx={{ marginBottom: "10px" }}
/>
<Typography>
Цена за единицу: {currentTariff.pricePerUnit}
</Typography>
<Typography>Цена за единицу: {currentTariff.pricePerUnit}</Typography>
<TextField
onChange={(event) => setPrice(event.target.value)}
label="Цена за единицу"
@ -122,9 +105,7 @@ export default function EditModal({
<Button
onClick={() => {
if (!currentTariff.isFront) {
const privilege = findPrivilegeById(
currentTariff.privilegeId
);
const privilege = findPrivilegeById(currentTariff.privilegeId);
privilege.privilegeId = privilege.id;
console.log(privilege);
@ -139,11 +120,7 @@ export default function EditModal({
editTariff({
tarifIid: currentTariff.id,
tariffName: name ? name : currentTariff.name,
tariffPrice: price
? Number(price)
: currentTariff.price
? currentTariff.price
: privilege.price,
tariffPrice: price ? Number(price) : currentTariff.price ? currentTariff.price : privilege.price,
isDeleted: currentTariff.isDeleted,
customPricePerUnit: currentTariff.price,
privilege: privilege,

@ -3,6 +3,7 @@ import DataGrid from "@kitUI/datagrid";
import { Tooltip, IconButton } from "@mui/material";
import { usePrivilegeStore } from "@root/stores/privilegesStore";
import AutorenewIcon from "@mui/icons-material/Autorenew";
import { requestPrivilegies } from "@root/services/privilegies.service";
const columns: GridColDef[] = [
{ field: "id", headerName: "id", width: 150 },
@ -12,13 +13,8 @@ const columns: GridColDef[] = [
{ field: "price", headerName: "Стоимость", width: 100 },
];
type Props = {
requestPrivilegies: () => Promise<void>;
};
export default function Privileges({ requestPrivilegies }: Props) {
export default function Privileges() {
const privileges = usePrivilegeStore((state) => state.privileges);
// const { mergedPrivileges } = useCombinedPrivileges();
const privilegesGridData = privileges
.filter((privilege) => !privilege.isDeleted)
.map((privilege) => ({

@ -0,0 +1,26 @@
import { useState } from "react";
import { Typography } from "@mui/material";
import { GridSelectionModel } from "@mui/x-data-grid";
import Cart from "@root/kitUI/Cart/Cart";
import TariffsDG from "./tariffsDG";
export default function TariffsInfo() {
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>(
[]
);
return (
<>
<Typography variant="h6" mt="20px">
Список тарифов
</Typography>
<TariffsDG
selectedTariffs={selectedTariffs}
handleSelectionChange={setSelectedTariffs}
/>
<Cart selectedTariffs={selectedTariffs} />
</>
);
}

@ -1,26 +1,15 @@
import { useState, useEffect } from "react";
import { useEffect } from "react";
import { Container, Typography } from "@mui/material";
import { GridSelectionModel } from "@mui/x-data-grid";
import Cart from "@root/kitUI/Cart/Cart";
import { requestTariffs } from "@root/services/tariffs.service";
import { requestPrivilegies } from "@root/services/privilegies.service";
import { useTariffs } from "@root/hooks/useTariffs.hook";
import { usePrivilegies } from "@root/hooks/usePrivilegies.hook";
import { useDiscounts } from "@root/hooks/useDiscounts.hook";
import TariffsDG from "./tariffsDG";
import CreateTariff from "./CreateTariff";
import Privileges from "./Privileges/Privileges";
import ChangePriceModal from "./Privileges/ChangePriceModal";
import TariffsInfo from "./TariffsInfo";
export default function Tariffs() {
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>(
[]
);
const { requestTariffs } = useTariffs();
const { requestPrivilegies } = usePrivilegies();
const { requestDiscounts } = useDiscounts();
useEffect(() => {
requestTariffs();
requestPrivilegies();
@ -37,27 +26,10 @@ export default function Tariffs() {
}}
>
<Typography variant="h6">Список привилегий</Typography>
<Privileges requestPrivilegies={requestPrivilegies} />
<Privileges />
<ChangePriceModal />
<CreateTariff requestTariffs={requestTariffs} />
<Typography variant="h6" mt="20px">
Список тарифов
</Typography>
<TariffsDG
selectedTariffs={selectedTariffs}
handleSelectionChange={setSelectedTariffs}
requestTariffs={requestTariffs}
/>
<Cart
requestPrivilegies={requestPrivilegies}
requestDiscounts={requestDiscounts}
selectedTariffs={selectedTariffs}
/>
<CreateTariff />
<TariffsInfo />
</Container>
);
}

@ -1,5 +1,5 @@
// @ts-nocheck
import React from "react";
import React, { useEffect } from "react";
import { useState } from "react";
import { GridColDef, GridSelectionModel, GridToolbar } from "@mui/x-data-grid";
import { Box, Button, IconButton, Tooltip } from "@mui/material";
@ -15,23 +15,49 @@ import DeleteModal from "@root/pages/dashboard/Content/Tariffs/DeleteModal";
import EditModal from "./EditModal";
import { Tariff } from "@root/model/tariff";
import AutorenewIcon from "@mui/icons-material/Autorenew";
import { requestTariffs } from "@root/services/tariffs.service";
interface Props {
selectedTariffs: GridSelectionModel;
handleSelectionChange: (selectionModel: GridSelectionModel) => void;
requestTariffs: () => Promise<void>;
}
export default function TariffsDG({
selectedTariffs,
handleSelectionChange,
requestTariffs,
}: Props) {
const tariffs = Object.values(useTariffStore().tariffs);
export default function TariffsDG({ selectedTariffs, handleSelectionChange }: Props) {
const tariffs = useTariffStore((state) => state.tariffs);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
const [changingTariff, setChangingTariff] = useState<Tariff | undefined>();
const [gridData, setGridData] = useState([]);
console.log(selectedTariffs);
useEffect(() => {
const data = Object.values(tariffs)
?.map((tariff) => {
console.log(tariff);
const privilege = findPrivilegeById(tariff.privilegeId);
return {
id: tariff.id,
name: tariff.name,
serviceName: privilege?.serviceKey == "templategen" ? "Шаблонизатор" : privilege?.serviceKey,
privilegeName: privilege?.name,
amount: tariff.amount,
pricePerUnit: tariff.isCustom ? (tariff.customPricePerUnit || 0) / 100 : (tariff?.price || 0) / 100,
type:
findPrivilegeById(tariff.privilegeId)?.value === "шаблон"
? "штука"
: findPrivilegeById(tariff.privilegeId)?.value,
customPricePerUnit: tariff.customPricePerUnit === 0 ? "Нет" : "Да",
total: tariff.amount
? (tariff.amount *
(tariff.isCustom
? tariff.customPricePerUnit || 0 * tariff.amount
: findPrivilegeById(tariff.privilegeId)?.price || 0)) /
100
: 0,
};
})
.sort((item, previous) => (!item?.isFront && previous?.isFront ? 1 : -1));
setGridData(data);
}, [tariffs]);
const closeDeleteModal = () => {
setOpenDeleteModal(false);
@ -66,6 +92,7 @@ export default function TariffsDG({
renderCell: ({ row }) => {
return (
<IconButton
className="delete-tariff-button"
onClick={() => {
setOpenDeleteModal(row.id);
}}
@ -77,40 +104,6 @@ export default function TariffsDG({
},
];
const gridData = tariffs
?.map((tariff) => {
console.log(tariff);
const privilege = findPrivilegeById(tariff.privilegeId);
return {
id: tariff.id,
name: tariff.name,
serviceName:
privilege?.serviceKey == "templategen"
? "Шаблонизатор"
: privilege?.serviceKey,
privilegeName: privilege?.name,
amount: tariff.amount,
pricePerUnit: tariff.isCustom
? (tariff.customPricePerUnit || 0) / 100
: (tariff?.price || 0) / 100,
type:
findPrivilegeById(tariff.privilegeId)?.value === "шаблон"
? "штука"
: findPrivilegeById(tariff.privilegeId)?.value,
customPricePerUnit: tariff.customPricePerUnit === 0 ? "Нет" : "Да",
total: tariff.amount
? (tariff.amount *
(tariff.isCustom
? tariff.customPricePerUnit || 0 * tariff.amount
: findPrivilegeById(tariff.privilegeId)?.price || 0)) /
100
: 0,
};
})
.sort((item, previous) => (!item?.isFront && previous?.isFront ? 1 : -1));
// console.log(gridData)
return (
<>
<Tooltip title="обновить список тарифов">
@ -119,6 +112,7 @@ export default function TariffsDG({
</IconButton>
</Tooltip>
<DataGrid
className="tariffs-data-grid"
disableSelectionOnClick={true}
checkboxSelection={true}
rows={gridData}
@ -137,10 +131,7 @@ export default function TariffsDG({
mb: "30px",
}}
>
<Button
onClick={() => setOpenDeleteModal(true)}
sx={{ mr: "20px", zIndex: "10000" }}
>
<Button onClick={() => setOpenDeleteModal(true)} sx={{ mr: "20px", zIndex: "10000" }}>
Удаление
</Button>
</Box>
@ -153,9 +144,8 @@ export default function TariffsDG({
closeDeleteModal();
}}
selectedTariffs={selectedTariffs}
requestTariffs={requestTariffs}
/>
<EditModal tariff={changingTariff} requestTariffs={requestTariffs} />
<EditModal tariff={changingTariff} closeModal={setChangingTariff} />
</>
);
}

@ -30,7 +30,12 @@ import { getRoles_mock, TMockData } from "../../../api/roles";
import theme from "../../../theme";
import type { UsersType } from "../../../api/roles";
import type { UserType } from "../../../api/roles";
type RegisteredUsersResponse = {
tatalPages: number;
users: UserType[];
};
const baseUrl =
process.env.NODE_ENV === "production" ? "" : "https://admin.pena.digital";
@ -85,8 +90,8 @@ const Users: React.FC = () => {
handleChangeData();
const [roles, setRoles] = React.useState<TMockData>([]);
const [users, setUsers] = React.useState<UsersType>();
const [manager, setManager] = React.useState<UsersType>();
const [users, setUsers] = React.useState<UserType[]>([]);
const [manager, setManager] = React.useState<UserType[]>([]);
useEffect(() => {
async function axiosRoles() {
@ -103,12 +108,12 @@ const Users: React.FC = () => {
}
async function gettingRegisteredUsers() {
try {
const usersResponse = await makeRequest<never, UsersType>({
const { users } = await makeRequest<never, RegisteredUsersResponse>({
method: "get",
url: baseUrl + "/user/",
});
setUsers(usersResponse);
setUsers(users);
} catch (error) {
console.error("Ошибка при получении пользователей!");
}
@ -116,12 +121,12 @@ const Users: React.FC = () => {
async function gettingListManagers() {
try {
const managersResponse = await makeRequest<never, UsersType>({
const { users } = await makeRequest<never, RegisteredUsersResponse>({
method: "get",
url: baseUrl + "/user/",
});
setManager(managersResponse);
setManager(users);
} catch (error) {
console.error("Ошибка при получении менеджеров!");
}

@ -0,0 +1,241 @@
import { useEffect, useRef, useState } from "react";
import { Box, useTheme, useMediaQuery } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import { scrollBlock } from "@root/utils/scrollBlock";
import forwardIcon from "@root/assets/icons/forward.svg";
import type { ChangeEvent } from "react";
import type { GridColDef } from "@mui/x-data-grid";
const COLUMNS: GridColDef[] = [
{
field: "date",
headerName: "Дата",
minWidth: 130,
maxWidth: 200,
flex: 1,
sortable: false,
},
{
field: "time",
headerName: "Время",
minWidth: 90,
maxWidth: 180,
flex: 1,
sortable: false,
},
{
field: "product",
headerName: "Товар",
minWidth: 200,
maxWidth: 280,
flex: 1,
sortable: false,
},
{
field: "amount",
headerName: "Кол-во",
minWidth: 70,
maxWidth: 70,
flex: 1,
sortable: false,
},
];
const ROWS = [
{
id: "row_1",
date: "19.02.2023",
time: "17:01",
product: "Шаблонизатор",
amount: "1 шт.",
},
{
id: "row_2",
date: "28.02.2023",
time: "10:43",
product: "Шаблонизатор",
amount: "1 шт.",
},
{
id: "row_3",
date: "04.03.2023",
time: "21:09",
product: "Сокращатель ссылок",
amount: "2 шт.",
},
];
export const PurchaseTab = () => {
const [canScrollToRight, setCanScrollToRight] = useState<boolean>(true);
const [canScrollToLeft, setCanScrollToLeft] = useState<boolean>(false);
const theme = useTheme();
const smallScreen = useMediaQuery(theme.breakpoints.down(830));
const gridContainer = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = (nativeEvent: unknown) => {
const { target } = nativeEvent as ChangeEvent<HTMLDivElement>;
if (target.scrollLeft > 0) {
setCanScrollToLeft(true);
} else {
setCanScrollToLeft(false);
}
if (target.clientWidth + target.scrollLeft >= target.scrollWidth - 1) {
setCanScrollToRight(false);
} else {
setCanScrollToRight(true);
}
};
const addScrollEvent = () => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
if (grid) {
grid.addEventListener("scroll", handleScroll);
return;
}
setTimeout(addScrollEvent, 100);
};
addScrollEvent();
return () => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
grid?.removeEventListener("scroll", handleScroll);
};
}, []);
const scrollDataGrid = (toStart = false) => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
if (grid) {
scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth });
}
};
return (
<Box
ref={gridContainer}
sx={{
height: "100%",
padding: "0 25px",
"&:before": {
content: '""',
height: "50px",
width: "100%",
position: "absolute",
left: "0",
top: "0",
background: "#F2F3F7",
},
}}
>
<DataGrid
rows={ROWS}
columns={COLUMNS}
pageSize={5}
rowsPerPageOptions={[5]}
hideFooter
disableColumnMenu
disableSelectionOnClick
rowHeight={50}
headerHeight={50}
experimentalFeatures={{ newEditingApi: true }}
sx={{
borderRadius: "0",
border: "none",
"& .MuiDataGrid-columnHeaders": {
background: "#F2F3F7",
borderBottom: "none",
},
"& .MuiDataGrid-main .MuiDataGrid-columnHeader": {
outline: "none",
},
"& .MuiDataGrid-columnHeaderTitle": {
fontWeight: "bold",
userSelect: "none",
},
"& .MuiDataGrid-virtualScrollerRenderZone": {
width: "100%",
},
"& .MuiDataGrid-iconSeparator": { display: "none" },
"& .MuiDataGrid-main .MuiDataGrid-cell": {
outline: "none",
border: "none",
justifyContent: "flex-start",
},
"& .MuiDataGrid-row": {
width: "100%",
"&:hover": {
background: "#F2F3F7",
},
},
}}
/>
{smallScreen && (
<Box
sx={{
position: "absolute",
right: "15px",
bottom: "50px",
display: "flex",
columnGap: "5px",
}}
>
{canScrollToLeft && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(true)}
>
<img
src={forwardIcon}
alt="forward"
style={{ transform: "rotate(180deg)" }}
/>
</Box>
)}
{canScrollToRight && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(false)}
>
<img src={forwardIcon} alt="forward" />
</Box>
)}
</Box>
)}
</Box>
);
};

@ -0,0 +1,252 @@
import { useEffect, useRef, useState } from "react";
import { Typography, Box, useTheme, useMediaQuery } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import { scrollBlock } from "@root/utils/scrollBlock";
import forwardIcon from "@root/assets/icons/forward.svg";
import type { ChangeEvent } from "react";
import type { GridColDef } from "@mui/x-data-grid";
const COLUMNS: GridColDef[] = [
{
field: "date",
headerName: "Дата",
width: 130,
sortable: false,
},
{
field: "time",
headerName: "Время",
width: 90,
sortable: false,
},
{
field: "amount",
headerName: "Сумма",
width: 140,
sortable: false,
},
{
field: "email",
headerName: "Email",
width: 280,
sortable: false,
},
{
field: "status",
headerName: "Статус",
width: 100,
sortable: false,
renderCell: ({ value }) => (
<Typography
sx={{
position: "relative",
paddingLeft: "20px",
fontWeight: "bold",
fontSize: "14px",
"&::before": {
content: '""',
position: "absolute",
left: "0",
top: "50%",
transform: "translateY(-50%)",
height: "13px",
width: "13px",
background: value ? "#0D9F00" : "#F18956",
borderRadius: "50%",
},
}}
>
{value ? "accepted" : "timeout"}
</Typography>
),
},
];
const ROWS = [
{
id: "row_1",
date: "19.02.2023",
time: "17:01",
amount: "2 065 руб.",
email: "emailexample@gmail.com",
status: true,
},
{
id: "row_2",
date: "28.02.2023",
time: "10:43",
amount: "21 250 руб.",
email: "verylongemailexample@gmail.com",
status: true,
},
{
id: "row_3",
date: "04.03.2023",
time: "21:09",
amount: "108 065 руб.",
email: "emailexample@gmail.com",
status: false,
},
];
export const TransactionsTab = () => {
const [canScrollToRight, setCanScrollToRight] = useState<boolean>(true);
const [canScrollToLeft, setCanScrollToLeft] = useState<boolean>(false);
const theme = useTheme();
const smallScreen = useMediaQuery(theme.breakpoints.down(1070));
const gridContainer = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = (nativeEvent: unknown) => {
const { target } = nativeEvent as ChangeEvent<HTMLDivElement>;
if (target.scrollLeft > 0) {
setCanScrollToLeft(true);
} else {
setCanScrollToLeft(false);
}
if (target.clientWidth + target.scrollLeft >= target.scrollWidth - 1) {
setCanScrollToRight(false);
} else {
setCanScrollToRight(true);
}
};
const addScrollEvent = () => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
if (grid) {
grid.addEventListener("scroll", handleScroll);
return;
}
setTimeout(addScrollEvent, 100);
};
addScrollEvent();
return () => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
grid?.removeEventListener("scroll", handleScroll);
};
}, []);
const scrollDataGrid = (toStart = false) => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
if (grid) {
scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth });
}
};
return (
<Box sx={{ height: "100%" }} ref={gridContainer}>
<DataGrid
rows={ROWS}
columns={COLUMNS}
pageSize={5}
rowsPerPageOptions={[5]}
hideFooter
disableColumnMenu
disableSelectionOnClick
rowHeight={50}
headerHeight={50}
experimentalFeatures={{ newEditingApi: true }}
sx={{
borderRadius: "0",
border: "none",
"& .MuiDataGrid-columnHeaders": {
padding: "0 25px",
background: "#F2F3F7",
borderBottom: "none",
},
"& .MuiDataGrid-main .MuiDataGrid-columnHeader": {
outline: "none",
},
"& .MuiDataGrid-columnHeaderTitle": {
fontWeight: "bold",
userSelect: "none",
},
"& .MuiDataGrid-virtualScrollerRenderZone": {
width: "100%",
},
"& .MuiDataGrid-iconSeparator": { display: "none" },
"& .MuiDataGrid-main .MuiDataGrid-cell": {
outline: "none",
border: "none",
justifyContent: "flex-start",
},
"& .MuiDataGrid-row": {
padding: "0 25px",
width: "100%",
"&:hover": {
background: "#F2F3F7",
},
},
}}
/>
{smallScreen && (
<Box
sx={{
position: "absolute",
right: "15px",
bottom: "50px",
display: "flex",
columnGap: "5px",
}}
>
{canScrollToLeft && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(true)}
>
<img
src={forwardIcon}
alt="forward"
style={{ transform: "rotate(180deg)" }}
/>
</Box>
)}
{canScrollToRight && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(false)}
>
<img src={forwardIcon} alt="forward" />
</Box>
)}
</Box>
)}
</Box>
);
};

@ -0,0 +1,66 @@
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
export const UserTab = () => {
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(700));
return (
<Box
sx={{
display: mobile ? "block" : "flex",
columnGap: "15px",
padding: "25px",
}}
>
<Box sx={{ maxWidth: "300px", width: "100%" }}>
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>ID</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{" "}
2810
</Typography>
</Box>
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Дата регистрации</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
17.02.2023
</Typography>
</Box>
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Email</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
emailexamle@gmail.com
</Typography>
</Box>
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Телефон</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
+7 123 456 78 90
</Typography>
</Box>
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Тип:</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
НКО
</Typography>
</Box>
</Box>
<Box sx={{ maxWidth: "300px", width: "100%" }}>
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>ФИО:</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
Куликов Геннадий Викторович
</Typography>
</Box>
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>
Внутренний кошелек
</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
2 096 руб.
</Typography>
</Box>
</Box>
</Box>
);
};

@ -0,0 +1,155 @@
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { Box, Typography, TextField, Button } from "@mui/material";
import { authStore } from "@root/stores/auth";
import type { ChangeEvent } from "react";
type File = {
name: "inn" | "rule" | "egrule" | "certificate";
url: string;
};
type Verification = {
_id: string;
accepted: boolean;
status: "org" | "nko";
updated_at: string;
comment: string;
files: File[];
};
type PatchVerificationBody = {
id: string;
status: "org" | "nko";
comment: string;
accepted: boolean;
};
const baseUrl =
process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
export const VerificationTab = () => {
const [user, setUser] = useState<Verification | null>(null);
const [comment, setComment] = useState<string>("");
const { userId } = useParams();
const { makeRequest } = authStore();
const requestVefification = async () =>
makeRequest<never, Verification>({
method: "get",
url: baseUrl + `/verification/verification/${userId}`,
}).then((verification) => {
setUser(verification);
setComment(verification.comment);
});
useEffect(() => {
requestVefification();
}, []);
const verify = async (accepted: boolean) => {
if (!user) {
return;
}
try {
await makeRequest<PatchVerificationBody, never>({
method: "patch",
useToken: true,
url: baseUrl + `/verification/verification`,
body: {
accepted,
comment,
id: user._id,
status: user.status,
},
});
await requestVefification();
} catch {}
};
return (
<Box sx={{ padding: "25px" }}>
<Typography
sx={{
marginBottom: "10px",
fontWeight: "bold",
color: user?.accepted ? "#0D9F00" : "#E02C2C",
}}
>
{user?.accepted ? "Верификация пройдена" : "Не верифицирован"}
</Typography>
{user?.files?.map(({ name, url }, index) => (
<Box sx={{ marginBottom: "25px" }} key={name + url}>
<Typography sx={{ fontWeight: "bold", fontSize: "18px" }}>
{index + 1}.{" "}
{name === "inn"
? "Скан ИНН организации (выписка из ЕГЮРЛ)"
: name === "rule"
? "Устав организации"
: name === "certificate"
? "Свидетельство о регистрации НКО"
: `Скан документа ${index + 1}`}
</Typography>
<Typography>
<a
style={{
color: "#7E2AEA",
textDecoration: "none",
fontSize: "18px",
}}
href={url}
>
{url.split("/").pop()?.split(".")?.[0]}
</a>
</Typography>
</Box>
))}
{user?.comment && (
<Box sx={{ marginBottom: "15px" }}>
<Typography
component="span"
sx={{ fontWeight: "bold", marginBottom: "10px" }}
>
Комментарий:
</Typography>
<Typography component="span"> {user.comment}</Typography>
</Box>
)}
<TextField
multiline
value={comment}
rows={4}
label="Комментарий"
type=""
sx={{
width: "100%",
maxWidth: "500px",
marginBottom: "10px",
}}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
setComment(event.target.value)
}
/>
<Box sx={{ display: "flex", columnGap: "10px" }}>
<Button
variant="text"
sx={{ background: "#9A9AAF" }}
onClick={() => verify(false)}
>
Отклонить
</Button>
<Button
variant="text"
sx={{ background: "#9A9AAF" }}
onClick={() => verify(true)}
>
Подтвердить
</Button>
</Box>
</Box>
);
};

@ -1,183 +1,190 @@
import * as React from "react";
import { useLinkClickHandler } from "react-router-dom";
import { Box, Modal, Fade, Backdrop, Typography } from "@mui/material";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import theme from "../../../theme";
import { useState } from "react";
import { useLinkClickHandler } from "react-router-dom";
import {
Box,
Modal,
Fade,
Backdrop,
Typography,
Tabs,
Tab,
useTheme,
useMediaQuery,
} from "@mui/material";
import { UserTab } from "./UserTab";
import { PurchaseTab } from "./PurchaseTab";
import { TransactionsTab } from "./TransactionsTab";
import { VerificationTab } from "./VerificationTab";
export interface MWProps {
open: boolean
}
import { ReactComponent as UserIcon } from "@root/assets/icons/user.svg";
import { ReactComponent as PackageIcon } from "@root/assets/icons/package.svg";
import { ReactComponent as TransactionsIcon } from "@root/assets/icons/transactions.svg";
import { ReactComponent as CheckIcon } from "@root/assets/icons/check.svg";
const columns: GridColDef[] = [
import forwardIcon from "@root/assets/icons/forward.svg";
import type { SyntheticEvent } from "react";
const TABS = [
{ name: "Пользователь", icon: UserIcon, activeStyles: { fill: "#7E2AEA" } },
{
field: 'id',
headerName: 'ID',
width: 30,
sortable: false,
name: "Покупка товаров и услуг",
icon: PackageIcon,
activeStyles: { stroke: "#7E2AEA" },
},
{
field: 'dateTime',
headerName: 'Дата / время',
width: 150,
sortable: false,
},
{
field: 'email',
headerName: 'Почта',
width: 110,
sortable: false,
},{
field: 'summa',
headerName: 'Сумма',
type: 'number',
width: 110,
sortable: false,
},
{
field: 'idLong',
headerName: 'ID long',
type: 'number',
width: 110,
sortable: false,
},
{
field: 'paymentStatus',
headerName: 'Статус платежа',
width: 160,
sortable: false,
name: "Транзакции",
icon: TransactionsIcon,
activeStyles: { stroke: "#7E2AEA" },
},
{ name: "Верификация", icon: CheckIcon, activeStyles: { stroke: "#7E2AEA" } },
];
const rows = [
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" },
];
const ModalUser = ({open}: MWProps ) => {
const [value, setValue] = React.useState(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
const ModalUser = () => {
const [value, setValue] = useState<number>(0);
const [openNavigation, setOpenNavigation] = useState<boolean>(false);
const theme = useTheme();
const tablet = useMediaQuery(theme.breakpoints.down(1070));
const mobile = useMediaQuery(theme.breakpoints.down(700));
return (
<React.Fragment>
<>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={ open }
onClose={ useLinkClickHandler('/users') }
open
onClose={useLinkClickHandler("/users")}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Box sx={{
position: "absolute" as "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "980px",
height: "480px",
bgcolor: theme.palette.menu.main,
boxShadow: 24,
color: theme.palette.secondary.main,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}>
<Typography id="transition-modal-title" variant="caption">
<Fade in>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
display: "flex",
flexDirection: "column",
width: mobile ? "100%" : "98vw",
maxWidth: tablet ? "920px" : "1070px",
height: mobile ? "100%" : "605px",
bgcolor: theme.palette.background.default,
color: theme.palette.secondary.main,
boxShadow: 24,
borderRadius: mobile ? "0" : "12px",
outline: "none",
overflowX: "hidden",
}}
>
<Typography
id="transition-modal-title"
variant="caption"
sx={{
display: "block",
textAlign: "center",
fontSize: "18px",
padding: "12px",
color: theme.palette.common.white,
background: "#9A9AAF",
}}
>
Пользователь сервиса
</Typography>
<Box sx={{
width: "100%",
marginTop: "50px",
display: "flex"
}}>
<Tabs
orientation="vertical"
variant="scrollable"
value={value}
onChange={handleChange}
aria-label="Vertical tabs example"
<Box sx={{ width: "100%", height: "100%", display: "flex" }}>
<Box
sx={{
borderRight: 1,
borderColor: theme.palette.secondary.main,
"& .MuiTab-root.Mui-selected": {
color: theme.palette.secondary.main
}
width: "100%",
height: "100%",
maxWidth: mobile
? openNavigation
? "276px"
: "68px"
: "276px",
}}
TabIndicatorProps={{style: {
background: theme.palette.secondary.main
}}}
>
<Tab sx={{
color: theme.palette.grayDisabled.main,
width: "180px",
fontSize: "15px"
}}
label="Пользовательская информация" />
<Tab sx={{
color: theme.palette.grayDisabled.main,
width: "180px",
fontSize: "15px"
}}
label="История транзакций" />
</Tabs>
{ value == 0 && (
<Box sx={{ marginLeft: "20px" }}>
<Typography>Id: 1</Typography>
<Typography>Email: 2</Typography>
<Typography>Номер телефона: 3</Typography>
</Box>
) }
{ value == 1 && (
<Box sx={{ marginLeft: "20px" }}>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
disableSelectionOnClick
experimentalFeatures={{ newEditingApi: true }}
sx={{
color: theme.palette.secondary.main,
width: "720px",
height: "350px",
overflowY: "auto",
"& .MuiDataGrid-iconSeparator": {
display: "none"
},
"& .css-levciy-MuiTablePagination-displayedRows": {
color: theme.palette.secondary.main
},
"& .MuiSvgIcon-root": {
color: theme.palette.secondary.main
}
{mobile && (
<img
src={forwardIcon}
alt="forward"
style={{
display: "block",
width: "11px",
margin: "25px 0 15px 28px",
transform: openNavigation ? "rotate(180deg)" : "",
transition: ".2s",
}}
onClick={() => setOpenNavigation((isOpened) => !isOpened)}
/>
</Box>
) }
)}
<Tabs
orientation="vertical"
variant="scrollable"
value={value}
onChange={(event: SyntheticEvent, newValue: number) =>
setValue(newValue)
}
aria-label="Vertical tabs example"
sx={{
padding: mobile ? "16px" : "10px",
width: "100%",
}}
TabIndicatorProps={{ style: { background: "transparent" } }}
>
{TABS.map(({ name, icon: Icon, activeStyles }) => (
<Tab
icon={<Icon />}
iconPosition="start"
key={name}
label={mobile ? (openNavigation ? name : "") : name}
sx={{
justifyContent: "flex-start",
textTransform: "inherit",
minHeight: "auto",
minWidth: "auto",
fontSize: "15px",
padding: mobile ? "9px" : "15px",
marginBottom: mobile ? "15px" : "5px",
color: theme.palette.common.black,
whiteSpace: "nowrap",
lineHeight: "18px",
transition: ".2s",
"&.MuiButtonBase-root.Mui-selected": {
borderRadius: "12px",
color: "#7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
"& svg": activeStyles,
},
}}
/>
))}
</Tabs>
</Box>
<Box
sx={{
position: "relative",
width: "100%",
color: theme.palette.common.black,
boxShadow: "inset 30px 0px 40px 0px rgba(210, 208, 225, 0.2)",
}}
>
{value === 0 && <UserTab />}
{value === 1 && <PurchaseTab />}
{value === 2 && <TransactionsTab />}
{value === 3 && <VerificationTab />}
</Box>
</Box>
</Box>
</Fade>
</Modal>
</React.Fragment>
</>
);
}
};
export default ModalUser;

@ -59,7 +59,6 @@ export default () => {
</Box>
<ModalAdmin open={useMatch("/modalAdmin") !== null} />
<ModalUser open={useMatch("/modalUser") !== null} />
<ModalEntities open={useMatch("/modalEntities") !== null} />
</React.Fragment>
);

@ -0,0 +1,32 @@
import { setDiscounts } from "@root/stores/discounts";
import { authStore } from "@root/stores/auth";
import type { GetDiscountResponse, Discount } from "@root/model/discount";
const baseUrl =
process.env.NODE_ENV === "production"
? "/price"
: "https://admin.pena.digital/price";
const { makeRequest } = authStore.getState();
const filterDiscounts = (discounts: Discount[]) => {
const activeDiscounts = discounts.filter((discount) => !discount.Deprecated);
setDiscounts(activeDiscounts);
};
export const requestDiscounts = async (): Promise<void> => {
try {
const { Discounts } = await makeRequest<never, GetDiscountResponse>({
url: baseUrl + "/discounts",
method: "get",
useToken: true,
bearer: true,
});
filterDiscounts(Discounts);
} catch {
throw new Error("Ошибка при получении скидок");
}
};

@ -0,0 +1,63 @@
import { authStore } from "@root/stores/auth";
import { resetPrivilegeArray } from "@root/stores/privilegesStore";
import { exampleCartValues } from "@stores/mocks/exampleCartValues";
import type { RealPrivilege } from "@root/model/privilege";
export type Privilege = {
createdAt: string;
description: string;
isDeleted: boolean;
name: string;
price: number;
privilegeId: string;
serviceKey: string;
type: "count" | "day" | "mb";
updatedAt: string;
value: string;
_id: string;
};
type SeverPrivilegiesResponse = {
templategen: RealPrivilege[];
};
const baseUrl =
process.env.NODE_ENV === "production"
? "/strator"
: "https://admin.pena.digital/strator";
const { makeRequest } = authStore.getState();
const mutatePrivilegies = (privilegies: RealPrivilege[]) => {
let extracted: RealPrivilege[] = [];
for (let serviceKey in privilegies) {
//Приходит объект. В его значениях массивы привилегий для разных сервисов. Высыпаем в общую кучу и обновляем стор
extracted = extracted.concat(privilegies[serviceKey]);
}
let readyArray = extracted.map((privilege) => ({
serviceKey: privilege.serviceKey,
privilegeId: privilege.privilegeId,
name: privilege.name,
description: privilege.description,
type: privilege.type,
price: privilege.price,
value: privilege.value,
id: privilege._id,
}));
resetPrivilegeArray([...readyArray, ...exampleCartValues.privileges]);
};
export const requestPrivilegies = async () => {
await makeRequest<never, SeverPrivilegiesResponse>({
url: baseUrl + "/privilege/service",
method: "get",
})
.then(({ templategen }) => mutatePrivilegies(templategen))
.catch(() => {
console.log("Ошибка при получении привилегий");
});
};

@ -0,0 +1,62 @@
import { resetTariffsStore } from "@root/stores/tariffsStore";
import { authStore } from "@root/stores/auth";
import type { Tariff, Tariff_BACKEND } from "@root/model/tariff";
type GetTariffsResponse = {
totalPages: number;
tariffs: Tariff_BACKEND[];
};
const baseUrl =
process.env.NODE_ENV === "production"
? "/strator"
: "https://admin.pena.digital/strator";
const { makeRequest } = authStore.getState();
const mutateTariffs = (tariffs: Tariff_BACKEND[]) => {
const convertedTariffs: Record<string, Tariff> = {};
tariffs
.filter(({ isDeleted }) => !isDeleted)
.forEach((tariff) => {
convertedTariffs[tariff._id] = {
id: tariff._id,
name: tariff.name,
isCustom: tariff.price ? true : false,
amount: tariff.privilegies[0].amount,
isFront: false,
privilegeId: tariff.privilegies[0].privilegeId,
price: tariff.privilegies[0].price,
isDeleted: tariff.isDeleted,
customPricePerUnit: tariff.price,
};
});
resetTariffsStore(convertedTariffs);
};
export const requestTariffs = async (
page: number = 1,
existingTariffs: Tariff_BACKEND[] = []
): Promise<void> => {
try {
const { tariffs, totalPages } = await makeRequest<
never,
GetTariffsResponse
>({
url: baseUrl + `/tariff/?page=${page}&limit=${100}`,
method: "get",
bearer: true,
});
if (page < totalPages) {
return requestTariffs(page + 1, [...existingTariffs, ...tariffs]);
}
mutateTariffs([...existingTariffs, ...tariffs]);
} catch {
throw new Error("Ошибка при получении тарифов");
}
};

@ -1,8 +1,6 @@
import { GridSelectionModel } from "@mui/x-data-grid";
import { AnyDiscount } from "@root/model/cart";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { exampleCartValues } from "./mocks/exampleCartValues";
import { Discount } from "@root/model/discount";
import { produce } from "immer";
@ -49,58 +47,3 @@ export const setSelectedDiscountIds = (selectedDiscountIds: DiscountStore["selec
export const openEditDiscountDialog = (editDiscountId: DiscountStore["editDiscountId"]) => useDiscountStore.setState({ editDiscountId });
export const closeEditDiscountDialog = () => useDiscountStore.setState({ editDiscountId: null });
/** @deprecated */
interface MockDiscountStore {
discounts: AnyDiscount[];
selectedDiscountIds: GridSelectionModel,
}
/** @deprecated */
export const useMockDiscountStore = create<MockDiscountStore>()(
devtools(
(set, get) => ({
discounts: exampleCartValues.discounts,
selectedDiscountIds: [],
}),
{
name: "Discount store"
}
)
);
/** @deprecated */
export const addMockDiscounts = (newDiscounts: AnyDiscount[]) => useMockDiscountStore.setState(state => ({ discounts: [...state.discounts, ...newDiscounts] }));
/** @deprecated */
export const setMockSelectedDiscountIds = (selectedDiscountIds: MockDiscountStore["selectedDiscountIds"]) => useMockDiscountStore.setState({ selectedDiscountIds });
/** @deprecated */
export const activateMockDiscounts = () => useMockDiscountStore.setState(state => {
const discounts: AnyDiscount[] = [];
state.discounts.forEach(discount => {
if (!state.selectedDiscountIds.includes(discount._id)) return discounts.push(discount);
discounts.push({
...discount,
disabled: false,
});
});
return { discounts };
});
/** @deprecated */
export const deactivateMockDiscounts = () => useMockDiscountStore.setState(state => {
const discounts: AnyDiscount[] = [];
state.discounts.forEach(discount => {
if (!state.selectedDiscountIds.includes(discount._id)) return discounts.push(discount);
discounts.push({
...discount,
disabled: true,
});
});
return { discounts };
});

@ -1,7 +1,8 @@
import { AnyDiscount } from "@root/model/cart";
import { Privilege } from "@root/model/tariff";
// @ts-nocheck
import { User } from "../../model/user";
/** @deprecated */
export type TestCase = {
input: {
UserInformation: User;
@ -17,12 +18,14 @@ export type TestCase = {
};
};
/** @deprecated */
type ExampleCartValues = {
privileges: Privilege[];
discounts: AnyDiscount[];
testCases: TestCase[];
};
/** @deprecated */
export const exampleCartValues: ExampleCartValues = {
privileges: [
{

@ -1,4 +1,3 @@
import { Theme } from "@mui/material/styles";
import { createTheme, PaletteColorOptions, ThemeOptions } from "@mui/material";
import { deepmerge } from "@mui/utils";
//import { createTheme } from "./types";
@ -19,6 +18,14 @@ declare module "@mui/material/styles" {
primary: {
main: string;
};
common: {
black: string;
white: string;
};
background: {
default: string;
paper: string;
};
secondary: {
main: string;
};

10
src/utils/scrollBlock.ts Normal file

@ -0,0 +1,10 @@
type Coordinates = {
top?: number;
left?: number;
};
export const scrollBlock = (
block: Element,
coordinates: Coordinates,
smooth = true
) => block.scroll({ ...coordinates, behavior: smooth ? "smooth" : "auto" });

@ -2,11 +2,7 @@
"extends": "./tsconfig.extend.json",
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@ -19,9 +15,10 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"types": ["node"],
},
"include": [
"src"
]
"include": ["src", "**/*.ts"]
}

801
yarn.lock

File diff suppressed because it is too large Load Diff