feat: eslint and format code

This commit is contained in:
IlyaDoronin 2024-05-21 10:41:31 +03:00
parent b0e1b4a4a6
commit 16901608af
149 changed files with 10606 additions and 10883 deletions

@ -24,7 +24,6 @@ deploy-to-staging:
- if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH" - if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH"
extends: .deploy_template extends: .deploy_template
deploy-to-prod: deploy-to-prod:
tags: tags:
- front - front

@ -1 +1 @@
# pena_hub_admin_front # pena_hub_admin_front

@ -1,6 +1,3 @@
module.exports = { module.exports = {
presets: [ presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"],
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
}; };

@ -1,17 +1,17 @@
const CracoAlias = require("craco-alias"); const CracoAlias = require("craco-alias");
module.exports = { module.exports = {
plugins: [ plugins: [
{ {
plugin: CracoAlias, plugin: CracoAlias,
options: { options: {
source: "tsconfig", source: "tsconfig",
// baseUrl SHOULD be specified // baseUrl SHOULD be specified
// plugin does not take it from tsconfig // plugin does not take it from tsconfig
baseUrl: "./src", baseUrl: "./src",
// tsConfigPath should point to the file where "baseUrl" and "paths" are specified // tsConfigPath should point to the file where "baseUrl" and "paths" are specified
tsConfigPath: "./tsconfig.extend.json" tsConfigPath: "./tsconfig.extend.json",
} },
} },
] ],
}; };

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

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

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

9
eslint.config.js Normal file

@ -0,0 +1,9 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, {
rules: {
semi: "error",
"prefer-const": "error",
},
});

@ -1,5 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */ /** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = { module.exports = {
preset: "ts-jest", preset: "ts-jest",
testEnvironment: "node", testEnvironment: "node",
}; };

@ -1,84 +1,83 @@
{ {
"name": "adminka", "name": "adminka",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "type": "module",
"@date-io/dayjs": "^2.15.0", "dependencies": {
"@emotion/react": "^11.10.4", "@date-io/dayjs": "^2.15.0",
"@emotion/styled": "^11.10.4", "@emotion/react": "^11.10.4",
"@frontend/kitui": "^1.0.82", "@emotion/styled": "^11.10.4",
"@material-ui/pickers": "^3.3.10", "@frontend/kitui": "^1.0.82",
"@mui/icons-material": "^5.10.3", "@material-ui/pickers": "^3.3.10",
"@mui/material": "^5.10.5", "@mui/icons-material": "^5.10.3",
"@mui/styled-engine-sc": "^5.10.3", "@mui/material": "^5.10.5",
"@mui/x-data-grid": "^5.17.4", "@mui/styled-engine-sc": "^5.10.3",
"@mui/x-data-grid-generator": "^5.17.5", "@mui/x-data-grid": "^5.17.4",
"@mui/x-data-grid-premium": "^5.17.5", "@mui/x-data-grid-generator": "^5.17.5",
"@mui/x-date-pickers": "^5.0.3", "@mui/x-data-grid-premium": "^5.17.5",
"@testing-library/jest-dom": "^5.16.5", "@mui/x-date-pickers": "^5.0.3",
"@testing-library/react": "^13.3.0", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^13.5.0", "@testing-library/react": "^13.3.0",
"@types/jest": "^27.5.2", "@testing-library/user-event": "^13.5.0",
"@types/node": "^16.11.56", "@types/jest": "^27.5.2",
"@types/react": "^18.0.18", "@types/node": "^16.11.56",
"@types/react-dom": "^18.0.6", "@types/react": "^18.0.18",
"@types/react-router-dom": "^5.3.3", "@types/react-dom": "^18.0.6",
"axios": "^1.4.0", "@types/react-router-dom": "^5.3.3",
"craco": "^0.0.3", "axios": "^1.4.0",
"cypress": "^12.17.2", "craco": "^0.0.3",
"date-fns": "^3.3.1", "cypress": "^12.17.2",
"dayjs": "^1.11.5", "date-fns": "^3.3.1",
"formik": "^2.2.9", "dayjs": "^1.11.5",
"immer": "^10.0.2", "formik": "^2.2.9",
"moment": "^2.29.4", "immer": "^10.0.2",
"nanoid": "^4.0.1", "moment": "^2.29.4",
"notistack": "^3.0.1", "nanoid": "^4.0.1",
"numeral": "^2.0.6", "notistack": "^3.0.1",
"react": "^18.2.0", "numeral": "^2.0.6",
"react-dom": "^18.2.0", "react": "^18.2.0",
"react-error-boundary": "^4.0.13", "react-dom": "^18.2.0",
"react-numeral": "^1.1.1", "react-error-boundary": "^4.0.13",
"react-router-dom": "^6.3.0", "react-numeral": "^1.1.1",
"react-scripts": "^5.0.1", "react-router-dom": "^6.3.0",
"reconnecting-eventsource": "^1.6.2", "react-scripts": "^5.0.1",
"start-server-and-test": "^2.0.0", "reconnecting-eventsource": "^1.6.2",
"styled-components": "^5.3.5", "start-server-and-test": "^2.0.0",
"swr": "^2.2.5", "styled-components": "^5.3.5",
"typescript": "^4.8.2", "swr": "^2.2.5",
"use-debounce": "^9.0.4", "typescript": "^4.8.2",
"web-vitals": "^2.1.4", "use-debounce": "^9.0.4",
"zustand": "^4.3.8" "web-vitals": "^2.1.4",
}, "zustand": "^4.3.8"
"scripts": { },
"start": "craco start", "scripts": {
"build": "craco build", "start": "craco start",
"test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"", "build": "craco build",
"test:cart": "craco test src/utils/calcCart --transformIgnorePatterns \"node_modules/(?!@frontend)/\"", "test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
"test:cypress": "start-server-and-test start http://localhost:3000 cypress", "test:cart": "craco test src/utils/calcCart --transformIgnorePatterns \"node_modules/(?!@frontend)/\"",
"cypress": "cypress open", "test:cypress": "start-server-and-test start http://localhost:3000 cypress",
"eject": "craco eject", "cypress": "cypress open",
"format": "prettier . --write" "eject": "craco eject",
}, "format": "prettier . --write",
"eslintConfig": { "lint": "eslint ./src --fix"
"extends": [ },
"react-app", "browserslist": {
"react-app/jest" "production": [
] ">0.2%",
}, "not dead",
"browserslist": { "not op_mini all"
"production": [ ],
">0.2%", "development": [
"not dead", "last 1 chrome version",
"not op_mini all" "last 1 firefox version",
], "last 1 safari version"
"development": [ ]
"last 1 chrome version", },
"last 1 firefox version", "devDependencies": {
"last 1 safari version" "@eslint/js": "^9.3.0",
] "craco-alias": "^3.0.1",
}, "eslint": "^9.3.0",
"devDependencies": { "prettier": "^3.2.5",
"craco-alias": "^3.0.1", "typescript-eslint": "^7.10.0"
"prettier": "^3.2.5" }
}
} }

@ -1,4 +1,6 @@
@font-face { @font-face {
font-family: "GilroyRegular"; font-family: "GilroyRegular";
src: local("GilroyRegular"), url(fonts/GilroyRegular.woff) format("woff"); src:
} local("GilroyRegular"),
url(fonts/GilroyRegular.woff) format("woff");
}

@ -1,22 +1,19 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Web site created using create-react-app" />
name="description" <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
content="Web site created using create-react-app" <link rel="stylesheet" href="fonts.css" />
/> <!--
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="stylesheet" href="fonts.css" />
<!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML. Only files inside the `public` folder can be referenced from the HTML.
@ -25,12 +22,12 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>React App</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.
@ -40,5 +37,5 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html> </html>

@ -1,15 +1,15 @@
{ {
"short_name": "React App", "short_name": "React App",
"name": "Create React App Sample", "name": "Create React App Sample",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
} }
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "#000000",
"background_color": "#ffffff" "background_color": "#ffffff"
} }

@ -1,44 +1,35 @@
const puppeteer = require('puppeteer'); const puppeteer = require("puppeteer");
const url = "http://localhost:3000/users"; const url = "http://localhost:3000/users";
const urlMass = ['/users','/tariffs','/discounts','/promocode','/support', '/entities']; const urlMass = ["/users", "/tariffs", "/discounts", "/promocode", "/support", "/entities"];
jest.setTimeout(1000 * 60 * 5); jest.setTimeout(1000 * 60 * 5);
let browser;
let page;
describe('Тест', (() => {
beforeAll(async()=>{
browser = puppeteer.launch({headless:true});
page = browser.newPage();
page.goto(url); let browser;
// Set screen size let page;
page.setViewport({width: 1080, height: 1024});
}) describe("Тест", () => {
afterAll(() => browser.quit()); beforeAll(async () => {
test('Тест меню',async () => { browser = puppeteer.launch({ headless: true });
page = browser.newPage();
// Ждем загрузки менюшек
page.waitForSelector('.menu')
// Берем все ссылки с кнопок, у которых есть класс menu и вставляем в массив
let menuLink = page.evaluate(()=>{
let menuArray = document.querySelectorAll('.menu')
let Urls = Object.values(menuArray).map(
menuItem => (
menuItem.href.slice(menuItem.href.lastIndexOf('/'))
)
)
return Urls
})
// Проверяем, какие ссылки есть в нашем массиве, а каких нет
for (let i = 0; i < menuLink.length; i++) {
expect(urlMass.find((elem)=>elem===menuLink[i])).toBe(true)
}
})
}))
page.goto(url);
// Set screen size
page.setViewport({ width: 1080, height: 1024 });
});
afterAll(() => browser.quit());
test("Тест меню", async () => {
// Ждем загрузки менюшек
page.waitForSelector(".menu");
// Берем все ссылки с кнопок, у которых есть класс menu и вставляем в массив
const menuLink = page.evaluate(() => {
const menuArray = document.querySelectorAll(".menu");
const Urls = Object.values(menuArray).map((menuItem) => menuItem.href.slice(menuItem.href.lastIndexOf("/")));
return Urls;
});
// Проверяем, какие ссылки есть в нашем массиве, а каких нет
for (let i = 0; i < menuLink.length; i++) {
expect(urlMass.find((elem) => elem === menuLink[i])).toBe(true);
}
});
});

@ -1,12 +1,12 @@
const puppeteer = require('puppeteer'); const puppeteer = require("puppeteer");
(async () => { (async () => {
const browser = await puppeteer.launch(); const browser = await puppeteer.launch();
const page = await browser.newPage(); const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', { await page.goto("https://news.ycombinator.com", {
waitUntil: 'networkidle2', waitUntil: "networkidle2",
}); });
await page.pdf({ path: 'hn.pdf', format: 'a4' }); await page.pdf({ path: "hn.pdf", format: "a4" });
await browser.close(); await browser.close();
})(); })();

@ -2,56 +2,56 @@ import axios from "axios";
const message = "Artem"; const message = "Artem";
describe("tests", () => { describe("tests", () => {
let statusGetTickets: number; let statusGetTickets: number;
let dataGetTickets: {}; let dataGetTickets: {};
let statusGetMessages: number; let statusGetMessages: number;
let dataGetMessages: []; let dataGetMessages: [];
beforeEach(async () => { beforeEach(async () => {
await axios({ await axios({
method: "post", method: "post",
url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets",
data: { data: {
amt: 20, amt: 20,
page: 0, page: 0,
status: "open", status: "open",
}, },
}).then((result) => { }).then((result) => {
dataGetTickets = result.data; dataGetTickets = result.data;
statusGetTickets = result.status; statusGetTickets = result.status;
}); });
await axios({ await axios({
method: "post", method: "post",
url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages",
data: { data: {
amt: 100, amt: 100,
page: 0, page: 0,
srch: "", srch: "",
ticket: "cgg25qsvc9gd0bq9ne7g", ticket: "cgg25qsvc9gd0bq9ne7g",
}, },
}).then((result) => { }).then((result) => {
dataGetMessages = result.data; dataGetMessages = result.data;
statusGetMessages = result.status; statusGetMessages = result.status;
}); });
}); });
// добавляем сообщения тикету с id cgg25qsvc9gd0bq9ne7g , вписываем текст в переменную message и проверяем тест // добавляем сообщения тикету с id cgg25qsvc9gd0bq9ne7g , вписываем текст в переменную message и проверяем тест
test("test sending messages to tickets", () => { test("test sending messages to tickets", () => {
expect(statusGetTickets).toEqual(200); expect(statusGetTickets).toEqual(200);
// проверяем кличество тикетов отсалось неизменным // проверяем кличество тикетов отсалось неизменным
expect(dataGetTickets).toMatchObject({ count: 12 }); expect(dataGetTickets).toMatchObject({ count: 12 });
expect(statusGetMessages).toBe(200); expect(statusGetMessages).toBe(200);
expect(dataGetMessages[dataGetMessages.length - 1]).toMatchObject({ expect(dataGetMessages[dataGetMessages.length - 1]).toMatchObject({
files: [], files: [],
message: message, message: message,
request_screenshot: "", request_screenshot: "",
session_id: "6421ccdad01874dcffa8b128", session_id: "6421ccdad01874dcffa8b128",
shown: {}, shown: {},
ticket_id: "cgg25qsvc9gd0bq9ne7g", ticket_id: "cgg25qsvc9gd0bq9ne7g",
user_id: "6421ccdad01874dcffa8b128", user_id: "6421ccdad01874dcffa8b128",
}); });
}); });
}); });

@ -3,49 +3,47 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
type Name = { type Name = {
firstname: string; firstname: string;
secondname: string; secondname: string;
middlename: string; middlename: string;
orgname: string; orgname: string;
}; };
type Wallet = { type Wallet = {
currency: string; currency: string;
cash: number; cash: number;
purchasesAmount: number; purchasesAmount: number;
spent: number; spent: number;
money: number; money: number;
}; };
export type Account = { export type Account = {
_id: string; _id: string;
userId: string; userId: string;
cart: string[]; cart: string[];
status: string; status: string;
isDeleted: boolean; isDeleted: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string; deletedAt: string;
name: Name; name: Name;
wallet: Wallet; wallet: Wallet;
}; };
const baseUrl = process.env.REACT_APP_DOMAIN + "/customer" const baseUrl = process.env.REACT_APP_DOMAIN + "/customer";
export const getAccountInfo = async ( export const getAccountInfo = async (id: string): Promise<[Account | null, string?]> => {
id: string try {
): Promise<[Account | null, string?]> => { const accountInfoResponse = await makeRequest<never, Account>({
try { url: `${baseUrl}/account/${id}`,
const accountInfoResponse = await makeRequest<never, Account>({ method: "GET",
url: `${baseUrl}/account/${id}`, useToken: true,
method: "GET", });
useToken: true,
});
return [accountInfoResponse]; return [accountInfoResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить информацию об аккаунте. ${error}`]; return [null, `Не удалось получить информацию об аккаунте. ${error}`];
} }
}; };

@ -2,68 +2,58 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
import type { import type { LoginRequest, RegisterRequest, RegisterResponse } from "@frontend/kitui";
LoginRequest,
RegisterRequest,
RegisterResponse,
} from "@frontend/kitui";
const baseUrl = process.env.REACT_APP_DOMAIN + "/auth" const baseUrl = process.env.REACT_APP_DOMAIN + "/auth";
export const signin = async ( export const signin = async (login: string, password: string): Promise<[RegisterResponse | null, string?]> => {
login: string, try {
password: string const signinResponse = await makeRequest<LoginRequest, RegisterResponse>({
): Promise<[RegisterResponse | null, string?]> => { url: baseUrl + "/login",
try { body: { login, password },
const signinResponse = await makeRequest<LoginRequest, RegisterResponse>({ useToken: false,
url: baseUrl + "/login", });
body: { login, password },
useToken: false,
});
return [signinResponse]; return [signinResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
console.error(error) console.error(error);
return [null, `Ошибка авторизации. ${error}`]; return [null, `Ошибка авторизации. ${error}`];
} }
}; };
export const register = async ( export const register = async (
login: string, login: string,
password: string, password: string,
phoneNumber: string = "--" phoneNumber: string = "--"
): Promise<[RegisterResponse | null, string?]> => { ): Promise<[RegisterResponse | null, string?]> => {
try { try {
const registerResponse = await makeRequest< const registerResponse = await makeRequest<RegisterRequest, RegisterResponse>({
RegisterRequest, url: baseUrl + "/register",
RegisterResponse body: { login, password, phoneNumber },
>({ useToken: false,
url: baseUrl + "/register", });
body: { login, password, phoneNumber },
useToken: false,
});
return [registerResponse]; return [registerResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка регистрации. ${error}`]; return [null, `Ошибка регистрации. ${error}`];
} }
}; };
export const logout = async (): Promise<[unknown, string?]> => { export const logout = async (): Promise<[unknown, string?]> => {
try { try {
const logoutResponse = await makeRequest<never, unknown>({ const logoutResponse = await makeRequest<never, unknown>({
url: baseUrl + "/logout", url: baseUrl + "/logout",
method: "post", method: "post",
contentType: true, contentType: true,
}); });
return [logoutResponse]; return [logoutResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка выхода из аккаунта. ${error}`]; return [null, `Ошибка выхода из аккаунта. ${error}`];
} }
}; };

@ -3,243 +3,224 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
import type { Discount } from "@frontend/kitui"; import type { Discount } from "@frontend/kitui";
import type { import type { CreateDiscountBody, DiscountType, GetDiscountResponse } from "@root/model/discount";
CreateDiscountBody,
DiscountType,
GetDiscountResponse,
} from "@root/model/discount";
import useSWR from "swr"; import useSWR from "swr";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
const baseUrl = process.env.REACT_APP_DOMAIN + "/price" const baseUrl = process.env.REACT_APP_DOMAIN + "/price";
interface CreateDiscountParams { interface CreateDiscountParams {
purchasesAmount: number; purchasesAmount: number;
cartPurchasesAmount: number; cartPurchasesAmount: number;
discountMinValue: number; discountMinValue: number;
discountFactor: number; discountFactor: number;
discountDescription: string; discountDescription: string;
discountName: string; discountName: string;
/** ISO string */ /** ISO string */
startDate: string; startDate: string;
/** ISO string */ /** ISO string */
endDate: string; endDate: string;
serviceType: string; serviceType: string;
discountType: DiscountType; discountType: DiscountType;
privilegeId: string; privilegeId: string;
} }
export function createDiscountObject({ export function createDiscountObject({
endDate, endDate,
startDate, startDate,
discountName, discountName,
cartPurchasesAmount, cartPurchasesAmount,
discountDescription, discountDescription,
discountFactor, discountFactor,
discountMinValue, discountMinValue,
purchasesAmount, purchasesAmount,
serviceType, serviceType,
discountType, discountType,
privilegeId, privilegeId,
}: CreateDiscountParams) { }: CreateDiscountParams) {
const discount: CreateDiscountBody = { const discount: CreateDiscountBody = {
Name: discountName, Name: discountName,
Layer: 1, Layer: 1,
Description: discountDescription, Description: discountDescription,
Condition: { Condition: {
Period: { Period: {
From: startDate, From: startDate,
To: endDate, To: endDate,
}, },
User: "", User: "",
UserType: "", UserType: "",
Coupon: "", Coupon: "",
Usage: 0, Usage: 0,
PurchasesAmount: 0, PurchasesAmount: 0,
CartPurchasesAmount: 0, CartPurchasesAmount: 0,
Product: "", Product: "",
Term: 0, Term: 0,
PriceFrom: 0, PriceFrom: 0,
Group: "", Group: "",
}, },
Target: { Target: {
Factor: discountFactor, Factor: discountFactor,
TargetScope: "Sum", TargetScope: "Sum",
Overhelm: false, Overhelm: false,
TargetGroup: "", TargetGroup: "",
Products: [ Products: [
{ {
ID: "", ID: "",
Factor: 0, Factor: 0,
Overhelm: false, Overhelm: false,
}, },
], ],
}, },
}; };
switch (discountType) { switch (discountType) {
case "privilege": case "privilege":
discount.Layer = 1; discount.Layer = 1;
discount.Condition.Product = privilegeId; discount.Condition.Product = privilegeId;
discount.Condition.Term = discountMinValue / 100; discount.Condition.Term = discountMinValue / 100;
discount.Target.Products = [ discount.Target.Products = [
{ {
Factor: discountFactor, Factor: discountFactor,
ID: privilegeId, ID: privilegeId,
Overhelm: false, Overhelm: false,
}, },
]; ];
break; break;
case "service": case "service":
discount.Layer = 2; discount.Layer = 2;
discount.Condition.PriceFrom = discountMinValue; discount.Condition.PriceFrom = discountMinValue;
discount.Condition.Group = serviceType; discount.Condition.Group = serviceType;
discount.Target.TargetGroup = serviceType; discount.Target.TargetGroup = serviceType;
break; break;
case "cartPurchasesAmount": case "cartPurchasesAmount":
discount.Layer = 3; discount.Layer = 3;
discount.Condition.CartPurchasesAmount = cartPurchasesAmount; discount.Condition.CartPurchasesAmount = cartPurchasesAmount;
break; break;
case "purchasesAmount": case "purchasesAmount":
discount.Layer = 4; discount.Layer = 4;
discount.Condition.PurchasesAmount = purchasesAmount; discount.Condition.PurchasesAmount = purchasesAmount;
break; break;
} }
return discount; return discount;
} }
export const changeDiscount = async ( export const changeDiscount = async (discountId: string, discount: Discount): Promise<[unknown, string?]> => {
discountId: string, try {
discount: Discount const changeDiscountResponse = await makeRequest<Discount, unknown>({
): Promise<[unknown, string?]> => { url: baseUrl + "/discount/" + discountId,
try { method: "patch",
const changeDiscountResponse = await makeRequest<Discount, unknown>({ useToken: true,
url: baseUrl + "/discount/" + discountId, body: discount,
method: "patch", });
useToken: true,
body: discount,
});
return [changeDiscountResponse]; return [changeDiscountResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка изменения скидки. ${error}`]; return [null, `Ошибка изменения скидки. ${error}`];
} }
}; };
export const createDiscount = async ( export const createDiscount = async (discountParams: CreateDiscountParams): Promise<[Discount | null, string?]> => {
discountParams: CreateDiscountParams const discount = createDiscountObject(discountParams);
): Promise<[Discount | null, string?]> => {
const discount = createDiscountObject(discountParams);
try { try {
const createdDiscountResponse = await makeRequest< const createdDiscountResponse = await makeRequest<CreateDiscountBody, Discount>({
CreateDiscountBody, url: baseUrl + "/discount",
Discount method: "post",
>({ useToken: true,
url: baseUrl + "/discount", body: discount,
method: "post", });
useToken: true,
body: discount,
});
return [createdDiscountResponse]; return [createdDiscountResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка создания скидки. ${error}`]; return [null, `Ошибка создания скидки. ${error}`];
} }
}; };
export const deleteDiscount = async ( export const deleteDiscount = async (discountId: string): Promise<[Discount | null, string?]> => {
discountId: string try {
): Promise<[Discount | null, string?]> => { const deleteDiscountResponse = await makeRequest<never, Discount>({
try { url: baseUrl + "/discount/" + discountId,
const deleteDiscountResponse = await makeRequest<never, Discount>({ method: "delete",
url: baseUrl + "/discount/" + discountId, useToken: true,
method: "delete", });
useToken: true,
});
return [deleteDiscountResponse]; return [deleteDiscountResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка удаления скидки. ${error}`]; return [null, `Ошибка удаления скидки. ${error}`];
} }
}; };
export const patchDiscount = async ( export const patchDiscount = async (
discountId: string, discountId: string,
discountParams: CreateDiscountParams discountParams: CreateDiscountParams
): Promise<[Discount | null, string?]> => { ): Promise<[Discount | null, string?]> => {
const discount = createDiscountObject(discountParams); const discount = createDiscountObject(discountParams);
try { try {
const patchDiscountResponse = await makeRequest< const patchDiscountResponse = await makeRequest<CreateDiscountBody, Discount>({
CreateDiscountBody, url: baseUrl + "/discount/" + discountId,
Discount method: "patch",
>({ useToken: true,
url: baseUrl + "/discount/" + discountId, body: discount,
method: "patch", });
useToken: true,
body: discount,
});
return [patchDiscountResponse]; return [patchDiscountResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка изменения скидки. ${error}`]; return [null, `Ошибка изменения скидки. ${error}`];
} }
}; };
export const requestDiscounts = async (): Promise< export const requestDiscounts = async (): Promise<[GetDiscountResponse | null, string?]> => {
[GetDiscountResponse | null, string?] try {
> => { const discountsResponse = await makeRequest<never, GetDiscountResponse>({
try { url: baseUrl + "/discounts",
const discountsResponse = await makeRequest<never, GetDiscountResponse>({ method: "get",
url: baseUrl + "/discounts", useToken: true,
method: "get", });
useToken: true,
});
return [discountsResponse]; return [discountsResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка получения скидок. ${error}`]; return [null, `Ошибка получения скидок. ${error}`];
} }
}; };
async function getDiscounts() { async function getDiscounts() {
try { try {
const discountsResponse = await makeRequest<never, GetDiscountResponse>({ const discountsResponse = await makeRequest<never, GetDiscountResponse>({
url: baseUrl + "/discounts", url: baseUrl + "/discounts",
method: "get", method: "get",
useToken: true, useToken: true,
}); });
return discountsResponse.Discounts.filter((discount) => !discount.Deprecated); return discountsResponse.Discounts.filter((discount) => !discount.Deprecated);
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка получения списка скидок. ${error}`); throw new Error(`Ошибка получения списка скидок. ${error}`);
} }
} }
export function useDiscounts() { export function useDiscounts() {
const { data } = useSWR("discounts", getDiscounts, { const { data } = useSWR("discounts", getDiscounts, {
keepPreviousData: true, keepPreviousData: true,
suspense: true, suspense: true,
onError: (error) => { onError: (error) => {
if (!(error instanceof Error)) return; if (!(error instanceof Error)) return;
enqueueSnackbar(error.message, { variant: "error" }); enqueueSnackbar(error.message, { variant: "error" });
} },
}); });
return data; return data;
} }

@ -3,48 +3,43 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
type RawDetail = { type RawDetail = {
Key: string; Key: string;
Value: number | string | RawDetail[]; Value: number | string | RawDetail[];
}; };
type History = { type History = {
id: string; id: string;
userId: string; userId: string;
comment: string; comment: string;
key: string; key: string;
rawDetails: RawDetail[]; rawDetails: RawDetail[];
isDeleted: boolean; isDeleted: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
type HistoryResponse = { type HistoryResponse = {
records: History[]; records: History[];
totalPages: number; totalPages: number;
}; };
const baseUrl = process.env.REACT_APP_DOMAIN + "/customer"; const baseUrl = process.env.REACT_APP_DOMAIN + "/customer";
const getUserHistory = async ( const getUserHistory = async (accountId: string, page: number): Promise<[HistoryResponse | null, string?]> => {
accountId: string, try {
page: number const historyResponse = await makeRequest<never, HistoryResponse>({
): Promise<[HistoryResponse | null, string?]> => { method: "GET",
try { url: baseUrl + `/history?page=${page}&limit=${100}&accountID=${accountId}&type=payCart`,
const historyResponse = await makeRequest<never, HistoryResponse>({ });
method: "GET",
url:
baseUrl +
`/history?page=${page}&limit=${100}&accountID=${accountId}&type=payCart`,
});
return [historyResponse]; return [historyResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении пользователей. ${error}`]; return [null, `Ошибка при получении пользователей. ${error}`];
} }
}; };
export const historyApi = { export const historyApi = {
getUserHistory, getUserHistory,
}; };

@ -5,39 +5,36 @@ import { enqueueSnackbar } from "notistack";
import { historyApi } from "./requests"; import { historyApi } from "./requests";
export function useHistory(accountId: string) { export function useHistory(accountId: string) {
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const swrResponse = useSWRInfinite( const swrResponse = useSWRInfinite(
() => `history-${currentPage}`, () => `history-${currentPage}`,
async () => { async () => {
const [historyResponse, error] = await historyApi.getUserHistory( const [historyResponse, error] = await historyApi.getUserHistory(accountId, currentPage);
accountId,
currentPage
);
if (error) { if (error) {
throw new Error(error); throw new Error(error);
} }
if (!historyResponse) { if (!historyResponse) {
throw new Error("Empty history data"); throw new Error("Empty history data");
} }
if (currentPage < historyResponse.totalPages) { if (currentPage < historyResponse.totalPages) {
setCurrentPage((page) => page + 1); setCurrentPage((page) => page + 1);
} }
return historyResponse; return historyResponse;
}, },
{ {
onError(err) { onError(err) {
console.error("Error fetching users", err); console.error("Error fetching users", err);
enqueueSnackbar(err.message, { variant: "error" }); enqueueSnackbar(err.message, { variant: "error" });
}, },
focusThrottleInterval: 60e3, focusThrottleInterval: 60e3,
keepPreviousData: true, keepPreviousData: true,
} }
); );
return swrResponse; return swrResponse;
} }

@ -3,21 +3,30 @@ import { Method, ResponseType, AxiosError } from "axios";
import { clearAuthToken } from "@frontend/kitui"; import { clearAuthToken } from "@frontend/kitui";
import { redirect } from "react-router-dom"; import { redirect } from "react-router-dom";
interface MakeRequest { method?: Method | undefined; url: string; body?: unknown; useToken?: boolean | undefined; contentType?: boolean | undefined; responseType?: ResponseType | undefined; signal?: AbortSignal | undefined; withCredentials?: boolean | undefined; } interface MakeRequest {
method?: Method | undefined;
url: string;
body?: unknown;
useToken?: boolean | undefined;
contentType?: boolean | undefined;
responseType?: ResponseType | undefined;
signal?: AbortSignal | undefined;
withCredentials?: boolean | undefined;
}
async function makeRequest<TRequest = unknown, TResponse = unknown> (data:MakeRequest): Promise<TResponse> { async function makeRequest<TRequest = unknown, TResponse = unknown>(data: MakeRequest): Promise<TResponse> {
try { try {
const response = await KIT.makeRequest<unknown>(data) const response = await KIT.makeRequest<unknown>(data);
return response as TResponse return response as TResponse;
} catch (e) { } catch (e) {
const error = e as AxiosError; const error = e as AxiosError;
//@ts-ignore //@ts-ignore
if (error.response?.status === 400 && error.response?.data?.message === "refreshToken is empty") { if (error.response?.status === 400 && error.response?.data?.message === "refreshToken is empty") {
clearAuthToken() clearAuthToken();
redirect("/"); redirect("/");
} }
throw e throw e;
}; }
}; }
export default makeRequest; export default makeRequest;

@ -7,85 +7,71 @@ import { Privilege } from "@frontend/kitui";
import type { TMockData } from "./roles"; import type { TMockData } from "./roles";
type SeverPrivilegesResponse = { type SeverPrivilegesResponse = {
templategen: CustomPrivilege[]; templategen: CustomPrivilege[];
squiz: CustomPrivilege[]; squiz: CustomPrivilege[];
}; };
const baseUrl = process.env.REACT_APP_DOMAIN + "/strator" const baseUrl = process.env.REACT_APP_DOMAIN + "/strator";
export const getRoles = async (): Promise<[TMockData | null, string?]> => { export const getRoles = async (): Promise<[TMockData | null, string?]> => {
try { try {
const rolesResponse = await makeRequest<never, TMockData>({ const rolesResponse = await makeRequest<never, TMockData>({
method: "get", method: "get",
url: baseUrl + "/role", url: baseUrl + "/role",
}); });
return [rolesResponse]; return [rolesResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка запроса ролей. ${error}`]; return [null, `Ошибка запроса ролей. ${error}`];
} }
}; };
export const putPrivilege = async ( export const putPrivilege = async (body: Omit<Privilege, "_id" | "updatedAt">): Promise<[unknown, string?]> => {
body: Omit<Privilege, "_id" | "updatedAt"> try {
): Promise<[unknown, string?]> => { const putedPrivilege = await makeRequest<Omit<Privilege, "_id" | "updatedAt">, unknown>({
try { url: baseUrl + "/privilege",
const putedPrivilege = await makeRequest< method: "put",
Omit<Privilege, "_id" | "updatedAt">, body,
unknown });
>({
url: baseUrl + "/privilege",
method: "put",
body,
});
return [putedPrivilege]; return [putedPrivilege];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка изменения привилегии. ${error}`]; return [null, `Ошибка изменения привилегии. ${error}`];
} }
}; };
export const requestServicePrivileges = async (): Promise< export const requestServicePrivileges = async (): Promise<[SeverPrivilegesResponse | null, string?]> => {
[SeverPrivilegesResponse | null, string?] try {
> => { const privilegesResponse = await makeRequest<never, SeverPrivilegesResponse>({
try { url: baseUrl + "/privilege/service",
const privilegesResponse = await makeRequest< method: "get",
never, });
SeverPrivilegesResponse
>({
url: baseUrl + "/privilege/service",
method: "get",
});
return [privilegesResponse]; return [privilegesResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка запроса привилегий. ${error}`]; return [null, `Ошибка запроса привилегий. ${error}`];
} }
}; };
export const requestPrivileges = async ( export const requestPrivileges = async (signal: AbortSignal | undefined): Promise<[CustomPrivilege[], string?]> => {
signal: AbortSignal | undefined try {
): Promise<[CustomPrivilege[], string?]> => { const privilegesResponse = await makeRequest<never, CustomPrivilege[]>({
try { url: baseUrl + "/privilege",
const privilegesResponse = await makeRequest<never, CustomPrivilege[]>( method: "get",
{ useToken: true,
url: baseUrl + "/privilege", signal,
method: "get", });
useToken: true,
signal,
}
);
return [privilegesResponse]; return [privilegesResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [[], `Ошибка запроса привилегий. ${error}`]; return [[], `Ошибка запроса привилегий. ${error}`];
} }
}; };

@ -1,11 +1,11 @@
import makeRequest from "@root/api/makeRequest"; import makeRequest from "@root/api/makeRequest";
import type { import type {
CreatePromocodeBody, CreatePromocodeBody,
GetPromocodeListBody, GetPromocodeListBody,
Promocode, Promocode,
PromocodeList, PromocodeList,
PromocodeStatistics, PromocodeStatistics,
} from "@root/model/promocodes"; } from "@root/model/promocodes";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
@ -14,129 +14,117 @@ import { isAxiosError } from "axios";
const baseUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode"; const baseUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode";
const getPromocodeList = async (body: GetPromocodeListBody) => { const getPromocodeList = async (body: GetPromocodeListBody) => {
try { try {
const promocodeListResponse = await makeRequest< const promocodeListResponse = await makeRequest<GetPromocodeListBody, PromocodeList>({
GetPromocodeListBody, url: baseUrl + "/getList",
PromocodeList method: "POST",
>({ body,
url: baseUrl + "/getList", useToken: false,
method: "POST", });
body,
useToken: false,
});
return promocodeListResponse; return promocodeListResponse;
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка при получении списка промокодов. ${error}`); throw new Error(`Ошибка при получении списка промокодов. ${error}`);
} }
}; };
const createFastlink = async (id: string) => { const createFastlink = async (id: string) => {
try { try {
return await makeRequest<{ id: string }, { fastlink: string }>({ return await makeRequest<{ id: string }, { fastlink: string }>({
url: baseUrl + "/fastlink", url: baseUrl + "/fastlink",
method: "POST", method: "POST",
body: { id }, body: { id },
}); });
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка при создании фастлинка. ${error}`); throw new Error(`Ошибка при создании фастлинка. ${error}`);
} }
}; };
export const getAllPromocodes = async () => { export const getAllPromocodes = async () => {
try { try {
const promocodes: Promocode[] = []; const promocodes: Promocode[] = [];
let page = 0; let page = 0;
while (true) { while (true) {
const promocodeList = await getPromocodeList({ const promocodeList = await getPromocodeList({
limit: 100, limit: 100,
filter: { filter: {
active: true, active: true,
}, },
page, page,
}); });
if (promocodeList.items.length === 0) break; if (promocodeList.items.length === 0) break;
promocodes.push(...promocodeList.items); promocodes.push(...promocodeList.items);
page++; page++;
} }
return promocodes; return promocodes;
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка при получении списка промокодов. ${error}`); throw new Error(`Ошибка при получении списка промокодов. ${error}`);
} }
}; };
const createPromocode = async (body: CreatePromocodeBody) => { const createPromocode = async (body: CreatePromocodeBody) => {
try { try {
const createPromocodeResponse = await makeRequest< const createPromocodeResponse = await makeRequest<CreatePromocodeBody, Promocode>({
CreatePromocodeBody, url: baseUrl + "/create",
Promocode method: "POST",
>({ body,
url: baseUrl + "/create", useToken: false,
method: "POST", });
body,
useToken: false,
});
return createPromocodeResponse; return createPromocodeResponse;
} catch (nativeError) { } catch (nativeError) {
if ( if (isAxiosError(nativeError) && nativeError.response?.data.error === "Duplicate Codeword") {
isAxiosError(nativeError) && throw new Error(`Промокод уже существует`);
nativeError.response?.data.error === "Duplicate Codeword" }
) {
throw new Error(`Промокод уже существует`);
}
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка создания промокода. ${error}`); throw new Error(`Ошибка создания промокода. ${error}`);
} }
}; };
const deletePromocode = async (id: string): Promise<void> => { const deletePromocode = async (id: string): Promise<void> => {
try { try {
await makeRequest<never, never>({ await makeRequest<never, never>({
url: `${baseUrl}/${id}`, url: `${baseUrl}/${id}`,
method: "DELETE", method: "DELETE",
useToken: false, useToken: false,
}); });
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка удаления промокода. ${error}`); throw new Error(`Ошибка удаления промокода. ${error}`);
} }
}; };
const getPromocodeStatistics = async (id: string, from: number, to: number) => { const getPromocodeStatistics = async (id: string, from: number, to: number) => {
try { try {
const promocodeStatisticsResponse = await makeRequest< const promocodeStatisticsResponse = await makeRequest<unknown, PromocodeStatistics>({
unknown, url: baseUrl + `/stats`,
PromocodeStatistics body: {
>({ id: id,
url: baseUrl + `/stats`, from: from,
body: { to: to,
id: id, },
from: from, method: "POST",
to: to, useToken: false,
}, });
method: "POST", return promocodeStatisticsResponse;
useToken: false, } catch (nativeError) {
}); const [error] = parseAxiosError(nativeError);
return promocodeStatisticsResponse; throw new Error(`Ошибка при получении статистики промокода. ${error}`);
} catch (nativeError) { }
const [error] = parseAxiosError(nativeError);
throw new Error(`Ошибка при получении статистики промокода. ${error}`);
}
}; };
export const promocodeApi = { export const promocodeApi = {
getPromocodeList, getPromocodeList,
createPromocode, createPromocode,
deletePromocode, deletePromocode,
getAllPromocodes, getAllPromocodes,
getPromocodeStatistics, getPromocodeStatistics,
createFastlink, createFastlink,
}; };

@ -3,147 +3,134 @@ import useSwr, { mutate } from "swr";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { promocodeApi } from "./requests"; import { promocodeApi } from "./requests";
import type { import type { CreatePromocodeBody, PromocodeList } from "@root/model/promocodes";
CreatePromocodeBody,
PromocodeList,
} from "@root/model/promocodes";
export function usePromocodes( export function usePromocodes(page: number, pageSize: number, promocodeId: string, to: number, from: number) {
page: number, const promocodesCountRef = useRef<number>(0);
pageSize: number, const swrResponse = useSwr(
promocodeId: string, ["promocodes", page, pageSize],
to: number, async (key) => {
from: number const result = await promocodeApi.getPromocodeList({
) { limit: key[2],
const promocodesCountRef = useRef<number>(0); filter: {
const swrResponse = useSwr( active: true,
["promocodes", page, pageSize], },
async (key) => { page: key[1],
const result = await promocodeApi.getPromocodeList({ });
limit: key[2],
filter: {
active: true,
},
page: key[1],
});
promocodesCountRef.current = result.count; promocodesCountRef.current = result.count;
return result; return result;
}, },
{ {
onError(err) { onError(err) {
console.error("Error fetching promocodes", err); console.error("Error fetching promocodes", err);
enqueueSnackbar(err.message, { variant: "error" }); enqueueSnackbar(err.message, { variant: "error" });
}, },
focusThrottleInterval: 60e3, focusThrottleInterval: 60e3,
keepPreviousData: true, keepPreviousData: true,
} }
); );
const createPromocode = useCallback( const createPromocode = useCallback(
async function (body: CreatePromocodeBody) { async function (body: CreatePromocodeBody) {
try { try {
await promocodeApi.createPromocode(body); await promocodeApi.createPromocode(body);
mutate(["promocodes", page, pageSize]); mutate(["promocodes", page, pageSize]);
} catch (error) { } catch (error) {
console.error("Error creating promocode", error); console.error("Error creating promocode", error);
if (error instanceof Error) if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
enqueueSnackbar(error.message, { variant: "error" }); }
} },
}, [page, pageSize]
[page, pageSize] );
);
const deletePromocode = useCallback( const deletePromocode = useCallback(
async function (id: string) { async function (id: string) {
try { try {
await mutate<PromocodeList | undefined, void>( await mutate<PromocodeList | undefined, void>(
["promocodes", page, pageSize], ["promocodes", page, pageSize],
promocodeApi.deletePromocode(id), promocodeApi.deletePromocode(id),
{ {
optimisticData(currentData, displayedData) { optimisticData(currentData, displayedData) {
if (!displayedData) return; if (!displayedData) return;
return { return {
count: displayedData.count - 1, count: displayedData.count - 1,
items: displayedData.items.filter((item) => item.id !== id), items: displayedData.items.filter((item) => item.id !== id),
}; };
}, },
rollbackOnError: true, rollbackOnError: true,
populateCache(result, currentData) { populateCache(result, currentData) {
if (!currentData) return; if (!currentData) return;
return { return {
count: currentData.count - 1, count: currentData.count - 1,
items: currentData.items.filter((item) => item.id !== id), items: currentData.items.filter((item) => item.id !== id),
}; };
}, },
} }
); );
} catch (error) { } catch (error) {
console.error("Error deleting promocode", error); console.error("Error deleting promocode", error);
if (error instanceof Error) if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
enqueueSnackbar(error.message, { variant: "error" }); }
} },
}, [page, pageSize]
[page, pageSize] );
);
const promocodeStatistics = useSwr( const promocodeStatistics = useSwr(
["promocodeStatistics", promocodeId, from, to], ["promocodeStatistics", promocodeId, from, to],
async ([_, id, from, to]) => { async ([_, id, from, to]) => {
if (!id) { if (!id) {
return null; return null;
} }
const promocodeStatisticsResponse = const promocodeStatisticsResponse = await promocodeApi.getPromocodeStatistics(id, from, to);
await promocodeApi.getPromocodeStatistics(id, from, to);
return promocodeStatisticsResponse; return promocodeStatisticsResponse;
}, },
{ {
onError(err) { onError(err) {
console.error("Error fetching promocode statistics", err); console.error("Error fetching promocode statistics", err);
enqueueSnackbar(err.message, { variant: "error" }); enqueueSnackbar(err.message, { variant: "error" });
}, },
focusThrottleInterval: 60e3, focusThrottleInterval: 60e3,
keepPreviousData: true, keepPreviousData: true,
} }
); );
const createFastLink = useCallback( const createFastLink = useCallback(
async function (id: string) { async function (id: string) {
try { try {
await promocodeApi.createFastlink(id); await promocodeApi.createFastlink(id);
mutate(["promocodes", page, pageSize]); mutate(["promocodes", page, pageSize]);
} catch (error) { } catch (error) {
console.error("Error creating fast link", error); console.error("Error creating fast link", error);
if (error instanceof Error) if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
enqueueSnackbar(error.message, { variant: "error" }); }
} },
}, [page, pageSize]
[page, pageSize] );
);
return { return {
...swrResponse, ...swrResponse,
createPromocode, createPromocode,
deletePromocode, deletePromocode,
createFastLink, createFastLink,
promocodeStatistics: promocodeStatistics.data, promocodeStatistics: promocodeStatistics.data,
promocodesCount: promocodesCountRef.current, promocodesCount: promocodesCountRef.current,
}; };
} }
export function useAllPromocodes() { export function useAllPromocodes() {
const { data } = useSwr("allPromocodes", promocodeApi.getAllPromocodes, { const { data } = useSwr("allPromocodes", promocodeApi.getAllPromocodes, {
keepPreviousData: true, keepPreviousData: true,
suspense: true, suspense: true,
onError(err) { onError(err) {
console.error("Error fetching all promocodes", err); console.error("Error fetching all promocodes", err);
enqueueSnackbar(err.message, { variant: "error" }); enqueueSnackbar(err.message, { variant: "error" });
}, },
}); });
return data; return data;
} }

@ -1,89 +1,77 @@
import makeRequest from "@root/api/makeRequest"; import makeRequest from "@root/api/makeRequest";
import type { import type {
GetStatisticSchildBody, GetStatisticSchildBody,
QuizStatisticsItem, QuizStatisticsItem,
GetPromocodeStatisticsBody, GetPromocodeStatisticsBody,
AllPromocodeStatistics, AllPromocodeStatistics,
} from "./types"; } from "./types";
export type QuizStatisticResponse = { export type QuizStatisticResponse = {
Registrations: number; Registrations: number;
Quizes: number; Quizes: number;
Results: number; Results: number;
}; };
type TRequest = { type TRequest = {
to: number; to: number;
from: number; from: number;
}; };
export const getStatistic = async ( export const getStatistic = async (to: number, from: number): Promise<QuizStatisticResponse> => {
to: number, try {
from: number const generalResponse = await makeRequest<TRequest, QuizStatisticResponse>({
): Promise<QuizStatisticResponse> => { url: `${process.env.REACT_APP_DOMAIN}/squiz/statistic`,
try { body: { to, from },
const generalResponse = await makeRequest<TRequest, QuizStatisticResponse>({ });
url: `${process.env.REACT_APP_DOMAIN}/squiz/statistic`, return generalResponse;
body: { to, from }, } catch (nativeError) {
}); return { Registrations: 0, Quizes: 0, Results: 0 };
return generalResponse; }
} catch (nativeError) {
return { Registrations: 0, Quizes: 0, Results: 0 };
}
}; };
export const getStatisticSchild = async ( export const getStatisticSchild = async (from: number, to: number): Promise<QuizStatisticsItem[]> => {
from: number, try {
to: number const StatisticResponse = await makeRequest<GetStatisticSchildBody, QuizStatisticsItem[]>({
): Promise<QuizStatisticsItem[]> => { url: process.env.REACT_APP_DOMAIN + "/customer/quizlogo/stat",
try { method: "post",
const StatisticResponse = await makeRequest< useToken: true,
GetStatisticSchildBody, body: { to, from, page: 0, limit: 100 },
QuizStatisticsItem[] });
>({
url: process.env.REACT_APP_DOMAIN + "/customer/quizlogo/stat",
method: "post",
useToken: true,
body: { to, from, page: 0, limit: 100 },
});
if (!StatisticResponse) { if (!StatisticResponse) {
throw new Error("Статистика не найдена"); throw new Error("Статистика не найдена");
} }
return StatisticResponse; return StatisticResponse;
} catch (nativeError) { } catch (nativeError) {
return [ return [
{ {
ID: "0", ID: "0",
Regs: 0, Regs: 0,
Money: 0, Money: 0,
Quizes: [{ QuizID: "0", Regs: 0, Money: 0 }], Quizes: [{ QuizID: "0", Regs: 0, Money: 0 }],
}, },
]; ];
} }
}; };
export const getStatisticPromocode = async ( export const getStatisticPromocode = async (
from: number, from: number,
to: number to: number
): Promise<Record<string, AllPromocodeStatistics>> => { ): Promise<Record<string, AllPromocodeStatistics>> => {
try { try {
const StatisticPromo = await makeRequest< const StatisticPromo = await makeRequest<GetPromocodeStatisticsBody, Record<string, AllPromocodeStatistics>>({
GetPromocodeStatisticsBody, url: process.env.REACT_APP_DOMAIN + "/customer/promocode/ltv",
Record<string, AllPromocodeStatistics> method: "post",
>({ useToken: true,
url: process.env.REACT_APP_DOMAIN + "/customer/promocode/ltv", body: { to, from },
method: "post", });
useToken: true,
body: { to, from },
});
return StatisticPromo; return StatisticPromo;
} catch (nativeError) { } catch (nativeError) {
console.log(nativeError); console.log(nativeError);
return {}; return {};
} }
}; };

@ -1,31 +1,31 @@
import { Moment } from "moment"; import { Moment } from "moment";
export type GetStatisticSchildBody = { export type GetStatisticSchildBody = {
to: Moment | null; to: Moment | null;
from: Moment | null; from: Moment | null;
page: number; page: number;
limit: number; limit: number;
}; };
type StatisticsQuizes = { type StatisticsQuizes = {
QuizID: string; QuizID: string;
Money: number; Money: number;
Regs: number; Regs: number;
}; };
export type QuizStatisticsItem = { export type QuizStatisticsItem = {
ID: string; ID: string;
Money: number; Money: number;
Quizes: StatisticsQuizes[]; Quizes: StatisticsQuizes[];
Regs: number; Regs: number;
}; };
export type GetPromocodeStatisticsBody = { export type GetPromocodeStatisticsBody = {
from: number; from: number;
to: number; to: number;
}; };
export type AllPromocodeStatistics = { export type AllPromocodeStatistics = {
Money: number; Money: number;
Regs: number; Regs: number;
}; };

@ -3,59 +3,59 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
export const MOCK_DATA_USERS = [ export const MOCK_DATA_USERS = [
{ {
key: 0, key: 0,
id: "someid1", id: "someid1",
name: "admin", name: "admin",
desc: "Администратор сервиса", desc: "Администратор сервиса",
}, },
{ {
key: 1, key: 1,
id: "someid2", id: "someid2",
name: "manager", name: "manager",
desc: "Менеджер сервиса", desc: "Менеджер сервиса",
}, },
{ {
key: 2, key: 2,
id: "someid3", id: "someid3",
name: "user", name: "user",
desc: "Пользователь сервиса", desc: "Пользователь сервиса",
}, },
]; ];
export type TMockData = typeof MOCK_DATA_USERS; export type TMockData = typeof MOCK_DATA_USERS;
export type UserType = { export type UserType = {
_id: string; _id: string;
login: string; login: string;
email: string; email: string;
phoneNumber: string; phoneNumber: string;
isDeleted: boolean; isDeleted: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
const baseUrl =process.env.REACT_APP_DOMAIN + "/role" const baseUrl = process.env.REACT_APP_DOMAIN + "/role";
export const getRoles_mock = (): Promise<TMockData> => { export const getRoles_mock = (): Promise<TMockData> => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(MOCK_DATA_USERS); resolve(MOCK_DATA_USERS);
}, 1000); }, 1000);
}); });
}; };
export const deleteRole = async (id: string): Promise<[unknown, string?]> => { export const deleteRole = async (id: string): Promise<[unknown, string?]> => {
try { try {
const deleteRoleResponse = await makeRequest({ const deleteRoleResponse = await makeRequest({
url: `${baseUrl}/${id}`, url: `${baseUrl}/${id}`,
method: "delete", method: "delete",
}); });
return [deleteRoleResponse]; return [deleteRoleResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка удаления роли. ${error}`]; return [null, `Ошибка удаления роли. ${error}`];
} }
}; };

@ -7,95 +7,87 @@ import type { Tariff } from "@frontend/kitui";
import type { EditTariffRequestBody } from "@root/model/tariff"; import type { EditTariffRequestBody } from "@root/model/tariff";
type CreateTariffBackendRequest = { type CreateTariffBackendRequest = {
name: string; name: string;
description: string; description: string;
order: number; order: number;
price: number; price: number;
isCustom: boolean; isCustom: boolean;
privileges: Omit<Privilege, "_id" | "updatedAt">[]; privileges: Omit<Privilege, "_id" | "updatedAt">[];
}; };
type GetTariffsResponse = { type GetTariffsResponse = {
totalPages: number; totalPages: number;
tariffs: Tariff[]; tariffs: Tariff[];
}; };
const baseUrl =process.env.REACT_APP_DOMAIN + "/strator" const baseUrl = process.env.REACT_APP_DOMAIN + "/strator";
export const createTariff = async ( export const createTariff = async (body: CreateTariffBackendRequest): Promise<[unknown, string?]> => {
body: CreateTariffBackendRequest try {
): Promise<[unknown, string?]> => { const createdTariffResponse = await makeRequest<CreateTariffBackendRequest>({
try { url: baseUrl + "/tariff/",
const createdTariffResponse = await makeRequest<CreateTariffBackendRequest>( method: "post",
{ body,
url: baseUrl + "/tariff/", });
method: "post",
body,
}
);
return [createdTariffResponse]; return [createdTariffResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка создания тарифа. ${error}`]; return [null, `Ошибка создания тарифа. ${error}`];
} }
}; };
export const putTariff = async (tariff: Tariff): Promise<[null, string?]> => { export const putTariff = async (tariff: Tariff): Promise<[null, string?]> => {
try { try {
const putedTariffResponse = await makeRequest<EditTariffRequestBody, null>({ const putedTariffResponse = await makeRequest<EditTariffRequestBody, null>({
method: "put", method: "put",
url: baseUrl + `/tariff/${tariff._id}`, url: baseUrl + `/tariff/${tariff._id}`,
body: { body: {
name: tariff.name, name: tariff.name,
price: tariff.price ?? 0, price: tariff.price ?? 0,
isCustom: false, isCustom: false,
order: tariff.order || 1, order: tariff.order || 1,
description: tariff.description ?? "", description: tariff.description ?? "",
privileges: tariff.privileges, privileges: tariff.privileges,
}, },
}); });
return [putedTariffResponse]; return [putedTariffResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка редактирования тарифа. ${error}`]; return [null, `Ошибка редактирования тарифа. ${error}`];
} }
}; };
export const deleteTariff = async ( export const deleteTariff = async (tariffId: string): Promise<[Tariff | null, string?]> => {
tariffId: string try {
): Promise<[Tariff | null, string?]> => { const deletedTariffResponse = await makeRequest<{ id: string }, Tariff>({
try { method: "delete",
const deletedTariffResponse = await makeRequest<{ id: string }, Tariff>({ url: baseUrl + "/tariff",
method: "delete", body: { id: tariffId },
url: baseUrl + "/tariff", });
body: { id: tariffId },
});
return [deletedTariffResponse]; return [deletedTariffResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка удаления тарифа. ${error}`]; return [null, `Ошибка удаления тарифа. ${error}`];
} }
}; };
export const requestTariffs = async ( export const requestTariffs = async (page: number): Promise<[GetTariffsResponse | null, string?]> => {
page: number try {
): Promise<[GetTariffsResponse | null, string?]> => { const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
try { url: baseUrl + `/tariff/?page=${page}&limit=${100}`,
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({ method: "get",
url: baseUrl + `/tariff/?page=${page}&limit=${100}`, });
method: "get",
});
return [tariffsResponse]; return [tariffsResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка запроса тарифов. ${error}`]; return [null, `Ошибка запроса тарифов. ${error}`];
} }
}; };

@ -4,26 +4,21 @@ import { parseAxiosError } from "@root/utils/parse-error";
import type { SendTicketMessageRequest } from "@root/model/ticket"; import type { SendTicketMessageRequest } from "@root/model/ticket";
const baseUrl = process.env.REACT_APP_DOMAIN + "/heruvym" const baseUrl = process.env.REACT_APP_DOMAIN + "/heruvym";
export const sendTicketMessage = async ( export const sendTicketMessage = async (body: SendTicketMessageRequest): Promise<[unknown, string?]> => {
body: SendTicketMessageRequest try {
): Promise<[unknown, string?]> => { const sendTicketMessageResponse = await makeRequest<SendTicketMessageRequest, unknown>({
try { url: `${baseUrl}/send`,
const sendTicketMessageResponse = await makeRequest< method: "POST",
SendTicketMessageRequest, useToken: true,
unknown body,
>({ });
url: `${baseUrl}/send`,
method: "POST",
useToken: true,
body,
});
return [sendTicketMessageResponse]; return [sendTicketMessageResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка отправки сообщения. ${error}`]; return [null, `Ошибка отправки сообщения. ${error}`];
} }
}; };

@ -5,85 +5,76 @@ import { parseAxiosError } from "@root/utils/parse-error";
import type { UserType } from "@root/api/roles"; import type { UserType } from "@root/api/roles";
export type UsersListResponse = { export type UsersListResponse = {
totalPages: number; totalPages: number;
users: UserType[]; users: UserType[];
}; };
const baseUrl = process.env.REACT_APP_DOMAIN + "/user"; const baseUrl = process.env.REACT_APP_DOMAIN + "/user";
const getUserInfo = async (id: string): Promise<[UserType | null, string?]> => { const getUserInfo = async (id: string): Promise<[UserType | null, string?]> => {
try { try {
const userInfoResponse = await makeRequest<never, UserType>({ const userInfoResponse = await makeRequest<never, UserType>({
url: `${baseUrl}/${id}`, url: `${baseUrl}/${id}`,
method: "GET", method: "GET",
useToken: true, useToken: true,
}); });
return [userInfoResponse]; return [userInfoResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка получения информации о пользователе. ${error}`]; return [null, `Ошибка получения информации о пользователе. ${error}`];
} }
}; };
const getUserList = async ( const getUserList = async (page = 1, limit = 10): Promise<[UsersListResponse | null, string?]> => {
page = 1, try {
limit = 10 const userResponse = await makeRequest<never, UsersListResponse>({
): Promise<[UsersListResponse | null, string?]> => { method: "get",
try { url: baseUrl + `/?page=${page}&limit=${limit}`,
const userResponse = await makeRequest<never, UsersListResponse>({ });
method: "get",
url: baseUrl + `/?page=${page}&limit=${limit}`,
});
return [userResponse]; return [userResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении пользователей. ${error}`]; return [null, `Ошибка при получении пользователей. ${error}`];
} }
}; };
const getManagerList = async ( const getManagerList = async (page = 1, limit = 10): Promise<[UsersListResponse | null, string?]> => {
page = 1, try {
limit = 10 const managerResponse = await makeRequest<never, UsersListResponse>({
): Promise<[UsersListResponse | null, string?]> => { method: "get",
try { url: baseUrl + `/?page=${page}&limit=${limit}`,
const managerResponse = await makeRequest<never, UsersListResponse>({ });
method: "get",
url: baseUrl + `/?page=${page}&limit=${limit}`,
});
return [managerResponse]; return [managerResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении менеджеров. ${error}`]; return [null, `Ошибка при получении менеджеров. ${error}`];
} }
}; };
const getAdminList = async ( const getAdminList = async (page = 1, limit = 10): Promise<[UsersListResponse | null, string?]> => {
page = 1, try {
limit = 10 const adminResponse = await makeRequest<never, UsersListResponse>({
): Promise<[UsersListResponse | null, string?]> => { method: "get",
try { url: baseUrl + `/?page=${page}&limit=${limit}`,
const adminResponse = await makeRequest<never, UsersListResponse>({ });
method: "get",
url: baseUrl + `/?page=${page}&limit=${limit}`,
});
return [adminResponse]; return [adminResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка при получении админов. ${error}`]; return [null, `Ошибка при получении админов. ${error}`];
} }
}; };
export const userApi = { export const userApi = {
getUserInfo, getUserInfo,
getUserList, getUserList,
getManagerList, getManagerList,
getAdminList, getAdminList,
}; };

@ -5,100 +5,94 @@ import { enqueueSnackbar } from "notistack";
import { userApi } from "./requests"; import { userApi } from "./requests";
export function useAdmins(page: number, pageSize: number) { export function useAdmins(page: number, pageSize: number) {
const adminPagesRef = useRef<number>(0); const adminPagesRef = useRef<number>(0);
const swrResponse = useSwr( const swrResponse = useSwr(
["admin", page, pageSize], ["admin", page, pageSize],
async ([_, page, pageSize]) => { async ([_, page, pageSize]) => {
const [adminResponse, error] = await userApi.getManagerList( const [adminResponse, error] = await userApi.getManagerList(page, pageSize);
page,
pageSize
);
if (error) { if (error) {
throw new Error(error); throw new Error(error);
} }
adminPagesRef.current = adminResponse?.totalPages || 1; adminPagesRef.current = adminResponse?.totalPages || 1;
return adminResponse; return adminResponse;
}, },
{ {
onError(err) { onError(err) {
console.error("Error fetching users", err); console.error("Error fetching users", err);
enqueueSnackbar(err.message, { variant: "error" }); enqueueSnackbar(err.message, { variant: "error" });
}, },
focusThrottleInterval: 60e3, focusThrottleInterval: 60e3,
keepPreviousData: true, keepPreviousData: true,
} }
); );
return { return {
...swrResponse, ...swrResponse,
adminPages: adminPagesRef.current, adminPages: adminPagesRef.current,
}; };
} }
export function useManagers(page: number, pageSize: number) { export function useManagers(page: number, pageSize: number) {
const managerPagesRef = useRef<number>(0); const managerPagesRef = useRef<number>(0);
const swrResponse = useSwr( const swrResponse = useSwr(
["manager", page, pageSize], ["manager", page, pageSize],
async ([_, page, pageSize]) => { async ([_, page, pageSize]) => {
const [managerResponse, error] = await userApi.getManagerList( const [managerResponse, error] = await userApi.getManagerList(page, pageSize);
page,
pageSize
);
if (error) { if (error) {
throw new Error(error); throw new Error(error);
} }
managerPagesRef.current = managerResponse?.totalPages || 1; managerPagesRef.current = managerResponse?.totalPages || 1;
return managerResponse; return managerResponse;
}, },
{ {
onError(err) { onError(err) {
console.error("Error fetching users", err); console.error("Error fetching users", err);
enqueueSnackbar(err.message, { variant: "error" }); enqueueSnackbar(err.message, { variant: "error" });
}, },
focusThrottleInterval: 60e3, focusThrottleInterval: 60e3,
keepPreviousData: true, keepPreviousData: true,
} }
); );
return { return {
...swrResponse, ...swrResponse,
managerPages: managerPagesRef.current, managerPages: managerPagesRef.current,
}; };
} }
export function useUsers(page: number, pageSize: number) { export function useUsers(page: number, pageSize: number) {
const userPagesRef = useRef<number>(0); const userPagesRef = useRef<number>(0);
const swrResponse = useSwr( const swrResponse = useSwr(
["users", page, pageSize], ["users", page, pageSize],
async ([_, page, pageSize]) => { async ([_, page, pageSize]) => {
const [userResponse, error] = await userApi.getUserList(page, pageSize); const [userResponse, error] = await userApi.getUserList(page, pageSize);
if (error) { if (error) {
throw new Error(error); throw new Error(error);
} }
userPagesRef.current = userResponse?.totalPages || 1; userPagesRef.current = userResponse?.totalPages || 1;
return userResponse; return userResponse;
}, },
{ {
onError(err) { onError(err) {
console.error("Error fetching users", err); console.error("Error fetching users", err);
enqueueSnackbar(err.message, { variant: "error" }); enqueueSnackbar(err.message, { variant: "error" });
}, },
focusThrottleInterval: 60e3, focusThrottleInterval: 60e3,
keepPreviousData: true, keepPreviousData: true,
} }
); );
return { return {
...swrResponse, ...swrResponse,
userPagesCount: userPagesRef.current, userPagesCount: userPagesRef.current,
}; };
} }

@ -3,65 +3,58 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
type File = { type File = {
name: "inn" | "rule" | "egrule" | "certificate"; name: "inn" | "rule" | "egrule" | "certificate";
url: string; url: string;
}; };
export type Verification = { export type Verification = {
_id: string; _id: string;
accepted: boolean; accepted: boolean;
status: "org" | "nko"; status: "org" | "nko";
updated_at: string; updated_at: string;
comment: string; comment: string;
taxnumber: string; taxnumber: string;
files: File[]; files: File[];
}; };
type PatchVerificationBody = { type PatchVerificationBody = {
id?: string; id?: string;
status?: "org" | "nko"; status?: "org" | "nko";
comment?: string; comment?: string;
accepted?: boolean; accepted?: boolean;
taxnumber?: string; taxnumber?: string;
}; };
const baseUrl = process.env.REACT_APP_DOMAIN + "/verification" const baseUrl = process.env.REACT_APP_DOMAIN + "/verification";
export const verification = async ( export const verification = async (userId: string): Promise<[Verification | null, string?]> => {
userId: string try {
): Promise<[Verification | null, string?]> => { const verificationResponse = await makeRequest<never, Verification>({
try { method: "get",
const verificationResponse = await makeRequest<never, Verification>({ url: baseUrl + `/verification/${userId}`,
method: "get", });
url: baseUrl + `/verification/${userId}`,
});
return [verificationResponse]; return [verificationResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка верификации. ${error}`]; return [null, `Ошибка верификации. ${error}`];
} }
}; };
export const patchVerification = async ( export const patchVerification = async (body: PatchVerificationBody): Promise<[unknown, string?]> => {
body: PatchVerificationBody try {
): Promise<[unknown, string?]> => { const patchedVerificationResponse = await makeRequest<PatchVerificationBody, unknown>({
try { method: "patch",
const patchedVerificationResponse = await makeRequest< useToken: true,
PatchVerificationBody, url: baseUrl + `/verification`,
unknown body,
>({ });
method: "patch",
useToken: true,
url: baseUrl + `/verification`,
body,
});
return [patchedVerificationResponse]; return [patchedVerificationResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Ошибка изменения верификации. ${error}`]; return [null, `Ошибка изменения верификации. ${error}`];
} }
}; };

@ -1,4 +1,6 @@
@font-face { @font-face {
font-family: "GilroyRegular"; font-family: "GilroyRegular";
src: local("GilroyRegular"), url(./fonts/GilroyRegular.woff) format("woff"); src:
} local("GilroyRegular"),
url(./fonts/GilroyRegular.woff) format("woff");
}

@ -2,18 +2,18 @@ import React from "react";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
type ArticleProps = { type ArticleProps = {
header: JSX.Element; header: JSX.Element;
body: JSX.Element; body: JSX.Element;
isBoby?: boolean; isBoby?: boolean;
}; };
export const Article = ({ header, body, isBoby = false }: ArticleProps) => { export const Article = ({ header, body, isBoby = false }: ArticleProps) => {
return ( return (
<Box component="section"> <Box component="section">
<Box> <Box>
<Typography variant="h1">{header}</Typography> <Typography variant="h1">{header}</Typography>
</Box> </Box>
{isBoby ? <Box>{body}</Box> : <React.Fragment />} {isBoby ? <Box>{body}</Box> : <React.Fragment />}
</Box> </Box>
); );
}; };

@ -1,20 +1,20 @@
import { calcCart } from "@frontend/kitui"; import { calcCart } from "@frontend/kitui";
import Input from "@kitUI/input"; import Input from "@kitUI/input";
import { import {
Alert, Alert,
Box, Box,
Button, Button,
Checkbox, Checkbox,
FormControlLabel, FormControlLabel,
Paper, Paper,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { useDiscounts } from "@root/api/discounts"; import { useDiscounts } from "@root/api/discounts";
import { useAllPromocodes } from "@root/api/promocode/swr"; import { useAllPromocodes } from "@root/api/promocode/swr";
@ -27,249 +27,237 @@ import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useState } from "react"; import { useState } from "react";
import CartItemRow from "./CartItemRow"; import CartItemRow from "./CartItemRow";
export default function Cart() { export default function Cart() {
const theme = useTheme(); const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(400)); const mobile = useMediaQuery(theme.breakpoints.down(400));
const discounts = useDiscounts(); const discounts = useDiscounts();
const promocodes = useAllPromocodes(); const promocodes = useAllPromocodes();
const cartData = useCartStore((store) => store.cartData); const cartData = useCartStore((store) => store.cartData);
const tariffs = useTariffStore(state => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const [couponField, setCouponField] = useState<string>(""); const [couponField, setCouponField] = useState<string>("");
const [loyaltyField, setLoyaltyField] = useState<string>(""); const [loyaltyField, setLoyaltyField] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isNonCommercial, setIsNonCommercial] = useState<boolean>(false); const [isNonCommercial, setIsNonCommercial] = useState<boolean>(false);
const selectedTariffIds = useTariffStore(state => state.selectedTariffIds); const selectedTariffIds = useTariffStore((state) => state.selectedTariffIds);
async function handleCalcCartClick() { async function handleCalcCartClick() {
await requestPrivileges(); await requestPrivileges();
await requestDiscounts(); await requestDiscounts();
const cartTariffs = tariffs.filter(tariff => selectedTariffIds.includes(tariff._id)); const cartTariffs = tariffs.filter((tariff) => selectedTariffIds.includes(tariff._id));
let loyaltyValue = parseInt(loyaltyField); let loyaltyValue = parseInt(loyaltyField);
if (!isFinite(loyaltyValue)) loyaltyValue = 0; if (!isFinite(loyaltyValue)) loyaltyValue = 0;
const promocode = promocodes.find(promocode => { const promocode = promocodes.find((promocode) => {
if (promocode.dueTo < (Date.now() / 1000)) return false; if (promocode.dueTo < Date.now() / 1000) return false;
return promocode.codeword === couponField.trim(); return promocode.codeword === couponField.trim();
}); });
const userId = crypto.randomUUID(); const userId = crypto.randomUUID();
const discountsWithPromocodeDiscount = promocode ? [ const discountsWithPromocodeDiscount = promocode
...discounts, ? [...discounts, createDiscountFromPromocode(promocode, userId)]
createDiscountFromPromocode(promocode, userId), : discounts;
] : discounts;
try { try {
const cartData = calcCart(cartTariffs, discountsWithPromocodeDiscount, loyaltyValue, userId); const cartData = calcCart(cartTariffs, discountsWithPromocodeDiscount, loyaltyValue, userId);
setErrorMessage(null); setErrorMessage(null);
setCartData(cartData); setCartData(cartData);
} catch (error: any) { } catch (error: any) {
setErrorMessage(error.message); setErrorMessage(error.message);
setCartData(null); setCartData(null);
} }
} }
return ( return (
<Box <Box
component="section" component="section"
sx={{ sx={{
border: "1px solid white", border: "1px solid white",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
pb: "20px", pb: "20px",
borderRadius: "4px", borderRadius: "4px",
}} }}
> >
<Typography variant="caption">корзина</Typography> <Typography variant="caption">корзина</Typography>
<Paper <Paper
variant="bar" variant="bar"
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
gap: "20px", gap: "20px",
flexDirection: mobile ? "column" : undefined flexDirection: mobile ? "column" : undefined,
}} }}
> >
<FormControlLabel <FormControlLabel
checked={isNonCommercial} checked={isNonCommercial}
onChange={(e, checked) => setIsNonCommercial(checked)} onChange={(e, checked) => setIsNonCommercial(checked)}
control={ control={
<Checkbox <Checkbox
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
"&.Mui-checked": { "&.Mui-checked": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
}} }}
/> />
} }
label="НКО" label="НКО"
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}} }}
/> />
<Box <Box
sx={{ sx={{
border: "1px solid white", border: "1px solid white",
padding: "3px", padding: "3px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}} }}
> >
<Input <Input
label="промокод" label="промокод"
size="small" size="small"
value={couponField} value={couponField}
onChange={(e) => setCouponField(e.target.value)} onChange={(e) => setCouponField(e.target.value)}
InputProps={{ InputProps={{
style: { style: {
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
}} }}
InputLabelProps={{ InputLabelProps={{
style: { style: {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
}} }}
/> />
</Box> </Box>
{/* {cartTotal?.couponState && {/* {cartTotal?.couponState &&
(cartTotal.couponState === "applied" ? ( (cartTotal.couponState === "applied" ? (
<Alert severity="success">Купон применен!</Alert> <Alert severity="success">Купон применен!</Alert>
) : ( ) : (
<Alert severity="error">Подходящий купон не найден!</Alert> <Alert severity="error">Подходящий купон не найден!</Alert>
)) ))
} */} } */}
<Box <Box
sx={{ sx={{
border: "1px solid white", border: "1px solid white",
padding: "3px", padding: "3px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
ml: mobile ? 0 : "auto", ml: mobile ? 0 : "auto",
}} }}
> >
<Input <Input
label="лояльность" label="лояльность"
size="small" size="small"
type="number" type="number"
value={loyaltyField} value={loyaltyField}
onChange={(e) => setLoyaltyField(e.target.value)} onChange={(e) => setLoyaltyField(e.target.value)}
InputProps={{ InputProps={{
style: { style: {
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
}} }}
InputLabelProps={{ InputLabelProps={{
style: { style: {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
}} }}
/> />
</Box> </Box>
<Button onClick={handleCalcCartClick}>рассчитать</Button> <Button onClick={handleCalcCartClick}>рассчитать</Button>
</Paper> </Paper>
{cartData?.services.length && ( {cartData?.services.length && (
<> <>
<Table <Table
sx={{ sx={{
width: "90%", width: "90%",
margin: "5px", margin: "5px",
border: "2px solid", border: "2px solid",
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
}} }}
aria-label="simple table" aria-label="simple table"
> >
<TableHead> <TableHead>
<TableRow <TableRow
sx={{ sx={{
borderBottom: "2px solid", borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: "100px", height: "100px",
}} }}
> >
<TableCell> <TableCell>
<Typography <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
variant="h4" Имя
sx={{ color: theme.palette.secondary.main }} </Typography>
> </TableCell>
Имя <TableCell>
</Typography> <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
</TableCell> Описание
<TableCell> </Typography>
<Typography </TableCell>
variant="h4" <TableCell>
sx={{ color: theme.palette.secondary.main }} <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
> Скидки
Описание </Typography>
</Typography> </TableCell>
</TableCell> <TableCell>
<TableCell> <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
<Typography стоимость
variant="h4" </Typography>
sx={{ color: theme.palette.secondary.main }} </TableCell>
> </TableRow>
Скидки </TableHead>
</Typography> <TableBody>
</TableCell> {cartData.services.flatMap((service) =>
<TableCell> service.tariffs.map((tariffCartData) => {
<Typography const appliedDiscounts = tariffCartData.privileges
variant="h4" .flatMap((privilege) => Array.from(privilege.appliedDiscounts))
sx={{ color: theme.palette.secondary.main }} .sort((a, b) => a.Layer - b.Layer);
>
стоимость
</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{cartData.services.flatMap(service => service.tariffs.map(tariffCartData => {
const appliedDiscounts = tariffCartData.privileges.flatMap(
privilege => Array.from(privilege.appliedDiscounts)
).sort((a, b) => a.Layer - b.Layer);
return ( return (
<CartItemRow <CartItemRow
key={tariffCartData.id} key={tariffCartData.id}
tariffCartData={tariffCartData} tariffCartData={tariffCartData}
appliedDiscounts={appliedDiscounts} appliedDiscounts={appliedDiscounts}
/> />
); );
}))} })
</TableBody> )}
</Table> </TableBody>
</Table>
<Typography <Typography
id="transition-modal-title" id="transition-modal-title"
variant="h6" variant="h6"
sx={{ sx={{
fontWeight: "normal", fontWeight: "normal",
textAlign: "center", textAlign: "center",
marginTop: "10px", marginTop: "10px",
}} }}
> >
ИТОГО: <span>{currencyFormatter.format(cartData.priceAfterDiscounts / 100)}</span> ИТОГО: <span>{currencyFormatter.format(cartData.priceAfterDiscounts / 100)}</span>
</Typography> </Typography>
</> </>
)} )}
{errorMessage !== null && ( {errorMessage !== null && (
<Alert variant="filled" severity="error" sx={{ mt: "20px" }}> <Alert variant="filled" severity="error" sx={{ mt: "20px" }}>
{errorMessage} {errorMessage}
</Alert> </Alert>
)} )}
</Box> </Box>
); );
} }

@ -4,51 +4,42 @@ import { Discount, TariffCartData } from "@frontend/kitui";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useEffect } from "react"; import { useEffect } from "react";
interface Props { interface Props {
tariffCartData: TariffCartData; tariffCartData: TariffCartData;
appliedDiscounts: Discount[]; appliedDiscounts: Discount[];
} }
export default function CartItemRow({ tariffCartData, appliedDiscounts }: Props) { export default function CartItemRow({ tariffCartData, appliedDiscounts }: Props) {
const theme = useTheme(); const theme = useTheme();
useEffect(() => { useEffect(() => {
if (tariffCartData.privileges.length > 1) { if (tariffCartData.privileges.length > 1) {
console.warn(`Количество привилегий в тарифе ${tariffCartData.name}(${tariffCartData.id}) больше одного`); console.warn(`Количество привилегий в тарифе ${tariffCartData.name}(${tariffCartData.id}) больше одного`);
} }
}, [tariffCartData.id, tariffCartData.name, tariffCartData.privileges.length]); }, [tariffCartData.id, tariffCartData.name, tariffCartData.privileges.length]);
return ( return (
<TableRow <TableRow
key={tariffCartData.id} key={tariffCartData.id}
sx={{ sx={{
borderBottom: "2px solid", borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
".MuiTableCell-root": { ".MuiTableCell-root": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
} },
}} }}
> >
<TableCell> <TableCell>{tariffCartData.name}</TableCell>
{tariffCartData.name} <TableCell>{tariffCartData.privileges[0].description}</TableCell>
</TableCell> <TableCell>
<TableCell> {appliedDiscounts.map((discount, index, arr) => (
{tariffCartData.privileges[0].description} <span key={discount.ID}>
</TableCell> <DiscountTooltip discount={discount} />
<TableCell> {index < arr.length - 1 && <span>,&nbsp;</span>}
{appliedDiscounts.map((discount, index, arr) => ( </span>
<span key={discount.ID}> ))}
<DiscountTooltip discount={discount} /> </TableCell>
{index < arr.length - 1 && ( <TableCell>{currencyFormatter.format(tariffCartData.price / 100)}</TableCell>
<span>,&nbsp;</span> </TableRow>
)} );
</span>
))}
</TableCell>
<TableCell>
{currencyFormatter.format(tariffCartData.price / 100)}
</TableCell>
</TableRow>
);
} }

@ -2,27 +2,26 @@ import { Tooltip, Typography } from "@mui/material";
import { Discount, findDiscountFactor } from "@frontend/kitui"; import { Discount, findDiscountFactor } from "@frontend/kitui";
import { formatDiscountFactor } from "@root/utils/formatDiscountFactor"; import { formatDiscountFactor } from "@root/utils/formatDiscountFactor";
interface Props { interface Props {
discount: Discount; discount: Discount;
} }
export function DiscountTooltip({ discount }: Props) { export function DiscountTooltip({ discount }: Props) {
const discountText = formatDiscountFactor(findDiscountFactor(discount)); const discountText = formatDiscountFactor(findDiscountFactor(discount));
return discountText ? ( return discountText ? (
<Tooltip <Tooltip
title={ title={
<> <>
<Typography>Слой: {discount.Layer}</Typography> <Typography>Слой: {discount.Layer}</Typography>
<Typography>Название: {discount.Name}</Typography> <Typography>Название: {discount.Name}</Typography>
<Typography>Описание: {discount.Description}</Typography> <Typography>Описание: {discount.Description}</Typography>
</> </>
} }
> >
<span>{discountText}</span> <span>{discountText}</span>
</Tooltip> </Tooltip>
) : ( ) : (
<span>Ошибка поиска значения скидки</span> <span>Ошибка поиска значения скидки</span>
); );
} }

@ -1,46 +1,55 @@
import { SxProps, TextField, Theme, useTheme } from "@mui/material"; import { SxProps, TextField, Theme, useTheme } from "@mui/material";
import { HTMLInputTypeAttribute, ChangeEvent } from "react"; import { HTMLInputTypeAttribute, ChangeEvent } from "react";
import {InputBaseProps} from "@mui/material/InputBase"; import { InputBaseProps } from "@mui/material/InputBase";
export function CustomTextField({
export function CustomTextField({ id, label, value, name, onBlur,error, type, sx, onChange: setValue }: { id,
id: string; label,
label: string; value,
value: number | string | null; name,
name?: string; onBlur,
onBlur?: InputBaseProps['onBlur']; error,
error?: boolean; type,
type?: HTMLInputTypeAttribute; sx,
sx?: SxProps<Theme>; onChange: setValue,
onChange: (e: ChangeEvent<HTMLInputElement>) => void; }: {
id: string;
label: string;
value: number | string | null;
name?: string;
onBlur?: InputBaseProps["onBlur"];
error?: boolean;
type?: HTMLInputTypeAttribute;
sx?: SxProps<Theme>;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) { }) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<TextField <TextField
fullWidth fullWidth
id={id} id={id}
label={label} label={label}
variant="filled" variant="filled"
name={name} name={name}
onBlur={onBlur} onBlur={onBlur}
error={error} error={error}
color="secondary" color="secondary"
type={type} type={type}
sx={sx} sx={sx}
InputProps={{ InputProps={{
style: { style: {
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
} },
}} }}
InputLabelProps={{ InputLabelProps={{
style: { style: {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
} },
}} }}
value={value ? value : ""} value={value ? value : ""}
onChange={setValue} onChange={setValue}
/> />
); );
} }

@ -3,26 +3,26 @@ import { useState } from "react";
import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
interface CustomWrapperProps { interface CustomWrapperProps {
text: string; text: string;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
result?: boolean; result?: boolean;
children: JSX.Element; children: JSX.Element;
} }
export const CustomWrapper = ({ text, sx, result, children }: CustomWrapperProps) => { export const CustomWrapper = ({ text, sx, result, children }: CustomWrapperProps) => {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm")); const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [isExpanded, setIsExpanded] = useState<boolean>(false); const [isExpanded, setIsExpanded] = useState<boolean>(false);
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
overflow: "hidden", overflow: "hidden",
borderRadius: "12px", borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24), boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525), 0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066), 0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12), 0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
@ -30,99 +30,99 @@ export const CustomWrapper = ({ text, sx, result, children }: CustomWrapperProps
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.067 0px 2.76726px 8.55082px rgba(210, 208, 225, 0.067
4749)`, 4749)`,
...sx, ...sx,
}} }}
> >
<Box <Box
sx={{ sx={{
border: "2px solid grey", border: "2px solid grey",
"&:first-of-type": { "&:first-of-type": {
borderTopLeftRadius: "12px ", borderTopLeftRadius: "12px ",
borderTopRightRadius: "12px", borderTopRightRadius: "12px",
}, },
"&:last-of-type": { "&:last-of-type": {
borderBottomLeftRadius: "12px", borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px", borderBottomRightRadius: "12px",
}, },
"&:not(:last-of-type)": { "&:not(:last-of-type)": {
borderBottom: `1px solid gray`, borderBottom: `1px solid gray`,
}, },
}} }}
> >
<Box <Box
onClick={() => setIsExpanded((prev) => !prev)} onClick={() => setIsExpanded((prev) => !prev)}
sx={{ sx={{
height: "88px", height: "88px",
px: "20px", px: "20px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
cursor: "pointer", cursor: "pointer",
userSelect: "none", userSelect: "none",
}} }}
> >
<Typography <Typography
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
fontSize: "18px", fontSize: "18px",
lineHeight: upMd ? undefined : "19px", lineHeight: upMd ? undefined : "19px",
fontWeight: 400, fontWeight: 400,
color: "#FFFFFF", color: "#FFFFFF",
px: 0, px: 0,
}} }}
> >
{text} {text}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
height: "100%", height: "100%",
alignItems: "center", alignItems: "center",
}} }}
> >
{result ? ( {result ? (
<> <>
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#252734" /> <rect y="0.8125" width="30" height="30" rx="6" fill="#252734" />
<path <path
d="M7.5 19.5625L15 12.0625L22.5 19.5625" d="M7.5 19.5625L15 12.0625L22.5 19.5625"
stroke="#7E2AEA" stroke="#7E2AEA"
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
<Box <Box
sx={{ sx={{
borderLeft: upSm ? "1px solid #9A9AAF" : "none", borderLeft: upSm ? "1px solid #9A9AAF" : "none",
pl: upSm ? "2px" : 0, pl: upSm ? "2px" : 0,
height: "50%", height: "50%",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}} }}
/> />
</> </>
) : ( ) : (
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#252734" /> <rect y="0.8125" width="30" height="30" rx="6" fill="#252734" />
<path <path
d="M7.5 19.5625L15 12.0625L22.5 19.5625" d="M7.5 19.5625L15 12.0625L22.5 19.5625"
stroke="#fe9903" stroke="#fe9903"
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
)} )}
</Box> </Box>
</Box> </Box>
{isExpanded && <>{children}</>} {isExpanded && <>{children}</>}
</Box> </Box>
</Box> </Box>
); );
}; };

@ -1,26 +1,21 @@
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { Button, Skeleton} from "@mui/material"; import { Button, Skeleton } from "@mui/material";
const BeautifulButton = styled(Button)(({ theme }) => ({ const BeautifulButton = styled(Button)(({ theme }) => ({
width: "250px", width: "250px",
margin: "15px auto", margin: "15px auto",
padding: "20px 30px", padding: "20px 30px",
fontSize: 18 fontSize: 18,
})); }));
interface Props { interface Props {
isReady: boolean isReady: boolean;
text:string text: string;
type?: "button" | "reset" | "submit" type?: "button" | "reset" | "submit";
} }
export default ({ export default ({ isReady = true, text, type = "button" }: Props) => {
isReady = true, if (isReady) {
text, return <BeautifulButton type={type}>{text}</BeautifulButton>;
type = "button" }
}:Props) => { return <Skeleton>{text}</Skeleton>;
};
if (isReady) {
return <BeautifulButton type={type}>{text}</BeautifulButton>
}
return <Skeleton>{text}</Skeleton>
}

@ -1,28 +1,27 @@
import { DataGrid } from "@mui/x-data-grid"; import { DataGrid } from "@mui/x-data-grid";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
export default styled(DataGrid)(({ theme }) => ({ export default styled(DataGrid)(({ theme }) => ({
width: "100%", width: "100%",
minHeight: "400px", minHeight: "400px",
margin: "10px 0", margin: "10px 0",
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
"& .MuiDataGrid-iconSeparator": { "& .MuiDataGrid-iconSeparator": {
display: "none" display: "none",
}, },
"& .css-levciy-MuiTablePagination-displayedRows": { "& .css-levciy-MuiTablePagination-displayedRows": {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}, },
"& .MuiSvgIcon-root": { "& .MuiSvgIcon-root": {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}, },
"& .MuiTablePagination-selectLabel": { "& .MuiTablePagination-selectLabel": {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}, },
"& .MuiInputBase-root": { "& .MuiInputBase-root": {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}, },
"& .MuiButton-text": { "& .MuiButton-text": {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}, },
})) as typeof DataGrid; })) as typeof DataGrid;

@ -1,16 +1,16 @@
import {TextField} from "@mui/material"; import { TextField } from "@mui/material";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
export default styled(TextField)(({ theme }) => ({ export default styled(TextField)(({ theme }) => ({
variant: "outlined", variant: "outlined",
height: "40px", height: "40px",
size: "small", size: "small",
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
width: "140px", width: "140px",
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
"& .MuiFormLabel-root": { "& .MuiFormLabel-root": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
"& .Mui-focused": { "& .Mui-focused": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
} },
})); }));

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

@ -3,15 +3,15 @@ import * as React from "react";
import { useLocation, Navigate } from "react-router-dom"; import { useLocation, Navigate } from "react-router-dom";
interface Props { interface Props {
children: JSX.Element; children: JSX.Element;
} }
export default function PrivateRoute({ children }: Props) { export default function PrivateRoute({ children }: Props) {
const token = useToken(); const token = useToken();
const location = useLocation(); const location = useLocation();
//Если пользователь авторизован, перенаправляем его на нужный путь. Иначе выкидываем в регистрацию //Если пользователь авторизован, перенаправляем его на нужный путь. Иначе выкидываем в регистрацию
if (token) { if (token) {
return children; return children;
} }
return <Navigate to="/" state={{ from: location }} />; return <Navigate to="/" state={{ from: location }} />;
}; }

@ -2,20 +2,19 @@ import { useLocation, Navigate } from "react-router-dom";
import { useToken } from "@frontend/kitui"; import { useToken } from "@frontend/kitui";
interface Props { interface Props {
children: JSX.Element; children: JSX.Element;
} }
const PublicRoute = ({ children }: Props) => { const PublicRoute = ({ children }: Props) => {
const location = useLocation(); const location = useLocation();
const token = useToken(); const token = useToken();
if (token) { if (token) {
return <Navigate to="/users" state={{ from: location }} />; return <Navigate to="/users" state={{ from: location }} />;
} }
return children; return children;
}; };
export default PublicRoute; export default PublicRoute;

@ -1,50 +1,51 @@
import { Discount } from "@frontend/kitui"; import { Discount } from "@frontend/kitui";
export type GetDiscountResponse = { export type GetDiscountResponse = {
Discounts: Discount[]; Discounts: Discount[];
}; };
export const discountTypes = { export const discountTypes = {
"purchasesAmount": "Лояльность", purchasesAmount: "Лояльность",
"cartPurchasesAmount": "Корзина", cartPurchasesAmount: "Корзина",
"service": "Сервис", service: "Сервис",
"privilege": "Товар", privilege: "Товар",
} as const; } as const;
export type DiscountType = keyof typeof discountTypes; export type DiscountType = keyof typeof discountTypes;
export type CreateDiscountBody = { export type CreateDiscountBody = {
Name: string; Name: string;
Layer: 1 | 2 | 3 | 4; Layer: 1 | 2 | 3 | 4;
Description: string; Description: string;
Condition: { Condition: {
Period: { Period: {
/** ISO string */ /** ISO string */
From: string; From: string;
/** ISO string */ /** ISO string */
To: string; To: string;
}; };
User: string; User: string;
UserType: string; UserType: string;
Coupon: string; Coupon: string;
PurchasesAmount: number; PurchasesAmount: number;
CartPurchasesAmount: number; CartPurchasesAmount: number;
Product: string; Product: string;
Term: number; Term: number;
Usage: number; Usage: number;
PriceFrom: number; PriceFrom: number;
Group: string; Group: string;
}; };
Target: { Target: {
Factor: number; Factor: number;
TargetScope: "Sum" | "Group" | "Each"; TargetScope: "Sum" | "Group" | "Each";
Overhelm: boolean; Overhelm: boolean;
TargetGroup: string; TargetGroup: string;
Products: [{ Products: [
ID: string; {
Factor: number; ID: string;
Overhelm: false; Factor: number;
}]; Overhelm: false;
}; },
];
};
}; };

@ -1,16 +1,16 @@
export const SERVICE_LIST = [ export const SERVICE_LIST = [
{ {
serviceKey: "templategen", serviceKey: "templategen",
displayName: "Шаблонизатор документов", displayName: "Шаблонизатор документов",
}, },
{ {
serviceKey: "squiz", serviceKey: "squiz",
displayName: "Опросник", displayName: "Опросник",
}, },
{ {
serviceKey: "dwarfener", serviceKey: "dwarfener",
displayName: "Аналитика сокращателя", displayName: "Аналитика сокращателя",
}, },
] as const; ] as const;
export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"]; export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];

@ -1,51 +1,51 @@
export type CreatePromocodeBody = { export type CreatePromocodeBody = {
codeword: string; codeword: string;
description: string; description: string;
greetings: string; greetings: string;
dueTo: number; dueTo: number;
activationCount: number; activationCount: number;
bonus: { bonus: {
privilege: { privilege: {
privilegeID: string; privilegeID: string;
amount: number; amount: number;
serviceKey: string; serviceKey: string;
}; };
discount: { discount: {
layer: number; layer: number;
factor: number; factor: number;
target: string; target: string;
threshold: number; threshold: number;
}; };
}; };
}; };
export type GetPromocodeListBody = { export type GetPromocodeListBody = {
page: number; page: number;
limit: number; limit: number;
filter: { filter: {
active: boolean; active: boolean;
text?: string; text?: string;
}; };
}; };
export type Promocode = CreatePromocodeBody & { export type Promocode = CreatePromocodeBody & {
id: string; id: string;
dueTo: number; dueTo: number;
activationLimit: number; activationLimit: number;
outdated: boolean; outdated: boolean;
offLimit: boolean; offLimit: boolean;
delete: boolean; delete: boolean;
createdAt: string; createdAt: string;
fastLinks: string[]; fastLinks: string[];
}; };
export type PromocodeList = { export type PromocodeList = {
count: number; count: number;
items: Promocode[]; items: Promocode[];
}; };
export type PromocodeStatistics = { export type PromocodeStatistics = {
id: string; id: string;
usageCount: number; usageCount: number;
usageMap: Record<string, number>; usageMap: Record<string, number>;
}; };

@ -1,18 +1,18 @@
import { Privilege } from "@frontend/kitui"; import { Privilege } from "@frontend/kitui";
export const SERVICE_LIST = [ export const SERVICE_LIST = [
{ {
serviceKey: "templategen", serviceKey: "templategen",
displayName: "Шаблонизатор документов", displayName: "Шаблонизатор документов",
}, },
{ {
serviceKey: "squiz", serviceKey: "squiz",
displayName: "Опросник", displayName: "Опросник",
}, },
{ {
serviceKey: "dwarfener", serviceKey: "dwarfener",
displayName: "Аналитика сокращателя", displayName: "Аналитика сокращателя",
}, },
] as const; ] as const;
export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"]; export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];
@ -20,10 +20,10 @@ export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];
export type PrivilegeType = "unlim" | "gencount" | "activequiz" | "abcount" | "extended"; export type PrivilegeType = "unlim" | "gencount" | "activequiz" | "abcount" | "extended";
export type EditTariffRequestBody = { export type EditTariffRequestBody = {
description: string; description: string;
name: string; name: string;
order: number; order: number;
price: number; price: number;
isCustom: boolean; isCustom: boolean;
privileges: Omit<Privilege, "_id" | "updatedAt">[]; privileges: Omit<Privilege, "_id" | "updatedAt">[];
}; };

@ -1,44 +1,42 @@
export interface CreateTicketRequest { export interface CreateTicketRequest {
Title: string; Title: string;
Message: string; Message: string;
}; }
export interface CreateTicketResponse { export interface CreateTicketResponse {
Ticket: string; Ticket: string;
}; }
export interface SendTicketMessageRequest { export interface SendTicketMessageRequest {
message: string; message: string;
ticket: string; ticket: string;
lang: string; lang: string;
files: string[]; files: string[];
}; }
export type TicketStatus = "open"; export type TicketStatus = "open";
export interface Ticket { export interface Ticket {
id: string; id: string;
user: string; user: string;
sess: string; sess: string;
ans: string; ans: string;
state: string; state: string;
top_message: TicketMessage; top_message: TicketMessage;
title: string; title: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
rate: number; rate: number;
}; }
export interface TicketMessage { export interface TicketMessage {
id: string; id: string;
ticket_id: string; ticket_id: string;
user_id: string, user_id: string;
session_id: string; session_id: string;
message: string; message: string;
files: string[], files: string[];
shown: { [key: string]: number; }, shown: { [key: string]: number };
request_screenshot: string, request_screenshot: string;
created_at: string; created_at: string;
}; }

@ -1,5 +1,5 @@
export interface User { export interface User {
ID: string; ID: string;
Type: "" | "nko"; Type: "" | "nko";
PurchasesAmount: number; PurchasesAmount: number;
} }

@ -11,118 +11,118 @@ import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import OutlinedInput from "@kitUI/outlinedInput"; import OutlinedInput from "@kitUI/outlinedInput";
export default () => { export default () => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const [restore, setRestore] = React.useState(true); const [restore, setRestore] = React.useState(true);
const [isReady, setIsReady] = React.useState(true); const [isReady, setIsReady] = React.useState(true);
if (restore) { if (restore) {
return ( return (
<Formik <Formik
initialValues={{ initialValues={{
mail: "", mail: "",
}} }}
onSubmit={(values) => { onSubmit={(values) => {
setRestore(false); setRestore(false);
}} }}
> >
<Form> <Form>
<Box <Box
component="section" component="section"
sx={{ sx={{
minHeight: "100vh", minHeight: "100vh",
height: "100%", height: "100%",
width: "100%", width: "100%",
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
padding: "15px 0", padding: "15px 0",
}} }}
> >
<Box <Box
component="article" component="article"
sx={{ sx={{
width: "350px", width: "350px",
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
"> *": { "> *": {
marginTop: "15px", marginTop: "15px",
}, },
}} }}
> >
<Typography variant="h6" color={theme.palette.secondary.main}> <Typography variant="h6" color={theme.palette.secondary.main}>
Восстановление пароля Восстановление пароля
</Typography> </Typography>
<Logo /> <Logo />
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}> <Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<EmailOutlinedIcon htmlColor={theme.palette.golden.main} /> <EmailOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field as={OutlinedInput} autoComplete="none" variant="filled" name="mail" label="Эл. почта" /> <Field as={OutlinedInput} autoComplete="none" variant="filled" name="mail" label="Эл. почта" />
</Box> </Box>
<CleverButton type="submit" text="Отправить" isReady={isReady} /> <CleverButton type="submit" text="Отправить" isReady={isReady} />
<Link to="/signin" style={{ textDecoration: "none" }}> <Link to="/signin" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}>Я помню пароль</Typography> <Typography color={theme.palette.golden.main}>Я помню пароль</Typography>
</Link> </Link>
</Box> </Box>
</Box> </Box>
</Form> </Form>
</Formik> </Formik>
); );
} else { } else {
return ( return (
<Formik <Formik
initialValues={{ initialValues={{
code: "", code: "",
}} }}
onSubmit={(values) => {}} onSubmit={(values) => {}}
> >
<Form> <Form>
<Box <Box
component="section" component="section"
sx={{ sx={{
minHeight: "100vh", minHeight: "100vh",
height: "100%", height: "100%",
width: "100%", width: "100%",
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
padding: "15px 0", padding: "15px 0",
}} }}
> >
<Box <Box
component="article" component="article"
sx={{ sx={{
width: "350px", width: "350px",
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
"> *": { "> *": {
marginTop: "15px", marginTop: "15px",
}, },
}} }}
> >
<Typography variant="h6" color={theme.palette.secondary.main}> <Typography variant="h6" color={theme.palette.secondary.main}>
Восстановление пароля Восстановление пароля
</Typography> </Typography>
<Logo /> <Logo />
<Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}> <Box sx={{ display: "flex", alignItems: "center", marginTop: "15px", "> *": { marginRight: "10px" } }}>
<LockOutlinedIcon htmlColor={theme.palette.golden.main} /> <LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field as={OutlinedInput} name="code" variant="filled" label="Код из сообщения" /> <Field as={OutlinedInput} name="code" variant="filled" label="Код из сообщения" />
</Box> </Box>
<CleverButton type="submit" text="Отправить" isReady={isReady} /> <CleverButton type="submit" text="Отправить" isReady={isReady} />
<Link to="/signin" style={{ textDecoration: "none" }}> <Link to="/signin" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}>Я помню пароль</Typography> <Typography color={theme.palette.golden.main}>Я помню пароль</Typography>
</Link> </Link>
</Box> </Box>
</Box> </Box>
</Form> </Form>
</Formik> </Formik>
); );
} }
}; };

@ -3,13 +3,7 @@ import { enqueueSnackbar } from "notistack";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { Formik, Field, Form, FormikHelpers } from "formik"; import { Formik, Field, Form, FormikHelpers } from "formik";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { import { Box, Checkbox, Typography, FormControlLabel, Button, useMediaQuery } from "@mui/material";
Box,
Checkbox,
Typography,
FormControlLabel,
Button, useMediaQuery,
} from "@mui/material";
import Logo from "@pages/Logo"; import Logo from "@pages/Logo";
import OutlinedInput from "@kitUI/outlinedInput"; import OutlinedInput from "@kitUI/outlinedInput";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined"; import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
@ -17,210 +11,197 @@ import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { signin } from "@root/api/auth"; import { signin } from "@root/api/auth";
interface Values { interface Values {
email: string; email: string;
password: string; password: string;
} }
function validate(values: Values) { function validate(values: Values) {
const errors = {} as any; const errors = {} as any;
if (!values.email) { if (!values.email) {
errors.email = "Обязательное поле"; errors.email = "Обязательное поле";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = "Неверный формат эл. почты"; errors.email = "Неверный формат эл. почты";
} }
if (!values.password) { if (!values.password) {
errors.password = "Введите пароль"; errors.password = "Введите пароль";
} }
if (values.password && !/^[\S]{8,25}$/i.test(values.password)) { if (values.password && !/^[\S]{8,25}$/i.test(values.password)) {
errors.password = "Invalid password"; errors.password = "Invalid password";
} }
return errors; return errors;
} }
const SigninForm = () => { const SigninForm = () => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const initialValues: Values = { const initialValues: Values = {
email: "", email: "",
password: "", password: "",
}; };
const onSignFormSubmit = async ( const onSignFormSubmit = async (values: Values, formikHelpers: FormikHelpers<Values>) => {
values: Values, formikHelpers.setSubmitting(true);
formikHelpers: FormikHelpers<Values>
) => {
formikHelpers.setSubmitting(true);
const [_, signinError] = await signin(values.email, values.password); const [_, signinError] = await signin(values.email, values.password);
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
if (signinError) { if (signinError) {
return enqueueSnackbar(signinError); return enqueueSnackbar(signinError);
} }
navigate("/users"); navigate("/users");
}; };
return ( return (
<Formik <Formik initialValues={initialValues} validate={validate} onSubmit={onSignFormSubmit}>
initialValues={initialValues} {(props) => (
validate={validate} <Form>
onSubmit={onSignFormSubmit} <Box
> component="section"
{(props) => ( sx={{
<Form> minHeight: "100vh",
<Box height: "100%",
component="section" width: "100%",
sx={{ backgroundColor: theme.palette.content.main,
minHeight: "100vh", display: "flex",
height: "100%", justifyContent: "center",
width: "100%", alignItems: "center",
backgroundColor: theme.palette.content.main, padding: "15px 0",
display: "flex", }}
justifyContent: "center", >
alignItems: "center", <Box
padding: "15px 0", component="article"
}} sx={{
> width: "350px",
<Box backgroundColor: theme.palette.content.main,
component="article" display: "flex",
sx={{ flexDirection: "column",
width: "350px", justifyContent: "center",
backgroundColor: theme.palette.content.main, "> *": {
display: "flex", marginTop: "15px",
flexDirection: "column", },
justifyContent: "center", padding: isMobile ? "0 16px" : undefined,
"> *": { }}
marginTop: "15px", >
}, <Logo />
padding: isMobile ? "0 16px" : undefined <Box>
}} <Typography variant="h5" color={theme.palette.secondary.main}>
> Добро пожаловать
<Logo /> </Typography>
<Box> <Typography variant="h6" color={theme.palette.secondary.main}>
<Typography variant="h5" color={theme.palette.secondary.main}> Мы рады что вы выбрали нас!
Добро пожаловать </Typography>
</Typography> </Box>
<Typography variant="h6" color={theme.palette.secondary.main}> <Box
Мы рады что вы выбрали нас! sx={{
</Typography> display: "flex",
</Box> alignItems: "center",
<Box marginTop: "15px",
sx={{ "> *": { marginRight: "10px" },
display: "flex", }}
alignItems: "center", >
marginTop: "15px", <EmailOutlinedIcon htmlColor={theme.palette.golden.main} />
"> *": { marginRight: "10px" }, <Field
}} as={OutlinedInput}
> name="email"
<EmailOutlinedIcon htmlColor={theme.palette.golden.main} /> variant="filled"
<Field label="Эл. почта"
as={OutlinedInput} error={props.touched.email && !!props.errors.email}
name="email" helperText={
variant="filled" <Typography sx={{ fontSize: "12px", width: "200px" }}>
label="Эл. почта" {props.touched.email && props.errors.email}
error={props.touched.email && !!props.errors.email} </Typography>
helperText={ }
<Typography sx={{ fontSize: "12px", width: "200px" }}> />
{props.touched.email && props.errors.email} </Box>
</Typography> <Box
} sx={{
/> display: "flex",
</Box> alignItems: "center",
<Box marginTop: "15px",
sx={{ "> *": { marginRight: "10px" },
display: "flex", }}
alignItems: "center", >
marginTop: "15px", <LockOutlinedIcon htmlColor={theme.palette.golden.main} />
"> *": { marginRight: "10px" }, <Field
}} as={OutlinedInput}
> type="password"
<LockOutlinedIcon htmlColor={theme.palette.golden.main} /> name="password"
<Field variant="filled"
as={OutlinedInput} label="Пароль"
type="password" error={props.touched.password && !!props.errors.password}
name="password" helperText={
variant="filled" <Typography sx={{ fontSize: "12px", width: "200px" }}>
label="Пароль" {props.touched.password && props.errors.password}
error={props.touched.password && !!props.errors.password} </Typography>
helperText={ }
<Typography sx={{ fontSize: "12px", width: "200px" }}> />
{props.touched.password && props.errors.password} </Box>
</Typography> <Box
} component="article"
/> sx={{
</Box> display: "flex",
<Box alignItems: "center",
component="article" }}
sx={{ >
display: "flex", <FormControlLabel
alignItems: "center", sx={{ color: "white" }}
}} control={
> <Checkbox
<FormControlLabel value="checkedA"
sx={{ color: "white" }} inputProps={{ "aria-label": "Checkbox A" }}
control={ sx={{
<Checkbox color: "white",
value="checkedA" transform: "scale(1.5)",
inputProps={{ "aria-label": "Checkbox A" }} "&.Mui-checked": {
sx={{ color: "white",
color: "white", },
transform: "scale(1.5)", "&.MuiFormControlLabel-root": {
"&.Mui-checked": { color: "white",
color: "white", },
}, }}
"&.MuiFormControlLabel-root": { />
color: "white", }
}, label="Запомнить этот компьютер"
}} />
/> </Box>
} <Link to="/restore" style={{ textDecoration: "none" }}>
label="Запомнить этот компьютер" <Typography color={theme.palette.golden.main}>Забыли пароль?</Typography>
/> </Link>
</Box> <Button
<Link to="/restore" style={{ textDecoration: "none" }}> type="submit"
<Typography color={theme.palette.golden.main}> disabled={props.isSubmitting}
Забыли пароль? sx={{
</Typography> width: "250px",
</Link> margin: "15px auto",
<Button padding: "20px 30px",
type="submit" fontSize: 18,
disabled={props.isSubmitting} }}
sx={{ >
width: "250px", Войти
margin: "15px auto", </Button>
padding: "20px 30px", <Box
fontSize: 18, sx={{
}} display: "flex",
> }}
Войти >
</Button> <Typography color={theme.palette.secondary.main}>У вас нет аккаунта?&nbsp;</Typography>
<Box <Link to="/signup" style={{ textDecoration: "none" }}>
sx={{ <Typography color={theme.palette.golden.main}>Зарегестрируйтесь</Typography>
display: "flex", </Link>
}} </Box>
> </Box>
<Typography color={theme.palette.secondary.main}> </Box>
У вас нет аккаунта?&nbsp; </Form>
</Typography> )}
<Link to="/signup" style={{ textDecoration: "none" }}> </Formik>
<Typography color={theme.palette.golden.main}> );
Зарегестрируйтесь
</Typography>
</Link>
</Box>
</Box>
</Box>
</Form>
)}
</Formik>
);
}; };
export default SigninForm; export default SigninForm;

@ -12,201 +12,192 @@ import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { register } from "@root/api/auth"; import { register } from "@root/api/auth";
interface Values { interface Values {
email: string; email: string;
password: string; password: string;
repeatPassword: string; repeatPassword: string;
} }
function validate(values: Values) { function validate(values: Values) {
const errors: Partial<Values> = {}; const errors: Partial<Values> = {};
if (!values.email) { if (!values.email) {
errors.email = "Обязательное поле"; errors.email = "Обязательное поле";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = "Неверный формат эл. почты"; errors.email = "Неверный формат эл. почты";
} }
if (!values.password) { if (!values.password) {
errors.password = "Обязательное поле"; errors.password = "Обязательное поле";
} else if (!/^[\S]{8,25}$/i.test(values.password)) { } else if (!/^[\S]{8,25}$/i.test(values.password)) {
errors.password = "Неверный пароль"; errors.password = "Неверный пароль";
} }
if (values.password !== values.repeatPassword) { if (values.password !== values.repeatPassword) {
errors.repeatPassword = "Пароли не совпадают"; errors.repeatPassword = "Пароли не совпадают";
} }
return errors; return errors;
} }
const SignUp = () => { const SignUp = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
return ( return (
<Formik <Formik
initialValues={{ initialValues={{
email: "", email: "",
password: "", password: "",
repeatPassword: "", repeatPassword: "",
}} }}
validate={validate} validate={validate}
onSubmit={async (values, formikHelpers) => { onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true); formikHelpers.setSubmitting(true);
const [_, registerError] = await register( const [_, registerError] = await register(values.email, values.repeatPassword);
values.email,
values.repeatPassword
);
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
if (registerError) { if (registerError) {
return enqueueSnackbar(registerError); return enqueueSnackbar(registerError);
} }
navigate("/users"); navigate("/users");
}} }}
> >
{(props) => ( {(props) => (
<Form> <Form>
<Box <Box
component="section" component="section"
sx={{ sx={{
minHeight: "100vh", minHeight: "100vh",
height: "100%", height: "100%",
width: "100%", width: "100%",
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
padding: "15px 0", padding: "15px 0",
}} }}
> >
<Box <Box
component="article" component="article"
sx={{ sx={{
width: "350px", width: "350px",
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
"> *": { "> *": {
marginTop: "15px", marginTop: "15px",
}, },
}} }}
> >
<Typography variant="h6" color={theme.palette.secondary.main}> <Typography variant="h6" color={theme.palette.secondary.main}>
Новый аккаунт Новый аккаунт
</Typography> </Typography>
<Logo /> <Logo />
<Box> <Box>
<Typography variant="h5" color={theme.palette.secondary.main}> <Typography variant="h5" color={theme.palette.secondary.main}>
Добро пожаловать Добро пожаловать
</Typography> </Typography>
<Typography variant="h6" color={theme.palette.secondary.main}> <Typography variant="h6" color={theme.palette.secondary.main}>
Мы рады что вы выбрали нас! Мы рады что вы выбрали нас!
</Typography> </Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
marginTop: "15px", marginTop: "15px",
"> *": { marginRight: "10px" }, "> *": { marginRight: "10px" },
}} }}
> >
<EmailOutlinedIcon htmlColor={theme.palette.golden.main} /> <EmailOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field <Field
as={OutlinedInput} as={OutlinedInput}
name="email" name="email"
variant="filled" variant="filled"
label="Эл. почта" label="Эл. почта"
id="email" id="email"
error={props.touched.email && !!props.errors.email} error={props.touched.email && !!props.errors.email}
helperText={ helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}> <Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.email && props.errors.email} {props.touched.email && props.errors.email}
</Typography> </Typography>
} }
/> />
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
marginTop: "15px", marginTop: "15px",
"> *": { marginRight: "10px" }, "> *": { marginRight: "10px" },
}} }}
> >
<LockOutlinedIcon htmlColor={theme.palette.golden.main} /> <LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field <Field
sx={{}} sx={{}}
as={OutlinedInput} as={OutlinedInput}
type="password" type="password"
name="password" name="password"
variant="filled" variant="filled"
label="Пароль" label="Пароль"
id="password" id="password"
error={props.touched.password && !!props.errors.password} error={props.touched.password && !!props.errors.password}
helperText={ helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}> <Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.password && props.errors.password} {props.touched.password && props.errors.password}
</Typography> </Typography>
} }
/> />
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
marginTop: "15px", marginTop: "15px",
"> *": { marginRight: "10px" }, "> *": { marginRight: "10px" },
}} }}
> >
<LockOutlinedIcon htmlColor={theme.palette.golden.main} /> <LockOutlinedIcon htmlColor={theme.palette.golden.main} />
<Field <Field
as={OutlinedInput} as={OutlinedInput}
type="password" type="password"
name="repeatPassword" name="repeatPassword"
variant="filled" variant="filled"
label="Повторите пароль" label="Повторите пароль"
id="repeatPassword" id="repeatPassword"
error={ error={props.touched.repeatPassword && !!props.errors.repeatPassword}
props.touched.repeatPassword && helperText={
!!props.errors.repeatPassword <Typography sx={{ fontSize: "12px", width: "200px" }}>
} {props.touched.repeatPassword && props.errors.repeatPassword}
helperText={ </Typography>
<Typography sx={{ fontSize: "12px", width: "200px" }}> }
{props.touched.repeatPassword && />
props.errors.repeatPassword} </Box>
</Typography> <Button
} type="submit"
/> disabled={props.isSubmitting}
</Box> sx={{
<Button width: "250px",
type="submit" margin: "15px auto",
disabled={props.isSubmitting} padding: "20px 30px",
sx={{ fontSize: 18,
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>
</Button> </Box>
<Link to="/signin" style={{ textDecoration: "none" }}> </Box>
<Typography color={theme.palette.golden.main}> </Form>
У меня уже есть аккаунт )}
</Typography> </Formik>
</Link> );
</Box>
</Box>
</Form>
)}
</Formik>
);
}; };
export default SignUp; export default SignUp;

@ -1,65 +1,73 @@
import * as React from "react"; import * as React from "react";
import {Box, Button, Typography} from "@mui/material"; import { Box, Button, Typography } from "@mui/material";
import { ThemeProvider } from "@mui/material"; import { ThemeProvider } from "@mui/material";
import theme from "../../theme"; import theme from "../../theme";
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from "@mui/material/CssBaseline";
import {Link} from "react-router-dom"; import { Link } from "react-router-dom";
const Error404: React.FC = () => {
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.secondary.main,
height: "100%",
}}
>
<Box
sx={{
width: "100vw",
height: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<Box
sx={{
width: "150px",
height: "120px",
display: "flex",
flexDirection: "row",
}}
>
<Typography
sx={{
fontSize: "80px",
}}
>
4
</Typography>
<Typography
sx={{
color: theme.palette.golden.main,
fontSize: "80px",
}}
>
0
</Typography>
<Typography
sx={{
fontSize: "80px",
}}
>
4
</Typography>
</Box>
<Box>
<Link to="/users">
<Button>На главную</Button>
</Link>
</Box>
</Box>
</Box>
</ThemeProvider>
</React.Fragment>
);
};
const Error404: React.FC = () => { export default Error404;
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.secondary.main,
height: "100%"
}}>
<Box sx={{
width: "100vw",
height: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}>
<Box sx={{
width: "150px",
height: "120px",
display: "flex",
flexDirection: "row"
}}>
<Typography sx={{
fontSize: "80px"
}}>
4
</Typography>
<Typography sx={{
color: theme.palette.golden.main,
fontSize: "80px"
}}>
0
</Typography>
<Typography sx={{
fontSize: "80px"
}}>
4
</Typography>
</Box>
<Box>
<Link
to="/users">
<Button>На главную</Button>
</Link>
</Box>
</Box>
</Box>
</ThemeProvider>
</React.Fragment>
);
}
export default Error404;

@ -2,41 +2,42 @@ import * as React from "react";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import theme from "../../theme"; import theme from "../../theme";
const Authorization: React.FC = () => {
return (
<React.Fragment>
<Box
sx={{
display: "flex",
}}
>
<Typography
variant="subtitle1"
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
cursor: "default",
}}
>
PENA
</Typography>
<Typography
variant="subtitle2"
sx={{
width: "55px",
color: theme.palette.primary.main,
backgroundColor: theme.palette.goldenDark.main,
borderRadius: "5px",
margin: theme.spacing(0.5),
cursor: "default",
}}
>
HUB
</Typography>
</Box>
</React.Fragment>
);
};
const Authorization: React.FC = () => { export default Authorization;
return (
<React.Fragment>
<Box sx={{
display: "flex"
}}>
<Typography
variant="subtitle1"
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
cursor: "default"
}}>
PENA
</Typography>
<Typography
variant = "subtitle2"
sx = {{
width: "55px",
color: theme.palette.primary.main,
backgroundColor: theme.palette.goldenDark.main,
borderRadius: "5px",
margin: theme.spacing(0.5),
cursor: "default"
}}
>
HUB
</Typography>
</Box>
</React.Fragment>
);
}
export default Authorization;

@ -2,171 +2,173 @@ import * as React from "react";
import { Box, Typography, Button } from "@mui/material"; import { Box, Typography, Button } from "@mui/material";
import { ThemeProvider } from "@mui/material"; import { ThemeProvider } from "@mui/material";
import theme from "../../theme"; import theme from "../../theme";
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from "@mui/material/CssBaseline";
import Logo from "../Logo"; import Logo from "../Logo";
const Authorization: React.FC = () => {
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.secondary.main,
height: "100%",
}}
>
<Box
sx={{
width: "100vw",
height: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<Box
sx={{
width: "350px",
height: "700px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Logo />
const Authorization: React.FC = () => { <Box
return ( sx={{
<React.Fragment> width: "100%",
<ThemeProvider theme={theme}> height: "100px",
<CssBaseline /> display: "flex",
<Box sx={{ flexDirection: "column",
backgroundColor: theme.palette.primary.main, justifyContent: "space-between",
color: theme.palette.secondary.main, alignItems: "center",
height: "100%" }}
}}> >
<Box sx={{ <Box>
width: "100vw", <Typography variant="h6">Все пользователи</Typography>
height: "100vh", </Box>
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}>
<Box sx={{
width: "350px",
height: "700px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
}}>
<Logo />
<Box sx={{ <Button variant="enter">ВОЙТИ</Button>
width: "100%", </Box>
height: "100px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center"
}}>
<Box>
<Typography variant="h6">
Все пользователи
</Typography>
</Box>
<Button <Box
variant = 'enter' sx={{
> width: "100%",
ВОЙТИ height: "100px",
</Button> display: "flex",
</Box> flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box>
<Typography variant="h6">Общая статистика</Typography>
</Box>
<Box sx={{ <Button
width: "100%", variant="enter"
height: "100px", sx={{
display: "flex", backgroundColor: theme.palette.content.main,
flexDirection: "column", "&:hover": {
justifyContent: "space-between", backgroundColor: theme.palette.menu.main,
alignItems: "center" },
}}> }}
<Box> >
<Typography variant="h6"> ВОЙТИ
Общая статистика </Button>
</Typography> </Box>
</Box>
<Button <Box
variant = "enter" sx={{
sx={{ width: "100%",
backgroundColor: theme.palette.content.main, height: "100px",
"&:hover": { display: "flex",
backgroundColor: theme.palette.menu.main flexDirection: "column",
} justifyContent: "space-between",
}}> alignItems: "center",
ВОЙТИ }}
</Button> >
</Box> <Box>
<Typography variant="h6">Шаблонизатор документов</Typography>
</Box>
<Box sx={{ <Button
width: "100%", variant="enter"
height: "100px", sx={{
display: "flex", backgroundColor: theme.palette.content.main,
flexDirection: "column", "&:hover": {
justifyContent: "space-between", backgroundColor: theme.palette.menu.main,
alignItems: "center" },
}}> }}
<Box> >
<Typography variant="h6"> ВОЙТИ
Шаблонизатор документов </Button>
</Typography> </Box>
</Box>
<Button <Box
variant = "enter" sx={{
sx={{ width: "100%",
backgroundColor: theme.palette.content.main, height: "100px",
"&:hover": { display: "flex",
backgroundColor: theme.palette.menu.main flexDirection: "column",
} justifyContent: "space-between",
}}> alignItems: "center",
ВОЙТИ }}
</Button> >
</Box> <Box>
<Typography variant="h6">Конструктор опросов</Typography>
</Box>
<Box sx={{ <Button
width: "100%", variant="enter"
height: "100px", sx={{
display: "flex", backgroundColor: theme.palette.content.main,
flexDirection: "column", "&:hover": {
justifyContent: "space-between", backgroundColor: theme.palette.menu.main,
alignItems: "center" },
}}> }}
<Box> >
<Typography variant="h6"> ВОЙТИ
Конструктор опросов </Button>
</Typography> </Box>
</Box>
<Button <Box
variant = "enter" sx={{
sx={{ width: "100%",
backgroundColor: theme.palette.content.main, height: "100px",
"&:hover": { display: "flex",
backgroundColor: theme.palette.menu.main flexDirection: "column",
} justifyContent: "space-between",
}}> alignItems: "center",
ВОЙТИ }}
</Button> >
</Box> <Box>
<Typography variant="h6">Сокращатель ссылок</Typography>
</Box>
<Box sx={{ <Button
width: "100%", variant="enter"
height: "100px", sx={{
display: "flex", backgroundColor: theme.palette.content.main,
flexDirection: "column", "&:hover": {
justifyContent: "space-between", backgroundColor: theme.palette.menu.main,
alignItems: "center" },
}}> }}
<Box> >
<Typography variant="h6"> ВОЙТИ
Сокращатель ссылок </Button>
</Typography> </Box>
</Box> </Box>
</Box>
</Box>
</ThemeProvider>
</React.Fragment>
);
};
<Button export default Authorization;
variant = "enter"
sx={{
backgroundColor: theme.palette.content.main,
"&:hover": {
backgroundColor: theme.palette.menu.main
}
}}>
ВОЙТИ
</Button>
</Box>
</Box>
</Box>
</Box>
</ThemeProvider>
</React.Fragment>
);
}
export default Authorization;

@ -1,169 +1,177 @@
import { KeyboardEvent, useRef, useState } from "react"; import { KeyboardEvent, useRef, useState } from "react";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import {Box, IconButton, TextField, Tooltip, Typography, useMediaQuery, useTheme} from "@mui/material"; import { Box, IconButton, TextField, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined"; import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined";
import { CustomPrivilege } from "@frontend/kitui"; import { CustomPrivilege } from "@frontend/kitui";
import { putPrivilege } from "@root/api/privilegies"; import { putPrivilege } from "@root/api/privilegies";
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from "@mui/icons-material/Save";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
interface CardPrivilege { interface CardPrivilege {
privilege: CustomPrivilege; privilege: CustomPrivilege;
} }
export const СardPrivilege = ({ privilege }: CardPrivilege) => { export const СardPrivilege = ({ privilege }: CardPrivilege) => {
const [inputOpen, setInputOpen] = useState<boolean>(false); const [inputOpen, setInputOpen] = useState<boolean>(false);
const [inputValue, setInputValue] = useState<string>(""); const [inputValue, setInputValue] = useState<string>("");
const priceRef = useRef<HTMLDivElement>(null); const priceRef = useRef<HTMLDivElement>(null);
const theme = useTheme(); const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(600)); const mobile = useMediaQuery(theme.breakpoints.down(600));
const translationType = { const translationType = {
count: "за единицу", count: "за единицу",
day: "за день", day: "за день",
mb: "за МБ", mb: "за МБ",
}; };
const putPrivileges = async () => { const putPrivileges = async () => {
const [, putedPrivilegeError] = await putPrivilege({
name: privilege.name,
privilegeId: privilege.privilegeId,
serviceKey: privilege.serviceKey,
description: privilege.description,
amount: 1,
type: privilege.type,
value: privilege.value,
price: 100 * Number(inputValue),
});
const [, putedPrivilegeError] = await putPrivilege({ if (putedPrivilegeError) {
name: privilege.name, return enqueueSnackbar(putedPrivilegeError);
privilegeId: privilege.privilegeId, }
serviceKey: privilege.serviceKey,
description: privilege.description,
amount: 1,
type: privilege.type,
value: privilege.value,
price: 100 * Number(inputValue),
});
if (putedPrivilegeError) { if (!priceRef.current) {
return enqueueSnackbar(putedPrivilegeError); return;
} }
if (!priceRef.current) { enqueueSnackbar("Изменения сохранены");
return;
}
enqueueSnackbar("Изменения сохранены"); priceRef.current.innerText = "price: " + inputValue;
setInputValue("");
setInputOpen(false);
};
priceRef.current.innerText = "price: " + inputValue; const onTextfieldKeyDown = (event: KeyboardEvent) => {
setInputValue(""); if (event.key === "Escape") {
setInputOpen(false); return setInputOpen(false);
}; }
if (event.key === "Enter" && inputValue !== "") {
putPrivileges();
setInputOpen(false);
}
};
const onTextfieldKeyDown = (event: KeyboardEvent) => { function handleSavePrice() {
if (event.key === "Escape") { setInputOpen(false);
return setInputOpen(false); if (!inputValue) return;
}
if (event.key === "Enter" && inputValue !== "") {
putPrivileges();
setInputOpen(false);
}
};
function handleSavePrice() { putPrivileges();
setInputOpen(false); }
if (!inputValue) return;
putPrivileges(); return (
} <Box
key={privilege.type}
sx={{
px: "20px",
return ( display: "flex",
<Box alignItems: "center",
key={privilege.type} border: "1px solid gray",
sx={{ }}
px: "20px", >
<Box sx={{ display: "flex", borderRight: "1px solid gray" }}>
display: "flex", <Box sx={{ width: mobile ? "120px" : "200px", py: "25px" }}>
alignItems: "center", <Typography
border: "1px solid gray", variant="h6"
}} sx={{
> color: "#fe9903",
<Box sx={{ display: "flex", borderRight: "1px solid gray" }}> overflowWrap: "break-word",
<Box sx={{ width: mobile ? "120px" : "200px", py: "25px" }}> overflow: "hidden",
<Typography }}
variant="h6" >
sx={{ {privilege.name}
color: "#fe9903", </Typography>
overflowWrap: "break-word", <Tooltip
overflow: "hidden", sx={{
}} span: {
> fontSize: "1rem",
{privilege.name} },
</Typography> }}
<Tooltip placement="top"
sx={{ title={<Typography sx={{ fontSize: "16px" }}>{privilege.description}</Typography>}
span: { >
fontSize: "1rem", <IconButton disableRipple>
}, <svg width="40" height="40" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
}} <path
placement="top" d="M9.25 9.25H10V14.5H10.75"
title={<Typography sx={{ fontSize: "16px" }}>{privilege.description}</Typography>} stroke="#7E2AEA"
> strokeWidth="1.5"
<IconButton disableRipple> strokeLinecap="round"
<svg width="40" height="40" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> strokeLinejoin="round"
<path />
d="M9.25 9.25H10V14.5H10.75" <path
stroke="#7E2AEA" d="M9.8125 7C10.4338 7 10.9375 6.49632 10.9375 5.875C10.9375 5.25368 10.4338 4.75 9.8125 4.75C9.19118 4.75 8.6875 5.25368 8.6875 5.875C8.6875 6.49632 9.19118 7 9.8125 7Z"
strokeWidth="1.5" fill="#7E2AEA"
strokeLinecap="round" />
strokeLinejoin="round" </svg>
/> </IconButton>
<path </Tooltip>
d="M9.8125 7C10.4338 7 10.9375 6.49632 10.9375 5.875C10.9375 5.25368 10.4338 4.75 9.8125 4.75C9.19118 4.75 8.6875 5.25368 8.6875 5.875C8.6875 6.49632 9.19118 7 9.8125 7Z" <IconButton onClick={() => setInputOpen(!inputOpen)}>
fill="#7E2AEA" <ModeEditOutlineOutlinedIcon sx={{ color: "gray" }} />
/> </IconButton>
</svg> </Box>
</IconButton> </Box>
</Tooltip> <Box
<IconButton onClick={() => setInputOpen(!inputOpen)}> sx={{
<ModeEditOutlineOutlinedIcon sx={{ color: "gray" }} /> maxWidth: "600px",
</IconButton> width: "100%",
</Box> display: "flex",
</Box> alignItems: mobile ? "center" : undefined,
<Box sx={{ maxWidth: "600px", width: "100%", display: "flex", alignItems: mobile ? "center" : undefined, justifyContent: "space-around", flexDirection: mobile ? "column" : "row", gap: "5px" }}> justifyContent: "space-around",
{inputOpen ? ( flexDirection: mobile ? "column" : "row",
<TextField gap: "5px",
type="number" }}
onKeyDown={onTextfieldKeyDown} >
placeholder="введите число" {inputOpen ? (
fullWidth <TextField
onChange={(event) => setInputValue(event.target.value)} type="number"
sx={{ onKeyDown={onTextfieldKeyDown}
alignItems: "center", placeholder="введите число"
maxWidth: "400px", fullWidth
width: "100%", onChange={(event) => setInputValue(event.target.value)}
marginLeft: "5px", sx={{
"& .MuiInputBase-root": { alignItems: "center",
backgroundColor: "#F2F3F7", maxWidth: "400px",
height: "48px", width: "100%",
}, marginLeft: "5px",
}} "& .MuiInputBase-root": {
inputProps={{ backgroundColor: "#F2F3F7",
sx: { height: "48px",
borderRadius: "10px", },
fontSize: "18px", }}
lineHeight: "21px", inputProps={{
py: 0, sx: {
}, borderRadius: "10px",
}} fontSize: "18px",
InputProps={{ lineHeight: "21px",
endAdornment: ( py: 0,
<IconButton onClick={handleSavePrice}> },
<SaveIcon /> }}
</IconButton> InputProps={{
) endAdornment: (
}} <IconButton onClick={handleSavePrice}>
/> <SaveIcon />
) : ( </IconButton>
<div ref={priceRef} style={{ color: "white", marginRight: "5px" }}> ),
price: {currencyFormatter.format(privilege.price / 100)} }}
</div> />
)} ) : (
<Typography sx={{ color: "white" }}>{translationType[privilege.type]}</Typography> <div ref={priceRef} style={{ color: "white", marginRight: "5px" }}>
</Box> price: {currencyFormatter.format(privilege.price / 100)}
</Box> </div>
); )}
<Typography sx={{ color: "white" }}>{translationType[privilege.type]}</Typography>
</Box>
</Box>
);
}; };

@ -1,47 +1,47 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
type ConditionalRenderProps = { type ConditionalRenderProps = {
isLoading: boolean; isLoading: boolean;
role: string; role: string;
childrenUser?: JSX.Element; childrenUser?: JSX.Element;
childrenAdmin?: JSX.Element; childrenAdmin?: JSX.Element;
childrenManager?: JSX.Element; childrenManager?: JSX.Element;
}; };
const ConditionalRender = ({ const ConditionalRender = ({
isLoading, isLoading,
role, role,
childrenUser, childrenUser,
childrenAdmin, childrenAdmin,
childrenManager, childrenManager,
}: ConditionalRenderProps): JSX.Element => { }: ConditionalRenderProps): JSX.Element => {
// const [role, setRole] = useState<string>(""); // const [role, setRole] = useState<string>("");
// useEffect(() => { // useEffect(() => {
// const axiosAccount = async () => { // const axiosAccount = async () => {
// try { // try {
// const { data } = await axios.get(process.env.REACT_APP_DOMAIN + "/user/643e23f3dba63ba17272664d"); // const { data } = await axios.get(process.env.REACT_APP_DOMAIN + "/user/643e23f3dba63ba17272664d");
// setRole(data.role); // setRole(data.role);
// } catch (error) { // } catch (error) {
// console.error("Ошибка при получение роли пользавателя"); // console.error("Ошибка при получение роли пользавателя");
// } // }
// }; // };
// axiosAccount(); // axiosAccount();
// }, [role]); // }, [role]);
if (!isLoading) { if (!isLoading) {
if (role === "admin") { if (role === "admin") {
return childrenAdmin ? childrenAdmin : <div>Администратор</div>; return childrenAdmin ? childrenAdmin : <div>Администратор</div>;
} }
if (role === "user") { if (role === "user") {
return childrenUser ? childrenUser : <div>Пользователь</div>; return childrenUser ? childrenUser : <div>Пользователь</div>;
} }
if (role === "manager") { if (role === "manager") {
return childrenManager ? childrenManager : <div>Менеджер</div>; return childrenManager ? childrenManager : <div>Менеджер</div>;
} }
} }
return <React.Fragment />; return <React.Fragment />;
}; };
export default ConditionalRender; export default ConditionalRender;

@ -1,81 +1,83 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
Checkbox, Checkbox,
FormControl, FormControl,
ListItemText, ListItemText,
MenuItem, MenuItem,
Select, Select,
SelectChangeEvent, SelectChangeEvent,
TextField, useMediaQuery, useTheme, TextField,
useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import { MOCK_DATA_USERS } from "@root/api/roles"; import { MOCK_DATA_USERS } from "@root/api/roles";
const ITEM_HEIGHT = 48; const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8; const ITEM_PADDING_TOP = 8;
const MenuProps = { const MenuProps = {
PaperProps: { PaperProps: {
style: { style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
}, },
}, },
}; };
export default function CreateForm() { export default function CreateForm() {
const [personName, setPersonName] = useState<string[]>([]); const [personName, setPersonName] = useState<string[]>([]);
const theme = useTheme(); const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(600)); const mobile = useMediaQuery(theme.breakpoints.down(600));
const handleChange = (event: SelectChangeEvent<typeof personName>) => { const handleChange = (event: SelectChangeEvent<typeof personName>) => {
const { const {
target: { value }, target: { value },
} = event; } = event;
setPersonName(typeof value === "string" ? value.split(",") : value); setPersonName(typeof value === "string" ? value.split(",") : value);
}; };
return ( return (
<> <>
<TextField <TextField
placeholder="название" placeholder="название"
fullWidth fullWidth
sx={{ sx={{
alignItems: "center", alignItems: "center",
maxWidth: "400px", maxWidth: "400px",
width: "100%", width: "100%",
"& .MuiInputBase-root": { "& .MuiInputBase-root": {
backgroundColor: "#F2F3F7", backgroundColor: "#F2F3F7",
height: "48px", height: "48px",
}, },
}} }}
inputProps={{ inputProps={{
sx: { sx: {
borderRadius: "10px", borderRadius: "10px",
fontSize: "18px", fontSize: "18px",
lineHeight: "21px", lineHeight: "21px",
py: 0, py: 0,
}, },
}} }}
/> />
<Button sx={{ ml: "5px", bgcolor: "#fe9903", color: "white", minWidth: "85px" }}>Отправить</Button> <Button sx={{ ml: "5px", bgcolor: "#fe9903", color: "white", minWidth: "85px" }}>Отправить</Button>
<FormControl sx={{ ml: mobile ? "10px" : "25px", width: "80%" }}> <FormControl sx={{ ml: mobile ? "10px" : "25px", width: "80%" }}>
<Select <Select
sx={{ bgcolor: "white", height: "48px" }} sx={{ bgcolor: "white", height: "48px" }}
labelId="demo-multiple-checkbox-label" labelId="demo-multiple-checkbox-label"
id="demo-multiple-checkbox" id="demo-multiple-checkbox"
multiple multiple
value={personName} value={personName}
onChange={handleChange} onChange={handleChange}
renderValue={(selected) => selected.join(", ")} renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps} MenuProps={MenuProps}
> >
{MOCK_DATA_USERS.map(({ name, id }) => ( {MOCK_DATA_USERS.map(({ name, id }) => (
<MenuItem key={id} value={name}> <MenuItem key={id} value={name}>
<Checkbox checked={personName.indexOf(name) > -1} /> <Checkbox checked={personName.indexOf(name) > -1} />
<ListItemText primary={name} /> <ListItemText primary={name} />
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</> </>
); );
} }

@ -1,13 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
Checkbox, Checkbox,
FormControl, FormControl,
ListItemText, ListItemText,
MenuItem, MenuItem,
Select, Select,
SelectChangeEvent, SelectChangeEvent,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { MOCK_DATA_USERS } from "@root/api/roles"; import { MOCK_DATA_USERS } from "@root/api/roles";
import { deleteRole } from "@root/api/roles"; import { deleteRole } from "@root/api/roles";
@ -16,82 +16,76 @@ import { enqueueSnackbar } from "notistack";
const ITEM_HEIGHT = 48; const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8; const ITEM_PADDING_TOP = 8;
const MenuProps = { const MenuProps = {
PaperProps: { PaperProps: {
style: { style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
}, },
}, },
}; };
export default function DeleteForm() { export default function DeleteForm() {
const [personName, setPersonName] = useState<string[]>([]); const [personName, setPersonName] = useState<string[]>([]);
const [roleId, setRoleId] = useState<string>(); const [roleId, setRoleId] = useState<string>();
const handleChange = (event: SelectChangeEvent<typeof personName>) => { const handleChange = (event: SelectChangeEvent<typeof personName>) => {
const { const {
target: { value }, target: { value },
} = event; } = event;
setPersonName(typeof value === "string" ? value.split(",") : value); setPersonName(typeof value === "string" ? value.split(",") : value);
}; };
const rolesDelete = async (id = "") => { const rolesDelete = async (id = "") => {
const [_, deletedRoleError] = await deleteRole(id); const [_, deletedRoleError] = await deleteRole(id);
if (deletedRoleError) { if (deletedRoleError) {
return enqueueSnackbar(deletedRoleError); return enqueueSnackbar(deletedRoleError);
} }
}; };
return ( return (
<> <>
<Button <Button onClick={() => rolesDelete(roleId)} sx={{ mr: "5px", bgcolor: "#fe9903", color: "white" }}>
onClick={() => rolesDelete(roleId)} Удалить
sx={{ mr: "5px", bgcolor: "#fe9903", color: "white" }} </Button>
> <TextField
Удалить placeholder="название"
</Button> fullWidth
<TextField sx={{
placeholder="название" alignItems: "center",
fullWidth maxWidth: "400px",
sx={{ width: "100%",
alignItems: "center", "& .MuiInputBase-root": {
maxWidth: "400px", backgroundColor: "#F2F3F7",
width: "100%", height: "48px",
"& .MuiInputBase-root": { },
backgroundColor: "#F2F3F7", }}
height: "48px", inputProps={{
}, sx: {
}} borderRadius: "10px",
inputProps={{ fontSize: "18px",
sx: { lineHeight: "21px",
borderRadius: "10px", py: 0,
fontSize: "18px", },
lineHeight: "21px", }}
py: 0, />
}, <FormControl sx={{ ml: "25px", width: "80%" }}>
}} <Select
/> sx={{ bgcolor: "white", height: "48px" }}
<FormControl sx={{ ml: "25px", width: "80%" }}> labelId="demo-multiple-checkbox-label"
<Select id="demo-multiple-checkbox"
sx={{ bgcolor: "white", height: "48px" }} value={personName}
labelId="demo-multiple-checkbox-label" onChange={handleChange}
id="demo-multiple-checkbox" renderValue={(selected) => selected.join(", ")}
value={personName} MenuProps={MenuProps}
onChange={handleChange} >
renderValue={(selected) => selected.join(", ")} {MOCK_DATA_USERS.map(({ name, id }) => (
MenuProps={MenuProps} <MenuItem key={id} value={name}>
> <Checkbox onClick={() => setRoleId(id)} checked={personName.indexOf(name) > -1} />
{MOCK_DATA_USERS.map(({ name, id }) => ( <ListItemText primary={name} />
<MenuItem key={id} value={name}> </MenuItem>
<Checkbox ))}
onClick={() => setRoleId(id)} </Select>
checked={personName.indexOf(name) > -1} </FormControl>
/> </>
<ListItemText primary={name} /> );
</MenuItem>
))}
</Select>
</FormControl>
</>
);
} }

@ -6,21 +6,17 @@ import { requestPrivileges } from "@root/services/privilegies.service";
import { СardPrivilege } from "./CardPrivilegie"; import { СardPrivilege } from "./CardPrivilegie";
export default function ListPrivilege() { export default function ListPrivilege() {
const privileges = usePrivilegeStore((state) => state.privileges); const privileges = usePrivilegeStore((state) => state.privileges);
useEffect(() => { useEffect(() => {
requestPrivileges(); requestPrivileges();
}, []); }, []);
return ( return (
<> <>
{privileges.map(privilege => ( {privileges.map((privilege) => (
<СardPrivilege <СardPrivilege key={privilege._id} privilege={privilege} />
key={privilege._id} ))}
privilege={privilege} </>
/> );
)
)}
</>
);
} }

@ -5,24 +5,24 @@ import { Box, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/m
import ListPrivilege from "./ListPrivilegie"; import ListPrivilege from "./ListPrivilegie";
interface CustomWrapperProps { interface CustomWrapperProps {
text: string; text: string;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
result?: boolean; result?: boolean;
} }
export const PrivilegesWrapper = ({ text, sx, result }: CustomWrapperProps) => { export const PrivilegesWrapper = ({ text, sx, result }: CustomWrapperProps) => {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm")); const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const [isExpanded, setIsExpanded] = useState<boolean>(false); const [isExpanded, setIsExpanded] = useState<boolean>(false);
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
overflow: "hidden", overflow: "hidden",
borderRadius: "12px", borderRadius: "12px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24), boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525), 0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066), 0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12), 0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
@ -30,99 +30,99 @@ export const PrivilegesWrapper = ({ text, sx, result }: CustomWrapperProps) => {
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.067 0px 2.76726px 8.55082px rgba(210, 208, 225, 0.067
4749)`, 4749)`,
...sx, ...sx,
}} }}
> >
<Box <Box
sx={{ sx={{
border: "2px solid grey", border: "2px solid grey",
"&:first-of-type": { "&:first-of-type": {
borderTopLeftRadius: "12px ", borderTopLeftRadius: "12px ",
borderTopRightRadius: "12px", borderTopRightRadius: "12px",
}, },
"&:last-of-type": { "&:last-of-type": {
borderBottomLeftRadius: "12px", borderBottomLeftRadius: "12px",
borderBottomRightRadius: "12px", borderBottomRightRadius: "12px",
}, },
"&:not(:last-of-type)": { "&:not(:last-of-type)": {
borderBottom: `1px solid gray`, borderBottom: `1px solid gray`,
}, },
}} }}
> >
<Box <Box
onClick={() => setIsExpanded((prev) => !prev)} onClick={() => setIsExpanded((prev) => !prev)}
sx={{ sx={{
height: "88px", height: "88px",
px: "20px", px: "20px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
cursor: "pointer", cursor: "pointer",
userSelect: "none", userSelect: "none",
}} }}
> >
<Typography <Typography
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
fontSize: "18px", fontSize: "18px",
lineHeight: upMd ? undefined : "19px", lineHeight: upMd ? undefined : "19px",
fontWeight: 400, fontWeight: 400,
color: "#FFFFFF", color: "#FFFFFF",
px: 0, px: 0,
}} }}
> >
{text} {text}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
height: "100%", height: "100%",
alignItems: "center", alignItems: "center",
}} }}
> >
{result ? ( {result ? (
<> <>
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#252734" /> <rect y="0.8125" width="30" height="30" rx="6" fill="#252734" />
<path <path
d="M7.5 19.5625L15 12.0625L22.5 19.5625" d="M7.5 19.5625L15 12.0625L22.5 19.5625"
stroke="#7E2AEA" stroke="#7E2AEA"
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
<Box <Box
sx={{ sx={{
borderLeft: upSm ? "1px solid #9A9AAF" : "none", borderLeft: upSm ? "1px solid #9A9AAF" : "none",
pl: upSm ? "2px" : 0, pl: upSm ? "2px" : 0,
height: "50%", height: "50%",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}} }}
/> />
</> </>
) : ( ) : (
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#252734" /> <rect y="0.8125" width="30" height="30" rx="6" fill="#252734" />
<path <path
d="M7.5 19.5625L15 12.0625L22.5 19.5625" d="M7.5 19.5625L15 12.0625L22.5 19.5625"
stroke="#fe9903" stroke="#fe9903"
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
)} )}
</Box> </Box>
</Box> </Box>
{isExpanded && <ListPrivilege />} {isExpanded && <ListPrivilege />}
</Box> </Box>
</Box> </Box>
); );
}; };

@ -1,13 +1,13 @@
import { import {
AccordionDetails, AccordionDetails,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { CustomWrapper } from "@root/kitUI/CustomWrapper"; import { CustomWrapper } from "@root/kitUI/CustomWrapper";
@ -19,118 +19,116 @@ import { PrivilegesWrapper } from "./PrivilegiesWrapper";
import theme from "../../theme"; import theme from "../../theme";
export const SettingRoles = (): JSX.Element => { export const SettingRoles = (): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(600)); const mobile = useMediaQuery(theme.breakpoints.down(600));
return ( return (
<AccordionDetails sx={{ maxWidth: "890px", <AccordionDetails sx={{ maxWidth: "890px", width: "100%" }}>
width: "100%", }}> <CustomWrapper
<CustomWrapper text="Роли"
text="Роли" children={
children={ <>
<> <Table
<Table sx={{
sx={{ maxWidth: "890px",
maxWidth: "890px", width: "100%",
width: "100%", border: "2px solid",
border: "2px solid", borderColor: "gray",
borderColor: "gray", }}
}} aria-label="simple table"
aria-label="simple table" >
> <TableHead>
<TableHead> <TableRow
<TableRow sx={{
sx={{ borderBottom: "2px solid",
borderBottom: "2px solid", borderColor: theme.palette.grayLight.main,
borderColor: theme.palette.grayLight.main, height: "100px",
height: "100px", }}
}} >
> <TableCell>
<TableCell> <Typography
<Typography variant="h4"
variant="h4" sx={{
sx={{ color: theme.palette.secondary.main,
color: theme.palette.secondary.main, }}
}} >
> Настройки ролей
Настройки ролей </Typography>
</Typography> </TableCell>
</TableCell> </TableRow>
</TableRow> </TableHead>
</TableHead>
<TableBody> <TableBody>
<TableRow <TableRow
sx={{ sx={{
p: "5px", p: "5px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
borderTop: "2px solid", borderTop: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: mobile ? undefined : "100px", height: mobile ? undefined : "100px",
cursor: "pointer", cursor: "pointer",
flexDirection: mobile ? "column" : "row", flexDirection: mobile ? "column" : "row",
gap: "5px" gap: "5px",
}} }}
> >
<FormCreateRoles /> <FormCreateRoles />
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
<Table <Table
sx={{ sx={{
mt: "30px", mt: "30px",
maxWidth: "890px", maxWidth: "890px",
width: "100%", width: "100%",
border: "2px solid", border: "2px solid",
borderColor: "gray", borderColor: "gray",
}} }}
aria-label="simple table" aria-label="simple table"
> >
<TableHead> <TableHead>
<TableRow <TableRow
sx={{ sx={{
borderBottom: "2px solid", borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: "100px", height: "100px",
}} }}
> >
<TableCell> <TableCell>
<Typography <Typography
variant="h4" variant="h4"
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}} }}
> >
Удаление ролей Удаление ролей
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow <TableRow
sx={{ sx={{
p: "5px", p: "5px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
borderTop: "2px solid", borderTop: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: mobile ? undefined : "100px", height: mobile ? undefined : "100px",
cursor: "pointer", cursor: "pointer",
flexDirection: mobile ? "column" : "row", flexDirection: mobile ? "column" : "row",
gap: "5px" gap: "5px",
}} }}
> >
<FormDeleteRoles /> <FormDeleteRoles />
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
</> </>
} }
/> />
<PrivilegesWrapper text="Привилегии" sx={{ mt: "50px", maxWidth: "890px", <PrivilegesWrapper text="Привилегии" sx={{ mt: "50px", maxWidth: "890px", width: "100%" }} />
width: "100%", }} /> </AccordionDetails>
</AccordionDetails> );
);
}; };

@ -1,6 +1,6 @@
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { GridSelectionModel } from "@mui/x-data-grid"; import { GridSelectionModel } from "@mui/x-data-grid";
import {Box, Button, useMediaQuery, useTheme} from "@mui/material"; import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { changeDiscount } from "@root/api/discounts"; import { changeDiscount } from "@root/api/discounts";
import { findDiscountsById } from "@root/stores/discounts"; import { findDiscountsById } from "@root/stores/discounts";
@ -9,56 +9,58 @@ import { requestDiscounts } from "@root/services/discounts.service";
import { mutate } from "swr"; import { mutate } from "swr";
interface Props { interface Props {
selectedRows: GridSelectionModel; selectedRows: GridSelectionModel;
} }
export default function DiscountDataGrid({ selectedRows }: Props) { export default function DiscountDataGrid({ selectedRows }: Props) {
const theme = useTheme(); const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(400)); const mobile = useMediaQuery(theme.breakpoints.down(400));
const changeData = async (isActive: boolean) => { const changeData = async (isActive: boolean) => {
let done = 0; let done = 0;
let fatal = 0; let fatal = 0;
for (const id of selectedRows) { for (const id of selectedRows) {
const discount = findDiscountsById(String(id)); const discount = findDiscountsById(String(id));
if (!discount) { if (!discount) {
return enqueueSnackbar("Скидка не найдена"); return enqueueSnackbar("Скидка не найдена");
} }
const [, changedDiscountError] = await changeDiscount(String(id), { const [, changedDiscountError] = await changeDiscount(String(id), {
...discount, ...discount,
Deprecated: isActive, Deprecated: isActive,
}); });
if (changedDiscountError) { if (changedDiscountError) {
done += 1; done += 1;
} else { } else {
fatal += 1; fatal += 1;
} }
mutate("discounts"); mutate("discounts");
} }
await requestDiscounts(); await requestDiscounts();
if (done) { if (done) {
enqueueSnackbar("Успешно изменён статус " + done + " скидок"); enqueueSnackbar("Успешно изменён статус " + done + " скидок");
} }
if (fatal) { if (fatal) {
enqueueSnackbar(fatal + " скидок не изменили статус"); enqueueSnackbar(fatal + " скидок не изменили статус");
} }
}; };
return ( return (
<Box <Box
sx={{ sx={{
width: mobile ? "250px" : "400px", width: mobile ? "250px" : "400px",
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
flexDirection: mobile ? "column" : undefined, flexDirection: mobile ? "column" : undefined,
gap: "10px"}}> gap: "10px",
<Button onClick={() => changeData(false)}>Активировать</Button> }}
<Button onClick={() => changeData(true)}>Деактивировать</Button> >
</Box> <Button onClick={() => changeData(false)}>Активировать</Button>
); <Button onClick={() => changeData(true)}>Деактивировать</Button>
</Box>
);
} }

@ -1,22 +1,20 @@
import { import {
Box, Box,
Typography, Typography,
Button, Button,
useTheme, useTheme,
FormControl, FormControl,
FormLabel, FormLabel,
RadioGroup, RadioGroup,
FormControlLabel, FormControlLabel,
Radio, Radio,
InputLabel, TextField, InputLabel,
TextField,
} from "@mui/material"; } from "@mui/material";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Select, { SelectChangeEvent } from "@mui/material/Select"; import Select, { SelectChangeEvent } from "@mui/material/Select";
import { SERVICE_LIST, ServiceType } from "@root/model/tariff"; import { SERVICE_LIST, ServiceType } from "@root/model/tariff";
import { import { resetPrivilegeArray, usePrivilegeStore } from "@root/stores/privilegesStore";
resetPrivilegeArray,
usePrivilegeStore,
} from "@root/stores/privilegesStore";
import { addDiscount } from "@root/stores/discounts"; import { addDiscount } from "@root/stores/discounts";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { DiscountType, discountTypes } from "@root/model/discount"; import { DiscountType, discountTypes } from "@root/model/discount";
@ -26,494 +24,480 @@ import { Formik, Field, Form, FormikHelpers } from "formik";
import { mutate } from "swr"; import { mutate } from "swr";
interface Values { interface Values {
discountNameField: string, discountNameField: string;
discountDescriptionField: string, discountDescriptionField: string;
discountFactorField: string, discountFactorField: string;
serviceType: string, serviceType: string;
discountType: DiscountType, discountType: DiscountType;
purchasesAmountField: string, purchasesAmountField: string;
cartPurchasesAmountField: string, cartPurchasesAmountField: string;
discountMinValueField: string, discountMinValueField: string;
privilegeIdField: string, privilegeIdField: string;
} }
export default function CreateDiscount() { export default function CreateDiscount() {
const theme = useTheme(); const theme = useTheme();
const privileges = usePrivilegeStore((state) => state.privileges); const privileges = usePrivilegeStore((state) => state.privileges);
usePrivileges({ onNewPrivileges: resetPrivilegeArray }); usePrivileges({ onNewPrivileges: resetPrivilegeArray });
const initialValues: Values = { const initialValues: Values = {
discountNameField: "", discountNameField: "",
discountDescriptionField: "", discountDescriptionField: "",
discountFactorField: "", discountFactorField: "",
serviceType: "", serviceType: "",
discountType: "purchasesAmount", discountType: "purchasesAmount",
purchasesAmountField: "", purchasesAmountField: "",
cartPurchasesAmountField: "", cartPurchasesAmountField: "",
discountMinValueField: "", discountMinValueField: "",
privilegeIdField: "", privilegeIdField: "",
} };
const handleCreateDiscount = async( const handleCreateDiscount = async (values: Values, formikHelpers: FormikHelpers<Values>) => {
values: Values, const purchasesAmount = Number(parseFloat(values.purchasesAmountField.replace(",", "."))) * 100;
formikHelpers: FormikHelpers<Values>
) => {
const purchasesAmount = Number(parseFloat(values.purchasesAmountField.replace(",", "."))) * 100;
const discountFactor =
(100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100;
const cartPurchasesAmount = Number(parseFloat(
values.cartPurchasesAmountField.replace(",", ".")) * 100
);
const discountMinValue = Number(parseFloat(
values.discountMinValueField.replace(",", ".")) * 100
);
const [createdDiscountResponse, createdDiscountError] = const discountFactor = (100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100;
await createDiscount({ const cartPurchasesAmount = Number(parseFloat(values.cartPurchasesAmountField.replace(",", ".")) * 100);
cartPurchasesAmount, const discountMinValue = Number(parseFloat(values.discountMinValueField.replace(",", ".")) * 100);
discountFactor,
discountMinValue,
purchasesAmount,
discountDescription: values.discountDescriptionField,
discountName: values.discountNameField,
startDate: new Date().toISOString(),
endDate: new Date(Date.now() + 1000 * 3600 * 24 * 30).toISOString(),
serviceType: values.serviceType,
discountType: values.discountType,
privilegeId: values.privilegeIdField,
});
if (createdDiscountError) { const [createdDiscountResponse, createdDiscountError] = await createDiscount({
console.error("Error creating discount", createdDiscountError); cartPurchasesAmount,
discountFactor,
discountMinValue,
purchasesAmount,
discountDescription: values.discountDescriptionField,
discountName: values.discountNameField,
startDate: new Date().toISOString(),
endDate: new Date(Date.now() + 1000 * 3600 * 24 * 30).toISOString(),
serviceType: values.serviceType,
discountType: values.discountType,
privilegeId: values.privilegeIdField,
});
return enqueueSnackbar(createdDiscountError); if (createdDiscountError) {
} console.error("Error creating discount", createdDiscountError);
if (createdDiscountResponse) { return enqueueSnackbar(createdDiscountError);
mutate("discounts"); }
addDiscount(createdDiscountResponse);
}
}
const validateFulledFields = (values: Values) => { if (createdDiscountResponse) {
const errors = {} as any; mutate("discounts");
if (values.discountNameField.length === 0) { addDiscount(createdDiscountResponse);
errors.discountNameField = 'Поле "Имя" пустое' }
} };
if (values.discountDescriptionField.length === 0) {
errors.discountDescriptionField = 'Поле "Описание" пустое'
}
if (((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100) < 0) {
errors.discountFactorField = "Процент скидки не может быть больше 100"
}
if (!isFinite(((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100))) {
errors.discountFactorField = 'Поле "Процент скидки" не число'
}
if (values.discountType === "privilege" && !values.privilegeIdField) {
errors.privilegeIdField = "Привилегия не выбрана"
}
if (values.discountType === "service" && !values.serviceType) {
errors.serviceType = "Сервис не выбран"
}
if (values.discountType === "purchasesAmount" && !isFinite(parseFloat(values.purchasesAmountField.replace(",", ".")))) {
errors.purchasesAmountField = 'Поле "Внесено больше" не число'
}
if (values.discountType === "cartPurchasesAmount" && !isFinite(parseFloat(values.cartPurchasesAmountField.replace(",", ".")))) {
errors.cartPurchasesAmountField = 'Поле "Объём в корзине" не число'
}
if (values.discountType === ("service" || "privilege") && !isFinite(parseFloat(values.discountMinValueField.replace(",", ".")))) {
errors.discountMinValueField = 'Поле "Минимальное значение" не число'
}
console.error(errors)
return errors;
}
const validateFulledFields = (values: Values) => {
const errors = {} as any;
if (values.discountNameField.length === 0) {
errors.discountNameField = 'Поле "Имя" пустое';
}
if (values.discountDescriptionField.length === 0) {
errors.discountDescriptionField = 'Поле "Описание" пустое';
}
if ((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100 < 0) {
errors.discountFactorField = "Процент скидки не может быть больше 100";
}
if (!isFinite((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100)) {
errors.discountFactorField = 'Поле "Процент скидки" не число';
}
if (values.discountType === "privilege" && !values.privilegeIdField) {
errors.privilegeIdField = "Привилегия не выбрана";
}
if (values.discountType === "service" && !values.serviceType) {
errors.serviceType = "Сервис не выбран";
}
if (
values.discountType === "purchasesAmount" &&
!isFinite(parseFloat(values.purchasesAmountField.replace(",", ".")))
) {
errors.purchasesAmountField = 'Поле "Внесено больше" не число';
}
if (
values.discountType === "cartPurchasesAmount" &&
!isFinite(parseFloat(values.cartPurchasesAmountField.replace(",", ".")))
) {
errors.cartPurchasesAmountField = 'Поле "Объём в корзине" не число';
}
if (
values.discountType === ("service" || "privilege") &&
!isFinite(parseFloat(values.discountMinValueField.replace(",", ".")))
) {
errors.discountMinValueField = 'Поле "Минимальное значение" не число';
}
console.error(errors);
return errors;
};
return ( return (
<Formik <Formik initialValues={initialValues} onSubmit={handleCreateDiscount} validate={validateFulledFields}>
initialValues={initialValues} {(props) => (
onSubmit={handleCreateDiscount} <Form style={{ width: "100%", display: "flex", justifyContent: "center" }}>
validate={validateFulledFields} <Box
> sx={{
{(props) => ( display: "flex",
<Form style={{width: "100%", display: "flex", justifyContent: "center"}}> flexDirection: "column",
<Box justifyContent: "left",
sx={{ alignItems: "left",
display: "flex", marginTop: "15px",
flexDirection: "column", width: "100%",
justifyContent: "left", padding: "16px",
alignItems: "left", maxWidth: "600px",
marginTop: "15px", gap: "1em",
width: "100%", }}
padding: "16px", >
maxWidth: "600px", <Field
gap: "1em", as={TextField}
}} id="discount-name"
> label="Название"
<Field variant="filled"
as={TextField} name="discountNameField"
id="discount-name" error={props.touched.discountNameField && !!props.errors.discountNameField}
label="Название" helperText={
variant="filled" <Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.discountNameField}</Typography>
name="discountNameField" }
error={props.touched.discountNameField && !!props.errors.discountNameField} InputProps={{
helperText={ style: {
<Typography sx={{fontSize: "12px", width: "200px"}}> backgroundColor: theme.palette.content.main,
{props.errors.discountNameField} color: theme.palette.secondary.main,
</Typography> },
} }}
InputProps={{ InputLabelProps={{
style: { style: {
backgroundColor: theme.palette.content.main, color: theme.palette.secondary.main,
color: theme.palette.secondary.main, },
} }}
}} />
InputLabelProps={{ <Field
style: { as={TextField}
color: theme.palette.secondary.main id="discount-desc"
} label="Описание"
}} variant="filled"
/> name="discountDescriptionField"
<Field type="text"
as={TextField} error={props.touched.discountDescriptionField && !!props.errors.discountDescriptionField}
id="discount-desc" helperText={
label="Описание" <Typography sx={{ fontSize: "12px", width: "200px" }}>
variant="filled" {props.errors.discountDescriptionField}
name="discountDescriptionField" </Typography>
type="text" }
error={props.touched.discountDescriptionField && !!props.errors.discountDescriptionField} InputProps={{
helperText={ style: {
<Typography sx={{fontSize: "12px", width: "200px"}}> backgroundColor: theme.palette.content.main,
{props.errors.discountDescriptionField} color: theme.palette.secondary.main,
</Typography> },
} }}
InputProps={{ InputLabelProps={{
style: { style: {
backgroundColor: theme.palette.content.main, color: theme.palette.secondary.main,
color: theme.palette.secondary.main, },
} }}
}} />
InputLabelProps={{ <Typography
style: { variant="h4"
color: theme.palette.secondary.main sx={{
} width: "90%",
}} fontWeight: "normal",
/> color: theme.palette.grayDisabled.main,
<Typography paddingLeft: "10px",
variant="h4" }}
sx={{ >
width: "90%", Условия:
fontWeight: "normal", </Typography>
color: theme.palette.grayDisabled.main, <Field
paddingLeft: "10px", as={TextField}
}} id="discount-factor"
> label="Процент скидки"
Условия: variant="filled"
</Typography> name="discountFactorField"
<Field error={props.touched.discountFactorField && !!props.errors.discountFactorField}
as={TextField} value={props.values.discountFactorField}
id="discount-factor" helperText={
label="Процент скидки" <Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.discountFactorField}</Typography>
variant="filled" }
name="discountFactorField" InputProps={{
error={props.touched.discountFactorField && !!props.errors.discountFactorField} style: {
value={props.values.discountFactorField} backgroundColor: theme.palette.content.main,
helperText={ color: theme.palette.secondary.main,
<Typography sx={{fontSize: "12px", width: "200px"}}> },
{props.errors.discountFactorField} }}
</Typography> InputLabelProps={{
} style: {
InputProps={{ color: theme.palette.secondary.main,
style: { },
backgroundColor: theme.palette.content.main, }}
color: theme.palette.secondary.main, />
} <FormControl>
}} <FormLabel
InputLabelProps={{ id="discount-type"
style: { sx={{
color: theme.palette.secondary.main color: "white",
} "&.Mui-focused": {
}} color: "white",
/> },
<FormControl> }}
<FormLabel >
id="discount-type" Тип скидки
sx={{ </FormLabel>
color: "white", <RadioGroup
"&.Mui-focused": { row
color: "white", aria-labelledby="discount-type"
}, name="discountType"
}} value={props.values.discountType}
> onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
Тип скидки props.setFieldValue("discountType", event.target.value as DiscountType);
</FormLabel> }}
<RadioGroup onBlur={props.handleBlur}
row >
aria-labelledby="discount-type" {Object.keys(discountTypes).map((type) => (
name="discountType" <FormControlLabel
value={props.values.discountType} key={type}
onChange={( value={type}
event: React.ChangeEvent<HTMLInputElement> control={<Radio color="secondary" />}
) => { label={discountTypes[type as DiscountType]}
props.setFieldValue("discountType", event.target.value as DiscountType); />
}} ))}
onBlur={props.handleBlur} </RadioGroup>
> </FormControl>
{Object.keys(discountTypes).map((type) => ( {props.values.discountType === "purchasesAmount" && (
<FormControlLabel <TextField
key={type} id="discount-purchases"
value={type} name="purchasesAmountField"
control={<Radio color="secondary"/>} variant="filled"
label={discountTypes[type as DiscountType]} error={props.touched.purchasesAmountField && !!props.errors.purchasesAmountField}
/> label="Внесено больше"
))} onChange={(e) => {
</RadioGroup> props.setFieldValue("purchasesAmountField", e.target.value.replace(/[^\d]/g, ""));
</FormControl> }}
{props.values.discountType === "purchasesAmount" && ( value={props.values.purchasesAmountField}
<TextField onBlur={props.handleBlur}
id="discount-purchases" sx={{
name="purchasesAmountField" marginTop: "15px",
variant="filled" }}
error={props.touched.purchasesAmountField && !!props.errors.purchasesAmountField} helperText={
label="Внесено больше" <Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.purchasesAmountField}</Typography>
onChange={(e) => { }
props.setFieldValue("purchasesAmountField", e.target.value.replace(/[^\d]/g, '')) InputProps={{
}} style: {
value={props.values.purchasesAmountField} backgroundColor: theme.palette.content.main,
onBlur={props.handleBlur} color: theme.palette.secondary.main,
sx={{ },
marginTop: "15px", }}
}} InputLabelProps={{
helperText={ style: {
<Typography sx={{fontSize: "12px", width: "200px"}}> color: theme.palette.secondary.main,
{props.errors.purchasesAmountField} },
</Typography> }}
} />
InputProps={{ )}
style: { {props.values.discountType === "cartPurchasesAmount" && (
backgroundColor: theme.palette.content.main, <TextField
color: theme.palette.secondary.main, id="discount-cart-purchases"
} label="Объем в корзине"
}} name="cartPurchasesAmountField"
InputLabelProps={{ variant="filled"
style: { error={props.touched.cartPurchasesAmountField && !!props.errors.cartPurchasesAmountField}
color: theme.palette.secondary.main onChange={(e) => {
} props.setFieldValue("cartPurchasesAmountField", e.target.value.replace(/[^\d]/g, ""));
}} }}
/> value={props.values.cartPurchasesAmountField}
)} onBlur={props.handleBlur}
{props.values.discountType === "cartPurchasesAmount" && ( sx={{
<TextField marginTop: "15px",
id="discount-cart-purchases" }}
label="Объем в корзине" helperText={
name="cartPurchasesAmountField" <Typography sx={{ fontSize: "12px", width: "200px" }}>
variant="filled" {props.errors.cartPurchasesAmountField}
error={props.touched.cartPurchasesAmountField && !!props.errors.cartPurchasesAmountField} </Typography>
onChange={(e) => { }
props.setFieldValue("cartPurchasesAmountField", e.target.value.replace(/[^\d]/g, '')) InputProps={{
}} style: {
value={props.values.cartPurchasesAmountField} backgroundColor: theme.palette.content.main,
onBlur={props.handleBlur} color: theme.palette.secondary.main,
sx={{ },
marginTop: "15px", }}
}} InputLabelProps={{
helperText={ style: {
<Typography sx={{fontSize: "12px", width: "200px"}}> color: theme.palette.secondary.main,
{props.errors.cartPurchasesAmountField} },
</Typography> }}
} />
InputProps={{ )}
style: { {props.values.discountType === "service" && (
backgroundColor: theme.palette.content.main, <>
color: theme.palette.secondary.main, <Select
} labelId="discount-service-label"
}} id="discount-service"
InputLabelProps={{ name="serviceType"
style: { onBlur={props.handleBlur}
color: theme.palette.secondary.main onChange={(e) => {
} props.setFieldValue("serviceType", e.target.value as ServiceType);
}} }}
/> error={props.touched.serviceType && !!props.errors.serviceType}
)} sx={{
{props.values.discountType === "service" && ( color: theme.palette.secondary.main,
<> borderColor: theme.palette.secondary.main,
<Select "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
labelId="discount-service-label" borderColor: theme.palette.secondary.main,
id="discount-service" },
name="serviceType" ".MuiSvgIcon-root ": {
onBlur={props.handleBlur} fill: theme.palette.secondary.main,
onChange={(e) => { },
props.setFieldValue("serviceType", e.target.value as ServiceType); }}
}} >
error={props.touched.serviceType && !!props.errors.serviceType} {SERVICE_LIST.map((service) => (
sx={{ <MenuItem key={service.serviceKey} value={service.serviceKey}>
color: theme.palette.secondary.main, {service.displayName}
borderColor: theme.palette.secondary.main, </MenuItem>
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { ))}
borderColor: theme.palette.secondary.main, </Select>
}, <TextField
".MuiSvgIcon-root ": { id="discount-min-value"
fill: theme.palette.secondary.main, name="discountMinValueField"
}, label="Минимальное значение"
}} onBlur={props.handleBlur}
> variant="filled"
{SERVICE_LIST.map((service) => ( onChange={(e) => {
<MenuItem key={service.serviceKey} value={service.serviceKey}> props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, ""));
{service.displayName} }}
</MenuItem> value={props.values.discountMinValueField}
))} error={props.touched.discountMinValueField && !!props.errors.discountMinValueField}
</Select> sx={{
<TextField marginTop: "15px",
id="discount-min-value" }}
name="discountMinValueField" helperText={
label="Минимальное значение" <Typography sx={{ fontSize: "12px", width: "200px" }}>
onBlur={props.handleBlur} {props.errors.discountMinValueField}
variant="filled" </Typography>
onChange={(e) => { }
props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, '')) InputProps={{
}} style: {
value={props.values.discountMinValueField} backgroundColor: theme.palette.content.main,
error={props.touched.discountMinValueField && !!props.errors.discountMinValueField} color: theme.palette.secondary.main,
sx={{ },
marginTop: "15px", }}
}} InputLabelProps={{
helperText={ style: {
<Typography sx={{fontSize: "12px", width: "200px"}}> color: theme.palette.secondary.main,
{props.errors.discountMinValueField} },
</Typography> }}
} />
InputProps={{ </>
style: { )}
backgroundColor: theme.palette.content.main, {props.values.discountType === "privilege" && (
color: theme.palette.secondary.main, <>
} <FormControl
}} fullWidth
InputLabelProps={{ sx={{
style: { color: theme.palette.secondary.main,
color: theme.palette.secondary.main "& .MuiInputLabel-outlined": {
} color: theme.palette.secondary.main,
}} },
/> "& .MuiInputLabel-outlined.MuiInputLabel-shrink": {
</> color: theme.palette.secondary.main,
)} },
{props.values.discountType === "privilege" && ( }}
<> >
<FormControl <InputLabel
fullWidth id="privilege-select-label"
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
"& .MuiInputLabel-outlined": { fontSize: "16px",
color: theme.palette.secondary.main, lineHeight: "19px",
}, }}
"& .MuiInputLabel-outlined.MuiInputLabel-shrink": { >
color: theme.palette.secondary.main, Привилегия
}, </InputLabel>
}} <Select
> labelId="privilege-select-label"
<InputLabel id="privilege-select"
id="privilege-select-label" name="privilegeIdField"
sx={{ onBlur={props.handleBlur}
color: theme.palette.secondary.main, onChange={(e) => {
fontSize: "16px", props.setFieldValue("privilegeIdField", e.target.value);
lineHeight: "19px", }}
}} error={props.touched.privilegeIdField && !!props.errors.privilegeIdField}
> label="Привилегия"
Привилегия sx={{
</InputLabel> color: theme.palette.secondary.main,
<Select borderColor: theme.palette.secondary.main,
labelId="privilege-select-label" "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
id="privilege-select" borderColor: theme.palette.secondary.main,
name="privilegeIdField" },
onBlur={props.handleBlur} ".MuiSvgIcon-root ": {
onChange={(e) => { fill: theme.palette.secondary.main,
props.setFieldValue("privilegeIdField", e.target.value); },
}} }}
error={props.touched.privilegeIdField && !!props.errors.privilegeIdField} inputProps={{ sx: { pt: "12px" } }}
label="Привилегия" >
sx={{ {privileges.map((privilege, index) => (
color: theme.palette.secondary.main, <MenuItem key={index} value={privilege.privilegeId}>
borderColor: theme.palette.secondary.main, {privilege.description}
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { </MenuItem>
borderColor: theme.palette.secondary.main, ))}
}, </Select>
".MuiSvgIcon-root ": { </FormControl>
fill: theme.palette.secondary.main, <TextField
}, id="discount-min-value"
}} name="discountMinValueField"
inputProps={{sx: {pt: "12px"}}} label="Минимальное значение"
> onBlur={props.handleBlur}
{privileges.map((privilege, index) => ( variant="filled"
<MenuItem key={index} value={privilege.privilegeId}> onChange={(e) => {
{privilege.description} props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, ""));
</MenuItem> }}
))} value={props.values.discountMinValueField}
</Select> error={props.touched.discountMinValueField && !!props.errors.discountMinValueField}
</FormControl> sx={{
<TextField marginTop: "15px",
id="discount-min-value" }}
name="discountMinValueField" helperText={
label="Минимальное значение" <Typography sx={{ fontSize: "12px", width: "200px" }}>
onBlur={props.handleBlur} {props.errors.discountMinValueField}
variant="filled" </Typography>
onChange={(e) => { }
props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, '')) InputProps={{
}} style: {
value={props.values.discountMinValueField} backgroundColor: theme.palette.content.main,
error={props.touched.discountMinValueField && !!props.errors.discountMinValueField} color: theme.palette.secondary.main,
sx={{ },
marginTop: "15px", }}
}} InputLabelProps={{
helperText={ style: {
<Typography sx={{fontSize: "12px", width: "200px"}}> color: theme.palette.secondary.main,
{props.errors.discountMinValueField} },
</Typography> }}
} />
InputProps={{ </>
style: { )}
backgroundColor: theme.palette.content.main, <Box
color: theme.palette.secondary.main, sx={{
} width: "90%",
}} marginTop: "55px",
InputLabelProps={{ display: "flex",
style: { flexDirection: "column",
color: theme.palette.secondary.main justifyContent: "center",
} alignItems: "center",
}} }}
/> >
</> <Button
)} variant="contained"
<Box type="submit"
sx={{ sx={{
width: "90%", backgroundColor: theme.palette.menu.main,
marginTop: "55px", height: "52px",
display: "flex", fontWeight: "normal",
flexDirection: "column", fontSize: "17px",
justifyContent: "center", "&:hover": {
alignItems: "center", backgroundColor: theme.palette.grayMedium.main,
}} },
> }}
<Button >
variant="contained" Создать
type='submit' </Button>
sx={{ </Box>
backgroundColor: theme.palette.menu.main, </Box>
height: "52px", </Form>
fontWeight: "normal", )}
fontSize: "17px", </Formik>
"&:hover": { );
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Создать
</Button>
</Box>
</Box>
</Form>
)}
</Formik>
);
} }

@ -3,133 +3,133 @@ import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
import { useState } from "react"; import { useState } from "react";
export default function DatePickers() { export default function DatePickers() {
const theme = useTheme(); const theme = useTheme();
const [isInfinite, setIsInfinite] = useState<boolean>(false); const [isInfinite, setIsInfinite] = useState<boolean>(false);
const [startDate, setStartDate] = useState<Date>(new Date()); const [startDate, setStartDate] = useState<Date>(new Date());
const [endDate, setEndDate] = useState<Date>(new Date()); const [endDate, setEndDate] = useState<Date>(new Date());
return ( return (
<> <>
<Typography <Typography
variant="h4" variant="h4"
sx={{ sx={{
width: "90%", width: "90%",
height: "40px", height: "40px",
fontWeight: "normal", fontWeight: "normal",
color: theme.palette.grayDisabled.main, color: theme.palette.grayDisabled.main,
marginTop: "55px", marginTop: "55px",
}} }}
> >
Дата действия: Дата действия:
</Typography> </Typography>
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
}} }}
> >
<Typography <Typography
sx={{ sx={{
width: "35px", width: "35px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "left", alignItems: "left",
}} }}
> >
С С
</Typography> </Typography>
<DesktopDatePicker <DesktopDatePicker
inputFormat="DD/MM/YYYY" inputFormat="DD/MM/YYYY"
value={startDate} value={startDate}
onChange={(e) => { onChange={(e) => {
if (e) { if (e) {
setStartDate(e); setStartDate(e);
} }
}} }}
renderInput={(params) => <TextField {...params} />} renderInput={(params) => <TextField {...params} />}
InputProps={{ InputProps={{
sx: { sx: {
height: "40px", height: "40px",
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
border: "1px solid", border: "1px solid",
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
}, },
}} }}
/> />
<Typography <Typography
sx={{ sx={{
width: "65px", width: "65px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}} }}
> >
по по
</Typography> </Typography>
<DesktopDatePicker <DesktopDatePicker
inputFormat="DD/MM/YYYY" inputFormat="DD/MM/YYYY"
value={endDate} value={endDate}
onChange={(e) => { onChange={(e) => {
if (e) { if (e) {
setEndDate(e); setEndDate(e);
} }
}} }}
renderInput={(params) => <TextField {...params} />} renderInput={(params) => <TextField {...params} />}
InputProps={{ InputProps={{
sx: { sx: {
height: "40px", height: "40px",
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
border: "1px solid", border: "1px solid",
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
}, },
}} }}
/> />
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
width: "90%", width: "90%",
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
}} }}
> >
<Box <Box
sx={{ sx={{
width: "20px", width: "20px",
height: "42px", height: "42px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "left", justifyContent: "left",
alignItems: "left", alignItems: "left",
marginRight: theme.spacing(1), marginRight: theme.spacing(1),
}} }}
> >
<Checkbox <Checkbox
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
"&.Mui-checked": { "&.Mui-checked": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
}} }}
checked={isInfinite} checked={isInfinite}
onClick={() => setIsInfinite((p) => !p)} onClick={() => setIsInfinite((p) => !p)}
/> />
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}} }}
> >
Бессрочно Бессрочно
</Box> </Box>
</Box> </Box>
</> </>
); );
} }

@ -1,16 +1,11 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Box, IconButton, useTheme, Tooltip } from "@mui/material"; import { Box, IconButton, useTheme, Tooltip } from "@mui/material";
import { DataGrid, GridColDef, GridRowsProp, GridToolbar } from "@mui/x-data-grid";
import { import {
DataGrid, openEditDiscountDialog,
GridColDef, setSelectedDiscountIds,
GridRowsProp, updateDiscount,
GridToolbar, useDiscountStore,
} from "@mui/x-data-grid";
import {
openEditDiscountDialog,
setSelectedDiscountIds,
updateDiscount,
useDiscountStore,
} from "@root/stores/discounts"; } from "@root/stores/discounts";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
@ -22,180 +17,163 @@ import { formatDiscountFactor } from "@root/utils/formatDiscountFactor";
import { mutate } from "swr"; import { mutate } from "swr";
const columns: GridColDef[] = [ const columns: GridColDef[] = [
// { // {
// field: "id", // field: "id",
// headerName: "ID", // headerName: "ID",
// width: 70, // width: 70,
// sortable: false, // sortable: false,
// }, // },
{ {
field: "name", field: "name",
headerName: "Название скидки", headerName: "Название скидки",
width: 150, width: 150,
sortable: false, sortable: false,
renderCell: ({ row, value }) => ( renderCell: ({ row, value }) => <Box color={row.deleted && "#ff4545"}>{value}</Box>,
<Box color={row.deleted && "#ff4545"}>{value}</Box> },
), {
}, field: "description",
{ headerName: "Описание",
field: "description", width: 120,
headerName: "Описание", sortable: false,
width: 120, },
sortable: false, {
}, field: "conditionType",
{ headerName: "Тип условия",
field: "conditionType", width: 120,
headerName: "Тип условия", sortable: false,
width: 120, },
sortable: false, {
}, field: "factor",
{ headerName: "Процент скидки",
field: "factor", width: 120,
headerName: "Процент скидки", sortable: false,
width: 120, },
sortable: false, {
}, field: "value",
{ headerName: "Значение",
field: "value", width: 120,
headerName: "Значение", sortable: false,
width: 120, },
sortable: false, {
}, field: "active",
{ headerName: "Активна",
field: "active", width: 80,
headerName: "Активна", sortable: false,
width: 80, },
sortable: false, {
}, field: "edit",
{ headerName: "Изменить",
field: "edit", width: 80,
headerName: "Изменить", sortable: false,
width: 80, renderCell: ({ row }) => {
sortable: false, return (
renderCell: ({ row }) => { <IconButton
return ( onClick={() => {
<IconButton openEditDiscountDialog(row.id);
onClick={() => { }}
openEditDiscountDialog(row.id); >
}} <EditIcon />
> </IconButton>
<EditIcon /> );
</IconButton> },
); },
}, {
}, field: "delete",
{ headerName: "Удалить",
field: "delete", width: 80,
headerName: "Удалить", sortable: false,
width: 80, renderCell: ({ row }) => (
sortable: false, <IconButton
renderCell: ({ row }) => ( disabled={row.deleted}
<IconButton onClick={() => {
disabled={row.deleted} deleteDiscount(row.id).then(([discount]) => {
onClick={() => { mutate("discounts");
deleteDiscount(row.id).then(([discount]) => { if (discount) {
mutate("discounts"); updateDiscount(discount);
if (discount) { }
updateDiscount(discount); });
} }}
}); >
}} <DeleteIcon />
> </IconButton>
<DeleteIcon /> ),
</IconButton> },
),
},
]; ];
const layerTranslate = ["", "Товар", "Сервис", "корзина", "лояльность"]; const layerTranslate = ["", "Товар", "Сервис", "корзина", "лояльность"];
const layerValue = [ const layerValue = ["", "Term", "PriceFrom", "CartPurchasesAmount", "PurchasesAmount"];
"",
"Term",
"PriceFrom",
"CartPurchasesAmount",
"PurchasesAmount",
];
interface Props { interface Props {
selectedRowsHC: (array: GridSelectionModel) => void; selectedRowsHC: (array: GridSelectionModel) => void;
} }
export default function DiscountDataGrid({ selectedRowsHC }: Props) { export default function DiscountDataGrid({ selectedRowsHC }: Props) {
const theme = useTheme(); const theme = useTheme();
const selectedDiscountIds = useDiscountStore( const selectedDiscountIds = useDiscountStore((state) => state.selectedDiscountIds);
(state) => state.selectedDiscountIds const realDiscounts = useDiscountStore((state) => state.discounts);
);
const realDiscounts = useDiscountStore((state) => state.discounts);
useEffect(() => { useEffect(() => {
requestDiscounts(); requestDiscounts();
}, []); }, []);
const rowBackDicounts: GridRowsProp = realDiscounts const rowBackDicounts: GridRowsProp = realDiscounts
.filter(({ Layer }) => Layer > 0) .filter(({ Layer }) => Layer > 0)
.map((discount) => { .map((discount) => {
return { return {
id: discount.ID, id: discount.ID,
name: discount.Name, name: discount.Name,
description: discount.Description, description: discount.Description,
conditionType: layerTranslate[discount.Layer], conditionType: layerTranslate[discount.Layer],
factor: formatDiscountFactor(discount.Target.Factor), factor: formatDiscountFactor(discount.Target.Factor),
value: (discount.Layer === 1) ? value:
discount.Condition[ discount.Layer === 1
layerValue[discount.Layer] as keyof typeof discount.Condition ? discount.Condition[layerValue[discount.Layer] as keyof typeof discount.Condition]
]: Number(discount.Condition[ : Number(discount.Condition[layerValue[discount.Layer] as keyof typeof discount.Condition]) / 100,
layerValue[discount.Layer] as keyof typeof discount.Condition active: discount.Deprecated ? "🚫" : "✅",
])/100, deleted: discount.Audit.Deleted,
active: discount.Deprecated ? "🚫" : "✅", };
deleted: discount.Audit.Deleted, });
};
});
return ( return (
<Box <Box sx={{ width: "100%", marginTop: "55px", p: "16px", maxWidth: "1000px" }}>
sx={{ width: "100%", marginTop: "55px", p: "16px", maxWidth: "1000px" }} <Tooltip title="обновить список привилегий">
> <IconButton onClick={requestDiscounts} style={{ display: "block", margin: "0 auto" }}>
<Tooltip title="обновить список привилегий"> <AutorenewIcon sx={{ color: "white" }} />
<IconButton </IconButton>
onClick={requestDiscounts} </Tooltip>
style={{ display: "block", margin: "0 auto" }} <Box sx={{ height: 600 }}>
> <DataGrid
<AutorenewIcon sx={{ color: "white" }} /> checkboxSelection={true}
</IconButton> rows={rowBackDicounts}
</Tooltip> columns={columns}
<Box sx={{ height: 600 }}> selectionModel={selectedDiscountIds}
<DataGrid onSelectionModelChange={(array: GridSelectionModel) => {
checkboxSelection={true} selectedRowsHC(array);
rows={rowBackDicounts} setSelectedDiscountIds(array);
columns={columns} }}
selectionModel={selectedDiscountIds} disableSelectionOnClick
onSelectionModelChange={(array: GridSelectionModel) => { sx={{
selectedRowsHC(array); color: theme.palette.secondary.main,
setSelectedDiscountIds(array); "& .MuiDataGrid-iconSeparator": {
}} display: "none",
disableSelectionOnClick },
sx={{ "& .css-levciy-MuiTablePagination-displayedRows": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
"& .MuiDataGrid-iconSeparator": { },
display: "none", "& .MuiSvgIcon-root": {
}, color: theme.palette.secondary.main,
"& .css-levciy-MuiTablePagination-displayedRows": { },
color: theme.palette.secondary.main, "& .MuiTablePagination-selectLabel": {
}, color: theme.palette.secondary.main,
"& .MuiSvgIcon-root": { },
color: theme.palette.secondary.main, "& .MuiInputBase-root": {
}, color: theme.palette.secondary.main,
"& .MuiTablePagination-selectLabel": { },
color: theme.palette.secondary.main, "& .MuiButton-text": {
}, color: theme.palette.secondary.main,
"& .MuiInputBase-root": { },
color: theme.palette.secondary.main, }}
}, components={{ Toolbar: GridToolbar }}
"& .MuiButton-text": { />
color: theme.palette.secondary.main, </Box>
}, </Box>
}} );
components={{ Toolbar: GridToolbar }}
/>
</Box>
</Box>
);
} }

@ -8,38 +8,35 @@ import ControlPanel from "./ControlPanel";
import { useState } from "react"; import { useState } from "react";
import { GridSelectionModel } from "@mui/x-data-grid"; import { GridSelectionModel } from "@mui/x-data-grid";
const DiscountManagement: React.FC = () => { const DiscountManagement: React.FC = () => {
const theme = useTheme(); const theme = useTheme();
const [selectedRows, setSelectedRows] = useState<GridSelectionModel>([]) const [selectedRows, setSelectedRows] = useState<GridSelectionModel>([]);
const selectedRowsHC = (array:GridSelectionModel) => { const selectedRowsHC = (array: GridSelectionModel) => {
setSelectedRows(array) setSelectedRows(array);
} };
return ( return (
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
width: "90%", width: "90%",
height: "60px", height: "60px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}}> }}
СКИДКИ >
</Typography> СКИДКИ
<CreateDiscount /> </Typography>
<DiscountDataGrid <CreateDiscount />
selectedRowsHC={selectedRowsHC} <DiscountDataGrid selectedRowsHC={selectedRowsHC} />
/> <EditDiscountDialog />
<EditDiscountDialog /> <ControlPanel selectedRows={selectedRows} />
<ControlPanel selectedRows={selectedRows}/> </LocalizationProvider>
</LocalizationProvider> );
);
}; };
export default DiscountManagement; export default DiscountManagement;

@ -1,32 +1,25 @@
import { import {
Box, Box,
Button, Button,
Dialog, Dialog,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
FormLabel, FormLabel,
InputLabel, InputLabel,
MenuItem, MenuItem,
Radio, Radio,
RadioGroup, RadioGroup,
Select, Select,
SelectChangeEvent, SelectChangeEvent,
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { patchDiscount } from "@root/api/discounts"; import { patchDiscount } from "@root/api/discounts";
import { CustomTextField } from "@root/kitUI/CustomTextField"; import { CustomTextField } from "@root/kitUI/CustomTextField";
import { DiscountType, discountTypes } from "@root/model/discount"; import { DiscountType, discountTypes } from "@root/model/discount";
import { ServiceType, SERVICE_LIST } from "@root/model/tariff"; import { ServiceType, SERVICE_LIST } from "@root/model/tariff";
import { import { closeEditDiscountDialog, updateDiscount, useDiscountStore } from "@root/stores/discounts";
closeEditDiscountDialog, import { resetPrivilegeArray, usePrivilegeStore } from "@root/stores/privilegesStore";
updateDiscount,
useDiscountStore,
} from "@root/stores/discounts";
import {
resetPrivilegeArray,
usePrivilegeStore,
} from "@root/stores/privilegesStore";
import { getDiscountTypeFromLayer } from "@root/utils/discount"; import { getDiscountTypeFromLayer } from "@root/utils/discount";
import usePrivileges from "@root/utils/hooks/usePrivileges"; import usePrivileges from "@root/utils/hooks/usePrivileges";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -34,359 +27,336 @@ import { useEffect, useState } from "react";
import { mutate } from "swr"; import { mutate } from "swr";
export default function EditDiscountDialog() { export default function EditDiscountDialog() {
const theme = useTheme(); const theme = useTheme();
const editDiscountId = useDiscountStore((state) => state.editDiscountId); const editDiscountId = useDiscountStore((state) => state.editDiscountId);
const discounts = useDiscountStore((state) => state.discounts); const discounts = useDiscountStore((state) => state.discounts);
const privileges = usePrivilegeStore((state) => state.privileges); const privileges = usePrivilegeStore((state) => state.privileges);
const [serviceType, setServiceType] = useState<string>("templategen"); const [serviceType, setServiceType] = useState<string>("templategen");
const [discountType, setDiscountType] = const [discountType, setDiscountType] = useState<DiscountType>("purchasesAmount");
useState<DiscountType>("purchasesAmount"); const [discountNameField, setDiscountNameField] = useState<string>("");
const [discountNameField, setDiscountNameField] = useState<string>(""); const [discountDescriptionField, setDiscountDescriptionField] = useState<string>("");
const [discountDescriptionField, setDiscountDescriptionField] = const [privilegeIdField, setPrivilegeIdField] = useState<string | "">("");
useState<string>(""); const [discountFactorField, setDiscountFactorField] = useState<string>("0");
const [privilegeIdField, setPrivilegeIdField] = useState<string | "">(""); const [purchasesAmountField, setPurchasesAmountField] = useState<string>("0");
const [discountFactorField, setDiscountFactorField] = useState<string>("0"); const [cartPurchasesAmountField, setCartPurchasesAmountField] = useState<string>("0");
const [purchasesAmountField, setPurchasesAmountField] = useState<string>("0"); const [discountMinValueField, setDiscountMinValueField] = useState<string>("0");
const [cartPurchasesAmountField, setCartPurchasesAmountField] =
useState<string>("0");
const [discountMinValueField, setDiscountMinValueField] =
useState<string>("0");
const discount = discounts.find((discount) => discount.ID === editDiscountId); const discount = discounts.find((discount) => discount.ID === editDiscountId);
usePrivileges({ onNewPrivileges: resetPrivilegeArray }); usePrivileges({ onNewPrivileges: resetPrivilegeArray });
useEffect( useEffect(
function setDiscountFields() { function setDiscountFields() {
if (!discount) return; if (!discount) return;
setServiceType(discount.Condition.Group ?? ""); setServiceType(discount.Condition.Group ?? "");
setDiscountType(getDiscountTypeFromLayer(discount.Layer)); setDiscountType(getDiscountTypeFromLayer(discount.Layer));
setDiscountNameField(discount.Name); setDiscountNameField(discount.Name);
setDiscountDescriptionField(discount.Description); setDiscountDescriptionField(discount.Description);
setPrivilegeIdField(discount.Condition.Product ?? ""); setPrivilegeIdField(discount.Condition.Product ?? "");
setDiscountFactorField(((1 - discount.Target.Factor) * 100).toFixed(2)); setDiscountFactorField(((1 - discount.Target.Factor) * 100).toFixed(2));
setPurchasesAmountField(discount.Condition.PurchasesAmount ?? ""); setPurchasesAmountField(discount.Condition.PurchasesAmount ?? "");
setCartPurchasesAmountField( setCartPurchasesAmountField(discount.Condition.CartPurchasesAmount ?? "");
discount.Condition.CartPurchasesAmount ?? "" setDiscountMinValueField(discount.Condition.PriceFrom ?? "");
); },
setDiscountMinValueField(discount.Condition.PriceFrom ?? ""); [discount]
}, );
[discount]
);
const handleDiscountTypeChange = ( const handleDiscountTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event: React.ChangeEvent<HTMLInputElement> setDiscountType(event.target.value as DiscountType);
) => { };
setDiscountType(event.target.value as DiscountType);
};
const handleServiceTypeChange = (event: SelectChangeEvent) => { const handleServiceTypeChange = (event: SelectChangeEvent) => {
setServiceType(event.target.value as ServiceType); setServiceType(event.target.value as ServiceType);
}; };
async function handleSaveDiscount() { async function handleSaveDiscount() {
if (!discount) return; if (!discount) return;
const purchasesAmount = parseFloat(purchasesAmountField.replace(",", ".")); const purchasesAmount = parseFloat(purchasesAmountField.replace(",", "."));
const discountFactor = const discountFactor = (100 - parseFloat(discountFactorField.replace(",", "."))) / 100;
(100 - parseFloat(discountFactorField.replace(",", "."))) / 100; const cartPurchasesAmount = parseFloat(cartPurchasesAmountField.replace(",", "."));
const cartPurchasesAmount = parseFloat( const discountMinValue = parseFloat(discountMinValueField.replace(",", "."));
cartPurchasesAmountField.replace(",", ".")
);
const discountMinValue = parseFloat(
discountMinValueField.replace(",", ".")
);
if (!isFinite(purchasesAmount)) if (!isFinite(purchasesAmount)) return enqueueSnackbar("Поле purchasesAmount не число");
return enqueueSnackbar("Поле purchasesAmount не число"); if (!isFinite(discountFactor)) return enqueueSnackbar("Поле discountFactor не число");
if (!isFinite(discountFactor)) if (!isFinite(cartPurchasesAmount)) return enqueueSnackbar("Поле cartPurchasesAmount не число");
return enqueueSnackbar("Поле discountFactor не число"); if (!isFinite(discountMinValue)) return enqueueSnackbar("Поле discountMinValue не число");
if (!isFinite(cartPurchasesAmount)) if (discountType === "privilege" && !privilegeIdField) return enqueueSnackbar("Привилегия не выбрана");
return enqueueSnackbar("Поле cartPurchasesAmount не число"); if (!discountNameField) return enqueueSnackbar('Поле "Имя" пустое');
if (!isFinite(discountMinValue)) if (!discountDescriptionField) return enqueueSnackbar('Поле "Описание" пустое');
return enqueueSnackbar("Поле discountMinValue не число"); if (discountFactor < 0) return enqueueSnackbar("Процент скидки не может быть больше 100");
if (discountType === "privilege" && !privilegeIdField)
return enqueueSnackbar("Привилегия не выбрана");
if (!discountNameField) return enqueueSnackbar('Поле "Имя" пустое');
if (!discountDescriptionField)
return enqueueSnackbar('Поле "Описание" пустое');
if (discountFactor < 0)
return enqueueSnackbar("Процент скидки не может быть больше 100");
const [patchedDiscountResponse, patchedDiscountError] = await patchDiscount( const [patchedDiscountResponse, patchedDiscountError] = await patchDiscount(discount.ID, {
discount.ID, cartPurchasesAmount,
{ discountFactor,
cartPurchasesAmount, discountMinValue,
discountFactor, purchasesAmount,
discountMinValue, discountDescription: discountDescriptionField,
purchasesAmount, discountName: discountNameField,
discountDescription: discountDescriptionField, startDate: new Date().toISOString(),
discountName: discountNameField, endDate: new Date(Date.now() + 1000 * 3600 * 24 * 30).toISOString(),
startDate: new Date().toISOString(), serviceType,
endDate: new Date(Date.now() + 1000 * 3600 * 24 * 30).toISOString(), discountType,
serviceType, privilegeId: privilegeIdField,
discountType, });
privilegeId: privilegeIdField,
}
);
if (patchedDiscountError) { if (patchedDiscountError) {
console.error("Error patching discount", patchedDiscountError); console.error("Error patching discount", patchedDiscountError);
return enqueueSnackbar(patchedDiscountError); return enqueueSnackbar(patchedDiscountError);
} }
if (patchedDiscountResponse) { if (patchedDiscountResponse) {
mutate("discounts"); mutate("discounts");
updateDiscount(patchedDiscountResponse); updateDiscount(patchedDiscountResponse);
closeEditDiscountDialog(); closeEditDiscountDialog();
} }
} }
return ( return (
<Dialog <Dialog
open={editDiscountId !== null} open={editDiscountId !== null}
onClose={closeEditDiscountDialog} onClose={closeEditDiscountDialog}
PaperProps={{ PaperProps={{
sx: { sx: {
width: "600px", width: "600px",
maxWidth: "600px", maxWidth: "600px",
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
position: "relative", position: "relative",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
p: "20px", p: "20px",
gap: "20px", gap: "20px",
borderRadius: "12px", borderRadius: "12px",
boxShadow: "none", boxShadow: "none",
}, },
}} }}
slotProps={{ slotProps={{
backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } }, backdrop: { style: { backgroundColor: "rgb(0 0 0 / 0.7)" } },
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "left", justifyContent: "left",
alignItems: "left", alignItems: "left",
marginTop: "15px", marginTop: "15px",
width: "100%", width: "100%",
padding: "16px", padding: "16px",
maxWidth: "600px", maxWidth: "600px",
gap: "1em", gap: "1em",
}} }}
> >
<CustomTextField <CustomTextField
id="discount-name" id="discount-name"
label="Название" label="Название"
value={discountNameField} value={discountNameField}
onChange={(e) => setDiscountNameField(e.target.value)} onChange={(e) => setDiscountNameField(e.target.value)}
/> />
<CustomTextField <CustomTextField
id="discount-desc" id="discount-desc"
label="Описание" label="Описание"
value={discountDescriptionField} value={discountDescriptionField}
onChange={(e) => setDiscountDescriptionField(e.target.value)} onChange={(e) => setDiscountDescriptionField(e.target.value)}
/> />
<Typography <Typography
variant="h4" variant="h4"
sx={{ sx={{
width: "90%", width: "90%",
fontWeight: "normal", fontWeight: "normal",
color: theme.palette.grayDisabled.main, color: theme.palette.grayDisabled.main,
paddingLeft: "10px", paddingLeft: "10px",
}} }}
> >
Условия: Условия:
</Typography> </Typography>
<CustomTextField <CustomTextField
id="discount-factor" id="discount-factor"
label="Процент скидки" label="Процент скидки"
value={discountFactorField} value={discountFactorField}
type="number" type="number"
onChange={(e) => setDiscountFactorField(e.target.value)} onChange={(e) => setDiscountFactorField(e.target.value)}
/> />
<FormControl> <FormControl>
<FormLabel <FormLabel
id="discount-type" id="discount-type"
sx={{ sx={{
color: "white", color: "white",
"&.Mui-focused": { "&.Mui-focused": {
color: "white", color: "white",
}, },
}} }}
> >
Тип скидки Тип скидки
</FormLabel> </FormLabel>
<RadioGroup <RadioGroup
row row
aria-labelledby="discount-type" aria-labelledby="discount-type"
name="discount-type" name="discount-type"
value={discountType} value={discountType}
onChange={handleDiscountTypeChange} onChange={handleDiscountTypeChange}
> >
{Object.keys(discountTypes).map((type) => ( {Object.keys(discountTypes).map((type) => (
<FormControlLabel <FormControlLabel
key={type} key={type}
value={type} value={type}
control={<Radio color="secondary" />} control={<Radio color="secondary" />}
label={discountTypes[type as DiscountType]} label={discountTypes[type as DiscountType]}
sx={{ color: "white" }} sx={{ color: "white" }}
/> />
))} ))}
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
{discountType === "purchasesAmount" && ( {discountType === "purchasesAmount" && (
<CustomTextField <CustomTextField
id="discount-purchases" id="discount-purchases"
label="Внесено больше" label="Внесено больше"
type="number" type="number"
sx={{ sx={{
marginTop: "15px", marginTop: "15px",
}} }}
value={purchasesAmountField} value={purchasesAmountField}
onChange={(e) => setPurchasesAmountField(e.target.value)} onChange={(e) => setPurchasesAmountField(e.target.value)}
/> />
)} )}
{discountType === "cartPurchasesAmount" && ( {discountType === "cartPurchasesAmount" && (
<CustomTextField <CustomTextField
id="discount-cart-purchases" id="discount-cart-purchases"
label="Объем в корзине" label="Объем в корзине"
type="number" type="number"
sx={{ sx={{
marginTop: "15px", marginTop: "15px",
}} }}
value={cartPurchasesAmountField} value={cartPurchasesAmountField}
onChange={(e) => setCartPurchasesAmountField(e.target.value)} onChange={(e) => setCartPurchasesAmountField(e.target.value)}
/> />
)} )}
{discountType === "service" && ( {discountType === "service" && (
<> <>
<Select <Select
labelId="discount-service-label" labelId="discount-service-label"
id="discount-service" id="discount-service"
value={serviceType} value={serviceType}
onChange={handleServiceTypeChange} onChange={handleServiceTypeChange}
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
}, },
".MuiSvgIcon-root ": { ".MuiSvgIcon-root ": {
fill: theme.palette.secondary.main, fill: theme.palette.secondary.main,
}, },
}} }}
> >
{SERVICE_LIST.map((service) => ( {SERVICE_LIST.map((service) => (
<MenuItem key={service.serviceKey} value={service.serviceKey}> <MenuItem key={service.serviceKey} value={service.serviceKey}>
{service.displayName} {service.displayName}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
<CustomTextField <CustomTextField
id="discount-min-value" id="discount-min-value"
label="Минимальное значение" label="Минимальное значение"
type="number" type="number"
sx={{ sx={{
marginTop: "15px", marginTop: "15px",
}} }}
value={discountMinValueField} value={discountMinValueField}
onChange={(e) => setDiscountMinValueField(e.target.value)} onChange={(e) => setDiscountMinValueField(e.target.value)}
/> />
</> </>
)} )}
{discountType === "privilege" && ( {discountType === "privilege" && (
<> <>
<FormControl <FormControl
fullWidth fullWidth
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
"& .MuiInputLabel-outlined": { "& .MuiInputLabel-outlined": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
"& .MuiInputLabel-outlined.MuiInputLabel-shrink": { "& .MuiInputLabel-outlined.MuiInputLabel-shrink": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
}} }}
> >
<InputLabel <InputLabel
id="privilege-select-label" id="privilege-select-label"
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
fontSize: "16px", fontSize: "16px",
lineHeight: "19px", lineHeight: "19px",
}} }}
> >
Привилегия Привилегия
</InputLabel> </InputLabel>
<Select <Select
labelId="privilege-select-label" labelId="privilege-select-label"
id="privilege-select" id="privilege-select"
value={privilegeIdField} value={privilegeIdField}
label="Привилегия" label="Привилегия"
onChange={(e) => setPrivilegeIdField(e.target.value)} onChange={(e) => setPrivilegeIdField(e.target.value)}
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
}, },
".MuiSvgIcon-root ": { ".MuiSvgIcon-root ": {
fill: theme.palette.secondary.main, fill: theme.palette.secondary.main,
}, },
}} }}
inputProps={{ sx: { pt: "12px" } }} inputProps={{ sx: { pt: "12px" } }}
> >
{privileges.map((privilege, index) => ( {privileges.map((privilege, index) => (
<MenuItem key={index} value={privilege.privilegeId}> <MenuItem key={index} value={privilege.privilegeId}>
{privilege.description} {privilege.description}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<CustomTextField <CustomTextField
id="discount-min-value" id="discount-min-value"
label="Минимальное значение" label="Минимальное значение"
type="number" type="number"
sx={{ sx={{
marginTop: "15px", marginTop: "15px",
}} }}
value={discountMinValueField} value={discountMinValueField}
onChange={(e) => setDiscountMinValueField(e.target.value)} onChange={(e) => setDiscountMinValueField(e.target.value)}
/> />
</> </>
)} )}
<Box <Box
sx={{ sx={{
width: "90%", width: "90%",
marginTop: "55px", marginTop: "55px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}} }}
> >
<Button <Button
variant="contained" variant="contained"
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
height: "52px", height: "52px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
}, },
}} }}
onClick={handleSaveDiscount} onClick={handleSaveDiscount}
> >
Сохранить Сохранить
</Button> </Button>
</Box> </Box>
</Box> </Box>
</Dialog> </Dialog>
); );
} }

@ -1,142 +1,169 @@
import * as React from "react"; import * as React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import Table from '@mui/material/Table'; import Table from "@mui/material/Table";
import TableHead from '@mui/material/TableHead'; import TableHead from "@mui/material/TableHead";
import TableBody from '@mui/material/TableBody'; import TableBody from "@mui/material/TableBody";
import TableCell from '@mui/material/TableCell'; import TableCell from "@mui/material/TableCell";
import TableRow from '@mui/material/TableRow'; import TableRow from "@mui/material/TableRow";
import theme from "../../../theme"; import theme from "../../../theme";
const Users: React.FC = () => {
const [selectedValue, setSelectedValue] = React.useState("a");
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedValue(event.target.value);
};
const Users: React.FC = () => { const navigate = useNavigate();
const [selectedValue, setSelectedValue] = React.useState('a');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedValue(event.target.value);
};
const navigate = useNavigate(); return (
<React.Fragment>
<Typography
variant="subtitle1"
sx={{
width: "90%",
height: "60px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
color: theme.palette.secondary.main,
}}
>
Юридические лица
</Typography>
return ( <Table
<React.Fragment> sx={{
<Typography width: "90%",
variant="subtitle1" border: "2px solid",
sx={{ borderColor: theme.palette.grayLight.main,
width: "90%", marginTop: "35px",
height: "60px", }}
display: "flex", aria-label="simple table"
flexDirection: "column", >
justifyContent: "center", <TableHead>
alignItems: "center", <TableRow
color: theme.palette.secondary.main sx={{
}}> borderBottom: "2px solid",
Юридические лица borderColor: theme.palette.grayLight.main,
</Typography> height: "100px",
}}
>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
ID
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography
variant="h4"
sx={{
color: theme.palette.secondary.main,
}}
>
Дата / время регистрации
</Typography>
</TableCell>
</TableRow>
</TableHead>
<Table sx={{ <TableBody>
width: "90%", <TableRow
border: "2px solid", sx={{
borderColor: theme.palette.grayLight.main, borderBottom: "2px solid",
marginTop: "35px", borderColor: theme.palette.grayLight.main,
}} aria-label="simple table"> height: "100px",
<TableHead> cursor: "pointer",
<TableRow sx={{ }}
borderBottom: "2px solid", onClick={() => navigate("/modalEntities")}
borderColor: theme.palette.grayLight.main, >
height: "100px" <TableCell sx={{ textAlign: "center" }}>
}}> <Typography
<TableCell sx={{ textAlign: "center" }}> sx={{
<Typography color: theme.palette.secondary.main,
variant="h4" }}
sx={{ >
color: theme.palette.secondary.main, 1
}}> </Typography>
ID </TableCell>
</Typography> <TableCell sx={{ textAlign: "center" }}>
</TableCell> <Typography
<TableCell sx={{ textAlign: "center" }}> sx={{
<Typography color: theme.palette.secondary.main,
variant="h4" }}
sx={{ >
color: theme.palette.secondary.main, 2022
}}> </Typography>
Дата / время регистрации </TableCell>
</Typography> </TableRow>
</TableCell>
</TableRow>
</TableHead>
<TableBody> <TableRow
<TableRow sx={{ sx={{
borderBottom: "2px solid", borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: "100px", height: "100px",
cursor: "pointer" cursor: "pointer",
}} onClick={ () => navigate("/modalEntities") }> }}
<TableCell sx={{ textAlign: "center" }}> onClick={() => navigate("/modalEntities")}
<Typography sx={{ >
color: theme.palette.secondary.main <TableCell sx={{ textAlign: "center" }}>
}}> <Typography
1 sx={{
</Typography> color: theme.palette.secondary.main,
</TableCell> }}
<TableCell sx={{ textAlign: "center" }}> >
<Typography sx={{ 2
color: theme.palette.secondary.main </Typography>
}}> </TableCell>
2022 <TableCell sx={{ textAlign: "center" }}>
</Typography> <Typography
</TableCell> sx={{
</TableRow> color: theme.palette.secondary.main,
}}
>
2021
</Typography>
</TableCell>
</TableRow>
<TableRow sx={{ <TableRow
borderBottom: "2px solid", sx={{
borderColor: theme.palette.grayLight.main, borderBottom: "1px solid",
height: "100px", border: theme.palette.secondary.main,
cursor: "pointer" height: "100px",
}} onClick={ () => navigate("/modalEntities") }> cursor: "pointer",
<TableCell sx={{ textAlign: "center" }}> }}
<Typography sx={{ onClick={() => navigate("/modalEntities")}
color: theme.palette.secondary.main >
}}> <TableCell sx={{ textAlign: "center" }}>
2 <Typography
</Typography> sx={{
</TableCell> color: theme.palette.secondary.main,
<TableCell sx={{ textAlign: "center" }}> }}
<Typography sx={{ >
color: theme.palette.secondary.main 3
}}> </Typography>
2021 </TableCell>
</Typography> <TableCell sx={{ textAlign: "center" }}>
</TableCell> <Typography
</TableRow> sx={{
color: theme.palette.secondary.main,
}}
>
2020
</Typography>
</TableCell>
</TableRow>
</TableBody>
</Table>
</React.Fragment>
);
};
<TableRow sx={{ export default Users;
borderBottom: "1px solid",
border: theme.palette.secondary.main,
height: "100px",
cursor: "pointer"
}} onClick={ () => navigate("/modalEntities") }>
<TableCell sx={{ textAlign: "center" }}>
<Typography sx={{
color: theme.palette.secondary.main
}}>
3
</Typography>
</TableCell>
<TableCell sx={{ textAlign: "center" }}>
<Typography sx={{
color: theme.palette.secondary.main
}}>
2020
</Typography>
</TableCell>
</TableRow>
</TableBody>
</Table>
</React.Fragment>
);
}
export default Users;

@ -1,13 +1,4 @@
import { import { Button, FormControlLabel, MenuItem, Radio, RadioGroup, Select, TextField, Typography } from "@mui/material";
Button,
FormControlLabel,
MenuItem,
Radio,
RadioGroup,
Select,
TextField,
Typography,
} from "@mui/material";
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker"; import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik } from "formik";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -26,406 +17,347 @@ import { enqueueSnackbar } from "notistack";
type BonusType = "discount" | "privilege"; type BonusType = "discount" | "privilege";
type FormValues = { type FormValues = {
codeword: string; codeword: string;
description: string; description: string;
greetings: string; greetings: string;
dueTo: number; dueTo: number;
activationCount: number; activationCount: number;
privilegeId: string; privilegeId: string;
amount: number; amount: number;
layer: 1 | 2; layer: 1 | 2;
factor: number; factor: number;
target: string; target: string;
threshold: number; threshold: number;
serviceKey: string; serviceKey: string;
}; };
type SelectChangeProps = { type SelectChangeProps = {
target: { target: {
name: string; name: string;
value: string; value: string;
}; };
}; };
const initialValues: FormValues = { const initialValues: FormValues = {
codeword: "", codeword: "",
description: "", description: "",
greetings: "", greetings: "",
dueTo: 0, dueTo: 0,
activationCount: 0, activationCount: 0,
privilegeId: "", privilegeId: "",
amount: 0, amount: 0,
layer: 1, layer: 1,
factor: 0, factor: 0,
target: "", target: "",
threshold: 0, threshold: 0,
serviceKey: "", serviceKey: "",
}; };
type Props = { type Props = {
createPromocode: (body: CreatePromocodeBody) => Promise<void>; createPromocode: (body: CreatePromocodeBody) => Promise<void>;
}; };
export const CreatePromocodeForm = ({ createPromocode }: Props) => { export const CreatePromocodeForm = ({ createPromocode }: Props) => {
const [bonusType, setBonusType] = useState<BonusType>("discount"); const [bonusType, setBonusType] = useState<BonusType>("discount");
const { privileges } = usePrivilegeStore(); const { privileges } = usePrivilegeStore();
useEffect(() => { useEffect(() => {
requestPrivileges(); requestPrivileges();
}, []); }, []);
const submitForm = (values: FormValues) => { const submitForm = (values: FormValues) => {
const currentPrivilege = privileges.find( const currentPrivilege = privileges.find((item) => item.privilegeId === values.privilegeId);
(item) => item.privilegeId === values.privilegeId
);
const body = { ...values }; const body = { ...values };
if ( if ((body.layer === 1 && bonusType === "discount") || bonusType === "privilege") {
(body.layer === 1 && bonusType === "discount") || if (currentPrivilege === undefined) {
bonusType === "privilege" enqueueSnackbar("Привилегия не выбрана");
) {
if (currentPrivilege === undefined) {
enqueueSnackbar("Привилегия не выбрана");
return; return;
} }
body.serviceKey = currentPrivilege?.serviceKey; body.serviceKey = currentPrivilege?.serviceKey;
body.target = body.privilegeId; body.target = body.privilegeId;
} }
if (body.layer === 2 && bonusType === "discount") { if (body.layer === 2 && bonusType === "discount") {
if (!body.serviceKey) { if (!body.serviceKey) {
enqueueSnackbar("Сервис не выбран"); enqueueSnackbar("Сервис не выбран");
return; return;
} }
body.target = body.serviceKey; body.target = body.serviceKey;
} }
const factorFromDiscountValue = 1 - body.factor / 100; const factorFromDiscountValue = 1 - body.factor / 100;
return createPromocode({ return createPromocode({
codeword: body.codeword, codeword: body.codeword,
description: body.description, description: body.description,
greetings: body.greetings, greetings: body.greetings,
dueTo: body.dueTo, dueTo: body.dueTo,
activationCount: body.activationCount, activationCount: body.activationCount,
bonus: { bonus: {
privilege: { privilege: {
privilegeID: body.privilegeId, privilegeID: body.privilegeId,
amount: body.amount, amount: body.amount,
serviceKey: body.serviceKey, serviceKey: body.serviceKey,
}, },
discount: { discount: {
layer: body.layer, layer: body.layer,
factor: factorFromDiscountValue, factor: factorFromDiscountValue,
target: body.target, target: body.target,
threshold: body.threshold, threshold: body.threshold,
}, },
}, },
}); });
}; };
return ( return (
<Formik initialValues={initialValues} onSubmit={submitForm}> <Formik initialValues={initialValues} onSubmit={submitForm}>
{({ values, handleChange, handleBlur, setFieldValue }) => ( {({ values, handleChange, handleBlur, setFieldValue }) => (
<Form <Form
style={{ style={{
width: "100%", width: "100%",
maxWidth: "600px", maxWidth: "600px",
padding: "0 10px", padding: "0 10px",
}} }}
> >
<CustomTextField <CustomTextField name="codeword" label="Кодовое слово" required onChange={handleChange} />
name="codeword" <CustomTextField name="description" label="Описание" required onChange={handleChange} />
label="Кодовое слово" <CustomTextField name="greetings" label="Приветственное сообщение" required onChange={handleChange} />
required <Typography
onChange={handleChange} variant="h4"
/> sx={{
<CustomTextField height: "40px",
name="description" fontWeight: "normal",
label="Описание" marginTop: "15px",
required color: theme.palette.secondary.main,
onChange={handleChange} }}
/> >
<CustomTextField Время существования промокода
name="greetings" </Typography>
label="Приветственное сообщение" <Field
required name="dueTo"
onChange={handleChange} as={DesktopDatePicker}
/> inputFormat="DD/MM/YYYY"
<Typography value={values.dueTo ? new Date(Number(values.dueTo) * 1000) : null}
variant="h4" onChange={(event: any) => {
sx={{ setFieldValue("dueTo", event.$d.getTime() / 1000 || null);
height: "40px", }}
fontWeight: "normal", renderInput={(params: TextFieldProps) => <TextField {...params} />}
marginTop: "15px", InputProps={{
color: theme.palette.secondary.main, sx: {
}} height: "40px",
> color: theme.palette.secondary.main,
Время существования промокода border: "1px solid",
</Typography> borderColor: theme.palette.secondary.main,
<Field "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
name="dueTo" },
as={DesktopDatePicker} }}
inputFormat="DD/MM/YYYY" />
value={values.dueTo ? new Date(Number(values.dueTo) * 1000) : null} <CustomTextField
onChange={(event: any) => { name="activationCount"
setFieldValue("dueTo", event.$d.getTime() / 1000 || null); label="Количество активаций промокода"
}} required
renderInput={(params: TextFieldProps) => <TextField {...params} />} onChange={({ target }) => setFieldValue("activationCount", Number(target.value.replace(/\D/g, "")))}
InputProps={{ />
sx: { <RadioGroup
height: "40px", row
color: theme.palette.secondary.main, name="bonusType"
border: "1px solid", value={bonusType}
borderColor: theme.palette.secondary.main, sx={{ marginTop: "15px" }}
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
}, setBonusType(target.value as BonusType);
}} }}
/> onBlur={handleBlur}
<CustomTextField >
name="activationCount" <FormControlLabel value="discount" control={<Radio color="secondary" />} label="Скидка" />
label="Количество активаций промокода" <FormControlLabel value="privilege" control={<Radio color="secondary" />} label="Привилегия" />
required </RadioGroup>
onChange={({ target }) => {bonusType === "discount" && (
setFieldValue( <>
"activationCount", <RadioGroup
Number(target.value.replace(/\D/g, "")) row
) name="layer"
} value={values.layer}
/> sx={{ marginTop: "15px" }}
<RadioGroup onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
row setFieldValue("target", "");
name="bonusType" setFieldValue("layer", Number(target.value));
value={bonusType} }}
sx={{ marginTop: "15px" }} onBlur={handleBlur}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => { >
setBonusType(target.value as BonusType); <FormControlLabel value="1" control={<Radio color="secondary" />} label="Привилегия" />
}} <FormControlLabel value="2" control={<Radio color="secondary" />} label="Сервис" />
onBlur={handleBlur} </RadioGroup>
> <CustomTextField
<FormControlLabel name="factor"
value="discount" label="Процент скидки"
control={<Radio color="secondary" />} required
label="Скидка" onChange={({ target }) => {
/> setFieldValue("factor", Number(target.value.replace(/\D/g, "")));
<FormControlLabel }}
value="privilege" />
control={<Radio color="secondary" />} <Typography
label="Привилегия" variant="h4"
/> sx={{
</RadioGroup> height: "40px",
{bonusType === "discount" && ( fontWeight: "normal",
<> marginTop: "15px",
<RadioGroup padding: "0 12px",
row color: theme.palette.secondary.main,
name="layer" }}
value={values.layer} >
sx={{ marginTop: "15px" }} {values.layer === 1 ? "Выбор привилегии" : "Выбор сервиса"}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => { </Typography>
setFieldValue("target", ""); {values.layer === 1 ? (
setFieldValue("layer", Number(target.value)); <Field
}} name="privilegeId"
onBlur={handleBlur} as={Select}
> label={"Привилегия"}
<FormControlLabel sx={{
value="1" width: "100%",
control={<Radio color="secondary" />} border: "2px solid",
label="Привилегия" color: theme.palette.secondary.main,
/> borderColor: theme.palette.secondary.main,
<FormControlLabel "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
value="2" border: "none",
control={<Radio color="secondary" />} },
label="Сервис" ".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
/> }}
</RadioGroup> onChange={({ target }: SelectChangeProps) => {
<CustomTextField setFieldValue("target", target.value);
name="factor" setFieldValue("privilegeId", target.value);
label="Процент скидки" }}
required children={privileges.map(({ name, privilegeId }) => (
onChange={({ target }) => { <MenuItem key={privilegeId} value={privilegeId}>
setFieldValue( {name}
"factor", </MenuItem>
Number(target.value.replace(/\D/g, "")) ))}
); />
}} ) : (
/> <Field
<Typography name="serviceKey"
variant="h4" as={Select}
sx={{ label={"Сервис"}
height: "40px", sx={{
fontWeight: "normal", width: "100%",
marginTop: "15px", border: "2px solid",
padding: "0 12px", color: theme.palette.secondary.main,
color: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
}} "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
> border: "none",
{values.layer === 1 ? "Выбор привилегии" : "Выбор сервиса"} },
</Typography> ".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
{values.layer === 1 ? ( }}
<Field onChange={({ target }: SelectChangeProps) => {
name="privilegeId" setFieldValue("target", target.value);
as={Select} setFieldValue("serviceKey", target.value);
label={"Привилегия"} }}
sx={{ children={SERVICE_LIST.map(({ displayName, serviceKey }) => (
width: "100%", <MenuItem key={serviceKey} value={serviceKey}>
border: "2px solid", {displayName}
color: theme.palette.secondary.main, </MenuItem>
borderColor: theme.palette.secondary.main, ))}
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { />
border: "none", )}
}, <CustomTextField
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main }, name="threshold"
}} label="При каком значении применяется скидка"
onChange={({ target }: SelectChangeProps) => { onChange={({ target }) => setFieldValue("threshold", Number(target.value.replace(/\D/g, "")))}
setFieldValue("target", target.value); />
setFieldValue("privilegeId", target.value); </>
}} )}
children={privileges.map(({ name, privilegeId }) => ( {bonusType === "privilege" && (
<MenuItem key={privilegeId} value={privilegeId}> <>
{name} <Typography
</MenuItem> variant="h4"
))} sx={{
/> height: "40px",
) : ( fontWeight: "normal",
<Field marginTop: "15px",
name="serviceKey" padding: "0 12px",
as={Select} color: theme.palette.secondary.main,
label={"Сервис"} }}
sx={{ >
width: "100%", Выбор привилегии
border: "2px solid", </Typography>
color: theme.palette.secondary.main, <Field
borderColor: theme.palette.secondary.main, name="privilegeId"
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { as={Select}
border: "none", label="Привилегия"
}, sx={{
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main }, width: "100%",
}} border: "2px solid",
onChange={({ target }: SelectChangeProps) => { color: theme.palette.secondary.main,
setFieldValue("target", target.value); borderColor: theme.palette.secondary.main,
setFieldValue("serviceKey", target.value); "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
}} border: "none",
children={SERVICE_LIST.map(({ displayName, serviceKey }) => ( },
<MenuItem key={serviceKey} value={serviceKey}> ".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
{displayName} }}
</MenuItem> children={privileges.map(({ name, privilegeId }) => (
))} <MenuItem key={privilegeId} value={privilegeId}>
/> {name}
)} </MenuItem>
<CustomTextField ))}
name="threshold" />
label="При каком значении применяется скидка" <CustomTextField
onChange={({ target }) => name="amount"
setFieldValue( label="Количество"
"threshold", required
Number(target.value.replace(/\D/g, "")) onChange={({ target }) => setFieldValue("amount", Number(target.value.replace(/\D/g, "")))}
) />
} </>
/> )}
</> <Button
)} variant="contained"
{bonusType === "privilege" && ( sx={{
<> display: "block",
<Typography padding: "10px",
variant="h4" margin: "15px auto 0",
sx={{ fontWeight: "normal",
height: "40px", fontSize: "18px",
fontWeight: "normal", backgroundColor: theme.palette.menu.main,
marginTop: "15px", "&:hover": { backgroundColor: theme.palette.grayMedium.main },
padding: "0 12px", }}
color: theme.palette.secondary.main, type="submit"
}} >
> Создать
Выбор привилегии </Button>
</Typography> </Form>
<Field )}
name="privilegeId" </Formik>
as={Select} );
label="Привилегия"
sx={{
width: "100%",
border: "2px solid",
color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
}}
children={privileges.map(({ name, privilegeId }) => (
<MenuItem key={privilegeId} value={privilegeId}>
{name}
</MenuItem>
))}
/>
<CustomTextField
name="amount"
label="Количество"
required
onChange={({ target }) =>
setFieldValue(
"amount",
Number(target.value.replace(/\D/g, ""))
)
}
/>
</>
)}
<Button
variant="contained"
sx={{
display: "block",
padding: "10px",
margin: "15px auto 0",
fontWeight: "normal",
fontSize: "18px",
backgroundColor: theme.palette.menu.main,
"&:hover": { backgroundColor: theme.palette.grayMedium.main },
}}
type="submit"
>
Создать
</Button>
</Form>
)}
</Formik>
);
}; };
type CustomTextFieldProps = { type CustomTextFieldProps = {
name: string; name: string;
label: string; label: string;
required?: boolean; required?: boolean;
onChange: (event: ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}; };
const CustomTextField = ({ const CustomTextField = ({ name, label, required = false, onChange }: CustomTextFieldProps) => (
name, <Field
label, name={name}
required = false, label={label}
onChange, required={required}
}: CustomTextFieldProps) => ( variant="filled"
<Field color="secondary"
name={name} as={TextField}
label={label} onChange={onChange}
required={required} sx={{ width: "100%", marginTop: "15px" }}
variant="filled" InputProps={{
color="secondary" style: {
as={TextField} backgroundColor: theme.palette.content.main,
onChange={onChange} color: theme.palette.secondary.main,
sx={{ width: "100%", marginTop: "15px" }} },
InputProps={{ }}
style: { InputLabelProps={{
backgroundColor: theme.palette.content.main, style: { color: theme.palette.secondary.main },
color: theme.palette.secondary.main, }}
}, />
}}
InputLabelProps={{
style: { color: theme.palette.secondary.main },
}}
/>
); );

@ -1,59 +1,54 @@
import * as React from 'react'; import * as React from "react";
import Box from '@mui/material/Box'; import Box from "@mui/material/Box";
import Modal from '@mui/material/Modal'; import Modal from "@mui/material/Modal";
import Button from '@mui/material/Button'; import Button from "@mui/material/Button";
import { Typography } from '@mui/material'; import { Typography } from "@mui/material";
const style = { const style = {
position: 'absolute' as 'absolute', position: "absolute" as const,
top: '50%', top: "50%",
left: '50%', left: "50%",
transform: 'translate(-50%, -50%)', transform: "translate(-50%, -50%)",
width: 400, width: 400,
bgcolor: '#c1c1c1', bgcolor: "#c1c1c1",
border: '2px solid #000', border: "2px solid #000",
boxShadow: 24, boxShadow: 24,
pt: 2, pt: 2,
px: 4, px: 4,
pb: 3, pb: 3,
}; };
interface Props { interface Props {
id: string; id: string;
setModal: (id: string) => void; setModal: (id: string) => void;
deletePromocode: (id: string) => Promise<void>; deletePromocode: (id: string) => Promise<void>;
} }
export default function ({ export default function ({ id, setModal, deletePromocode }: Props) {
id, return (
setModal, <Modal open={Boolean(id)} onClose={() => setModal("")}>
deletePromocode <Box sx={{ ...style, width: 400 }}>
}: Props) { <Typography variant="h5" textAlign="center">
return ( Точно удалить промокод?
<Modal </Typography>
open={Boolean(id)} <Box
onClose={() => setModal("")} sx={{
> display: "flex",
<Box sx={{ ...style, width: 400 }}> justifyContent: "space-evenly",
<Typography mt: "15px",
variant='h5' }}
textAlign="center" >
>Точно удалить промокод?</Typography> <Button
<Box onClick={() => {
sx={{ deletePromocode(id);
display: "flex", setModal("");
justifyContent: "space-evenly", }}
mt: "15px" >
}} Да
> </Button>
<Button <Button onClick={() => setModal("")}>Нет</Button>
onClick={() => { deletePromocode(id); setModal("") }} </Box>
>Да</Button> </Box>
<Button </Modal>
onClick={() => setModal("")} );
>Нет</Button>
</Box>
</Box>
</Modal>
);
} }

@ -1,14 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import { Box, Button, Typography, Modal, TextField, useTheme, useMediaQuery, IconButton } from "@mui/material";
Box,
Button,
Typography,
Modal,
TextField,
useTheme,
useMediaQuery,
IconButton,
} from "@mui/material";
import { DataGrid, GridLoadingOverlay, GridToolbar } from "@mui/x-data-grid"; import { DataGrid, GridLoadingOverlay, GridToolbar } from "@mui/x-data-grid";
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { DatePicker } from "@mui/x-date-pickers/DatePicker";
@ -21,318 +12,293 @@ import type { GridColDef } from "@mui/x-data-grid";
import type { Promocode, PromocodeStatistics } from "@root/model/promocodes"; import type { Promocode, PromocodeStatistics } from "@root/model/promocodes";
const host = window.location.hostname; const host = window.location.hostname;
let isTest = host.includes("s"); const isTest = host.includes("s");
type StatisticsModalProps = { type StatisticsModalProps = {
id: string; id: string;
to: number; to: number;
from: number; from: number;
setId: (id: string) => void; setId: (id: string) => void;
setTo: (date: number) => void; setTo: (date: number) => void;
setFrom: (date: number) => void; setFrom: (date: number) => void;
promocodes: Promocode[]; promocodes: Promocode[];
promocodeStatistics: PromocodeStatistics | null | undefined; promocodeStatistics: PromocodeStatistics | null | undefined;
createFastLink: (id: string) => Promise<void>; createFastLink: (id: string) => Promise<void>;
}; };
type Row = { type Row = {
id: number; id: number;
link: string; link: string;
useCount: number; useCount: number;
}; };
const COLUMNS: GridColDef<Row, string>[] = [ const COLUMNS: GridColDef<Row, string>[] = [
{ {
field: "copy", field: "copy",
headerName: "копировать", headerName: "копировать",
width: 50, width: 50,
sortable: false, sortable: false,
valueGetter: ({ row }) => String(row.useCount), valueGetter: ({ row }) => String(row.useCount),
renderCell: (params) => { renderCell: (params) => {
return ( return (
<IconButton <IconButton
onClick={() => navigator.clipboard.writeText(`https://${isTest ? "s" : ""}quiz.pena.digital/?fl=${params.row.link}`)} onClick={() =>
> navigator.clipboard.writeText(`https://${isTest ? "s" : ""}quiz.pena.digital/?fl=${params.row.link}`)
<ContentCopyIcon /> }
</IconButton> >
); <ContentCopyIcon />
}, </IconButton>
}, );
{ },
field: "link", },
headerName: "Ссылка", {
width: 320, field: "link",
sortable: false, headerName: "Ссылка",
valueGetter: ({ row }) => row.link, width: 320,
renderCell: ({ value }) => sortable: false,
value?.split("|").map((link) => <Typography>{link}</Typography>), valueGetter: ({ row }) => row.link,
}, renderCell: ({ value }) => value?.split("|").map((link) => <Typography>{link}</Typography>),
{ },
field: "useCount", {
headerName: "Использований", field: "useCount",
width: 120, headerName: "Использований",
sortable: false, width: 120,
valueGetter: ({ row }) => String(row.useCount), sortable: false,
}, valueGetter: ({ row }) => String(row.useCount),
{ },
field: "purchasesCount", {
headerName: "Покупок", field: "purchasesCount",
width: 70, headerName: "Покупок",
sortable: false, width: 70,
valueGetter: ({ row }) => String(0), sortable: false,
}, valueGetter: ({ row }) => String(0),
},
]; ];
export const StatisticsModal = ({ export const StatisticsModal = ({
id, id,
setId, setId,
setFrom, setFrom,
from, from,
to, to,
setTo, setTo,
promocodeStatistics, promocodeStatistics,
promocodes, promocodes,
createFastLink, createFastLink,
}: StatisticsModalProps) => { }: StatisticsModalProps) => {
const [startDate, setStartDate] = useState<Date>(new Date()); const [startDate, setStartDate] = useState<Date>(new Date());
const [endDate, setEndDate] = useState<Date>(new Date()); const [endDate, setEndDate] = useState<Date>(new Date());
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(550)); const isMobile = useMediaQuery(theme.breakpoints.down(550));
const [rows, setRows] = useState<Row[]>([]); const [rows, setRows] = useState<Row[]>([]);
const { privileges } = usePrivilegeStore(); const { privileges } = usePrivilegeStore();
const currentPrivilegeId = promocodes.find((promocode) => promocode.id === id) const currentPrivilegeId = promocodes.find((promocode) => promocode.id === id)?.bonus.privilege.privilegeID;
?.bonus.privilege.privilegeID; const privilege = privileges.find((item) => item.privilegeId === currentPrivilegeId);
const privilege = privileges.find( const promocode = promocodes.find((item) => item.id === id);
(item) => item.privilegeId === currentPrivilegeId const createFastlink = async () => {
); await createFastLink(id);
const promocode = promocodes.find((item) => item.id === id); };
const createFastlink = async () => {
await createFastLink(id);
};
const getParseData = () => { const getParseData = () => {
const rows = promocodes const rows = promocodes
.find((promocode) => promocode.id === id) .find((promocode) => promocode.id === id)
?.fastLinks?.map((link, index) => ({ ?.fastLinks?.map((link, index) => ({
link, link,
id: index, id: index,
useCount: promocodeStatistics?.usageMap[link] ?? 0, useCount: promocodeStatistics?.usageMap[link] ?? 0,
})) as Row[]; })) as Row[];
setRows(rows); setRows(rows);
}; };
useEffect(() => { useEffect(() => {
if (id.length > 0) { if (id.length > 0) {
getParseData(); getParseData();
} }
if (!id) { if (!id) {
setRows([]); setRows([]);
} }
}, [id, promocodes]); }, [id, promocodes]);
// const formatTo = to === null ? 0 : moment(to).unix() // const formatTo = to === null ? 0 : moment(to).unix()
// const formatFrom = from === null ? 0 : moment(from).unix() // const formatFrom = from === null ? 0 : moment(from).unix()
// useEffect(() => { // useEffect(() => {
// (async () => { // (async () => {
// const gottenGeneral = await promocodeStatistics(id, startDate, endDate) // const gottenGeneral = await promocodeStatistics(id, startDate, endDate)
// setGeneral(gottenGeneral[0]) // setGeneral(gottenGeneral[0])
// })() // })()
// }, [to, from]); // }, [to, from]);
return ( return (
<Modal <Modal
open={Boolean(id)} open={Boolean(id)}
onClose={() => { onClose={() => {
setId(""); setId("");
setStartDate(new Date()); setStartDate(new Date());
setEndDate(new Date()); setEndDate(new Date());
}} }}
sx={{ sx={{
"& > .MuiBox-root": { outline: "none", padding: "32px 32px 16px" }, "& > .MuiBox-root": { outline: "none", padding: "32px 32px 16px" },
}} }}
> >
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",
top: "50%", top: "50%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
width: "95%", width: "95%",
maxWidth: "600px", maxWidth: "600px",
background: "#1F2126", background: "#1F2126",
border: "2px solid gray", border: "2px solid gray",
borderRadius: "6px", borderRadius: "6px",
boxShadow: 24, boxShadow: 24,
p: 4, p: 4,
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "30px", gap: "30px",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
flexDirection: isMobile ? "column" : "row", flexDirection: isMobile ? "column" : "row",
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "30px", gap: "30px",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}} }}
> >
<Button sx={{ maxWidth: "100px" }} onClick={createFastlink}> <Button sx={{ maxWidth: "100px" }} onClick={createFastlink}>
Создать короткую ссылку Создать короткую ссылку
</Button> </Button>
<Button sx={{ maxWidth: "100px" }} onClick={getParseData}> <Button sx={{ maxWidth: "100px" }} onClick={getParseData}>
Обновить статистику Обновить статистику
</Button> </Button>
</Box> </Box>
<Box sx={{ minWidth: "200px" }}> <Box sx={{ minWidth: "200px" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}> <Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}>от</Typography>
от <DatePicker
</Typography> inputFormat="DD/MM/YYYY"
<DatePicker value={startDate}
inputFormat="DD/MM/YYYY" onChange={(date) => date && setStartDate(date)}
value={startDate} renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
onChange={(date) => date && setStartDate(date)} InputProps={{
renderInput={(params) => ( sx: {
<TextField height: "40px",
{...params} color: theme.palette.secondary.main,
sx={{ background: "#1F2126", borderRadius: "5px" }} border: "1px solid",
/> borderColor: theme.palette.secondary.main,
)} "& .MuiSvgIcon-root": {
InputProps={{ color: theme.palette.secondary.main,
sx: { },
height: "40px", },
color: theme.palette.secondary.main, }}
border: "1px solid", />
borderColor: theme.palette.secondary.main, </Box>
"& .MuiSvgIcon-root": { <Box
color: theme.palette.secondary.main, sx={{
}, display: "flex",
}, alignItems: "center",
}} gap: "10px",
/> marginTop: "10px",
</Box> }}
<Box >
sx={{ <Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}>до</Typography>
display: "flex", <DatePicker
alignItems: "center", inputFormat="DD/MM/YYYY"
gap: "10px", value={endDate}
marginTop: "10px", onChange={(date) => date && setEndDate(date)}
}} renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
> InputProps={{
<Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}> sx: {
до height: "40px",
</Typography> color: theme.palette.secondary.main,
<DatePicker border: "1px solid",
inputFormat="DD/MM/YYYY" borderColor: theme.palette.secondary.main,
value={endDate} "& .MuiSvgIcon-root": {
onChange={(date) => date && setEndDate(date)} color: theme.palette.secondary.main,
renderInput={(params) => ( },
<TextField },
{...params} }}
sx={{ background: "#1F2126", borderRadius: "5px" }} />
/> </Box>
)} </Box>
InputProps={{ </Box>
sx: { <DataGrid
height: "40px", disableSelectionOnClick={true}
color: theme.palette.secondary.main, rows={rows}
border: "1px solid", columns={COLUMNS}
borderColor: theme.palette.secondary.main, sx={{
"& .MuiSvgIcon-root": { marginTop: "30px",
color: theme.palette.secondary.main, background: "#1F2126",
}, color: theme.palette.secondary.main,
}, "& .MuiDataGrid-iconSeparator": { display: "none" },
}} "& .css-levciy-MuiTablePagination-displayedRows": {
/> color: theme.palette.secondary.main,
</Box> },
</Box> "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
</Box> "& .MuiTablePagination-selectLabel": {
<DataGrid color: theme.palette.secondary.main,
disableSelectionOnClick={true} },
rows={rows} "& .MuiInputBase-root": { color: theme.palette.secondary.main },
columns={COLUMNS} "& .MuiButton-text": { color: theme.palette.secondary.main },
sx={{ "& .MuiDataGrid-overlay": {
marginTop: "30px", backgroundColor: "rgba(255, 255, 255, 0.1)",
background: "#1F2126", animation: `${fadeIn} 0.5s ease-out`,
color: theme.palette.secondary.main, },
"& .MuiDataGrid-iconSeparator": { display: "none" }, "& .MuiDataGrid-virtualScrollerContent": { maxHeight: "200px" },
"& .css-levciy-MuiTablePagination-displayedRows": { "& .MuiDataGrid-virtualScrollerRenderZone": {
color: theme.palette.secondary.main, maxHeight: "200px",
}, overflowY: "auto",
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, },
"& .MuiTablePagination-selectLabel": { }}
color: theme.palette.secondary.main, components={{
}, Toolbar: GridToolbar,
"& .MuiInputBase-root": { color: theme.palette.secondary.main }, LoadingOverlay: GridLoadingOverlay,
"& .MuiButton-text": { color: theme.palette.secondary.main }, }}
"& .MuiDataGrid-overlay": { rowsPerPageOptions={[10, 25, 50, 100]}
backgroundColor: "rgba(255, 255, 255, 0.1)", autoHeight
animation: `${fadeIn} 0.5s ease-out`, />
}, {privilege === undefined ? (
"& .MuiDataGrid-virtualScrollerContent": { maxHeight: "200px" }, <Typography
"& .MuiDataGrid-virtualScrollerRenderZone": { sx={{
maxHeight: "200px", margin: "10px 0 0",
overflowY: "auto", textAlign: "center",
}, color: theme.palette.secondary.main,
}} }}
components={{ >
Toolbar: GridToolbar, Нет привилегии
LoadingOverlay: GridLoadingOverlay, </Typography>
}} ) : (
rowsPerPageOptions={[10, 25, 50, 100]} <Box
autoHeight sx={{
/> color: "#e6e8ec",
{privilege === undefined ? ( display: "flex",
<Typography flexDirection: "column",
sx={{ margin: "20px 0",
margin: "10px 0 0", }}
textAlign: "center", >
color: theme.palette.secondary.main, <Typography>название привилегии: {privilege.name}</Typography>
}} <Typography>
> {promocode?.activationCount} активаций из {promocode?.activationLimit}
Нет привилегии </Typography>
</Typography> <Typography>приветствие: "{promocode?.greetings}"</Typography>
) : ( {promocode?.bonus?.discount?.factor !== undefined && (
<Box <Typography>скидка: {100 - promocode?.bonus?.discount?.factor * 100}%</Typography>
sx={{ )}
color: "#e6e8ec", {<Typography>количество привилегии: {promocode?.bonus?.privilege?.amount}</Typography>}
display: "flex", {promocode?.dueTo !== undefined && promocode.dueTo > 0 && (
flexDirection: "column", <Typography>действует до: {new Date(promocode.dueTo).toLocaleString()}</Typography>
margin: "20px 0", )}
}} </Box>
> )}
<Typography>название привилегии: {privilege.name}</Typography> </Box>
<Typography> </Modal>
{promocode?.activationCount} активаций из{" "} );
{promocode?.activationLimit}
</Typography>
<Typography>приветствие: "{promocode?.greetings}"</Typography>
{promocode?.bonus?.discount?.factor !== undefined && (
<Typography>
скидка: {100 - promocode?.bonus?.discount?.factor * 100}%
</Typography>
)}
{
<Typography>
количество привилегии: {promocode?.bonus?.privilege?.amount}
</Typography>
}
{promocode?.dueTo !== undefined && promocode.dueTo > 0 && (
<Typography>
действует до: {new Date(promocode.dueTo).toLocaleString()}
</Typography>
)}
</Box>
)}
</Box>
</Modal>
);
}; };

@ -11,104 +11,96 @@ import { StatisticsModal } from "./StatisticsModal";
import DeleteModal from "./DeleteModal"; import DeleteModal from "./DeleteModal";
export const PromocodeManagement = () => { export const PromocodeManagement = () => {
const theme = useTheme(); const theme = useTheme();
const [deleteModal, setDeleteModal] = useState<string>(""); const [deleteModal, setDeleteModal] = useState<string>("");
const deleteModalHC = (id: string) => setDeleteModal(id); const deleteModalHC = (id: string) => setDeleteModal(id);
const [showStatisticsModalId, setShowStatisticsModalId] = const [showStatisticsModalId, setShowStatisticsModalId] = useState<string>("");
useState<string>(""); const [page, setPage] = useState<number>(0);
const [page, setPage] = useState<number>(0); const [to, setTo] = useState(0);
const [to, setTo] = useState(0); const [from, setFrom] = useState(0);
const [from, setFrom] = useState(0); const [pageSize, setPageSize] = useState<number>(10);
const [pageSize, setPageSize] = useState<number>(10); const {
const { data,
data, error,
error, isValidating,
isValidating, promocodesCount,
promocodesCount, promocodeStatistics,
promocodeStatistics, deletePromocode,
deletePromocode, createPromocode,
createPromocode, createFastLink,
createFastLink, } = usePromocodes(page, pageSize, showStatisticsModalId, to, from);
} = usePromocodes(page, pageSize, showStatisticsModalId, to, from); const columns = usePromocodeGridColDef(setShowStatisticsModalId, deleteModalHC);
const columns = usePromocodeGridColDef( if (error) return <Typography>Ошибка загрузки промокодов</Typography>;
setShowStatisticsModalId,
deleteModalHC
);
if (error) return <Typography>Ошибка загрузки промокодов</Typography>;
return ( return (
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
width: "90%", width: "90%",
height: "60px", height: "60px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
textTransform: "uppercase", textTransform: "uppercase",
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}} }}
> >
Создание промокода Создание промокода
</Typography> </Typography>
<CreatePromocodeForm createPromocode={createPromocode} /> <CreatePromocodeForm createPromocode={createPromocode} />
<Box style={{ width: "80%", marginTop: "55px" }}> <Box style={{ width: "80%", marginTop: "55px" }}>
<DataGrid <DataGrid
disableSelectionOnClick={true} disableSelectionOnClick={true}
rows={data?.items ?? []} rows={data?.items ?? []}
columns={columns} columns={columns}
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
"& .MuiDataGrid-iconSeparator": { display: "none" }, "& .MuiDataGrid-iconSeparator": { display: "none" },
"& .css-levciy-MuiTablePagination-displayedRows": { "& .css-levciy-MuiTablePagination-displayedRows": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
"& .MuiTablePagination-selectLabel": { "& .MuiTablePagination-selectLabel": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}, },
"& .MuiInputBase-root": { color: theme.palette.secondary.main }, "& .MuiInputBase-root": { color: theme.palette.secondary.main },
"& .MuiButton-text": { color: theme.palette.secondary.main }, "& .MuiButton-text": { color: theme.palette.secondary.main },
"& .MuiDataGrid-overlay": { "& .MuiDataGrid-overlay": {
backgroundColor: "rgba(255, 255, 255, 0.1)", backgroundColor: "rgba(255, 255, 255, 0.1)",
animation: `${fadeIn} 0.5s ease-out`, animation: `${fadeIn} 0.5s ease-out`,
}, },
}} }}
components={{ components={{
Toolbar: GridToolbar, Toolbar: GridToolbar,
LoadingOverlay: GridLoadingOverlay, LoadingOverlay: GridLoadingOverlay,
}} }}
loading={isValidating} loading={isValidating}
paginationMode="server" paginationMode="server"
page={page} page={page}
onPageChange={setPage} onPageChange={setPage}
rowCount={promocodesCount} rowCount={promocodesCount}
pageSize={pageSize} pageSize={pageSize}
onPageSizeChange={setPageSize} onPageSizeChange={setPageSize}
rowsPerPageOptions={[10, 25, 50, 100]} rowsPerPageOptions={[10, 25, 50, 100]}
autoHeight autoHeight
/> />
</Box> </Box>
<StatisticsModal <StatisticsModal
id={showStatisticsModalId} id={showStatisticsModalId}
setId={setShowStatisticsModalId} setId={setShowStatisticsModalId}
promocodeStatistics={promocodeStatistics} promocodeStatistics={promocodeStatistics}
to={to} to={to}
setTo={setTo} setTo={setTo}
from={from} from={from}
setFrom={setFrom} setFrom={setFrom}
promocodes={data?.items ?? []} promocodes={data?.items ?? []}
createFastLink={createFastLink} createFastLink={createFastLink}
/> />
<DeleteModal <DeleteModal id={deleteModal} setModal={setDeleteModal} deletePromocode={deletePromocode} />
id={deleteModal} </LocalizationProvider>
setModal={setDeleteModal} );
deletePromocode={deletePromocode}
/>
</LocalizationProvider>
);
}; };

@ -6,96 +6,92 @@ import { useMemo, useState } from "react";
import { BarChart, Delete } from "@mui/icons-material"; import { BarChart, Delete } from "@mui/icons-material";
import { promocodeApi } from "@root/api/promocode/requests"; import { promocodeApi } from "@root/api/promocode/requests";
export function usePromocodeGridColDef( export function usePromocodeGridColDef(setStatistics: (id: string) => void, deletePromocode: (id: string) => void) {
setStatistics: (id: string) => void, const validity = (value: string | number) => {
deletePromocode: (id: string) => void if (value === 0) {
) { return "неоганичен";
const validity = (value: string | number) => { } else {
if (value === 0) { return new Date(value).toLocaleString();
return "неоганичен"; }
} else { };
return new Date(value).toLocaleString(); return useMemo<GridColDef<Promocode, string | number, string>[]>(
} () => [
}; {
return useMemo<GridColDef<Promocode, string | number, string>[]>( field: "id",
() => [ headerName: "ID",
{ width: 30,
field: "id", sortable: false,
headerName: "ID", valueGetter: ({ row }) => row.id,
width: 30, },
sortable: false, {
valueGetter: ({ row }) => row.id, field: "codeword",
}, headerName: "Кодовое слово",
{ width: 160,
field: "codeword", sortable: false,
headerName: "Кодовое слово", valueGetter: ({ row }) => row.codeword,
width: 160, },
sortable: false, {
valueGetter: ({ row }) => row.codeword, field: "factor",
}, headerName: "Коэф. скидки",
{ width: 120,
field: "factor", sortable: false,
headerName: "Коэф. скидки", valueGetter: ({ row }) => Math.round(row.bonus.discount.factor * 1000) / 1000,
width: 120, },
sortable: false, {
valueGetter: ({ row }) => field: "activationCount",
Math.round(row.bonus.discount.factor * 1000) / 1000, headerName: "Кол-во активаций",
}, width: 140,
{ sortable: false,
field: "activationCount", valueGetter: ({ row }) => row.activationCount,
headerName: "Кол-во активаций", },
width: 140, {
sortable: false, field: "dueTo",
valueGetter: ({ row }) => row.activationCount, headerName: "Истекает",
}, width: 160,
{ sortable: false,
field: "dueTo", valueGetter: ({ row }) => row.dueTo * 1000,
headerName: "Истекает", valueFormatter: ({ value }) => `${validity(value)}`,
width: 160, },
sortable: false, {
valueGetter: ({ row }) => row.dueTo * 1000, field: "description",
valueFormatter: ({ value }) => `${validity(value)}`, headerName: "Описание",
}, minWidth: 200,
{ flex: 1,
field: "description", sortable: false,
headerName: "Описание", valueGetter: ({ row }) => row.description,
minWidth: 200, },
flex: 1, {
sortable: false, field: "settings",
valueGetter: ({ row }) => row.description, headerName: "",
}, width: 60,
{ sortable: false,
field: "settings", renderCell: (params) => {
headerName: "", return (
width: 60, <IconButton
sortable: false, onClick={() => {
renderCell: (params) => { setStatistics(params.row.id);
return ( promocodeApi.getPromocodeStatistics(params.row.id, 0, 0);
<IconButton }}
onClick={() => { >
setStatistics(params.row.id); <BarChart />
promocodeApi.getPromocodeStatistics(params.row.id, 0, 0); </IconButton>
}} );
> },
<BarChart /> },
</IconButton> {
); field: "delete",
}, headerName: "",
}, width: 60,
{ sortable: false,
field: "delete", renderCell: (params) => {
headerName: "", return (
width: 60, <IconButton onClick={() => deletePromocode(params.row.id)}>
sortable: false, <Delete />
renderCell: (params) => { </IconButton>
return ( );
<IconButton onClick={() => deletePromocode(params.row.id)}> },
<Delete /> },
</IconButton> ],
); [deletePromocode, setStatistics]
}, );
},
],
[deletePromocode, setStatistics]
);
} }

@ -4,83 +4,73 @@ import { DatePicker } from "@mui/x-date-pickers";
import type { Moment } from "moment"; import type { Moment } from "moment";
type DateFilterProps = { type DateFilterProps = {
from: Moment | null; from: Moment | null;
to: Moment | null; to: Moment | null;
setFrom: (date: Moment | null) => void; setFrom: (date: Moment | null) => void;
setTo: (date: Moment | null) => void; setTo: (date: Moment | null) => void;
}; };
export const DateFilter = ({ to, setTo, from, setFrom }: DateFilterProps) => { export const DateFilter = ({ to, setTo, from, setFrom }: DateFilterProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<> <>
<Box> <Box>
<Typography <Typography
sx={{ sx={{
fontSize: "16px", fontSize: "16px",
marginBottom: "5px", marginBottom: "5px",
fontWeight: 500, fontWeight: 500,
color: "4D4D4D", color: "4D4D4D",
}} }}
> >
Дата начала Дата начала
</Typography> </Typography>
<DatePicker <DatePicker
inputFormat="DD/MM/YYYY" inputFormat="DD/MM/YYYY"
value={from} value={from}
onChange={(date) => date && setFrom(date.startOf('day'))} onChange={(date) => date && setFrom(date.startOf("day"))}
renderInput={(params) => ( renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
<TextField InputProps={{
{...params} sx: {
sx={{ background: "#1F2126", borderRadius: "5px" }} height: "40px",
/> color: theme.palette.secondary.main,
)} border: "1px solid",
InputProps={{ borderColor: theme.palette.secondary.main,
sx: { "& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
height: "40px", },
color: theme.palette.secondary.main, }}
border: "1px solid", />
borderColor: theme.palette.secondary.main, </Box>
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, <Box>
}, <Typography
}} sx={{
/> fontSize: "16px",
</Box> marginBottom: "5px",
<Box> fontWeight: 500,
<Typography color: "4D4D4D",
sx={{ }}
fontSize: "16px", >
marginBottom: "5px", Дата окончания
fontWeight: 500, </Typography>
color: "4D4D4D", <DatePicker
}} inputFormat="DD/MM/YYYY"
> value={to}
Дата окончания onChange={(date) => date && setTo(date.endOf("day"))}
</Typography> renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
<DatePicker InputProps={{
inputFormat="DD/MM/YYYY" sx: {
value={to} height: "40px",
onChange={(date) => date && setTo(date.endOf('day'))} color: theme.palette.secondary.main,
renderInput={(params) => ( border: "1px solid",
<TextField borderColor: theme.palette.secondary.main,
{...params} "& .MuiSvgIcon-root": {
sx={{ background: "#1F2126", borderRadius: "5px" }} color: theme.palette.secondary.main,
/> },
)} },
InputProps={{ }}
sx: { />
height: "40px", </Box>
color: theme.palette.secondary.main, </>
border: "1px solid", );
borderColor: theme.palette.secondary.main,
"& .MuiSvgIcon-root": {
color: theme.palette.secondary.main,
},
},
}}
/>
</Box>
</>
);
}; };

@ -1,14 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import moment from "moment"; import moment from "moment";
import { import { Table, TableBody, TableCell, TableHead, TableRow, Button, useTheme } from "@mui/material";
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Button,
useTheme,
} from "@mui/material";
import { DateFilter } from "./DateFilter"; import { DateFilter } from "./DateFilter";
@ -17,67 +9,67 @@ import { useQuizStatistic } from "@root/utils/hooks/useQuizStatistic";
import type { Moment } from "moment"; import type { Moment } from "moment";
export const QuizInfo = () => { export const QuizInfo = () => {
const [from, setFrom] = useState<Moment | null>(null); const [from, setFrom] = useState<Moment | null>(null);
const [to, setTo] = useState<Moment | null>(moment()); const [to, setTo] = useState<Moment | null>(moment());
const theme = useTheme(); const theme = useTheme();
const { Registrations, Quizes, Results } = useQuizStatistic({ const { Registrations, Quizes, Results } = useQuizStatistic({
from, from,
to, to,
}); });
const resetTime = () => { const resetTime = () => {
setFrom(moment()); setFrom(moment());
setTo(moment()); setTo(moment());
}; };
return ( return (
<> <>
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} /> <DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
<Button sx={{ m: "10px 0" }} onClick={resetTime}> <Button sx={{ m: "10px 0" }} onClick={resetTime}>
Сбросить даты Сбросить даты
</Button> </Button>
<Table <Table
sx={{ sx={{
width: "80%", width: "80%",
border: "2px solid", border: "2px solid",
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
bgcolor: theme.palette.content.main, bgcolor: theme.palette.content.main,
color: "white", color: "white",
}} }}
> >
<TableHead> <TableHead>
<TableRow <TableRow
sx={{ sx={{
borderBottom: "2px solid", borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: "100px", height: "100px",
}} }}
> >
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
Регистраций Регистраций
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
Quiz Quiz
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
Результаты Результаты
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
{Registrations} {Registrations}
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
{Quizes} {Quizes}
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
{Results} {Results}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
</> </>
); );
}; };

@ -1,14 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import moment from "moment"; import moment from "moment";
import { import { Table, TableBody, TableCell, TableHead, TableRow, Typography, useTheme } from "@mui/material";
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
useTheme,
} from "@mui/material";
import { DateFilter } from "./DateFilter"; import { DateFilter } from "./DateFilter";
@ -18,63 +10,61 @@ import { usePromocodeStatistics } from "@root/utils/hooks/usePromocodeStatistics
import type { Moment } from "moment"; import type { Moment } from "moment";
export const StatisticsPromocode = () => { export const StatisticsPromocode = () => {
const [from, setFrom] = useState<Moment | null>( const [from, setFrom] = useState<Moment | null>(moment(moment().subtract(4, "weeks")));
moment(moment().subtract(4, "weeks")) const [to, setTo] = useState<Moment | null>(moment());
); const promocodes = useAllPromocodes();
const [to, setTo] = useState<Moment | null>(moment()); const promocodeStatistics = usePromocodeStatistics({ to, from });
const promocodes = useAllPromocodes(); const theme = useTheme();
const promocodeStatistics = usePromocodeStatistics({ to, from });
const theme = useTheme();
return ( return (
<> <>
<Typography sx={{ marginTop: "30px" }}>Статистика промокодов</Typography> <Typography sx={{ marginTop: "30px" }}>Статистика промокодов</Typography>
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} /> <DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
<Table <Table
sx={{ sx={{
width: "80%", width: "80%",
border: "2px solid", border: "2px solid",
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
bgcolor: theme.palette.content.main, bgcolor: theme.palette.content.main,
color: "white", color: "white",
marginTop: "30px", marginTop: "30px",
}} }}
> >
<TableHead> <TableHead>
<TableRow <TableRow
sx={{ sx={{
borderBottom: "2px solid", borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: "100px", height: "100px",
}} }}
> >
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
Промокод Промокод
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
Регистации Регистации
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
Внесено Внесено
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
{Object.entries(promocodeStatistics).map(([key, { Regs, Money }]) => ( {Object.entries(promocodeStatistics).map(([key, { Regs, Money }]) => (
<TableBody key={key}> <TableBody key={key}>
<TableRow> <TableRow>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
{promocodes.find(({ id }) => id === key)?.codeword ?? ""} {promocodes.find(({ id }) => id === key)?.codeword ?? ""}
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
{Regs} {Regs}
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
{(Money / 100).toFixed(2)} {(Money / 100).toFixed(2)}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
))} ))}
</Table> </Table>
</> </>
); );
}; };

@ -1,17 +1,17 @@
import moment from "moment"; import moment from "moment";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
Typography, Typography,
Button, Button,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { GridToolbar } from "@mui/x-data-grid"; import { GridToolbar } from "@mui/x-data-grid";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -29,168 +29,154 @@ import type { QuizStatisticsItem } from "@root/api/quizStatistics/types";
import type { GridColDef } from "@mui/x-data-grid"; import type { GridColDef } from "@mui/x-data-grid";
const COLUMNS: GridColDef<QuizStatisticsItem, string>[] = [ const COLUMNS: GridColDef<QuizStatisticsItem, string>[] = [
{ {
field: "user", field: "user",
headerName: "Пользователь", headerName: "Пользователь",
width: 250, width: 250,
valueGetter: ({ row }) => row.ID, valueGetter: ({ row }) => row.ID,
}, },
{ {
field: "regs", field: "regs",
headerName: "Регистраций", headerName: "Регистраций",
width: 200, width: 200,
valueGetter: ({ row }) => String(row.Regs), valueGetter: ({ row }) => String(row.Regs),
}, },
{ {
field: "money", field: "money",
headerName: "Деньги", headerName: "Деньги",
width: 200, width: 200,
valueGetter: ({ row }) => String(row.Money), valueGetter: ({ row }) => String(row.Money),
}, },
]; ];
export const StatisticsSchild = () => { export const StatisticsSchild = () => {
const [openUserModal, setOpenUserModal] = useState<boolean>(false); const [openUserModal, setOpenUserModal] = useState<boolean>(false);
const [activeUserId, setActiveUserId] = useState<string>(""); const [activeUserId, setActiveUserId] = useState<string>("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [from, setFrom] = useState<Moment | null>( const [from, setFrom] = useState<Moment | null>(moment(moment().subtract(4, "weeks")));
moment(moment().subtract(4, "weeks")) const [to, setTo] = useState<Moment | null>(moment());
);
const [to, setTo] = useState<Moment | null>(moment());
const theme = useTheme(); const theme = useTheme();
const statistics = useSchildStatistics(from, to) const statistics = useSchildStatistics(from, to).map((obj) => ({
.map((obj) => ({...obj, Money: Number((obj.Money / 100).toFixed(2))})); ...obj,
Money: Number((obj.Money / 100).toFixed(2)),
}));
useEffect(() => { useEffect(() => {
if (!openUserModal) { if (!openUserModal) {
setActiveUserId(""); setActiveUserId("");
} }
}, [openUserModal]); }, [openUserModal]);
useEffect(() => { useEffect(() => {
if (activeUserId) { if (activeUserId) {
setOpenUserModal(true); setOpenUserModal(true);
return; return;
} }
setOpenUserModal(false); setOpenUserModal(false);
}, [activeUserId]); }, [activeUserId]);
const copyQuizLink = (quizId: string) => { const copyQuizLink = (quizId: string) => {
navigator.clipboard.writeText( navigator.clipboard.writeText(`https://${window.location.href.includes("/admin.") ? "" : "s."}hbpn.link/${quizId}`);
`https://${
window.location.href.includes("/admin.") ? "" : "s."
}hbpn.link/${quizId}`
);
enqueueSnackbar("Ссылка успешно скопирована"); enqueueSnackbar("Ссылка успешно скопирована");
}; };
return ( return (
<> <>
<Typography sx={{ mt: "20px", mb: "20px" }}> <Typography sx={{ mt: "20px", mb: "20px" }}>Статистика переходов с шильдика</Typography>
Статистика переходов с шильдика <DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
</Typography> <DataGrid
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} /> sx={{ marginTop: "30px", width: "80%" }}
<DataGrid getRowId={({ ID }) => ID}
sx={{ marginTop: "30px", width: "80%" }} checkboxSelection={true}
getRowId={({ ID }) => ID} rows={statistics}
checkboxSelection={true} components={{ Toolbar: GridToolbar }}
rows={statistics} rowCount={statistics.length}
components={{ Toolbar: GridToolbar }} rowsPerPageOptions={[1, 10, 25, 50, 100]}
rowCount={statistics.length} paginationMode="client"
rowsPerPageOptions={[1, 10, 25, 50, 100]} disableSelectionOnClick
paginationMode="client" page={page}
disableSelectionOnClick pageSize={pageSize}
page={page} onPageChange={setPage}
pageSize={pageSize} onPageSizeChange={setPageSize}
onPageChange={setPage} onCellClick={({ id, field }) => field === "user" && setActiveUserId(String(id))}
onPageSizeChange={setPageSize} getRowHeight={() => "auto"}
onCellClick={({ id, field }) => columns={[
field === "user" && setActiveUserId(String(id)) ...COLUMNS,
} {
getRowHeight={() => "auto"} field: "quizes",
columns={[ headerName: "Квизы",
...COLUMNS, flex: 1,
{ minWidth: 220,
field: "quizes", valueGetter: ({ row }) => String(row.Quizes.length),
headerName: "Квизы", renderCell: ({ row }) => (
flex: 1, <Accordion sx={{ width: "100%" }}>
minWidth: 220, <AccordionSummary
valueGetter: ({ row }) => String(row.Quizes.length), sx={{ backgroundColor: "#26272c", color: "#FFFFFF" }}
renderCell: ({ row }) => ( expandIcon={<ExpandMoreIcon sx={{ color: "#FFFFFF" }} />}
<Accordion sx={{ width: "100%" }}> aria-controls="panel1-content"
<AccordionSummary id="panel1-header"
sx={{ backgroundColor: "#26272c", color: "#FFFFFF" }} >
expandIcon={<ExpandMoreIcon sx={{ color: "#FFFFFF" }} />} Статистика по квизам
aria-controls="panel1-content" </AccordionSummary>
id="panel1-header" <AccordionDetails sx={{ backgroundColor: "#26272c" }}>
> <Table
Статистика по квизам sx={{
</AccordionSummary> width: "80%",
<AccordionDetails sx={{ backgroundColor: "#26272c" }}> border: "2px solid",
<Table borderColor: theme.palette.secondary.main,
sx={{ bgcolor: theme.palette.content.main,
width: "80%", color: "white",
border: "2px solid", }}
borderColor: theme.palette.secondary.main, >
bgcolor: theme.palette.content.main, <TableHead>
color: "white", <TableRow
}} sx={{
> borderBottom: "2px solid",
<TableHead> borderColor: theme.palette.grayLight.main,
<TableRow height: "100px",
sx={{ }}
borderBottom: "2px solid", >
borderColor: theme.palette.grayLight.main, <TableCell sx={{ color: "inherit" }} align="center">
height: "100px", QuizID
}} </TableCell>
> <TableCell sx={{ color: "inherit" }} align="center">
<TableCell sx={{ color: "inherit" }} align="center"> Регистрации
QuizID </TableCell>
</TableCell> <TableCell sx={{ color: "inherit" }} align="center">
<TableCell sx={{ color: "inherit" }} align="center"> Деньги
Регистрации </TableCell>
</TableCell> </TableRow>
<TableCell sx={{ color: "inherit" }} align="center"> </TableHead>
Деньги
</TableCell>
</TableRow>
</TableHead>
<TableBody> <TableBody>
{row.Quizes.map(({ QuizID, Regs, Money }) => ( {row.Quizes.map(({ QuizID, Regs, Money }) => (
<TableRow key={QuizID}> <TableRow key={QuizID}>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
<Button onClick={() => copyQuizLink(QuizID)}> <Button onClick={() => copyQuizLink(QuizID)}>{QuizID}</Button>
{QuizID} </TableCell>
</Button> <TableCell sx={{ color: "inherit" }} align="center">
</TableCell> {Regs}
<TableCell sx={{ color: "inherit" }} align="center"> </TableCell>
{Regs} <TableCell sx={{ color: "inherit" }} align="center">
</TableCell> {(Money / 100).toFixed(2)}
<TableCell sx={{ color: "inherit" }} align="center"> </TableCell>
{(Money / 100).toFixed(2)} </TableRow>
</TableCell> ))}
</TableRow> </TableBody>
))} </Table>
</TableBody> </AccordionDetails>
</Table> </Accordion>
</AccordionDetails> ),
</Accordion> },
), ]}
}, />
]} <ModalUser open={openUserModal} onClose={() => setOpenUserModal(false)} userId={activeUserId} />
/> </>
<ModalUser );
open={openUserModal}
onClose={() => setOpenUserModal(false)}
userId={activeUserId}
/>
</>
);
}; };

@ -8,11 +8,11 @@ import { StatisticsSchild } from "./StatisticsSchild";
import { StatisticsPromocode } from "./StastisticsPromocode"; import { StatisticsPromocode } from "./StastisticsPromocode";
export const QuizStatistics = () => ( export const QuizStatistics = () => (
<LocalizationProvider dateAdapter={AdapterMoment}> <LocalizationProvider dateAdapter={AdapterMoment}>
<QuizInfo /> <QuizInfo />
<StatisticsSchild /> <StatisticsSchild />
<Suspense fallback={<Box>Loading...</Box>}> <Suspense fallback={<Box>Loading...</Box>}>
<StatisticsPromocode /> <StatisticsPromocode />
</Suspense> </Suspense>
</LocalizationProvider> </LocalizationProvider>
); );

@ -7,86 +7,86 @@ import DataGrid from "@kitUI/datagrid";
import type { UserType } from "@root/api/roles"; import type { UserType } from "@root/api/roles";
const columns: GridColDef<UserType, string>[] = [ const columns: GridColDef<UserType, string>[] = [
{ {
field: "login", field: "login",
headerName: "Логин", headerName: "Логин",
width: 200, width: 200,
valueGetter: ({ row }) => row.login, valueGetter: ({ row }) => row.login,
}, },
{ {
field: "email", field: "email",
headerName: "E-mail", headerName: "E-mail",
width: 200, width: 200,
valueGetter: ({ row }) => row.email, valueGetter: ({ row }) => row.email,
}, },
{ {
field: "phoneNumber", field: "phoneNumber",
headerName: "Номер телефона", headerName: "Номер телефона",
width: 200, width: 200,
valueGetter: ({ row }) => row.phoneNumber, valueGetter: ({ row }) => row.phoneNumber,
}, },
{ {
field: "isDeleted", field: "isDeleted",
headerName: "Удалено", headerName: "Удалено",
width: 100, width: 100,
valueGetter: ({ row }) => `${row.isDeleted ? "true" : "false"}`, valueGetter: ({ row }) => `${row.isDeleted ? "true" : "false"}`,
}, },
{ {
field: "createdAt", field: "createdAt",
headerName: "Дата создания", headerName: "Дата создания",
width: 200, width: 200,
valueGetter: ({ row }) => row.createdAt, valueGetter: ({ row }) => row.createdAt,
}, },
]; ];
interface Props { interface Props {
handleSelectionChange: (selectionModel: GridSelectionModel) => void; handleSelectionChange: (selectionModel: GridSelectionModel) => void;
users: UserType[]; users: UserType[];
page: number; page: number;
setPage: (page: number) => void; setPage: (page: number) => void;
pageSize: number; pageSize: number;
pagesCount: number; pagesCount: number;
onPageSizeChange?: (count: number) => void; onPageSizeChange?: (count: number) => void;
} }
export default function ServiceUsersDG({ export default function ServiceUsersDG({
handleSelectionChange, handleSelectionChange,
users = [], users = [],
page, page,
setPage, setPage,
pageSize = 10, pageSize = 10,
pagesCount = 1, pagesCount = 1,
onPageSizeChange, onPageSizeChange,
}: Props) { }: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<> <>
{users.length ? ( {users.length ? (
<DataGrid <DataGrid
sx={{ maxWidth: "90%", mt: "30px" }} sx={{ maxWidth: "90%", mt: "30px" }}
getRowId={(users) => users._id} getRowId={(users) => users._id}
checkboxSelection={true} checkboxSelection={true}
rows={users} rows={users}
columns={columns} columns={columns}
components={{ Toolbar: GridToolbar }} components={{ Toolbar: GridToolbar }}
rowCount={pageSize * pagesCount} rowCount={pageSize * pagesCount}
rowsPerPageOptions={[10, 25, 50, 100]} rowsPerPageOptions={[10, 25, 50, 100]}
paginationMode="server" paginationMode="server"
page={page} page={page}
pageSize={pageSize} pageSize={pageSize}
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={onPageSizeChange} onPageSizeChange={onPageSizeChange}
onSelectionModelChange={handleSelectionChange} onSelectionModelChange={handleSelectionChange}
onCellClick={({ row }, event) => { onCellClick={({ row }, event) => {
event.stopPropagation(); event.stopPropagation();
navigate(row._id); navigate(row._id);
}} }}
/> />
) : ( ) : (
<Skeleton>Loading...</Skeleton> <Skeleton>Loading...</Skeleton>
)} )}
</> </>
); );
} }

@ -1,5 +1,12 @@
import { Box, IconButton, InputAdornment, TextField, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, IconButton, InputAdornment, TextField, Typography, useMediaQuery, useTheme } from "@mui/material";
import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setIsPreventAutoscroll, setTicketMessagesFetchState, useMessageStore } from "@root/stores/messages"; import {
addOrUpdateMessages,
clearMessageState,
incrementMessageApiPage,
setIsPreventAutoscroll,
setTicketMessagesFetchState,
useMessageStore,
} from "@root/stores/messages";
import Message from "./Message"; import Message from "./Message";
import SendIcon from "@mui/icons-material/Send"; import SendIcon from "@mui/icons-material/Send";
import AttachFileIcon from "@mui/icons-material/AttachFile"; import AttachFileIcon from "@mui/icons-material/AttachFile";
@ -9,7 +16,14 @@ import { TicketMessage } from "@root/model/ticket";
import { sendTicketMessage } from "@root/api/tickets"; import { sendTicketMessage } from "@root/api/tickets";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useTicketStore } from "@root/stores/tickets"; import { useTicketStore } from "@root/stores/tickets";
import { getMessageFromFetchError, throttle, useEventListener, useSSESubscription, useTicketMessages, useToken } from "@frontend/kitui"; import {
getMessageFromFetchError,
throttle,
useEventListener,
useSSESubscription,
useTicketMessages,
useToken,
} from "@frontend/kitui";
import makeRequest from "@root/api/makeRequest"; import makeRequest from "@root/api/makeRequest";
import ChatImage from "./ChatImage"; import ChatImage from "./ChatImage";
import ChatDocument from "./ChatDocument"; import ChatDocument from "./ChatDocument";
@ -17,319 +31,334 @@ import ChatVideo from "./ChatVideo";
import ChatMessage from "./ChatMessage"; import ChatMessage from "./ChatMessage";
import { ACCEPT_SEND_MEDIA_TYPES_MAP, MAX_FILE_SIZE, MAX_PHOTO_SIZE, MAX_VIDEO_SIZE } from "./fileUpload"; import { ACCEPT_SEND_MEDIA_TYPES_MAP, MAX_FILE_SIZE, MAX_PHOTO_SIZE, MAX_VIDEO_SIZE } from "./fileUpload";
const tooLarge = "Файл слишком большой" const tooLarge = "Файл слишком большой";
const checkAcceptableMediaType = (file: File) => { const checkAcceptableMediaType = (file: File) => {
if (file === null) return "" if (file === null) return "";
const segments = file?.name.split('.'); const segments = file?.name.split(".");
const extension = segments[segments.length - 1]; const extension = segments[segments.length - 1];
const type = extension.toLowerCase(); const type = extension.toLowerCase();
switch (type) { switch (type) {
case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find(name => name === type): case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find((name) => name === type):
if (file.size > MAX_FILE_SIZE) return tooLarge if (file.size > MAX_FILE_SIZE) return tooLarge;
return "" return "";
case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find(name => name === type): case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find((name) => name === type):
if (file.size > MAX_PHOTO_SIZE) return tooLarge if (file.size > MAX_PHOTO_SIZE) return tooLarge;
return "" return "";
case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find(name => name === type): case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find((name) => name === type):
if (file.size > MAX_VIDEO_SIZE) return tooLarge if (file.size > MAX_VIDEO_SIZE) return tooLarge;
return "" return "";
default: default:
return "Не удалось отправить файл. Недопустимый тип" return "Не удалось отправить файл. Недопустимый тип";
} }
} };
export default function Chat() { export default function Chat() {
const token = useToken(); const token = useToken();
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const tickets = useTicketStore(state => state.tickets); const tickets = useTicketStore((state) => state.tickets);
const messages = useMessageStore(state => state.messages); const messages = useMessageStore((state) => state.messages);
const [messageField, setMessageField] = useState<string>(""); const [messageField, setMessageField] = useState<string>("");
const ticketId = useParams().ticketId; const ticketId = useParams().ticketId;
const chatBoxRef = useRef<HTMLDivElement>(null); const chatBoxRef = useRef<HTMLDivElement>(null);
const messageApiPage = useMessageStore(state => state.apiPage); const messageApiPage = useMessageStore((state) => state.apiPage);
const messagesPerPage = useMessageStore(state => state.messagesPerPage); const messagesPerPage = useMessageStore((state) => state.messagesPerPage);
const isPreventAutoscroll = useMessageStore(state => state.isPreventAutoscroll); const isPreventAutoscroll = useMessageStore((state) => state.isPreventAutoscroll);
const fetchState = useMessageStore(state => state.ticketMessagesFetchState); const fetchState = useMessageStore((state) => state.ticketMessagesFetchState);
const lastMessageId = useMessageStore(state => state.lastMessageId); const lastMessageId = useMessageStore((state) => state.lastMessageId);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [disableFileButton, setDisableFileButton] = useState(false); const [disableFileButton, setDisableFileButton] = useState(false);
const ticket = tickets.find(ticket => ticket.id === ticketId); const ticket = tickets.find((ticket) => ticket.id === ticketId);
useTicketMessages({ useTicketMessages({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages",
ticketId, ticketId,
messagesPerPage, messagesPerPage,
messageApiPage, messageApiPage,
onSuccess: messages => { onSuccess: (messages) => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1; if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
addOrUpdateMessages(messages); addOrUpdateMessages(messages);
}, },
onError: (error: Error) => { onError: (error: Error) => {
const message = getMessageFromFetchError(error); const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}, },
onFetchStateChange: setTicketMessagesFetchState, onFetchStateChange: setTicketMessagesFetchState,
}); });
useSSESubscription<TicketMessage>({ useSSESubscription<TicketMessage>({
enabled: Boolean(token) && Boolean(ticketId), enabled: Boolean(token) && Boolean(ticketId),
url: process.env.REACT_APP_DOMAIN + `/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`, url: process.env.REACT_APP_DOMAIN + `/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`,
onNewData: addOrUpdateMessages, onNewData: addOrUpdateMessages,
onDisconnect: () => { onDisconnect: () => {
clearMessageState(); clearMessageState();
setIsPreventAutoscroll(false); setIsPreventAutoscroll(false);
}, },
marker: "ticket message" marker: "ticket message",
}); });
const throttledScrollHandler = useMemo(() => throttle(() => { const throttledScrollHandler = useMemo(
const chatBox = chatBoxRef.current; () =>
if (!chatBox) return; throttle(() => {
const chatBox = chatBoxRef.current;
if (!chatBox) return;
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20; const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20;
setIsPreventAutoscroll(isPreventAutoscroll); setIsPreventAutoscroll(isPreventAutoscroll);
if (fetchState !== "idle") return; if (fetchState !== "idle") return;
if (chatBox.scrollTop < chatBox.clientHeight) { if (chatBox.scrollTop < chatBox.clientHeight) {
incrementMessageApiPage(); incrementMessageApiPage();
} }
}, 200), [fetchState]); }, 200),
[fetchState]
);
useEventListener("scroll", throttledScrollHandler, chatBoxRef); useEventListener("scroll", throttledScrollHandler, chatBoxRef);
useEffect(function scrollOnNewMessage() { useEffect(
if (!chatBoxRef.current) return; function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) { if (!isPreventAutoscroll) {
setTimeout(() => { setTimeout(() => {
scrollToBottom(); scrollToBottom();
}, 50); }, 50);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessageId]); },
[lastMessageId]
);
function scrollToBottom(behavior?: ScrollBehavior) { function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return; if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current; const chatBox = chatBoxRef.current;
chatBox.scroll({ chatBox.scroll({
left: 0, left: 0,
top: chatBox.scrollHeight, top: chatBox.scrollHeight,
behavior, behavior,
}); });
} }
function handleSendMessage() { function handleSendMessage() {
if (!ticket || !messageField) return; if (!ticket || !messageField) return;
sendTicketMessage({ sendTicketMessage({
files: [], files: [],
lang: "ru", lang: "ru",
message: messageField, message: messageField,
ticket: ticket.id, ticket: ticket.id,
}); });
setMessageField(""); setMessageField("");
} }
const sendFile = async (file: File) => { const sendFile = async (file: File) => {
if (file === undefined) return true; if (file === undefined) return true;
let data;
const ticketId = ticket?.id
if (ticketId !== undefined) {
try {
const body = new FormData();
body.append(file.name, file);
body.append("ticket", ticketId);
await makeRequest({
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles",
body: body,
method: "POST",
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
}
return true;
}
};
const sendFileHC = async (file: File) => {
const check = checkAcceptableMediaType(file)
if (check.length > 0) {
enqueueSnackbar(check)
return
}
setDisableFileButton(true)
await sendFile(file)
setDisableFileButton(false)
};
function handleTextfieldKeyPress(e: KeyboardEvent) { let data;
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}
return ( const ticketId = ticket?.id;
<Box sx={{ if (ticketId !== undefined) {
border: "1px solid", try {
borderColor: theme.palette.grayDark.main, const body = new FormData();
height: "600px",
borderRadius: "3px",
p: "8px",
display: "flex",
flex: upMd ? "2 0 0" : undefined,
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
gap: "8px",
}}>
<Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
<Box
ref={chatBoxRef}
sx={{
width: "100%",
backgroundColor: "#46474a",
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: "12px",
p: "8px",
overflow: "auto",
colorScheme: "dark",
}}
>
{ticket &&
messages.map((message) => {
const isFileVideo = () => {
if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType),
))
}
};
const isFileImage = () => {
if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType),
))
}
};
const isFileDocument = () => {
if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType),
))
}
};
if (message.files !== null && message.files.length > 0 && isFileImage()) {
return <ChatImage
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
}
if (message.files !== null && message.files.length > 0 && isFileVideo()) {
return <ChatVideo
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
}
if (message.files !== null && message.files.length > 0 && isFileDocument()) {
return <ChatDocument
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
}
return <ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
}) body.append(file.name, file);
} body.append("ticket", ticketId);
</Box> await makeRequest({
{ticket && url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles",
<TextField body: body,
value={messageField} method: "POST",
onChange={e => setMessageField(e.target.value)} });
onKeyPress={handleTextfieldKeyPress} } catch (error: any) {
id="message-input" const errorMessage = getMessageFromFetchError(error);
placeholder="Написать сообщение" if (errorMessage) enqueueSnackbar(errorMessage);
fullWidth }
multiline return true;
maxRows={8} }
InputProps={{ };
style: { const sendFileHC = async (file: File) => {
backgroundColor: theme.palette.content.main, const check = checkAcceptableMediaType(file);
color: theme.palette.secondary.main, if (check.length > 0) {
}, enqueueSnackbar(check);
endAdornment: ( return;
<InputAdornment position="end"> }
<IconButton setDisableFileButton(true);
onClick={handleSendMessage} await sendFile(file);
sx={{ setDisableFileButton(false);
height: "45px", };
width: "45px",
p: 0, function handleTextfieldKeyPress(e: KeyboardEvent) {
}} if (e.key === "Enter" && !e.shiftKey) {
> e.preventDefault();
<SendIcon sx={{ color: theme.palette.golden.main }} /> handleSendMessage();
</IconButton> }
<IconButton }
onClick={() => {
if (!disableFileButton) fileInputRef.current?.click() return (
}} <Box
sx={{ sx={{
height: "45px", border: "1px solid",
width: "45px", borderColor: theme.palette.grayDark.main,
p: 0, height: "600px",
}} borderRadius: "3px",
> p: "8px",
<input display: "flex",
ref={fileInputRef} flex: upMd ? "2 0 0" : undefined,
id="fileinput" flexDirection: "column",
onChange={(e) => { justifyContent: "center",
if (e.target.files?.[0]) sendFileHC(e.target.files?.[0]); alignItems: "center",
}} gap: "8px",
style={{ display: "none" }} }}
type="file" >
/> <Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
<AttachFileIcon sx={{ color: theme.palette.golden.main }} /> <Box
</IconButton> ref={chatBoxRef}
</InputAdornment> sx={{
) width: "100%",
}} backgroundColor: "#46474a",
InputLabelProps={{ flexGrow: 1,
style: { display: "flex",
color: theme.palette.secondary.main, flexDirection: "column",
} gap: "12px",
}} p: "8px",
/> overflow: "auto",
} colorScheme: "dark",
</Box> }}
); >
{ticket &&
messages.map((message) => {
const isFileVideo = () => {
if (message.files) {
return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
const isFileImage = () => {
if (message.files) {
return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
const isFileDocument = () => {
if (message.files) {
return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
if (message.files !== null && message.files.length > 0 && isFileImage()) {
return (
<ChatImage
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
if (message.files !== null && message.files.length > 0 && isFileVideo()) {
return (
<ChatVideo
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
if (message.files !== null && message.files.length > 0 && isFileDocument()) {
return (
<ChatDocument
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
return (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
})}
</Box>
{ticket && (
<TextField
value={messageField}
onChange={(e) => setMessageField(e.target.value)}
onKeyPress={handleTextfieldKeyPress}
id="message-input"
placeholder="Написать сообщение"
fullWidth
multiline
maxRows={8}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
},
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleSendMessage}
sx={{
height: "45px",
width: "45px",
p: 0,
}}
>
<SendIcon sx={{ color: theme.palette.golden.main }} />
</IconButton>
<IconButton
onClick={() => {
if (!disableFileButton) fileInputRef.current?.click();
}}
sx={{
height: "45px",
width: "45px",
p: 0,
}}
>
<input
ref={fileInputRef}
id="fileinput"
onChange={(e) => {
if (e.target.files?.[0]) sendFileHC(e.target.files?.[0]);
}}
style={{ display: "none" }}
type="file"
/>
<AttachFileIcon sx={{ color: theme.palette.golden.main }} />
</IconButton>
</InputAdornment>
),
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main,
},
}}
/>
)}
</Box>
);
} }

@ -1,62 +1,58 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from "@mui/icons-material/Download";
interface Props { interface Props {
unAuthenticated?: boolean; unAuthenticated?: boolean;
isSelf: boolean; isSelf: boolean;
file: string; file: string;
createdAt: string; createdAt: string;
} }
export default function ChatDocument({ export default function ChatDocument({ unAuthenticated = false, isSelf, file, createdAt }: Props) {
unAuthenticated = false, const theme = useTheme();
isSelf,
file,
createdAt,
}: Props) {
const theme = useTheme();
const date = new Date(createdAt); const date = new Date(createdAt);
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "9px", gap: "9px",
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px", padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
justifyContent: isSelf ? "end" : "start", justifyContent: isSelf ? "end" : "start",
}} }}
> >
<Typography sx={{ <Typography
fontSize: "12px", sx={{
alignSelf: "end", fontSize: "12px",
}}> alignSelf: "end",
{new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} }}
</Typography> >
<Box {new Date(createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
sx={{ </Typography>
backgroundColor: "#2a2b2c", <Box
p: "12px", sx={{
border: `1px solid ${theme.palette.golden.main}`, backgroundColor: "#2a2b2c",
borderRadius: "20px", p: "12px",
borderTopLeftRadius: isSelf ? "20px" : 0, border: `1px solid ${theme.palette.golden.main}`,
borderTopRightRadius: isSelf ? 0 : "20px", borderRadius: "20px",
maxWidth: "90%", borderTopLeftRadius: isSelf ? "20px" : 0,
}} borderTopRightRadius: isSelf ? 0 : "20px",
> maxWidth: "90%",
<Link }}
>
download <Link
href={`https://admin.pena/pair/${file}`} download
style={{ href={`https://admin.pena/pair/${file}`}
color: "#7E2AEA", style={{
display: "flex", color: "#7E2AEA",
gap: "10px", display: "flex",
}} gap: "10px",
> }}
<DownloadIcon/> >
</Link> <DownloadIcon />
</Box> </Link>
</Box> </Box>
); </Box>
);
} }

@ -1,68 +1,57 @@
import { import { Box, ButtonBase, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
Box,
ButtonBase,
Link,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
interface Props { interface Props {
unAuthenticated?: boolean; unAuthenticated?: boolean;
isSelf: boolean; isSelf: boolean;
file: string; file: string;
createdAt: string; createdAt: string;
} }
export default function ChatImage({ export default function ChatImage({ unAuthenticated = false, isSelf, file, createdAt }: Props) {
unAuthenticated = false, const theme = useTheme();
isSelf, const upMd = useMediaQuery(theme.breakpoints.up("md"));
file, const navigate = useNavigate();
createdAt,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
return (
return ( <Box
<Box sx={{
sx={{ display: "flex",
display: "flex", gap: "9px",
gap: "9px", padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px", justifyContent: isSelf ? "end" : "start",
justifyContent: isSelf ? "end" : "start", }}
}} >
> <Typography
<Typography sx={{ sx={{
fontSize: "12px", fontSize: "12px",
alignSelf: "end", alignSelf: "end",
}}> }}
{new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} >
</Typography> {new Date(createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
<Box </Typography>
sx={{ <Box
backgroundColor: "#2a2b2c", sx={{
p: "12px", backgroundColor: "#2a2b2c",
border: `1px solid ${theme.palette.golden.main}`, p: "12px",
borderRadius: "20px", border: `1px solid ${theme.palette.golden.main}`,
borderTopLeftRadius: isSelf ? "20px" : 0, borderRadius: "20px",
borderTopRightRadius: isSelf ? 0 : "20px", borderTopLeftRadius: isSelf ? "20px" : 0,
maxWidth: "90%", borderTopRightRadius: isSelf ? 0 : "20px",
}} maxWidth: "90%",
> }}
<ButtonBase target="_blank" href={`/image/${file}`}> >
<Box <ButtonBase target="_blank" href={`/image/${file}`}>
component="img" <Box
sx={{ component="img"
height: "217px", sx={{
width: "217px", height: "217px",
}} width: "217px",
src={`https://admin.pena/pair/${file}`} }}
/> src={`https://admin.pena/pair/${file}`}
</ButtonBase> />
</Box> </ButtonBase>
</Box> </Box>
); </Box>
);
} }

@ -1,53 +1,48 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
interface Props { interface Props {
unAuthenticated?: boolean; unAuthenticated?: boolean;
isSelf: boolean; isSelf: boolean;
text: string; text: string;
createdAt: string; createdAt: string;
} }
export default function ChatMessage({ export default function ChatMessage({ unAuthenticated = false, isSelf, text, createdAt }: Props) {
unAuthenticated = false, const theme = useTheme();
isSelf, const upMd = useMediaQuery(theme.breakpoints.up("md"));
text,
createdAt,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<Box
return ( sx={{
<Box display: "flex",
sx={{ gap: "9px",
display: "flex", padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
gap: "9px", justifyContent: isSelf ? "end" : "start",
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px", }}
justifyContent: isSelf ? "end" : "start", >
}} <Typography
> sx={{
<Typography sx={{ fontSize: "12px",
fontSize: "12px", alignSelf: "end",
alignSelf: "end", }}
}}> >
{new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {new Date(createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
backgroundColor: "#2a2b2c", backgroundColor: "#2a2b2c",
p: "12px", p: "12px",
border: `1px solid ${theme.palette.golden.main}`, border: `1px solid ${theme.palette.golden.main}`,
borderRadius: "20px", borderRadius: "20px",
borderTopLeftRadius: isSelf ? "20px" : 0, borderTopLeftRadius: isSelf ? "20px" : 0,
borderTopRightRadius: isSelf ? 0 : "20px", borderTopRightRadius: isSelf ? 0 : "20px",
maxWidth: "90%", maxWidth: "90%",
}} }}
> >
<Typography fontSize="14px" sx={{ wordBreak: "break-word" }}> <Typography fontSize="14px" sx={{ wordBreak: "break-word" }}>
{text} {text}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
); );
} }

@ -2,66 +2,61 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
interface Props { interface Props {
unAuthenticated?: boolean; unAuthenticated?: boolean;
isSelf: boolean; isSelf: boolean;
file: string; file: string;
createdAt: string; createdAt: string;
} }
export default function ChatImage({ export default function ChatImage({ unAuthenticated = false, isSelf, file, createdAt }: Props) {
unAuthenticated = false, const theme = useTheme();
isSelf, const upMd = useMediaQuery(theme.breakpoints.up("md"));
file, const navigate = useNavigate();
createdAt,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "9px", gap: "9px",
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px", padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
justifyContent: isSelf ? "end" : "start", justifyContent: isSelf ? "end" : "start",
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "12px", fontSize: "12px",
alignSelf: "end", alignSelf: "end",
}} }}
> >
{new Date(createdAt).toLocaleTimeString([], { {new Date(createdAt).toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
})} })}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
backgroundColor: "#2a2b2c", backgroundColor: "#2a2b2c",
p: "12px", p: "12px",
border: `1px solid ${theme.palette.golden.main}`, border: `1px solid ${theme.palette.golden.main}`,
borderRadius: "20px", borderRadius: "20px",
borderTopLeftRadius: isSelf ? "20px" : 0, borderTopLeftRadius: isSelf ? "20px" : 0,
borderTopRightRadius: isSelf ? 0 : "20px", borderTopRightRadius: isSelf ? 0 : "20px",
maxWidth: "90%", maxWidth: "90%",
}} }}
> >
<Box <Box
component="video" component="video"
sx={{ sx={{
pointerEvents: "auto", pointerEvents: "auto",
height: "217px", height: "217px",
width: "auto", width: "auto",
minWidth: "217px", minWidth: "217px",
}} }}
controls controls
> >
<source src={`https://admin.pena/pair/${file}`} /> <source src={`https://admin.pena/pair/${file}`} />
</Box> </Box>
</Box> </Box>
</Box> </Box>
); );
} }

@ -1,44 +1,49 @@
import { Box, Typography, useTheme } from "@mui/material"; import { Box, Typography, useTheme } from "@mui/material";
import { TicketMessage } from "@root/model/ticket"; import { TicketMessage } from "@root/model/ticket";
interface Props { interface Props {
message: TicketMessage; message: TicketMessage;
isSelf?: boolean; isSelf?: boolean;
} }
export default function Message({ message, isSelf }: Props) { export default function Message({ message, isSelf }: Props) {
const theme = useTheme(); const theme = useTheme();
const time = ( const time = (
<Typography sx={{ <Typography
fontSize: "12px", sx={{
alignSelf: "end", fontSize: "12px",
}}> alignSelf: "end",
{new Date(message.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} }}
</Typography> >
); {new Date(message.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
return ( </Typography>
<Box sx={{ );
display: "flex", return (
justifyContent: isSelf ? "end" : "start", <Box
gap: "6px", sx={{
}}> display: "flex",
{isSelf && time} justifyContent: isSelf ? "end" : "start",
<Box sx={{ gap: "6px",
backgroundColor: "#2a2b2c", }}
p: "12px", >
border: `1px solid ${theme.palette.golden.main}`, {isSelf && time}
borderRadius: "20px", <Box
borderTopLeftRadius: isSelf ? "20px" : 0, sx={{
borderTopRightRadius: isSelf ? 0 : "20px", backgroundColor: "#2a2b2c",
maxWidth: "90%", p: "12px",
}}> border: `1px solid ${theme.palette.golden.main}`,
<Typography fontSize="14px" sx={{ wordBreak: "break-word" }}> borderRadius: "20px",
{message.message} borderTopLeftRadius: isSelf ? "20px" : 0,
</Typography> borderTopRightRadius: isSelf ? 0 : "20px",
</Box> maxWidth: "90%",
{!isSelf && time} }}
</Box> >
); <Typography fontSize="14px" sx={{ wordBreak: "break-word" }}>
} {message.message}
</Typography>
</Box>
{!isSelf && time}
</Box>
);
}

@ -3,7 +3,7 @@ export const MAX_PHOTO_SIZE = 5242880;
export const MAX_VIDEO_SIZE = 52428800; export const MAX_VIDEO_SIZE = 52428800;
export const ACCEPT_SEND_MEDIA_TYPES_MAP = { export const ACCEPT_SEND_MEDIA_TYPES_MAP = {
picture: ["jpg", "png"], picture: ["jpg", "png"],
video: ["mp4"], video: ["mp4"],
document: ["doc", "docx", "pdf", "txt", "xlsx", "csv"], document: ["doc", "docx", "pdf", "txt", "xlsx", "csv"],
} as const; } as const;

@ -2,18 +2,18 @@ import { Box } from "@mui/material";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
export default function ChatImageNewWindow() { export default function ChatImageNewWindow() {
const location = useLocation(); const location = useLocation();
const srcImage = location.pathname.split("image/")[1]; const srcImage = location.pathname.split("image/")[1];
return ( return (
<> <>
<Box <Box
component="img" component="img"
sx={{ sx={{
maxHeight: "100vh", maxHeight: "100vh",
maxWidth: "100vw", maxWidth: "100vw",
}} }}
src={`https://admin.pena/pair/${srcImage}`} src={`https://admin.pena/pair/${srcImage}`}
/> />
</> </>
); );
} }

@ -3,51 +3,51 @@ import { Box, Typography, useTheme } from "@mui/material";
import ExpandIcon from "./ExpandIcon"; import ExpandIcon from "./ExpandIcon";
interface Props { interface Props {
headerText: string; headerText: string;
children: (callback: () => void) => ReactNode; children: (callback: () => void) => ReactNode;
} }
export default function Collapse({ headerText, children }: Props) { export default function Collapse({ headerText, children }: Props) {
const theme = useTheme(); const theme = useTheme();
const [isExpanded, setIsExpanded] = useState<boolean>(false); const [isExpanded, setIsExpanded] = useState<boolean>(false);
return ( return (
<Box <Box
sx={{ sx={{
position: "relative", position: "relative",
}} }}
> >
<Box <Box
onClick={() => setIsExpanded((prev) => !prev)} onClick={() => setIsExpanded((prev) => !prev)}
sx={{ sx={{
height: "72px", height: "72px",
p: "16px", p: "16px",
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
borderRadius: "12px", borderRadius: "12px",
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
cursor: "pointer", cursor: "pointer",
userSelect: "none", userSelect: "none",
}} }}
> >
<Typography variant="h4">{headerText}</Typography> <Typography variant="h4">{headerText}</Typography>
<ExpandIcon isExpanded={isExpanded} /> <ExpandIcon isExpanded={isExpanded} />
</Box> </Box>
{isExpanded && ( {isExpanded && (
<Box <Box
sx={{ sx={{
mt: "8px", mt: "8px",
position: "absolute", position: "absolute",
zIndex: 100, zIndex: 100,
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
width: "100%", width: "100%",
}} }}
> >
{children(() => setIsExpanded(false))} {children(() => setIsExpanded(false))}
</Box> </Box>
)} )}
</Box> </Box>
); );
} }

@ -1,17 +1,34 @@
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material";
interface Props { interface Props {
isExpanded: boolean; isExpanded: boolean;
} }
export default function ExpandIcon({ isExpanded }: Props) { export default function ExpandIcon({ isExpanded }: Props) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<svg style={{ transform: isExpanded ? "rotate(180deg)" : undefined }} xmlns="http://www.w3.org/2000/svg" width="32" height="33" viewBox="0 0 32 33" fill="none"> <svg
<path stroke={isExpanded ? theme.palette.golden.main : theme.palette.goldenDark.main} d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z" strokeWidth="2" strokeMiterlimit="10" /> style={{ transform: isExpanded ? "rotate(180deg)" : undefined }}
<path stroke={isExpanded ? theme.palette.golden.main : theme.palette.goldenDark.main} d="M20.5 15.2949L16 20.2949L11.5 15.2949" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> xmlns="http://www.w3.org/2000/svg"
</svg> width="32"
); height="33"
viewBox="0 0 32 33"
fill="none"
>
<path
stroke={isExpanded ? theme.palette.golden.main : theme.palette.goldenDark.main}
d="M16 28.7949C22.6274 28.7949 28 23.4223 28 16.7949C28 10.1675 22.6274 4.79492 16 4.79492C9.37258 4.79492 4 10.1675 4 16.7949C4 23.4223 9.37258 28.7949 16 28.7949Z"
strokeWidth="2"
strokeMiterlimit="10"
/>
<path
stroke={isExpanded ? theme.palette.golden.main : theme.palette.goldenDark.main}
d="M20.5 15.2949L16 20.2949L11.5 15.2949"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
} }

@ -4,100 +4,79 @@ import Chat from "./Chat/Chat";
import Collapse from "./Collapse"; import Collapse from "./Collapse";
import TicketList from "./TicketList/TicketList"; import TicketList from "./TicketList/TicketList";
import { Ticket } from "@root/model/ticket"; import { Ticket } from "@root/model/ticket";
import { import { clearTickets, setTicketsFetchState, updateTickets, useTicketStore } from "@root/stores/tickets";
clearTickets,
setTicketsFetchState,
updateTickets,
useTicketStore,
} from "@root/stores/tickets";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { clearMessageState } from "@root/stores/messages"; import { clearMessageState } from "@root/stores/messages";
import { import { getMessageFromFetchError, useSSESubscription, useTicketsFetcher, useToken } from "@frontend/kitui";
getMessageFromFetchError,
useSSESubscription,
useTicketsFetcher,
useToken,
} from "@frontend/kitui";
import ModalUser from "@root/pages/dashboard/ModalUser"; import ModalUser from "@root/pages/dashboard/ModalUser";
export default function Support() { export default function Support() {
const [openUserModal, setOpenUserModal] = useState<boolean>(false); const [openUserModal, setOpenUserModal] = useState<boolean>(false);
const [activeUserId, setActiveUserId] = useState<string>(""); const [activeUserId, setActiveUserId] = useState<string>("");
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage); const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage);
const ticketApiPage = useTicketStore((state) => state.apiPage); const ticketApiPage = useTicketStore((state) => state.apiPage);
const token = useToken(); const token = useToken();
useTicketsFetcher({ useTicketsFetcher({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets",
ticketsPerPage, ticketsPerPage,
ticketApiPage, ticketApiPage,
onSuccess: (result) => { onSuccess: (result) => {
if (result.data) updateTickets(result.data); if (result.data) updateTickets(result.data);
}, },
onError: (error: Error) => { onError: (error: Error) => {
const message = getMessageFromFetchError(error); const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}, },
onFetchStateChange: setTicketsFetchState, onFetchStateChange: setTicketsFetchState,
}); });
useSSESubscription<Ticket>({ useSSESubscription<Ticket>({
enabled: Boolean(token), enabled: Boolean(token),
url: url: process.env.REACT_APP_DOMAIN + `/heruvym/subscribe?Authorization=${token}`,
process.env.REACT_APP_DOMAIN + onNewData: updateTickets,
`/heruvym/subscribe?Authorization=${token}`, onDisconnect: () => {
onNewData: updateTickets, clearMessageState();
onDisconnect: () => { clearTickets();
clearMessageState(); },
clearTickets(); marker: "ticket",
}, });
marker: "ticket",
});
useEffect(() => { useEffect(() => {
if (!openUserModal) { if (!openUserModal) {
setActiveUserId(""); setActiveUserId("");
} }
}, [openUserModal]); }, [openUserModal]);
useEffect(() => { useEffect(() => {
if (activeUserId) { if (activeUserId) {
setOpenUserModal(true); setOpenUserModal(true);
return; return;
} }
setOpenUserModal(false); setOpenUserModal(false);
}, [activeUserId]); }, [activeUserId]);
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
width: "100%", width: "100%",
flexDirection: upMd ? "row" : "column", flexDirection: upMd ? "row" : "column",
gap: "12px", gap: "12px",
}} }}
> >
{!upMd && ( {!upMd && (
<Collapse headerText="Тикеты"> <Collapse headerText="Тикеты">
{(closeCollapse) => ( {(closeCollapse) => <TicketList closeCollapse={closeCollapse} setActiveUserId={setActiveUserId} />}
<TicketList </Collapse>
closeCollapse={closeCollapse} )}
setActiveUserId={setActiveUserId} <Chat />
/> {upMd && <TicketList setActiveUserId={setActiveUserId} />}
)} <ModalUser open={openUserModal} onClose={() => setOpenUserModal(false)} userId={activeUserId} />
</Collapse> </Box>
)} );
<Chat />
{upMd && <TicketList setActiveUserId={setActiveUserId} />}
<ModalUser
open={openUserModal}
onClose={() => setOpenUserModal(false)}
userId={activeUserId}
/>
</Box>
);
} }

@ -1,87 +1,83 @@
import Modal from "@mui/material/Modal"; import Modal from "@mui/material/Modal";
import {closeDeleteTariffDialog} from "@stores/tariffs"; import { closeDeleteTariffDialog } from "@stores/tariffs";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import makeRequest from "@root/api/makeRequest"; import makeRequest from "@root/api/makeRequest";
import {parseAxiosError} from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
interface Props{ interface Props {
ticketId: string | undefined, ticketId: string | undefined;
openModal: boolean, openModal: boolean;
setOpenModal: (a: boolean) => void setOpenModal: (a: boolean) => void;
} }
export default function CloseTicketModal({ticketId, openModal, setOpenModal}: Props) { export default function CloseTicketModal({ ticketId, openModal, setOpenModal }: Props) {
const CloseTicket = async () => {
try {
const ticketCloseResponse = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/heruvym/close",
method: "post",
useToken: true,
body: {
ticket: ticketId,
},
});
const CloseTicket = async () => { return [ticketCloseResponse];
try { } catch (nativeError) {
const ticketCloseResponse = await makeRequest<unknown, unknown>({ const [error] = parseAxiosError(nativeError);
url: process.env.REACT_APP_DOMAIN + "/heruvym/close" ,
method: "post",
useToken: true,
body: {
"ticket": ticketId
},
});
return [ticketCloseResponse]; return [null, `Не удалось закрыть тикет. ${error}`];
} catch (nativeError) { }
const [error] = parseAxiosError(nativeError); };
return [null, `Не удалось закрыть тикет. ${error}`]; return (
} <Modal
} open={openModal}
onClose={() => setOpenModal(false)}
return ( aria-labelledby="modal-modal-title"
<Modal aria-describedby="modal-modal-description"
open={openModal} >
onClose={() => setOpenModal(false)} <Box
aria-labelledby="modal-modal-title" sx={{
aria-describedby="modal-modal-description" position: "absolute",
> top: "50%",
<Box left: "50%",
sx={{ transform: "translate(-50%, -50%)",
position: "absolute", width: 400,
top: "50%", bgcolor: "background.paper",
left: "50%", border: "2px solid gray",
transform: "translate(-50%, -50%)", borderRadius: "6px",
width: 400, boxShadow: 24,
bgcolor: "background.paper", p: 4,
border: "2px solid gray", }}
borderRadius: "6px", >
boxShadow: 24, <Typography id="modal-modal-title" variant="h6" component="h2">
p: 4, Вы уверены, что хотите закрыть тикет?
}} </Typography>
> <Box
<Typography id="modal-modal-title" variant="h6" component="h2"> sx={{
Вы уверены, что хотите закрыть тикет? mt: "20px",
</Typography> display: "flex",
<Box width: "332px",
sx={{ justifyContent: "space-between",
mt: "20px", alignItems: "center",
display: "flex", }}
width: "332px", >
justifyContent: "space-between", <Button
alignItems: "center", onClick={async () => {
}} CloseTicket();
> setOpenModal(false);
<Button }}
onClick={async ()=>{ sx={{ width: "40px", height: "25px" }}
CloseTicket() >
setOpenModal(false) Да
}} </Button>
sx={{width: "40px", height: "25px"}} <Button onClick={() => setOpenModal(false)} sx={{ width: "40px", height: "25px" }}>
> Нет
Да </Button>
</Button> </Box>
<Button </Box>
onClick={() => setOpenModal(false)} </Modal>
sx={{width: "40px", height: "25px"}} );
> }
Нет
</Button>
</Box>
</Box>
</Modal>
)
}

@ -1,108 +1,97 @@
import CircleIcon from "@mui/icons-material/Circle"; import CircleIcon from "@mui/icons-material/Circle";
import { import { Box, Card, CardActionArea, CardContent, CardHeader, Divider, Typography, useTheme } from "@mui/material";
Box,
Card,
CardActionArea,
CardContent,
CardHeader,
Divider,
Typography,
useTheme,
} from "@mui/material";
import { green } from "@mui/material/colors"; import { green } from "@mui/material/colors";
import { Ticket } from "@root/model/ticket"; import { Ticket } from "@root/model/ticket";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
const flexCenterSx = { const flexCenterSx = {
textAlign: "center", textAlign: "center",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
padding: "10px", padding: "10px",
}; };
interface Props { interface Props {
ticket: Ticket; ticket: Ticket;
setActiveUserId: (userId: string) => void; setActiveUserId: (userId: string) => void;
} }
export default function TicketItem({ ticket, setActiveUserId }: Props) { export default function TicketItem({ ticket, setActiveUserId }: Props) {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const ticketId = useParams().ticketId; const ticketId = useParams().ticketId;
const isUnread = ticket.user === ticket.top_message.user_id; const isUnread = ticket.user === ticket.top_message.user_id;
const isSelected = ticket.id === ticketId; const isSelected = ticket.id === ticketId;
const unreadSx = { const unreadSx = {
border: "1px solid", border: "1px solid",
borderColor: theme.palette.golden.main, borderColor: theme.palette.golden.main,
backgroundColor: theme.palette.goldenMedium.main, backgroundColor: theme.palette.goldenMedium.main,
}; };
const selectedSx = { const selectedSx = {
border: `2px solid ${theme.palette.secondary.main}`, border: `2px solid ${theme.palette.secondary.main}`,
}; };
function handleCardClick() { function handleCardClick() {
navigate(`/support/${ticket.id}`); navigate(`/support/${ticket.id}`);
} }
return ( return (
<Card <Card
sx={{ sx={{
minHeight: "70px", minHeight: "70px",
backgroundColor: "transparent", backgroundColor: "transparent",
color: "white", color: "white",
...(isUnread && unreadSx), ...(isUnread && unreadSx),
...(isSelected && selectedSx), ...(isSelected && selectedSx),
}} }}
> >
<CardActionArea onClick={handleCardClick}> <CardActionArea onClick={handleCardClick}>
<CardHeader <CardHeader
title={<Typography>{ticket.title}</Typography>} title={<Typography>{ticket.title}</Typography>}
disableTypography disableTypography
sx={{ sx={{
textAlign: "center", textAlign: "center",
p: "4px", p: "4px",
}} }}
/> />
<Divider /> <Divider />
<CardContent <CardContent
sx={{ sx={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
backgroundColor: "transparent", backgroundColor: "transparent",
p: 0, p: 0,
}} }}
> >
<Box sx={flexCenterSx}> <Box sx={flexCenterSx}>{new Date(ticket.top_message.created_at).toLocaleDateString()}</Box>
{new Date(ticket.top_message.created_at).toLocaleDateString()} <Box
</Box> sx={{
<Box ...flexCenterSx,
sx={{ overflow: "hidden",
...flexCenterSx, whiteSpace: "nowrap",
overflow: "hidden", display: "block",
whiteSpace: "nowrap", flexGrow: 1,
display: "block", }}
flexGrow: 1, >
}} {ticket.top_message.message}
> </Box>
{ticket.top_message.message} <Box sx={flexCenterSx}>
</Box> <CircleIcon
<Box sx={flexCenterSx}> sx={{
<CircleIcon color: green[700],
sx={{ transform: "scale(0.8)",
color: green[700], }}
transform: "scale(0.8)", />
}} </Box>
/> <Box sx={flexCenterSx} onClick={() => setActiveUserId(ticket.user)}>
</Box> ИНФО
<Box sx={flexCenterSx} onClick={() => setActiveUserId(ticket.user)}> </Box>
ИНФО </CardContent>
</Box> </CardActionArea>
</CardContent> </Card>
</CardActionArea> );
</Card>
);
} }

@ -3,152 +3,140 @@ import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined";
import { Box, Button, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { Ticket } from "@root/model/ticket"; import { Ticket } from "@root/model/ticket";
import { incrementTicketsApiPage, useTicketStore } from "@root/stores/tickets"; import { incrementTicketsApiPage, useTicketStore } from "@root/stores/tickets";
import {useEffect, useRef, useState} from "react"; import { useEffect, useRef, useState } from "react";
import TicketItem from "./TicketItem"; import TicketItem from "./TicketItem";
import { throttle } from "@frontend/kitui"; import { throttle } from "@frontend/kitui";
import makeRequest from "@root/api/makeRequest"; import makeRequest from "@root/api/makeRequest";
import {parseAxiosError} from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
import {useParams} from "react-router-dom"; import { useParams } from "react-router-dom";
import CloseTicketModal from "@pages/dashboard/Content/Support/TicketList/CloseTicketModal"; import CloseTicketModal from "@pages/dashboard/Content/Support/TicketList/CloseTicketModal";
type TicketListProps = { type TicketListProps = {
closeCollapse?: () => void; closeCollapse?: () => void;
setActiveUserId: (id: string) => void; setActiveUserId: (id: string) => void;
}; };
export default function TicketList({ export default function TicketList({ closeCollapse, setActiveUserId }: TicketListProps) {
closeCollapse, const theme = useTheme();
setActiveUserId, const upMd = useMediaQuery(theme.breakpoints.up("md"));
}: TicketListProps) { const tickets = useTicketStore((state) => state.tickets);
const theme = useTheme(); const ticketsFetchState = useTicketStore((state) => state.ticketsFetchState);
const upMd = useMediaQuery(theme.breakpoints.up("md")); const ticketsBoxRef = useRef<HTMLDivElement>(null);
const tickets = useTicketStore((state) => state.tickets); const ticketId = useParams().ticketId;
const ticketsFetchState = useTicketStore((state) => state.ticketsFetchState); const [openModal, setOpenModal] = useState(false);
const ticketsBoxRef = useRef<HTMLDivElement>(null);
const ticketId = useParams().ticketId;
const [openModal, setOpenModal] = useState(false)
useEffect( useEffect(
function updateCurrentPageOnScroll() { function updateCurrentPageOnScroll() {
if (!ticketsBoxRef.current) return; if (!ticketsBoxRef.current) return;
const ticketsBox = ticketsBoxRef.current; const ticketsBox = ticketsBoxRef.current;
const scrollHandler = () => { const scrollHandler = () => {
const scrollBottom = const scrollBottom = ticketsBox.scrollHeight - ticketsBox.scrollTop - ticketsBox.clientHeight;
ticketsBox.scrollHeight - if (scrollBottom < ticketsBox.clientHeight && ticketsFetchState === "idle") incrementTicketsApiPage();
ticketsBox.scrollTop - };
ticketsBox.clientHeight;
if (
scrollBottom < ticketsBox.clientHeight &&
ticketsFetchState === "idle"
)
incrementTicketsApiPage();
};
const throttledScrollHandler = throttle(scrollHandler, 200); const throttledScrollHandler = throttle(scrollHandler, 200);
ticketsBox.addEventListener("scroll", throttledScrollHandler); ticketsBox.addEventListener("scroll", throttledScrollHandler);
return () => { return () => {
ticketsBox.removeEventListener("scroll", throttledScrollHandler); ticketsBox.removeEventListener("scroll", throttledScrollHandler);
}; };
}, },
[ticketsFetchState] [ticketsFetchState]
); );
const sortedTickets = tickets const sortedTickets = tickets.sort(sortTicketsByUpdateTime).sort(sortTicketsByUnread);
.sort(sortTicketsByUpdateTime)
.sort(sortTicketsByUnread);
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flex: upMd ? "3 0 0" : undefined, flex: upMd ? "3 0 0" : undefined,
maxWidth: upMd ? "400px" : undefined, maxWidth: upMd ? "400px" : undefined,
maxHeight: "600px", maxHeight: "600px",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
}} }}
> >
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
border: "1px solid", border: "1px solid",
borderColor: theme.palette.grayDark.main, borderColor: theme.palette.grayDark.main,
borderRadius: "3px", borderRadius: "3px",
padding: "10px", padding: "10px",
}} }}
> >
<Button <Button
variant="contained" variant="contained"
sx={{ sx={{
backgroundColor: theme.palette.grayDark.main, backgroundColor: theme.palette.grayDark.main,
width: "100%", width: "100%",
height: "45px", height: "45px",
fontSize: "15px", fontSize: "15px",
fontWeight: "normal", fontWeight: "normal",
textTransform: "capitalize", textTransform: "capitalize",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
}, },
}} }}
> >
Поиск Поиск
<SearchOutlinedIcon /> <SearchOutlinedIcon />
</Button> </Button>
<Button <Button
onClick={()=> setOpenModal(true)} onClick={() => setOpenModal(true)}
variant="text" variant="text"
sx={{ sx={{
width: "100%", width: "100%",
height: "35px", height: "35px",
fontSize: "14px", fontSize: "14px",
fontWeight: "normal", fontWeight: "normal",
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
border: "1px solid", border: "1px solid",
borderColor: theme.palette.golden.main, borderColor: theme.palette.golden.main,
borderRadius: 0, borderRadius: 0,
"&:hover": { "&:hover": {
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
}, },
}} }}
> >
ЗАКРЫТЬ ТИКЕТ ЗАКРЫТЬ ТИКЕТ
<HighlightOffOutlinedIcon /> <HighlightOffOutlinedIcon />
</Button> </Button>
<CloseTicketModal openModal={openModal} setOpenModal={setOpenModal} ticketId={ticketId}/> <CloseTicketModal openModal={openModal} setOpenModal={setOpenModal} ticketId={ticketId} />
</Box> </Box>
<Box <Box
ref={ticketsBoxRef} ref={ticketsBoxRef}
sx={{ sx={{
width: "100%", width: "100%",
border: "1px solid", border: "1px solid",
borderColor: theme.palette.grayDark.main, borderColor: theme.palette.grayDark.main,
borderRadius: "3px", borderRadius: "3px",
overflow: "auto", overflow: "auto",
overflowY: "auto", overflowY: "auto",
padding: "10px", padding: "10px",
colorScheme: "dark", colorScheme: "dark",
}} }}
> >
{sortedTickets.map((ticket) => ( {sortedTickets.map((ticket) => (
<Box key={ticket.id} onClick={closeCollapse}> <Box key={ticket.id} onClick={closeCollapse}>
<TicketItem ticket={ticket} setActiveUserId={setActiveUserId} /> <TicketItem ticket={ticket} setActiveUserId={setActiveUserId} />
</Box> </Box>
))} ))}
</Box> </Box>
</Box> </Box>
); );
} }
function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) { function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) {
const date1 = new Date(ticket1.updated_at).getTime(); const date1 = new Date(ticket1.updated_at).getTime();
const date2 = new Date(ticket2.updated_at).getTime(); const date2 = new Date(ticket2.updated_at).getTime();
return date2 - date1; return date2 - date1;
} }
function sortTicketsByUnread(ticket1: Ticket, ticket2: Ticket) { function sortTicketsByUnread(ticket1: Ticket, ticket2: Ticket) {
const isUnread1 = ticket1.user === ticket1.top_message.user_id; const isUnread1 = ticket1.user === ticket1.top_message.user_id;
const isUnread2 = ticket2.user === ticket2.top_message.user_id; const isUnread2 = ticket2.user === ticket2.top_message.user_id;
return Number(isUnread2) - Number(isUnread1); return Number(isUnread2) - Number(isUnread1);
} }

@ -2,80 +2,84 @@ import * as React from "react";
import { Box, Typography, Button } from "@mui/material"; import { Box, Typography, Button } from "@mui/material";
import theme from "../../../../../theme"; import theme from "../../../../../theme";
export interface MWProps { export interface MWProps {
openModal: (type:number, num: number) => void openModal: (type: number, num: number) => void;
} }
const Contractor: React.FC<MWProps> = ({ openModal }) => { const Contractor: React.FC<MWProps> = ({ openModal }) => {
return ( return (
<React.Fragment> <React.Fragment>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
width: "90%", width: "90%",
height: "60px", height: "60px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}}> }}
Сокращатель ссылок >
</Typography> Сокращатель ссылок
</Typography>
<Box sx={{ <Box
marginTop: "35px", sx={{
display: "grid", marginTop: "35px",
gridTemplateColumns: "repeat(2, 1fr)", display: "grid",
gridGap: "20px", gridTemplateColumns: "repeat(2, 1fr)",
marginBottom: "120px", gridGap: "20px",
}}> marginBottom: "120px",
<Button }}
variant = "contained" >
onClick={ () => openModal(3, 1) } <Button
sx={{ variant="contained"
backgroundColor: theme.palette.menu.main, onClick={() => openModal(3, 1)}
padding: '11px 65px', sx={{
fontWeight: "normal", backgroundColor: theme.palette.menu.main,
fontSize: "17px", padding: "11px 65px",
"&:hover": { fontWeight: "normal",
backgroundColor: theme.palette.grayMedium.main fontSize: "17px",
} "&:hover": {
}}> backgroundColor: theme.palette.grayMedium.main,
Создать тариф <br /> на аналитику время },
</Button> }}
<Button >
variant = "contained" Создать тариф <br /> на аналитику время
onClick={ () => openModal(3, 1) } </Button>
sx={{ <Button
backgroundColor: theme.palette.menu.main, variant="contained"
padding: '11px 65px', onClick={() => openModal(3, 1)}
fontWeight: "normal", sx={{
fontSize: "17px", backgroundColor: theme.palette.menu.main,
"&:hover": { padding: "11px 65px",
backgroundColor: theme.palette.grayMedium.main fontWeight: "normal",
} fontSize: "17px",
}}> "&:hover": {
Создать тариф <br /> на a/b тесты время backgroundColor: theme.palette.grayMedium.main,
</Button> },
<Button }}
variant = "contained" >
sx={{ Создать тариф <br /> на a/b тесты время
backgroundColor: theme.palette.menu.main, </Button>
padding: '11px 65px', <Button
fontWeight: "normal", variant="contained"
fontSize: "17px", sx={{
"&:hover": { backgroundColor: theme.palette.menu.main,
backgroundColor: theme.palette.grayMedium.main padding: "11px 65px",
} fontWeight: "normal",
}}> fontSize: "17px",
Изменить тариф "&:hover": {
</Button> backgroundColor: theme.palette.grayMedium.main,
</Box> },
</React.Fragment> }}
); >
} Изменить тариф
</Button>
</Box>
</React.Fragment>
);
};
export default Contractor;
export default Contractor;

@ -1,354 +1,331 @@
import { import {
Typography, Typography,
Container, Container,
Button, Button,
Select, Select,
MenuItem, MenuItem,
FormControl, FormControl,
InputLabel, InputLabel,
useTheme, useTheme,
Box, TextField, Box,
TextField,
} from "@mui/material"; } from "@mui/material";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { requestTariffs } from "@root/services/tariffs.service"; import { requestTariffs } from "@root/services/tariffs.service";
import { createTariff } from "@root/api/tariffs"; import { createTariff } from "@root/api/tariffs";
import { import { findPrivilegeById, usePrivilegeStore } from "@root/stores/privilegesStore";
findPrivilegeById,
usePrivilegeStore,
} from "@root/stores/privilegesStore";
import { Privilege } from "@frontend/kitui"; import { Privilege } from "@frontend/kitui";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import { Formik, Field, Form, FormikHelpers } from "formik"; import { Formik, Field, Form, FormikHelpers } from "formik";
interface Values { interface Values {
nameField: string, nameField: string;
descriptionField: string, descriptionField: string;
amountField: string, amountField: string;
customPriceField: string, customPriceField: string;
privilegeIdField: string, privilegeIdField: string;
orderField: number, orderField: number;
privilege: Privilege | null privilege: Privilege | null;
} }
export default function CreateTariff() { export default function CreateTariff() {
const theme = useTheme(); const theme = useTheme();
const privileges = usePrivilegeStore((store) => store.privileges); const privileges = usePrivilegeStore((store) => store.privileges);
const checkFulledFields = (values: Values) => { const checkFulledFields = (values: Values) => {
const errors = {} as any; const errors = {} as any;
if (values.nameField.length === 0) { if (values.nameField.length === 0) {
errors.nameField = "Пустое название тарифа" errors.nameField = "Пустое название тарифа";
} }
if (values.amountField.length === 0) { if (values.amountField.length === 0) {
errors.amountField = "Пустое кол-во едениц привилегии" errors.amountField = "Пустое кол-во едениц привилегии";
} }
if (values.privilegeIdField.length === 0) { if (values.privilegeIdField.length === 0) {
errors.privilegeIdField = "Не выбрана привилегия" errors.privilegeIdField = "Не выбрана привилегия";
} }
return errors; return errors;
};
}; const initialValues: Values = {
nameField: "",
descriptionField: "",
amountField: "",
customPriceField: "",
privilegeIdField: "",
orderField: 0,
privilege: null,
};
const initialValues: Values = { const createTariffBackend = async (values: Values, formikHelpers: FormikHelpers<Values>) => {
nameField: "", if (values.privilege !== null) {
descriptionField: "", const [, createdTariffError] = await createTariff({
amountField: "", name: values.nameField,
customPriceField: "", price: Number(values.customPriceField) * 100,
privilegeIdField: "", order: values.orderField,
orderField: 0, isCustom: false,
privilege: null description: values.descriptionField,
}; privileges: [
{
name: values.privilege.name,
privilegeId: values.privilege.privilegeId ?? "",
serviceKey: values.privilege.serviceKey,
description: values.privilege.description,
type: values.privilege.type,
value: values.privilege.value ?? "",
price: values.privilege.price,
amount: Number(values.amountField),
},
],
});
const createTariffBackend = async ( if (createdTariffError) {
values: Values, return enqueueSnackbar(createdTariffError);
formikHelpers: FormikHelpers<Values> }
) => {
if (values.privilege !== null) {
const [, createdTariffError] = await createTariff({
name: values.nameField,
price: Number(values.customPriceField) * 100,
order: values.orderField,
isCustom: false,
description: values.descriptionField,
privileges: [
{
name: values.privilege.name,
privilegeId: values.privilege.privilegeId ?? "",
serviceKey: values.privilege.serviceKey,
description: values.privilege.description,
type: values.privilege.type,
value: values.privilege.value ?? "",
price: values.privilege.price,
amount: Number(values.amountField),
},
],
});
if (createdTariffError) { enqueueSnackbar("Тариф создан");
return enqueueSnackbar(createdTariffError);
}
enqueueSnackbar("Тариф создан"); await requestTariffs();
}
};
await requestTariffs(); // const createTariffFrontend = () => {
} // if (checkFulledFields() && privilege !== null) {
}; // updateTariffStore({
// id: nanoid(5),
// name: nameField,
// amount: Number(amountField),
// isFront: true,
// privilegeId: privilege.privilegeId,
// customPricePerUnit: customPriceField.length !== 0 ? Number(customPriceField)*100: undefined,
// })
// }
// }
// const createTariffFrontend = () => { return (
// if (checkFulledFields() && privilege !== null) { <Formik initialValues={initialValues} validate={checkFulledFields} onSubmit={createTariffBackend}>
// updateTariffStore({ {(props) => (
// id: nanoid(5), <Form style={{ width: "100%" }}>
// name: nameField, <Container
// amount: Number(amountField), sx={{
// isFront: true, p: "20px",
// privilegeId: privilege.privilegeId, border: "1px solid rgba(224, 224, 224, 1)",
// customPricePerUnit: customPriceField.length !== 0 ? Number(customPriceField)*100: undefined, borderRadius: "4px",
// }) display: "flex",
// } flexDirection: "column",
// } gap: "12px",
}}
return ( >
<Formik <Typography variant="h6" sx={{ textAlign: "center", mb: "16px" }}>
initialValues={initialValues} Создание тарифа
validate={checkFulledFields} </Typography>
onSubmit={createTariffBackend} <FormControl
> fullWidth
{(props) => ( sx={{
<Form style={{ width: "100%" }} > height: "52px",
<Container color: theme.palette.secondary.main,
sx={{ "& .MuiInputLabel-outlined": {
p: "20px", color: theme.palette.secondary.main,
border: "1px solid rgba(224, 224, 224, 1)", },
borderRadius: "4px", "& .MuiInputLabel-outlined.MuiInputLabel-shrink": {
display: "flex", color: theme.palette.secondary.main,
flexDirection: "column", },
gap: "12px", }}
>
}} <InputLabel
> id="privilege-select-label"
<Typography variant="h6" sx={{ textAlign: "center", mb: "16px" }}> sx={{
Создание тарифа color: theme.palette.secondary.main,
</Typography> fontSize: "16px",
<FormControl lineHeight: "19px",
fullWidth }}
sx={{ >
height: "52px", Привилегия
color: theme.palette.secondary.main, </InputLabel>
"& .MuiInputLabel-outlined": { <Select
color: theme.palette.secondary.main, labelId="privilege-select-label"
}, id="privilege-select"
"& .MuiInputLabel-outlined.MuiInputLabel-shrink": { value={props.values.privilegeIdField}
color: theme.palette.secondary.main, label="Привилегия"
}, error={props.touched.privilegeIdField && !!props.errors.privilegeIdField}
}} onChange={(e) => {
> console.log(e.target.value);
<InputLabel console.log(findPrivilegeById(e.target.value));
id="privilege-select-label" if (findPrivilegeById(e.target.value) === null) {
sx={{ return enqueueSnackbar("Привилегия не найдена");
color: theme.palette.secondary.main, }
fontSize: "16px", props.setFieldValue("privilegeIdField", e.target.value);
lineHeight: "19px", props.setFieldValue("privilege", findPrivilegeById(e.target.value));
}} }}
> onBlur={props.handleBlur}
Привилегия sx={{
</InputLabel> color: theme.palette.secondary.main,
<Select borderColor: theme.palette.secondary.main,
labelId="privilege-select-label" "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
id="privilege-select" borderColor: theme.palette.secondary.main,
value={props.values.privilegeIdField} border: "1px solid",
label="Привилегия" },
error={props.touched.privilegeIdField && !!props.errors.privilegeIdField} ".MuiSvgIcon-root ": {
onChange={(e) => { fill: theme.palette.secondary.main,
console.log(e.target.value) },
console.log(findPrivilegeById(e.target.value)) }}
if (findPrivilegeById(e.target.value) === null) { inputProps={{ sx: { pt: "12px" } }}
return enqueueSnackbar("Привилегия не найдена"); >
} {privileges.map((privilege) => (
props.setFieldValue("privilegeIdField", e.target.value) <MenuItem
props.setFieldValue("privilege", findPrivilegeById(e.target.value)) data-cy={`select-option-${privilege.description}`}
}} key={privilege._id}
onBlur={props.handleBlur} value={privilege._id}
sx={{ sx={{ whiteSpace: "normal", wordBreak: "break-world" }}
color: theme.palette.secondary.main, >
borderColor: theme.palette.secondary.main, {privilege.serviceKey}:{privilege.description}
"&.Mui-focused .MuiOutlinedInput-notchedOutline": { </MenuItem>
borderColor: theme.palette.secondary.main, ))}
border: "1px solid", </Select>
}, </FormControl>
".MuiSvgIcon-root ": { {props.values.privilege && (
fill: theme.palette.secondary.main, <Box
}, sx={{
}} display: "flex",
inputProps={{ sx: { pt: "12px" } }} flexDirection: "column",
> }}
{privileges.map((privilege) => ( >
<MenuItem <Typography>
data-cy={`select-option-${privilege.description}`} Имя: <span>{props.values.privilege.name}</span>
key={privilege._id} </Typography>
value={privilege._id} <Typography>
sx={{ whiteSpace: "normal", wordBreak: "break-world" }} Сервис: <span>{props.values.privilege.serviceKey}</span>
> </Typography>
{privilege.serviceKey}:{privilege.description} <Typography>
</MenuItem> Единица: <span>{props.values.privilege.type}</span>
))} </Typography>
</Select> <Typography>
</FormControl> Стандартная цена за единицу:{" "}
{props.values.privilege && ( <span>{currencyFormatter.format(props.values.privilege.price / 100)}</span>
<Box </Typography>
sx={{ </Box>
display: "flex", )}
flexDirection: "column", <Field
}} as={TextField}
> id="tariff-name"
<Typography> name="nameField"
Имя: <span>{props.values.privilege.name}</span> variant="filled"
</Typography> label="Название тарифа"
<Typography> type="text"
Сервис: <span>{props.values.privilege.serviceKey}</span> error={props.touched.nameField && !!props.errors.nameField}
</Typography> helperText={<Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.nameField}</Typography>}
<Typography> InputProps={{
Единица: <span>{props.values.privilege.type}</span> style: {
</Typography> backgroundColor: theme.palette.content.main,
<Typography> color: theme.palette.secondary.main,
Стандартная цена за единицу: <span>{currencyFormatter.format(props.values.privilege.price / 100)}</span> },
</Typography> }}
</Box> InputLabelProps={{
)} style: {
<Field color: theme.palette.secondary.main,
as={TextField} },
id="tariff-name" }}
name="nameField" />
variant="filled" <TextField
label="Название тарифа" id="amountField"
type="text" name="amountField"
error={props.touched.nameField && !!props.errors.nameField} variant="filled"
helperText={ onChange={(e) => {
<Typography sx={{ fontSize: "12px", width: "200px" }}> props.setFieldValue("amountField", e.target.value.replace(/[^\d]/g, ""));
{props.errors.nameField} }}
</Typography> value={props.values.amountField}
} onBlur={props.handleBlur}
InputProps={{ label="Кол-во единиц привилегии"
style: { error={props.touched.amountField && !!props.errors.amountField}
backgroundColor: theme.palette.content.main, helperText={<Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.amountField}</Typography>}
color: theme.palette.secondary.main, InputProps={{
} style: {
}} backgroundColor: theme.palette.content.main,
InputLabelProps={{ color: theme.palette.secondary.main,
style: { },
color: theme.palette.secondary.main }}
} InputLabelProps={{
}} style: {
/> color: theme.palette.secondary.main,
<TextField },
id="amountField" }}
name="amountField" />
variant="filled" <TextField
onChange={(e) => { id="tariff-custom-price"
props.setFieldValue("amountField", e.target.value.replace(/[^\d]/g, '')) name="customPriceField"
}} variant="filled"
value={props.values.amountField} onChange={(e) => {
onBlur={props.handleBlur} props.setFieldValue("customPriceField", e.target.value.replace(/[^\d]/g, ""));
label="Кол-во единиц привилегии" }}
error={props.touched.amountField && !!props.errors.amountField} value={props.values.customPriceField}
helperText={ onBlur={props.handleBlur}
<Typography sx={{ fontSize: "12px", width: "200px" }}> label="Кастомная цена (не обязательно)"
{props.errors.amountField} InputProps={{
</Typography> style: {
} backgroundColor: theme.palette.content.main,
InputProps={{ color: theme.palette.secondary.main,
style: { },
backgroundColor: theme.palette.content.main, }}
color: theme.palette.secondary.main, InputLabelProps={{
} style: {
}} color: theme.palette.secondary.main,
InputLabelProps={{ },
style: { }}
color: theme.palette.secondary.main />
} <TextField
}} id="tariff-dezcription"
/> name="descriptionField"
<TextField variant="filled"
id="tariff-custom-price" onChange={(e) => {
name="customPriceField" props.setFieldValue("descriptionField", e.target.value);
variant="filled" }}
onChange={(e) => { value={props.values.descriptionField}
props.setFieldValue("customPriceField", e.target.value.replace(/[^\d]/g, '')) onBlur={props.handleBlur}
}} label="Описание"
value={props.values.customPriceField} multiline={true}
onBlur={props.handleBlur} InputProps={{
label="Кастомная цена (не обязательно)" style: {
InputProps={{ backgroundColor: theme.palette.content.main,
style: { color: theme.palette.secondary.main,
backgroundColor: theme.palette.content.main, },
color: theme.palette.secondary.main, }}
} InputLabelProps={{
}} style: {
InputLabelProps={{ color: theme.palette.secondary.main,
style: { },
color: theme.palette.secondary.main }}
} />
}} <TextField
/> id="tariff-order"
<TextField name="orderField"
id="tariff-dezcription" variant="filled"
name="descriptionField" onChange={(e) => {
variant="filled" props.setFieldValue("orderField", e.target.value);
onChange={(e) => { }}
props.setFieldValue("descriptionField", e.target.value) value={props.values.orderField}
}} onBlur={props.handleBlur}
value={props.values.descriptionField} label="порядковый номер"
onBlur={props.handleBlur} InputProps={{
label="Описание" style: {
multiline={true} backgroundColor: theme.palette.content.main,
InputProps={{ color: theme.palette.secondary.main,
style: { },
backgroundColor: theme.palette.content.main, }}
color: theme.palette.secondary.main, type={"number"}
} InputLabelProps={{
}} style: {
InputLabelProps={{ color: theme.palette.secondary.main,
style: { },
color: theme.palette.secondary.main }}
} />
}} <Button className="btn_createTariffBackend" type="submit" disabled={props.isSubmitting}>
/> Создать
<TextField </Button>
id="tariff-order" </Container>
name="orderField" </Form>
variant="filled" )}
onChange={(e) => { </Formik>
props.setFieldValue("orderField", e.target.value) );
}}
value={props.values.orderField}
onBlur={props.handleBlur}
label="порядковый номер"
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
type={'number'}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<Button
className="btn_createTariffBackend"
type="submit"
disabled={props.isSubmitting}
>
Создать
</Button>
</Container>
</Form>
)}
</Formik>
);
} }

@ -1,32 +1,31 @@
import { Button, SxProps, Theme, useTheme } from "@mui/material"; import { Button, SxProps, Theme, useTheme } from "@mui/material";
import { MouseEventHandler, ReactNode } from "react"; import { MouseEventHandler, ReactNode } from "react";
interface Props { interface Props {
onClick?: MouseEventHandler<HTMLButtonElement>; onClick?: MouseEventHandler<HTMLButtonElement>;
children: ReactNode; children: ReactNode;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
export default function CustomButton({ onClick, children, sx }: Props) { export default function CustomButton({ onClick, children, sx }: Props) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Button <Button
variant="contained" variant="contained"
onClick={onClick} onClick={onClick}
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
padding: '11px 25px', padding: "11px 25px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
}, },
...sx, ...sx,
}} }}
> >
{children} {children}
</Button> </Button>
); );
} }

Some files were not shown because too many files have changed in this diff Show More