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,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",
],
}; };

@ -10,8 +10,8 @@ module.exports = {
// 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",
} },
} },
] ],
}; };

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",
},
});

@ -2,6 +2,7 @@
"name": "adminka", "name": "adminka",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"dependencies": { "dependencies": {
"@date-io/dayjs": "^2.15.0", "@date-io/dayjs": "^2.15.0",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.10.4",
@ -57,13 +58,8 @@
"test:cypress": "start-server-and-test start http://localhost:3000 cypress", "test:cypress": "start-server-and-test start http://localhost:3000 cypress",
"cypress": "cypress open", "cypress": "cypress open",
"eject": "craco eject", "eject": "craco eject",
"format": "prettier . --write" "format": "prettier . --write",
}, "lint": "eslint ./src --fix"
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -78,7 +74,10 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0",
"craco-alias": "^3.0.1", "craco-alias": "^3.0.1",
"prettier": "^3.2.5" "eslint": "^9.3.0",
"prettier": "^3.2.5",
"typescript-eslint": "^7.10.0"
} }
} }

@ -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,14 +1,11 @@
<!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"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="stylesheet" href="fonts.css" /> <link rel="stylesheet" href="fonts.css" />
<!-- <!--

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

@ -30,11 +30,9 @@ export type Account = {
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
): Promise<[Account | null, string?]> => {
try { try {
const accountInfoResponse = await makeRequest<never, Account>({ const accountInfoResponse = await makeRequest<never, Account>({
url: `${baseUrl}/account/${id}`, url: `${baseUrl}/account/${id}`,

@ -2,18 +2,11 @@ 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,
password: string
): Promise<[RegisterResponse | null, string?]> => {
try { try {
const signinResponse = await makeRequest<LoginRequest, RegisterResponse>({ const signinResponse = await makeRequest<LoginRequest, RegisterResponse>({
url: baseUrl + "/login", url: baseUrl + "/login",
@ -24,7 +17,7 @@ export const signin = async (
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}`];
} }
}; };
@ -35,10 +28,7 @@ export const register = async (
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,
RegisterResponse
>({
url: baseUrl + "/register", url: baseUrl + "/register",
body: { login, password, phoneNumber }, body: { login, password, phoneNumber },
useToken: false, useToken: false,

@ -3,15 +3,11 @@ 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;
@ -109,10 +105,7 @@ export function createDiscountObject({
return discount; return discount;
} }
export const changeDiscount = async ( export const changeDiscount = async (discountId: string, discount: Discount): Promise<[unknown, string?]> => {
discountId: string,
discount: Discount
): Promise<[unknown, string?]> => {
try { try {
const changeDiscountResponse = await makeRequest<Discount, unknown>({ const changeDiscountResponse = await makeRequest<Discount, unknown>({
url: baseUrl + "/discount/" + discountId, url: baseUrl + "/discount/" + discountId,
@ -129,16 +122,11 @@ export const changeDiscount = async (
} }
}; };
export const createDiscount = async ( export const createDiscount = async (discountParams: CreateDiscountParams): Promise<[Discount | null, string?]> => {
discountParams: CreateDiscountParams
): Promise<[Discount | null, string?]> => {
const discount = createDiscountObject(discountParams); const discount = createDiscountObject(discountParams);
try { try {
const createdDiscountResponse = await makeRequest< const createdDiscountResponse = await makeRequest<CreateDiscountBody, Discount>({
CreateDiscountBody,
Discount
>({
url: baseUrl + "/discount", url: baseUrl + "/discount",
method: "post", method: "post",
useToken: true, useToken: true,
@ -153,9 +141,7 @@ export const createDiscount = async (
} }
}; };
export const deleteDiscount = async ( export const deleteDiscount = async (discountId: string): Promise<[Discount | null, string?]> => {
discountId: string
): Promise<[Discount | null, string?]> => {
try { try {
const deleteDiscountResponse = await makeRequest<never, Discount>({ const deleteDiscountResponse = await makeRequest<never, Discount>({
url: baseUrl + "/discount/" + discountId, url: baseUrl + "/discount/" + discountId,
@ -178,10 +164,7 @@ export const patchDiscount = async (
const discount = createDiscountObject(discountParams); const discount = createDiscountObject(discountParams);
try { try {
const patchDiscountResponse = await makeRequest< const patchDiscountResponse = await makeRequest<CreateDiscountBody, Discount>({
CreateDiscountBody,
Discount
>({
url: baseUrl + "/discount/" + discountId, url: baseUrl + "/discount/" + discountId,
method: "patch", method: "patch",
useToken: true, useToken: true,
@ -196,9 +179,7 @@ export const patchDiscount = async (
} }
}; };
export const requestDiscounts = async (): Promise< export const requestDiscounts = async (): Promise<[GetDiscountResponse | null, string?]> => {
[GetDiscountResponse | null, string?]
> => {
try { try {
const discountsResponse = await makeRequest<never, GetDiscountResponse>({ const discountsResponse = await makeRequest<never, GetDiscountResponse>({
url: baseUrl + "/discounts", url: baseUrl + "/discounts",
@ -238,7 +219,7 @@ export function useDiscounts() {
if (!(error instanceof Error)) return; if (!(error instanceof Error)) return;
enqueueSnackbar(error.message, { variant: "error" }); enqueueSnackbar(error.message, { variant: "error" });
} },
}); });
return data; return data;

@ -25,16 +25,11 @@ type HistoryResponse = {
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,
page: number
): Promise<[HistoryResponse | null, string?]> => {
try { try {
const historyResponse = await makeRequest<never, HistoryResponse>({ const historyResponse = await makeRequest<never, HistoryResponse>({
method: "GET", method: "GET",
url: url: baseUrl + `/history?page=${page}&limit=${100}&accountID=${accountId}&type=payCart`,
baseUrl +
`/history?page=${page}&limit=${100}&accountID=${accountId}&type=payCart`,
}); });
return [historyResponse]; return [historyResponse];

@ -10,10 +10,7 @@ export function useHistory(accountId: string) {
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);

@ -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;

@ -11,7 +11,7 @@ type SeverPrivilegesResponse = {
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 {
@ -28,14 +28,9 @@ export const getRoles = async (): Promise<[TMockData | null, string?]> => {
} }
}; };
export const putPrivilege = async ( export const putPrivilege = async (body: Omit<Privilege, "_id" | "updatedAt">): Promise<[unknown, string?]> => {
body: Omit<Privilege, "_id" | "updatedAt">
): Promise<[unknown, string?]> => {
try { try {
const putedPrivilege = await makeRequest< const putedPrivilege = await makeRequest<Omit<Privilege, "_id" | "updatedAt">, unknown>({
Omit<Privilege, "_id" | "updatedAt">,
unknown
>({
url: baseUrl + "/privilege", url: baseUrl + "/privilege",
method: "put", method: "put",
body, body,
@ -49,14 +44,9 @@ export const putPrivilege = async (
} }
}; };
export const requestServicePrivileges = async (): Promise< export const requestServicePrivileges = async (): Promise<[SeverPrivilegesResponse | null, string?]> => {
[SeverPrivilegesResponse | null, string?]
> => {
try { try {
const privilegesResponse = await makeRequest< const privilegesResponse = await makeRequest<never, SeverPrivilegesResponse>({
never,
SeverPrivilegesResponse
>({
url: baseUrl + "/privilege/service", url: baseUrl + "/privilege/service",
method: "get", method: "get",
}); });
@ -69,18 +59,14 @@ export const requestServicePrivileges = async (): Promise<
} }
}; };
export const requestPrivileges = async ( export const requestPrivileges = async (signal: AbortSignal | undefined): Promise<[CustomPrivilege[], string?]> => {
signal: AbortSignal | undefined
): Promise<[CustomPrivilege[], string?]> => {
try { try {
const privilegesResponse = await makeRequest<never, CustomPrivilege[]>( const privilegesResponse = await makeRequest<never, CustomPrivilege[]>({
{
url: baseUrl + "/privilege", url: baseUrl + "/privilege",
method: "get", method: "get",
useToken: true, useToken: true,
signal, signal,
} });
);
return [privilegesResponse]; return [privilegesResponse];
} catch (nativeError) { } catch (nativeError) {

@ -15,10 +15,7 @@ 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,
PromocodeList
>({
url: baseUrl + "/getList", url: baseUrl + "/getList",
method: "POST", method: "POST",
body, body,
@ -73,10 +70,7 @@ export const getAllPromocodes = async () => {
const createPromocode = async (body: CreatePromocodeBody) => { const createPromocode = async (body: CreatePromocodeBody) => {
try { try {
const createPromocodeResponse = await makeRequest< const createPromocodeResponse = await makeRequest<CreatePromocodeBody, Promocode>({
CreatePromocodeBody,
Promocode
>({
url: baseUrl + "/create", url: baseUrl + "/create",
method: "POST", method: "POST",
body, body,
@ -85,10 +79,7 @@ const createPromocode = async (body: CreatePromocodeBody) => {
return createPromocodeResponse; return createPromocodeResponse;
} catch (nativeError) { } catch (nativeError) {
if ( if (isAxiosError(nativeError) && nativeError.response?.data.error === "Duplicate Codeword") {
isAxiosError(nativeError) &&
nativeError.response?.data.error === "Duplicate Codeword"
) {
throw new Error(`Промокод уже существует`); throw new Error(`Промокод уже существует`);
} }
@ -112,10 +103,7 @@ const deletePromocode = async (id: string): Promise<void> => {
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,
PromocodeStatistics
>({
url: baseUrl + `/stats`, url: baseUrl + `/stats`,
body: { body: {
id: id, id: id,

@ -3,18 +3,9 @@ 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,
pageSize: number,
promocodeId: string,
to: number,
from: number
) {
const promocodesCountRef = useRef<number>(0); const promocodesCountRef = useRef<number>(0);
const swrResponse = useSwr( const swrResponse = useSwr(
["promocodes", page, pageSize], ["promocodes", page, pageSize],
@ -47,8 +38,7 @@ export function usePromocodes(
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]
@ -82,8 +72,7 @@ export function usePromocodes(
); );
} 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]
@ -96,8 +85,7 @@ export function usePromocodes(
return null; return null;
} }
const promocodeStatisticsResponse = const promocodeStatisticsResponse = await promocodeApi.getPromocodeStatistics(id, from, to);
await promocodeApi.getPromocodeStatistics(id, from, to);
return promocodeStatisticsResponse; return promocodeStatisticsResponse;
}, },
@ -118,8 +106,7 @@ export function usePromocodes(
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]

@ -18,10 +18,7 @@ type TRequest = {
from: number; from: number;
}; };
export const getStatistic = async ( export const getStatistic = async (to: number, from: number): Promise<QuizStatisticResponse> => {
to: number,
from: number
): Promise<QuizStatisticResponse> => {
try { try {
const generalResponse = await makeRequest<TRequest, QuizStatisticResponse>({ const generalResponse = await makeRequest<TRequest, QuizStatisticResponse>({
url: `${process.env.REACT_APP_DOMAIN}/squiz/statistic`, url: `${process.env.REACT_APP_DOMAIN}/squiz/statistic`,
@ -33,15 +30,9 @@ export const getStatistic = async (
} }
}; };
export const getStatisticSchild = async ( export const getStatisticSchild = async (from: number, to: number): Promise<QuizStatisticsItem[]> => {
from: number,
to: number
): Promise<QuizStatisticsItem[]> => {
try { try {
const StatisticResponse = await makeRequest< const StatisticResponse = await makeRequest<GetStatisticSchildBody, QuizStatisticsItem[]>({
GetStatisticSchildBody,
QuizStatisticsItem[]
>({
url: process.env.REACT_APP_DOMAIN + "/customer/quizlogo/stat", url: process.env.REACT_APP_DOMAIN + "/customer/quizlogo/stat",
method: "post", method: "post",
useToken: true, useToken: true,
@ -70,10 +61,7 @@ export const getStatisticPromocode = async (
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,
Record<string, AllPromocodeStatistics>
>({
url: process.env.REACT_APP_DOMAIN + "/customer/promocode/ltv", url: process.env.REACT_APP_DOMAIN + "/customer/promocode/ltv",
method: "post", method: "post",
useToken: true, useToken: true,

@ -35,7 +35,7 @@ export type UserType = {
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) => {

@ -20,19 +20,15 @@ type GetTariffsResponse = {
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
): Promise<[unknown, string?]> => {
try { try {
const createdTariffResponse = await makeRequest<CreateTariffBackendRequest>( const createdTariffResponse = await makeRequest<CreateTariffBackendRequest>({
{
url: baseUrl + "/tariff/", url: baseUrl + "/tariff/",
method: "post", method: "post",
body, body,
} });
);
return [createdTariffResponse]; return [createdTariffResponse];
} catch (nativeError) { } catch (nativeError) {
@ -65,9 +61,7 @@ export const putTariff = async (tariff: Tariff): Promise<[null, string?]> => {
} }
}; };
export const deleteTariff = async ( export const deleteTariff = async (tariffId: string): Promise<[Tariff | null, string?]> => {
tariffId: string
): Promise<[Tariff | null, string?]> => {
try { try {
const deletedTariffResponse = await makeRequest<{ id: string }, Tariff>({ const deletedTariffResponse = await makeRequest<{ id: string }, Tariff>({
method: "delete", method: "delete",
@ -83,9 +77,7 @@ export const deleteTariff = async (
} }
}; };
export const requestTariffs = async ( export const requestTariffs = async (page: number): Promise<[GetTariffsResponse | null, string?]> => {
page: number
): Promise<[GetTariffsResponse | null, string?]> => {
try { try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({ const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
url: baseUrl + `/tariff/?page=${page}&limit=${100}`, url: baseUrl + `/tariff/?page=${page}&limit=${100}`,

@ -4,16 +4,11 @@ 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
): Promise<[unknown, string?]> => {
try { try {
const sendTicketMessageResponse = await makeRequest< const sendTicketMessageResponse = await makeRequest<SendTicketMessageRequest, unknown>({
SendTicketMessageRequest,
unknown
>({
url: `${baseUrl}/send`, url: `${baseUrl}/send`,
method: "POST", method: "POST",
useToken: true, useToken: true,

@ -27,10 +27,7 @@ const getUserInfo = async (id: string): Promise<[UserType | null, string?]> => {
} }
}; };
const getUserList = async ( const getUserList = async (page = 1, limit = 10): Promise<[UsersListResponse | null, string?]> => {
page = 1,
limit = 10
): Promise<[UsersListResponse | null, string?]> => {
try { try {
const userResponse = await makeRequest<never, UsersListResponse>({ const userResponse = await makeRequest<never, UsersListResponse>({
method: "get", method: "get",
@ -45,10 +42,7 @@ const getUserList = async (
} }
}; };
const getManagerList = async ( const getManagerList = async (page = 1, limit = 10): Promise<[UsersListResponse | null, string?]> => {
page = 1,
limit = 10
): Promise<[UsersListResponse | null, string?]> => {
try { try {
const managerResponse = await makeRequest<never, UsersListResponse>({ const managerResponse = await makeRequest<never, UsersListResponse>({
method: "get", method: "get",
@ -63,10 +57,7 @@ const getManagerList = async (
} }
}; };
const getAdminList = async ( const getAdminList = async (page = 1, limit = 10): Promise<[UsersListResponse | null, string?]> => {
page = 1,
limit = 10
): Promise<[UsersListResponse | null, string?]> => {
try { try {
const adminResponse = await makeRequest<never, UsersListResponse>({ const adminResponse = await makeRequest<never, UsersListResponse>({
method: "get", method: "get",

@ -10,10 +10,7 @@ export function useAdmins(page: number, pageSize: number) {
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);
@ -44,10 +41,7 @@ export function useManagers(page: number, pageSize: number) {
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);

@ -25,11 +25,9 @@ type PatchVerificationBody = {
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
): Promise<[Verification | null, string?]> => {
try { try {
const verificationResponse = await makeRequest<never, Verification>({ const verificationResponse = await makeRequest<never, Verification>({
method: "get", method: "get",
@ -44,14 +42,9 @@ export const verification = async (
} }
}; };
export const patchVerification = async ( export const patchVerification = async (body: PatchVerificationBody): Promise<[unknown, string?]> => {
body: PatchVerificationBody
): Promise<[unknown, string?]> => {
try { try {
const patchedVerificationResponse = await makeRequest< const patchedVerificationResponse = await makeRequest<PatchVerificationBody, unknown>({
PatchVerificationBody,
unknown
>({
method: "patch", method: "patch",
useToken: true, useToken: true,
url: baseUrl + `/verification`, url: baseUrl + `/verification`,

@ -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");
} }

@ -14,7 +14,7 @@ import {
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,42 +27,40 @@ 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);
@ -96,7 +94,7 @@ export default function Cart() {
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
gap: "20px", gap: "20px",
flexDirection: mobile ? "column" : undefined flexDirection: mobile ? "column" : undefined,
}} }}
> >
<FormControlLabel <FormControlLabel
@ -201,44 +199,33 @@ export default function Cart() {
}} }}
> >
<TableCell> <TableCell>
<Typography <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
Имя Имя
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
Описание Описание
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
Скидки Скидки
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography <Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
стоимость стоимость
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{cartData.services.flatMap(service => service.tariffs.map(tariffCartData => { {cartData.services.flatMap((service) =>
const appliedDiscounts = tariffCartData.privileges.flatMap( service.tariffs.map((tariffCartData) => {
privilege => Array.from(privilege.appliedDiscounts) const appliedDiscounts = tariffCartData.privileges
).sort((a, b) => a.Layer - b.Layer); .flatMap((privilege) => Array.from(privilege.appliedDiscounts))
.sort((a, b) => a.Layer - b.Layer);
return ( return (
<CartItemRow <CartItemRow
@ -247,7 +234,8 @@ export default function Cart() {
appliedDiscounts={appliedDiscounts} appliedDiscounts={appliedDiscounts}
/> />
); );
}))} })
)}
</TableBody> </TableBody>
</Table> </Table>

@ -4,7 +4,6 @@ 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[];
@ -27,28 +26,20 @@ export default function CartItemRow({ tariffCartData, appliedDiscounts }: Props)
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>
{tariffCartData.privileges[0].description}
</TableCell>
<TableCell> <TableCell>
{appliedDiscounts.map((discount, index, arr) => ( {appliedDiscounts.map((discount, index, arr) => (
<span key={discount.ID}> <span key={discount.ID}>
<DiscountTooltip discount={discount} /> <DiscountTooltip discount={discount} />
{index < arr.length - 1 && ( {index < arr.length - 1 && <span>,&nbsp;</span>}
<span>,&nbsp;</span>
)}
</span> </span>
))} ))}
</TableCell> </TableCell>
<TableCell> <TableCell>{currencyFormatter.format(tariffCartData.price / 100)}</TableCell>
{currencyFormatter.format(tariffCartData.price / 100)}
</TableCell>
</TableRow> </TableRow>
); );
} }

@ -2,7 +2,6 @@ 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;
} }

@ -1,14 +1,23 @@
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,
label,
value,
name,
onBlur,
error,
type,
sx,
onChange: setValue,
}: {
id: string; id: string;
label: string; label: string;
value: number | string | null; value: number | string | null;
name?: string; name?: string;
onBlur?: InputBaseProps['onBlur']; onBlur?: InputBaseProps["onBlur"];
error?: boolean; error?: boolean;
type?: HTMLInputTypeAttribute; type?: HTMLInputTypeAttribute;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
@ -32,12 +41,12 @@ export function CustomTextField({ id, label, value, name, onBlur,error, type, sx
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}

@ -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,
text,
type = "button"
}:Props) => {
if (isReady) { if (isReady) {
return <BeautifulButton type={type}>{text}</BeautifulButton> return <BeautifulButton type={type}>{text}</BeautifulButton>;
} }
return <Skeleton>{text}</Skeleton> 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,4 +1,4 @@
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",
@ -12,5 +12,5 @@ export default styled(TextField)(({ theme }) => ({
}, },
"& .Mui-focused": { "& .Mui-focused": {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
} },
})); }));

@ -14,4 +14,4 @@ export default function PrivateRoute({ children }: Props) {
return children; return children;
} }
return <Navigate to="/" state={{ from: location }} />; return <Navigate to="/" state={{ from: location }} />;
}; }

@ -2,7 +2,6 @@ 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;
} }

@ -1,15 +1,14 @@
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;
@ -41,10 +40,12 @@ export type CreateDiscountBody = {
TargetScope: "Sum" | "Group" | "Each"; TargetScope: "Sum" | "Group" | "Each";
Overhelm: boolean; Overhelm: boolean;
TargetGroup: string; TargetGroup: string;
Products: [{ Products: [
{
ID: string; ID: string;
Factor: number; Factor: number;
Overhelm: false; Overhelm: false;
}]; },
];
}; };
}; };

@ -11,6 +11,6 @@ export const SERVICE_LIST = [
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,20 +1,18 @@
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";
@ -29,16 +27,16 @@ export interface Ticket {
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;
}; }

@ -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";
@ -50,10 +44,7 @@ const SigninForm = () => {
password: "", password: "",
}; };
const onSignFormSubmit = async ( const onSignFormSubmit = async (values: Values, formikHelpers: FormikHelpers<Values>) => {
values: Values,
formikHelpers: FormikHelpers<Values>
) => {
formikHelpers.setSubmitting(true); formikHelpers.setSubmitting(true);
const [_, signinError] = await signin(values.email, values.password); const [_, signinError] = await signin(values.email, values.password);
@ -68,11 +59,7 @@ const SigninForm = () => {
}; };
return ( return (
<Formik <Formik initialValues={initialValues} validate={validate} onSubmit={onSignFormSubmit}>
initialValues={initialValues}
validate={validate}
onSubmit={onSignFormSubmit}
>
{(props) => ( {(props) => (
<Form> <Form>
<Box <Box
@ -99,7 +86,7 @@ const SigninForm = () => {
"> *": { "> *": {
marginTop: "15px", marginTop: "15px",
}, },
padding: isMobile ? "0 16px" : undefined padding: isMobile ? "0 16px" : undefined,
}} }}
> >
<Logo /> <Logo />
@ -185,9 +172,7 @@ const SigninForm = () => {
/> />
</Box> </Box>
<Link to="/restore" style={{ textDecoration: "none" }}> <Link to="/restore" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}> <Typography color={theme.palette.golden.main}>Забыли пароль?</Typography>
Забыли пароль?
</Typography>
</Link> </Link>
<Button <Button
type="submit" type="submit"
@ -206,13 +191,9 @@ const SigninForm = () => {
display: "flex", display: "flex",
}} }}
> >
<Typography color={theme.palette.secondary.main}> <Typography color={theme.palette.secondary.main}>У вас нет аккаунта?&nbsp;</Typography>
У вас нет аккаунта?&nbsp;
</Typography>
<Link to="/signup" style={{ textDecoration: "none" }}> <Link to="/signup" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}> <Typography color={theme.palette.golden.main}>Зарегестрируйтесь</Typography>
Зарегестрируйтесь
</Typography>
</Link> </Link>
</Box> </Box>
</Box> </Box>

@ -53,10 +53,7 @@ const SignUp = () => {
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);
@ -172,14 +169,10 @@ const SignUp = () => {
variant="filled" variant="filled"
label="Повторите пароль" label="Повторите пароль"
id="repeatPassword" id="repeatPassword"
error={ error={props.touched.repeatPassword && !!props.errors.repeatPassword}
props.touched.repeatPassword &&
!!props.errors.repeatPassword
}
helperText={ helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}> <Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.touched.repeatPassword && {props.touched.repeatPassword && props.errors.repeatPassword}
props.errors.repeatPassword}
</Typography> </Typography>
} }
/> />
@ -197,9 +190,7 @@ const SignUp = () => {
Войти Войти
</Button> </Button>
<Link to="/signin" style={{ textDecoration: "none" }}> <Link to="/signin" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}> <Typography color={theme.palette.golden.main}>У меня уже есть аккаунт</Typography>
У меня уже есть аккаунт
</Typography>
</Link> </Link>
</Box> </Box>
</Box> </Box>

@ -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 = () => { const Error404: React.FC = () => {
return ( return (
<React.Fragment> <React.Fragment>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<Box sx={{ <Box
sx={{
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
height: "100%" height: "100%",
}}> }}
<Box sx={{ >
<Box
sx={{
width: "100vw", width: "100vw",
height: "100vh", height: "100vh",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center" alignItems: "center",
}}> }}
<Box sx={{ >
<Box
sx={{
width: "150px", width: "150px",
height: "120px", height: "120px",
display: "flex", display: "flex",
flexDirection: "row" flexDirection: "row",
}}> }}
<Typography sx={{ >
fontSize: "80px" <Typography
}}> sx={{
fontSize: "80px",
}}
>
4 4
</Typography> </Typography>
<Typography sx={{ <Typography
sx={{
color: theme.palette.golden.main, color: theme.palette.golden.main,
fontSize: "80px" fontSize: "80px",
}}> }}
>
0 0
</Typography> </Typography>
<Typography sx={{ <Typography
fontSize: "80px" sx={{
}}> fontSize: "80px",
}}
>
4 4
</Typography> </Typography>
</Box> </Box>
<Box> <Box>
<Link <Link to="/users">
to="/users">
<Button>На главную</Button> <Button>На главную</Button>
</Link> </Link>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</ThemeProvider> </ThemeProvider>
</React.Fragment> </React.Fragment>
); );
} };
export default Error404; export default Error404;

@ -2,13 +2,14 @@ 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 = () => { const Authorization: React.FC = () => {
return ( return (
<React.Fragment> <React.Fragment>
<Box sx={{ <Box
display: "flex" sx={{
}}> display: "flex",
}}
>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
@ -16,19 +17,20 @@ const Authorization: React.FC = () => {
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
cursor: "default" cursor: "default",
}}> }}
>
PENA PENA
</Typography> </Typography>
<Typography <Typography
variant = "subtitle2" variant="subtitle2"
sx = {{ sx={{
width: "55px", width: "55px",
color: theme.palette.primary.main, color: theme.palette.primary.main,
backgroundColor: theme.palette.goldenDark.main, backgroundColor: theme.palette.goldenDark.main,
borderRadius: "5px", borderRadius: "5px",
margin: theme.spacing(0.5), margin: theme.spacing(0.5),
cursor: "default" cursor: "default",
}} }}
> >
HUB HUB
@ -36,7 +38,6 @@ const Authorization: React.FC = () => {
</Box> </Box>
</React.Fragment> </React.Fragment>
); );
} };
export default Authorization; 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 = () => { const Authorization: React.FC = () => {
return ( return (
<React.Fragment> <React.Fragment>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<Box sx={{ <Box
sx={{
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
height: "100%" height: "100%",
}}> }}
<Box sx={{ >
<Box
sx={{
width: "100vw", width: "100vw",
height: "100vh", height: "100vh",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center" alignItems: "center",
}}> }}
<Box sx={{ >
<Box
sx={{
width: "350px", width: "350px",
height: "700px", height: "700px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
}}> }}
>
<Logo /> <Logo />
<Box sx={{ <Box
sx={{
width: "100%", width: "100%",
height: "100px", height: "100px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center" alignItems: "center",
}}> }}
>
<Box> <Box>
<Typography variant="h6"> <Typography variant="h6">Все пользователи</Typography>
Все пользователи </Box>
</Typography>
<Button variant="enter">ВОЙТИ</Button>
</Box>
<Box
sx={{
width: "100%",
height: "100px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box>
<Typography variant="h6">Общая статистика</Typography>
</Box> </Box>
<Button <Button
variant = 'enter' variant="enter"
sx={{
backgroundColor: theme.palette.content.main,
"&:hover": {
backgroundColor: theme.palette.menu.main,
},
}}
> >
ВОЙТИ ВОЙТИ
</Button> </Button>
</Box> </Box>
<Box sx={{ <Box
sx={{
width: "100%", width: "100%",
height: "100px", height: "100px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center" alignItems: "center",
}}> }}
>
<Box> <Box>
<Typography variant="h6"> <Typography variant="h6">Шаблонизатор документов</Typography>
Общая статистика
</Typography>
</Box> </Box>
<Button <Button
variant = "enter" variant="enter"
sx={{ sx={{
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
"&:hover": { "&:hover": {
backgroundColor: theme.palette.menu.main backgroundColor: theme.palette.menu.main,
} },
}}> }}
>
ВОЙТИ ВОЙТИ
</Button> </Button>
</Box> </Box>
<Box sx={{ <Box
sx={{
width: "100%", width: "100%",
height: "100px", height: "100px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center" alignItems: "center",
}}> }}
>
<Box> <Box>
<Typography variant="h6"> <Typography variant="h6">Конструктор опросов</Typography>
Шаблонизатор документов
</Typography>
</Box> </Box>
<Button <Button
variant = "enter" variant="enter"
sx={{ sx={{
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
"&:hover": { "&:hover": {
backgroundColor: theme.palette.menu.main backgroundColor: theme.palette.menu.main,
} },
}}> }}
>
ВОЙТИ ВОЙТИ
</Button> </Button>
</Box> </Box>
<Box sx={{ <Box
sx={{
width: "100%", width: "100%",
height: "100px", height: "100px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center" alignItems: "center",
}}> }}
>
<Box> <Box>
<Typography variant="h6"> <Typography variant="h6">Сокращатель ссылок</Typography>
Конструктор опросов
</Typography>
</Box> </Box>
<Button <Button
variant = "enter" variant="enter"
sx={{ sx={{
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
"&:hover": { "&:hover": {
backgroundColor: theme.palette.menu.main backgroundColor: theme.palette.menu.main,
} },
}}> }}
>
ВОЙТИ ВОЙТИ
</Button> </Button>
</Box> </Box>
<Box sx={{
width: "100%",
height: "100px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center"
}}>
<Box>
<Typography variant="h6">
Сокращатель ссылок
</Typography>
</Box>
<Button
variant = "enter"
sx={{
backgroundColor: theme.palette.content.main,
"&:hover": {
backgroundColor: theme.palette.menu.main
}
}}>
ВОЙТИ
</Button>
</Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</ThemeProvider> </ThemeProvider>
</React.Fragment> </React.Fragment>
); );
} };
export default Authorization; export default Authorization;

@ -1,13 +1,12 @@
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;
} }
@ -26,7 +25,6 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
}; };
const putPrivileges = async () => { const putPrivileges = async () => {
const [, putedPrivilegeError] = await putPrivilege({ const [, putedPrivilegeError] = await putPrivilege({
name: privilege.name, name: privilege.name,
privilegeId: privilege.privilegeId, privilegeId: privilege.privilegeId,
@ -68,7 +66,7 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
if (!inputValue) return; if (!inputValue) return;
putPrivileges(); putPrivileges();
} }
return ( return (
<Box <Box
@ -123,7 +121,17 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
</IconButton> </IconButton>
</Box> </Box>
</Box> </Box>
<Box sx={{ maxWidth: "600px", width: "100%", display: "flex", alignItems: mobile ? "center" : undefined, justifyContent: "space-around", flexDirection: mobile ? "column" : "row", gap: "5px" }}> <Box
sx={{
maxWidth: "600px",
width: "100%",
display: "flex",
alignItems: mobile ? "center" : undefined,
justifyContent: "space-around",
flexDirection: mobile ? "column" : "row",
gap: "5px",
}}
>
{inputOpen ? ( {inputOpen ? (
<TextField <TextField
type="number" type="number"
@ -154,7 +162,7 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
<IconButton onClick={handleSavePrice}> <IconButton onClick={handleSavePrice}>
<SaveIcon /> <SaveIcon />
</IconButton> </IconButton>
) ),
}} }}
/> />
) : ( ) : (

@ -8,7 +8,9 @@ import {
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";

@ -44,10 +44,7 @@ export default function DeleteForm() {
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> </Button>
<TextField <TextField
@ -83,10 +80,7 @@ export default function DeleteForm() {
> >
{MOCK_DATA_USERS.map(({ name, id }) => ( {MOCK_DATA_USERS.map(({ name, id }) => (
<MenuItem key={id} value={name}> <MenuItem key={id} value={name}>
<Checkbox <Checkbox onClick={() => setRoleId(id)} checked={personName.indexOf(name) > -1} />
onClick={() => setRoleId(id)}
checked={personName.indexOf(name) > -1}
/>
<ListItemText primary={name} /> <ListItemText primary={name} />
</MenuItem> </MenuItem>
))} ))}

@ -14,13 +14,9 @@ export default function ListPrivilege() {
return ( return (
<> <>
{privileges.map(privilege => ( {privileges.map((privilege) => (
<СardPrivilege <СardPrivilege key={privilege._id} privilege={privilege} />
key={privilege._id} ))}
privilege={privilege}
/>
)
)}
</> </>
); );
} }

@ -7,7 +7,7 @@ import {
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";
@ -22,8 +22,7 @@ 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={
@ -69,7 +68,7 @@ export const SettingRoles = (): JSX.Element => {
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 />
@ -118,7 +117,7 @@ export const SettingRoles = (): JSX.Element => {
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 />
@ -129,8 +128,7 @@ export const SettingRoles = (): JSX.Element => {
} }
/> />
<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";
@ -56,7 +56,9 @@ export default function DiscountDataGrid({ selectedRows }: Props) {
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(false)}>Активировать</Button>
<Button onClick={() => changeData(true)}>Деактивировать</Button> <Button onClick={() => changeData(true)}>Деактивировать</Button>
</Box> </Box>

@ -8,15 +8,13 @@ import {
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,15 +24,15 @@ 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() {
@ -53,25 +51,16 @@ export default function CreateDiscount() {
cartPurchasesAmountField: "", cartPurchasesAmountField: "",
discountMinValueField: "", discountMinValueField: "",
privilegeIdField: "", privilegeIdField: "",
} };
const handleCreateDiscount = async( const handleCreateDiscount = async (values: Values, formikHelpers: FormikHelpers<Values>) => {
values: Values,
formikHelpers: FormikHelpers<Values>
) => {
const purchasesAmount = Number(parseFloat(values.purchasesAmountField.replace(",", "."))) * 100; const purchasesAmount = Number(parseFloat(values.purchasesAmountField.replace(",", "."))) * 100;
const discountFactor = const discountFactor = (100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100;
(100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100; const cartPurchasesAmount = Number(parseFloat(values.cartPurchasesAmountField.replace(",", ".")) * 100);
const cartPurchasesAmount = Number(parseFloat( const discountMinValue = Number(parseFloat(values.discountMinValueField.replace(",", ".")) * 100);
values.cartPurchasesAmountField.replace(",", ".")) * 100
);
const discountMinValue = Number(parseFloat(
values.discountMinValueField.replace(",", ".")) * 100
);
const [createdDiscountResponse, createdDiscountError] = const [createdDiscountResponse, createdDiscountError] = await createDiscount({
await createDiscount({
cartPurchasesAmount, cartPurchasesAmount,
discountFactor, discountFactor,
discountMinValue, discountMinValue,
@ -95,50 +84,54 @@ export default function CreateDiscount() {
mutate("discounts"); mutate("discounts");
addDiscount(createdDiscountResponse); addDiscount(createdDiscountResponse);
} }
} };
const validateFulledFields = (values: Values) => { const validateFulledFields = (values: Values) => {
const errors = {} as any; const errors = {} as any;
if (values.discountNameField.length === 0) { if (values.discountNameField.length === 0) {
errors.discountNameField = 'Поле "Имя" пустое' errors.discountNameField = 'Поле "Имя" пустое';
} }
if (values.discountDescriptionField.length === 0) { if (values.discountDescriptionField.length === 0) {
errors.discountDescriptionField = 'Поле "Описание" пустое' errors.discountDescriptionField = 'Поле "Описание" пустое';
} }
if (((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100) < 0) { if ((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100 < 0) {
errors.discountFactorField = "Процент скидки не может быть больше 100" errors.discountFactorField = "Процент скидки не может быть больше 100";
} }
if (!isFinite(((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100))) { if (!isFinite((100 - parseFloat(values.discountFactorField.replace(",", "."))) / 100)) {
errors.discountFactorField = 'Поле "Процент скидки" не число' errors.discountFactorField = 'Поле "Процент скидки" не число';
} }
if (values.discountType === "privilege" && !values.privilegeIdField) { if (values.discountType === "privilege" && !values.privilegeIdField) {
errors.privilegeIdField = "Привилегия не выбрана" errors.privilegeIdField = "Привилегия не выбрана";
} }
if (values.discountType === "service" && !values.serviceType) { if (values.discountType === "service" && !values.serviceType) {
errors.serviceType = "Сервис не выбран" errors.serviceType = "Сервис не выбран";
} }
if (values.discountType === "purchasesAmount" && !isFinite(parseFloat(values.purchasesAmountField.replace(",", ".")))) { if (
errors.purchasesAmountField = 'Поле "Внесено больше" не число' values.discountType === "purchasesAmount" &&
!isFinite(parseFloat(values.purchasesAmountField.replace(",", ".")))
) {
errors.purchasesAmountField = 'Поле "Внесено больше" не число';
} }
if (values.discountType === "cartPurchasesAmount" && !isFinite(parseFloat(values.cartPurchasesAmountField.replace(",", ".")))) { if (
errors.cartPurchasesAmountField = 'Поле "Объём в корзине" не число' values.discountType === "cartPurchasesAmount" &&
!isFinite(parseFloat(values.cartPurchasesAmountField.replace(",", ".")))
) {
errors.cartPurchasesAmountField = 'Поле "Объём в корзине" не число';
} }
if (values.discountType === ("service" || "privilege") && !isFinite(parseFloat(values.discountMinValueField.replace(",", ".")))) { if (
errors.discountMinValueField = 'Поле "Минимальное значение" не число' values.discountType === ("service" || "privilege") &&
!isFinite(parseFloat(values.discountMinValueField.replace(",", ".")))
) {
errors.discountMinValueField = 'Поле "Минимальное значение" не число';
} }
console.error(errors) console.error(errors);
return errors; return errors;
} };
return ( return (
<Formik <Formik initialValues={initialValues} onSubmit={handleCreateDiscount} validate={validateFulledFields}>
initialValues={initialValues}
onSubmit={handleCreateDiscount}
validate={validateFulledFields}
>
{(props) => ( {(props) => (
<Form style={{width: "100%", display: "flex", justifyContent: "center"}}> <Form style={{ width: "100%", display: "flex", justifyContent: "center" }}>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -160,20 +153,18 @@ export default function CreateDiscount() {
name="discountNameField" name="discountNameField"
error={props.touched.discountNameField && !!props.errors.discountNameField} error={props.touched.discountNameField && !!props.errors.discountNameField}
helperText={ helperText={
<Typography sx={{fontSize: "12px", width: "200px"}}> <Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.discountNameField}</Typography>
{props.errors.discountNameField}
</Typography>
} }
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,
} },
}} }}
/> />
<Field <Field
@ -185,7 +176,7 @@ export default function CreateDiscount() {
type="text" type="text"
error={props.touched.discountDescriptionField && !!props.errors.discountDescriptionField} error={props.touched.discountDescriptionField && !!props.errors.discountDescriptionField}
helperText={ helperText={
<Typography sx={{fontSize: "12px", width: "200px"}}> <Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.discountDescriptionField} {props.errors.discountDescriptionField}
</Typography> </Typography>
} }
@ -193,12 +184,12 @@ export default function CreateDiscount() {
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,
} },
}} }}
/> />
<Typography <Typography
@ -221,20 +212,18 @@ export default function CreateDiscount() {
error={props.touched.discountFactorField && !!props.errors.discountFactorField} error={props.touched.discountFactorField && !!props.errors.discountFactorField}
value={props.values.discountFactorField} value={props.values.discountFactorField}
helperText={ helperText={
<Typography sx={{fontSize: "12px", width: "200px"}}> <Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.discountFactorField}</Typography>
{props.errors.discountFactorField}
</Typography>
} }
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,
} },
}} }}
/> />
<FormControl> <FormControl>
@ -254,9 +243,7 @@ export default function CreateDiscount() {
aria-labelledby="discount-type" aria-labelledby="discount-type"
name="discountType" name="discountType"
value={props.values.discountType} value={props.values.discountType}
onChange={( onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
event: React.ChangeEvent<HTMLInputElement>
) => {
props.setFieldValue("discountType", event.target.value as DiscountType); props.setFieldValue("discountType", event.target.value as DiscountType);
}} }}
onBlur={props.handleBlur} onBlur={props.handleBlur}
@ -265,7 +252,7 @@ export default function CreateDiscount() {
<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]}
/> />
))} ))}
@ -279,7 +266,7 @@ export default function CreateDiscount() {
error={props.touched.purchasesAmountField && !!props.errors.purchasesAmountField} error={props.touched.purchasesAmountField && !!props.errors.purchasesAmountField}
label="Внесено больше" label="Внесено больше"
onChange={(e) => { onChange={(e) => {
props.setFieldValue("purchasesAmountField", e.target.value.replace(/[^\d]/g, '')) props.setFieldValue("purchasesAmountField", e.target.value.replace(/[^\d]/g, ""));
}} }}
value={props.values.purchasesAmountField} value={props.values.purchasesAmountField}
onBlur={props.handleBlur} onBlur={props.handleBlur}
@ -287,20 +274,18 @@ export default function CreateDiscount() {
marginTop: "15px", marginTop: "15px",
}} }}
helperText={ helperText={
<Typography sx={{fontSize: "12px", width: "200px"}}> <Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.purchasesAmountField}</Typography>
{props.errors.purchasesAmountField}
</Typography>
} }
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,
} },
}} }}
/> />
)} )}
@ -312,7 +297,7 @@ export default function CreateDiscount() {
variant="filled" variant="filled"
error={props.touched.cartPurchasesAmountField && !!props.errors.cartPurchasesAmountField} error={props.touched.cartPurchasesAmountField && !!props.errors.cartPurchasesAmountField}
onChange={(e) => { onChange={(e) => {
props.setFieldValue("cartPurchasesAmountField", e.target.value.replace(/[^\d]/g, '')) props.setFieldValue("cartPurchasesAmountField", e.target.value.replace(/[^\d]/g, ""));
}} }}
value={props.values.cartPurchasesAmountField} value={props.values.cartPurchasesAmountField}
onBlur={props.handleBlur} onBlur={props.handleBlur}
@ -320,7 +305,7 @@ export default function CreateDiscount() {
marginTop: "15px", marginTop: "15px",
}} }}
helperText={ helperText={
<Typography sx={{fontSize: "12px", width: "200px"}}> <Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.cartPurchasesAmountField} {props.errors.cartPurchasesAmountField}
</Typography> </Typography>
} }
@ -328,12 +313,12 @@ export default function CreateDiscount() {
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,
} },
}} }}
/> />
)} )}
@ -372,7 +357,7 @@ export default function CreateDiscount() {
onBlur={props.handleBlur} onBlur={props.handleBlur}
variant="filled" variant="filled"
onChange={(e) => { onChange={(e) => {
props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, '')) props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, ""));
}} }}
value={props.values.discountMinValueField} value={props.values.discountMinValueField}
error={props.touched.discountMinValueField && !!props.errors.discountMinValueField} error={props.touched.discountMinValueField && !!props.errors.discountMinValueField}
@ -380,7 +365,7 @@ export default function CreateDiscount() {
marginTop: "15px", marginTop: "15px",
}} }}
helperText={ helperText={
<Typography sx={{fontSize: "12px", width: "200px"}}> <Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.discountMinValueField} {props.errors.discountMinValueField}
</Typography> </Typography>
} }
@ -388,12 +373,12 @@ export default function CreateDiscount() {
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,
} },
}} }}
/> />
</> </>
@ -442,7 +427,7 @@ export default function CreateDiscount() {
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}>
@ -458,7 +443,7 @@ export default function CreateDiscount() {
onBlur={props.handleBlur} onBlur={props.handleBlur}
variant="filled" variant="filled"
onChange={(e) => { onChange={(e) => {
props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, '')) props.setFieldValue("discountMinValueField", e.target.value.replace(/[^\d]/g, ""));
}} }}
value={props.values.discountMinValueField} value={props.values.discountMinValueField}
error={props.touched.discountMinValueField && !!props.errors.discountMinValueField} error={props.touched.discountMinValueField && !!props.errors.discountMinValueField}
@ -466,7 +451,7 @@ export default function CreateDiscount() {
marginTop: "15px", marginTop: "15px",
}} }}
helperText={ helperText={
<Typography sx={{fontSize: "12px", width: "200px"}}> <Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.discountMinValueField} {props.errors.discountMinValueField}
</Typography> </Typography>
} }
@ -474,12 +459,12 @@ export default function CreateDiscount() {
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,
} },
}} }}
/> />
</> </>
@ -496,7 +481,7 @@ export default function CreateDiscount() {
> >
<Button <Button
variant="contained" variant="contained"
type='submit' type="submit"
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
height: "52px", height: "52px",
@ -514,6 +499,5 @@ export default function CreateDiscount() {
</Form> </Form>
)} )}
</Formik> </Formik>
); );
} }

@ -1,11 +1,6 @@
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 { import { DataGrid, GridColDef, GridRowsProp, GridToolbar } from "@mui/x-data-grid";
DataGrid,
GridColDef,
GridRowsProp,
GridToolbar,
} from "@mui/x-data-grid";
import { import {
openEditDiscountDialog, openEditDiscountDialog,
setSelectedDiscountIds, setSelectedDiscountIds,
@ -33,9 +28,7 @@ const columns: GridColDef[] = [
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", field: "description",
@ -108,21 +101,13 @@ const columns: GridColDef[] = [
]; ];
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(() => {
@ -138,26 +123,19 @@ export default function DiscountDataGrid({ selectedRowsHC }: Props) {
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
])/100,
active: discount.Deprecated ? "🚫" : "✅", active: discount.Deprecated ? "🚫" : "✅",
deleted: discount.Audit.Deleted, 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="обновить список привилегий"> <Tooltip title="обновить список привилегий">
<IconButton <IconButton onClick={requestDiscounts} style={{ display: "block", margin: "0 auto" }}>
onClick={requestDiscounts}
style={{ display: "block", margin: "0 auto" }}
>
<AutorenewIcon sx={{ color: "white" }} /> <AutorenewIcon sx={{ color: "white" }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>

@ -8,13 +8,12 @@ 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}>
@ -27,19 +26,17 @@ const DiscountManagement: React.FC = () => {
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}}> }}
>
СКИДКИ СКИДКИ
</Typography> </Typography>
<CreateDiscount /> <CreateDiscount />
<DiscountDataGrid <DiscountDataGrid selectedRowsHC={selectedRowsHC} />
selectedRowsHC={selectedRowsHC}
/>
<EditDiscountDialog /> <EditDiscountDialog />
<ControlPanel selectedRows={selectedRows}/> <ControlPanel selectedRows={selectedRows} />
</LocalizationProvider> </LocalizationProvider>
); );
}; };
export default DiscountManagement; export default DiscountManagement;

@ -18,15 +18,8 @@ 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";
@ -39,18 +32,14 @@ export default function EditDiscountDialog() {
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] = const [discountDescriptionField, setDiscountDescriptionField] = useState<string>("");
useState<string>("");
const [privilegeIdField, setPrivilegeIdField] = useState<string | "">(""); const [privilegeIdField, setPrivilegeIdField] = useState<string | "">("");
const [discountFactorField, setDiscountFactorField] = useState<string>("0"); const [discountFactorField, setDiscountFactorField] = useState<string>("0");
const [purchasesAmountField, setPurchasesAmountField] = useState<string>("0"); const [purchasesAmountField, setPurchasesAmountField] = useState<string>("0");
const [cartPurchasesAmountField, setCartPurchasesAmountField] = const [cartPurchasesAmountField, setCartPurchasesAmountField] = useState<string>("0");
useState<string>("0"); const [discountMinValueField, setDiscountMinValueField] = 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);
@ -67,17 +56,13 @@ export default function EditDiscountDialog() {
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);
}; };
@ -89,34 +74,20 @@ export default function EditDiscountDialog() {
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 (!isFinite(discountMinValue))
return enqueueSnackbar("Поле discountMinValue не число");
if (discountType === "privilege" && !privilegeIdField)
return enqueueSnackbar("Привилегия не выбрана");
if (!discountNameField) return enqueueSnackbar('Поле "Имя" пустое'); if (!discountNameField) return enqueueSnackbar('Поле "Имя" пустое');
if (!discountDescriptionField) if (!discountDescriptionField) return enqueueSnackbar('Поле "Описание" пустое');
return enqueueSnackbar('Поле "Описание" пустое'); if (discountFactor < 0) return enqueueSnackbar("Процент скидки не может быть больше 100");
if (discountFactor < 0)
return enqueueSnackbar("Процент скидки не может быть больше 100");
const [patchedDiscountResponse, patchedDiscountError] = await patchDiscount( const [patchedDiscountResponse, patchedDiscountError] = await patchDiscount(discount.ID, {
discount.ID,
{
cartPurchasesAmount, cartPurchasesAmount,
discountFactor, discountFactor,
discountMinValue, discountMinValue,
@ -128,8 +99,7 @@ export default function EditDiscountDialog() {
serviceType, serviceType,
discountType, discountType,
privilegeId: privilegeIdField, privilegeId: privilegeIdField,
} });
);
if (patchedDiscountError) { if (patchedDiscountError) {
console.error("Error patching discount", patchedDiscountError); console.error("Error patching discount", patchedDiscountError);

@ -1,16 +1,15 @@
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 Users: React.FC = () => {
const [selectedValue, setSelectedValue] = React.useState('a'); const [selectedValue, setSelectedValue] = React.useState("a");
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedValue(event.target.value); setSelectedValue(event.target.value);
}; };
@ -28,29 +27,36 @@ const Users: React.FC = () => {
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}}> }}
>
Юридические лица Юридические лица
</Typography> </Typography>
<Table sx={{ <Table
sx={{
width: "90%", width: "90%",
border: "2px solid", border: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
marginTop: "35px", marginTop: "35px",
}} aria-label="simple table"> }}
aria-label="simple table"
>
<TableHead> <TableHead>
<TableRow sx={{ <TableRow
sx={{
borderBottom: "2px solid", borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main, borderColor: theme.palette.grayLight.main,
height: "100px" height: "100px",
}}> }}
>
<TableCell sx={{ textAlign: "center" }}> <TableCell sx={{ textAlign: "center" }}>
<Typography <Typography
variant="h4" variant="h4"
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}}> }}
>
ID ID
</Typography> </Typography>
</TableCell> </TableCell>
@ -59,7 +65,8 @@ const Users: React.FC = () => {
variant="h4" variant="h4"
sx={{ sx={{
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
}}> }}
>
Дата / время регистрации Дата / время регистрации
</Typography> </Typography>
</TableCell> </TableCell>
@ -67,67 +74,88 @@ const Users: React.FC = () => {
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow sx={{ <TableRow
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") }> }}
onClick={() => navigate("/modalEntities")}
>
<TableCell sx={{ textAlign: "center" }}> <TableCell sx={{ textAlign: "center" }}>
<Typography sx={{ <Typography
color: theme.palette.secondary.main sx={{
}}> color: theme.palette.secondary.main,
}}
>
1 1
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell sx={{ textAlign: "center" }}> <TableCell sx={{ textAlign: "center" }}>
<Typography sx={{ <Typography
color: theme.palette.secondary.main sx={{
}}> color: theme.palette.secondary.main,
}}
>
2022 2022
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow sx={{ <TableRow
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") }> }}
onClick={() => navigate("/modalEntities")}
>
<TableCell sx={{ textAlign: "center" }}> <TableCell sx={{ textAlign: "center" }}>
<Typography sx={{ <Typography
color: theme.palette.secondary.main sx={{
}}> color: theme.palette.secondary.main,
}}
>
2 2
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell sx={{ textAlign: "center" }}> <TableCell sx={{ textAlign: "center" }}>
<Typography sx={{ <Typography
color: theme.palette.secondary.main sx={{
}}> color: theme.palette.secondary.main,
}}
>
2021 2021
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow sx={{ <TableRow
sx={{
borderBottom: "1px solid", borderBottom: "1px solid",
border: theme.palette.secondary.main, border: theme.palette.secondary.main,
height: "100px", height: "100px",
cursor: "pointer" cursor: "pointer",
}} onClick={ () => navigate("/modalEntities") }> }}
onClick={() => navigate("/modalEntities")}
>
<TableCell sx={{ textAlign: "center" }}> <TableCell sx={{ textAlign: "center" }}>
<Typography sx={{ <Typography
color: theme.palette.secondary.main sx={{
}}> color: theme.palette.secondary.main,
}}
>
3 3
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell sx={{ textAlign: "center" }}> <TableCell sx={{ textAlign: "center" }}>
<Typography sx={{ <Typography
color: theme.palette.secondary.main sx={{
}}> color: theme.palette.secondary.main,
}}
>
2020 2020
</Typography> </Typography>
</TableCell> </TableCell>
@ -136,7 +164,6 @@ const Users: React.FC = () => {
</Table> </Table>
</React.Fragment> </React.Fragment>
); );
} };
export default Users; 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";
@ -75,16 +66,11 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}, []); }, []);
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") ||
bonusType === "privilege"
) {
if (currentPrivilege === undefined) { if (currentPrivilege === undefined) {
enqueueSnackbar("Привилегия не выбрана"); enqueueSnackbar("Привилегия не выбрана");
@ -138,24 +124,9 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
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
onChange={handleChange}
/>
<CustomTextField
name="description"
label="Описание"
required
onChange={handleChange}
/>
<CustomTextField
name="greetings"
label="Приветственное сообщение"
required
onChange={handleChange}
/>
<Typography <Typography
variant="h4" variant="h4"
sx={{ sx={{
@ -190,12 +161,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
name="activationCount" name="activationCount"
label="Количество активаций промокода" label="Количество активаций промокода"
required required
onChange={({ target }) => onChange={({ target }) => setFieldValue("activationCount", Number(target.value.replace(/\D/g, "")))}
setFieldValue(
"activationCount",
Number(target.value.replace(/\D/g, ""))
)
}
/> />
<RadioGroup <RadioGroup
row row
@ -207,16 +173,8 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}} }}
onBlur={handleBlur} onBlur={handleBlur}
> >
<FormControlLabel <FormControlLabel value="discount" control={<Radio color="secondary" />} label="Скидка" />
value="discount" <FormControlLabel value="privilege" control={<Radio color="secondary" />} label="Привилегия" />
control={<Radio color="secondary" />}
label="Скидка"
/>
<FormControlLabel
value="privilege"
control={<Radio color="secondary" />}
label="Привилегия"
/>
</RadioGroup> </RadioGroup>
{bonusType === "discount" && ( {bonusType === "discount" && (
<> <>
@ -231,26 +189,15 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}} }}
onBlur={handleBlur} onBlur={handleBlur}
> >
<FormControlLabel <FormControlLabel value="1" control={<Radio color="secondary" />} label="Привилегия" />
value="1" <FormControlLabel value="2" control={<Radio color="secondary" />} label="Сервис" />
control={<Radio color="secondary" />}
label="Привилегия"
/>
<FormControlLabel
value="2"
control={<Radio color="secondary" />}
label="Сервис"
/>
</RadioGroup> </RadioGroup>
<CustomTextField <CustomTextField
name="factor" name="factor"
label="Процент скидки" label="Процент скидки"
required required
onChange={({ target }) => { onChange={({ target }) => {
setFieldValue( setFieldValue("factor", Number(target.value.replace(/\D/g, "")));
"factor",
Number(target.value.replace(/\D/g, ""))
);
}} }}
/> />
<Typography <Typography
@ -319,12 +266,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
<CustomTextField <CustomTextField
name="threshold" name="threshold"
label="При каком значении применяется скидка" label="При каком значении применяется скидка"
onChange={({ target }) => onChange={({ target }) => setFieldValue("threshold", Number(target.value.replace(/\D/g, "")))}
setFieldValue(
"threshold",
Number(target.value.replace(/\D/g, ""))
)
}
/> />
</> </>
)} )}
@ -366,12 +308,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
name="amount" name="amount"
label="Количество" label="Количество"
required required
onChange={({ target }) => onChange={({ target }) => setFieldValue("amount", Number(target.value.replace(/\D/g, "")))}
setFieldValue(
"amount",
Number(target.value.replace(/\D/g, ""))
)
}
/> />
</> </>
)} )}
@ -403,12 +340,7 @@ type CustomTextFieldProps = {
onChange: (event: ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}; };
const CustomTextField = ({ const CustomTextField = ({ name, label, required = false, onChange }: CustomTextFieldProps) => (
name,
label,
required = false,
onChange,
}: CustomTextFieldProps) => (
<Field <Field
name={name} name={name}
label={label} label={label}

@ -1,17 +1,17 @@
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,
@ -24,34 +24,29 @@ interface Props {
deletePromocode: (id: string) => Promise<void>; deletePromocode: (id: string) => Promise<void>;
} }
export default function ({ export default function ({ id, setModal, deletePromocode }: Props) {
id,
setModal,
deletePromocode
}: Props) {
return ( return (
<Modal <Modal open={Boolean(id)} onClose={() => setModal("")}>
open={Boolean(id)}
onClose={() => setModal("")}
>
<Box sx={{ ...style, width: 400 }}> <Box sx={{ ...style, width: 400 }}>
<Typography <Typography variant="h5" textAlign="center">
variant='h5' Точно удалить промокод?
textAlign="center" </Typography>
>Точно удалить промокод?</Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
justifyContent: "space-evenly", justifyContent: "space-evenly",
mt: "15px" mt: "15px",
}} }}
> >
<Button <Button
onClick={() => { deletePromocode(id); setModal("") }} onClick={() => {
>Да</Button> deletePromocode(id);
<Button setModal("");
onClick={() => setModal("")} }}
>Нет</Button> >
Да
</Button>
<Button onClick={() => setModal("")}>Нет</Button>
</Box> </Box>
</Box> </Box>
</Modal> </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,7 +12,7 @@ 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;
@ -51,7 +42,9 @@ const COLUMNS: GridColDef<Row, string>[] = [
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 /> <ContentCopyIcon />
</IconButton> </IconButton>
@ -64,8 +57,7 @@ const COLUMNS: GridColDef<Row, string>[] = [
width: 320, width: 320,
sortable: false, sortable: false,
valueGetter: ({ row }) => row.link, valueGetter: ({ row }) => row.link,
renderCell: ({ value }) => renderCell: ({ value }) => value?.split("|").map((link) => <Typography>{link}</Typography>),
value?.split("|").map((link) => <Typography>{link}</Typography>),
}, },
{ {
field: "useCount", field: "useCount",
@ -100,11 +92,8 @@ export const StatisticsModal = ({
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(
(item) => item.privilegeId === currentPrivilegeId
);
const promocode = promocodes.find((item) => item.id === id); const promocode = promocodes.find((item) => item.id === id);
const createFastlink = async () => { const createFastlink = async () => {
await createFastLink(id); await createFastLink(id);
@ -193,19 +182,12 @@ export const StatisticsModal = ({
</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>
от
</Typography>
<DatePicker <DatePicker
inputFormat="DD/MM/YYYY" inputFormat="DD/MM/YYYY"
value={startDate} value={startDate}
onChange={(date) => date && setStartDate(date)} onChange={(date) => date && setStartDate(date)}
renderInput={(params) => ( renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{ InputProps={{
sx: { sx: {
height: "40px", height: "40px",
@ -227,19 +209,12 @@ export const StatisticsModal = ({
marginTop: "10px", marginTop: "10px",
}} }}
> >
<Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}> <Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}>до</Typography>
до
</Typography>
<DatePicker <DatePicker
inputFormat="DD/MM/YYYY" inputFormat="DD/MM/YYYY"
value={endDate} value={endDate}
onChange={(date) => date && setEndDate(date)} onChange={(date) => date && setEndDate(date)}
renderInput={(params) => ( renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{ InputProps={{
sx: { sx: {
height: "40px", height: "40px",
@ -311,24 +286,15 @@ export const StatisticsModal = ({
> >
<Typography>название привилегии: {privilege.name}</Typography> <Typography>название привилегии: {privilege.name}</Typography>
<Typography> <Typography>
{promocode?.activationCount} активаций из{" "} {promocode?.activationCount} активаций из {promocode?.activationLimit}
{promocode?.activationLimit}
</Typography> </Typography>
<Typography>приветствие: "{promocode?.greetings}"</Typography> <Typography>приветствие: "{promocode?.greetings}"</Typography>
{promocode?.bonus?.discount?.factor !== undefined && ( {promocode?.bonus?.discount?.factor !== undefined && (
<Typography> <Typography>скидка: {100 - promocode?.bonus?.discount?.factor * 100}%</Typography>
скидка: {100 - promocode?.bonus?.discount?.factor * 100}%
</Typography>
)} )}
{ {<Typography>количество привилегии: {promocode?.bonus?.privilege?.amount}</Typography>}
<Typography>
количество привилегии: {promocode?.bonus?.privilege?.amount}
</Typography>
}
{promocode?.dueTo !== undefined && promocode.dueTo > 0 && ( {promocode?.dueTo !== undefined && promocode.dueTo > 0 && (
<Typography> <Typography>действует до: {new Date(promocode.dueTo).toLocaleString()}</Typography>
действует до: {new Date(promocode.dueTo).toLocaleString()}
</Typography>
)} )}
</Box> </Box>
)} )}

@ -16,8 +16,7 @@ export const PromocodeManagement = () => {
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);
@ -32,10 +31,7 @@ export const PromocodeManagement = () => {
createPromocode, createPromocode,
createFastLink, createFastLink,
} = usePromocodes(page, pageSize, showStatisticsModalId, to, from); } = usePromocodes(page, pageSize, showStatisticsModalId, to, from);
const columns = usePromocodeGridColDef( const columns = usePromocodeGridColDef(setShowStatisticsModalId, deleteModalHC);
setShowStatisticsModalId,
deleteModalHC
);
if (error) return <Typography>Ошибка загрузки промокодов</Typography>; if (error) return <Typography>Ошибка загрузки промокодов</Typography>;
return ( return (
@ -104,11 +100,7 @@ export const PromocodeManagement = () => {
promocodes={data?.items ?? []} promocodes={data?.items ?? []}
createFastLink={createFastLink} createFastLink={createFastLink}
/> />
<DeleteModal <DeleteModal id={deleteModal} setModal={setDeleteModal} deletePromocode={deletePromocode} />
id={deleteModal}
setModal={setDeleteModal}
deletePromocode={deletePromocode}
/>
</LocalizationProvider> </LocalizationProvider>
); );
}; };

@ -6,10 +6,7 @@ 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,
deletePromocode: (id: string) => void
) {
const validity = (value: string | number) => { const validity = (value: string | number) => {
if (value === 0) { if (value === 0) {
return "неоганичен"; return "неоганичен";
@ -38,8 +35,7 @@ export function usePromocodeGridColDef(
headerName: "Коэф. скидки", headerName: "Коэф. скидки",
width: 120, width: 120,
sortable: false, sortable: false,
valueGetter: ({ row }) => valueGetter: ({ row }) => Math.round(row.bonus.discount.factor * 1000) / 1000,
Math.round(row.bonus.discount.factor * 1000) / 1000,
}, },
{ {
field: "activationCount", field: "activationCount",

@ -29,13 +29,8 @@ export const DateFilter = ({ to, setTo, from, setFrom }: DateFilterProps) => {
<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
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{ InputProps={{
sx: { sx: {
height: "40px", height: "40px",
@ -61,13 +56,8 @@ export const DateFilter = ({ to, setTo, from, setFrom }: DateFilterProps) => {
<DatePicker <DatePicker
inputFormat="DD/MM/YYYY" inputFormat="DD/MM/YYYY"
value={to} value={to}
onChange={(date) => date && setTo(date.endOf('day'))} onChange={(date) => date && setTo(date.endOf("day"))}
renderInput={(params) => ( renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{ InputProps={{
sx: { sx: {
height: "40px", height: "40px",

@ -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";

@ -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,9 +10,7 @@ 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 [to, setTo] = useState<Moment | null>(moment());
const promocodes = useAllPromocodes(); const promocodes = useAllPromocodes();
const promocodeStatistics = usePromocodeStatistics({ to, from }); const promocodeStatistics = usePromocodeStatistics({ to, from });

@ -55,14 +55,14 @@ export const StatisticsSchild = () => {
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) {
@ -81,20 +81,14 @@ export const StatisticsSchild = () => {
}, [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>
Статистика переходов с шильдика
</Typography>
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} /> <DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
<DataGrid <DataGrid
sx={{ marginTop: "30px", width: "80%" }} sx={{ marginTop: "30px", width: "80%" }}
@ -110,9 +104,7 @@ export const StatisticsSchild = () => {
pageSize={pageSize} pageSize={pageSize}
onPageChange={setPage} onPageChange={setPage}
onPageSizeChange={setPageSize} onPageSizeChange={setPageSize}
onCellClick={({ id, field }) => onCellClick={({ id, field }) => field === "user" && setActiveUserId(String(id))}
field === "user" && setActiveUserId(String(id))
}
getRowHeight={() => "auto"} getRowHeight={() => "auto"}
columns={[ columns={[
...COLUMNS, ...COLUMNS,
@ -166,9 +158,7 @@ export const StatisticsSchild = () => {
{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}
</Button>
</TableCell> </TableCell>
<TableCell sx={{ color: "inherit" }} align="center"> <TableCell sx={{ color: "inherit" }} align="center">
{Regs} {Regs}
@ -186,11 +176,7 @@ export const StatisticsSchild = () => {
}, },
]} ]}
/> />
<ModalUser <ModalUser open={openUserModal} onClose={() => setOpenUserModal(false)} userId={activeUserId} />
open={openUserModal}
onClose={() => setOpenUserModal(false)}
userId={activeUserId}
/>
</> </>
); );
}; };

@ -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,57 +31,57 @@ 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);
}, },
@ -86,10 +100,12 @@ export default function Chat() {
clearMessageState(); clearMessageState();
setIsPreventAutoscroll(false); setIsPreventAutoscroll(false);
}, },
marker: "ticket message" marker: "ticket message",
}); });
const throttledScrollHandler = useMemo(() => throttle(() => { const throttledScrollHandler = useMemo(
() =>
throttle(() => {
const chatBox = chatBoxRef.current; const chatBox = chatBoxRef.current;
if (!chatBox) return; if (!chatBox) return;
@ -102,11 +118,14 @@ export default function Chat() {
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(
function scrollOnNewMessage() {
if (!chatBoxRef.current) return; if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) { if (!isPreventAutoscroll) {
@ -115,7 +134,9 @@ export default function Chat() {
}, 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;
@ -145,7 +166,7 @@ export default function Chat() {
let data; let data;
const ticketId = ticket?.id const ticketId = ticket?.id;
if (ticketId !== undefined) { if (ticketId !== undefined) {
try { try {
const body = new FormData(); const body = new FormData();
@ -165,14 +186,14 @@ export default function Chat() {
} }
}; };
const sendFileHC = async (file: File) => { const sendFileHC = async (file: File) => {
const check = checkAcceptableMediaType(file) const check = checkAcceptableMediaType(file);
if (check.length > 0) { if (check.length > 0) {
enqueueSnackbar(check) enqueueSnackbar(check);
return return;
} }
setDisableFileButton(true) setDisableFileButton(true);
await sendFile(file) await sendFile(file);
setDisableFileButton(false) setDisableFileButton(false);
}; };
function handleTextfieldKeyPress(e: KeyboardEvent) { function handleTextfieldKeyPress(e: KeyboardEvent) {
@ -183,7 +204,8 @@ export default function Chat() {
} }
return ( return (
<Box sx={{ <Box
sx={{
border: "1px solid", border: "1px solid",
borderColor: theme.palette.grayDark.main, borderColor: theme.palette.grayDark.main,
height: "600px", height: "600px",
@ -195,7 +217,8 @@ export default function Chat() {
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
gap: "8px", gap: "8px",
}}> }}
>
<Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography> <Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
<Box <Box
ref={chatBoxRef} ref={chatBoxRef}
@ -215,67 +238,73 @@ export default function Chat() {
messages.map((message) => { messages.map((message) => {
const isFileVideo = () => { const isFileVideo = () => {
if (message.files) { if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) => return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType), message.files[0].toLowerCase().endsWith(fileType)
)) );
} }
}; };
const isFileImage = () => { const isFileImage = () => {
if (message.files) { if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) => return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType), message.files[0].toLowerCase().endsWith(fileType)
)) );
} }
}; };
const isFileDocument = () => { const isFileDocument = () => {
if (message.files) { if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) => return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType), message.files[0].toLowerCase().endsWith(fileType)
)) );
} }
}; };
if (message.files !== null && message.files.length > 0 && isFileImage()) { if (message.files !== null && message.files.length > 0 && isFileImage()) {
return <ChatImage return (
<ChatImage
unAuthenticated unAuthenticated
key={message.id} key={message.id}
file={message.files[0]} file={message.files[0]}
createdAt={message.created_at} createdAt={message.created_at}
isSelf={ticket.user !== message.user_id} isSelf={ticket.user !== message.user_id}
/> />
);
} }
if (message.files !== null && message.files.length > 0 && isFileVideo()) { if (message.files !== null && message.files.length > 0 && isFileVideo()) {
return <ChatVideo return (
<ChatVideo
unAuthenticated unAuthenticated
key={message.id} key={message.id}
file={message.files[0]} file={message.files[0]}
createdAt={message.created_at} createdAt={message.created_at}
isSelf={ticket.user !== message.user_id} isSelf={ticket.user !== message.user_id}
/> />
);
} }
if (message.files !== null && message.files.length > 0 && isFileDocument()) { if (message.files !== null && message.files.length > 0 && isFileDocument()) {
return <ChatDocument return (
<ChatDocument
unAuthenticated unAuthenticated
key={message.id} key={message.id}
file={message.files[0]} file={message.files[0]}
createdAt={message.created_at} createdAt={message.created_at}
isSelf={ticket.user !== message.user_id} isSelf={ticket.user !== message.user_id}
/> />
);
} }
return <ChatMessage return (
<ChatMessage
unAuthenticated unAuthenticated
key={message.id} key={message.id}
text={message.message} text={message.message}
createdAt={message.created_at} createdAt={message.created_at}
isSelf={ticket.user !== message.user_id} isSelf={ticket.user !== message.user_id}
/> />
);
}) })}
}
</Box> </Box>
{ticket && {ticket && (
<TextField <TextField
value={messageField} value={messageField}
onChange={e => setMessageField(e.target.value)} onChange={(e) => setMessageField(e.target.value)}
onKeyPress={handleTextfieldKeyPress} onKeyPress={handleTextfieldKeyPress}
id="message-input" id="message-input"
placeholder="Написать сообщение" placeholder="Написать сообщение"
@ -301,7 +330,7 @@ export default function Chat() {
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => { onClick={() => {
if (!disableFileButton) fileInputRef.current?.click() if (!disableFileButton) fileInputRef.current?.click();
}} }}
sx={{ sx={{
height: "45px", height: "45px",
@ -321,15 +350,15 @@ export default function Chat() {
<AttachFileIcon sx={{ color: theme.palette.golden.main }} /> <AttachFileIcon sx={{ color: theme.palette.golden.main }} />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
) ),
}} }}
InputLabelProps={{ InputLabelProps={{
style: { style: {
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
} },
}} }}
/> />
} )}
</Box> </Box>
); );
} }

@ -1,5 +1,5 @@
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;
@ -8,12 +8,7 @@ interface Props {
createdAt: string; createdAt: string;
} }
export default function ChatDocument({ export default function ChatDocument({ unAuthenticated = false, isSelf, file, createdAt }: Props) {
unAuthenticated = false,
isSelf,
file,
createdAt,
}: Props) {
const theme = useTheme(); const theme = useTheme();
const date = new Date(createdAt); const date = new Date(createdAt);
@ -27,11 +22,13 @@ export default function ChatDocument({
justifyContent: isSelf ? "end" : "start", 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={{
@ -45,7 +42,6 @@ export default function ChatDocument({
}} }}
> >
<Link <Link
download download
href={`https://admin.pena/pair/${file}`} href={`https://admin.pena/pair/${file}`}
style={{ style={{
@ -54,7 +50,7 @@ export default function ChatDocument({
gap: "10px", gap: "10px",
}} }}
> >
<DownloadIcon/> <DownloadIcon />
</Link> </Link>
</Box> </Box>
</Box> </Box>

@ -1,11 +1,4 @@
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 {
@ -15,17 +8,11 @@ interface Props {
createdAt: string; createdAt: string;
} }
export default function ChatImage({ export default function ChatImage({ unAuthenticated = false, isSelf, file, createdAt }: Props) {
unAuthenticated = false,
isSelf,
file,
createdAt,
}: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<Box <Box
sx={{ sx={{
@ -35,11 +22,13 @@ export default function ChatImage({
justifyContent: isSelf ? "end" : "start", 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={{

@ -7,17 +7,10 @@ interface Props {
createdAt: string; createdAt: string;
} }
export default function ChatMessage({ export default function ChatMessage({ unAuthenticated = false, isSelf, text, createdAt }: Props) {
unAuthenticated = false,
isSelf,
text,
createdAt,
}: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
return ( return (
<Box <Box
sx={{ sx={{
@ -27,11 +20,13 @@ export default function ChatMessage({
justifyContent: isSelf ? "end" : "start", 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={{

@ -8,12 +8,7 @@ interface Props {
createdAt: string; createdAt: string;
} }
export default function ChatImage({ export default function ChatImage({ unAuthenticated = false, isSelf, file, createdAt }: Props) {
unAuthenticated = false,
isSelf,
file,
createdAt,
}: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate(); const navigate = useNavigate();

@ -1,7 +1,6 @@
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;
@ -11,21 +10,26 @@ export default function Message({ message, isSelf }: Props) {
const theme = useTheme(); const theme = useTheme();
const time = ( const time = (
<Typography sx={{ <Typography
sx={{
fontSize: "12px", fontSize: "12px",
alignSelf: "end", alignSelf: "end",
}}> }}
{new Date(message.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} >
{new Date(message.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</Typography> </Typography>
); );
return ( return (
<Box sx={{ <Box
sx={{
display: "flex", display: "flex",
justifyContent: isSelf ? "end" : "start", justifyContent: isSelf ? "end" : "start",
gap: "6px", gap: "6px",
}}> }}
>
{isSelf && time} {isSelf && time}
<Box sx={{ <Box
sx={{
backgroundColor: "#2a2b2c", backgroundColor: "#2a2b2c",
p: "12px", p: "12px",
border: `1px solid ${theme.palette.golden.main}`, border: `1px solid ${theme.palette.golden.main}`,
@ -33,7 +37,8 @@ export default function Message({ message, isSelf }: Props) {
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" }}>
{message.message} {message.message}
</Typography> </Typography>

@ -6,4 +6,4 @@ 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;

@ -1,6 +1,5 @@
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material";
interface Props { interface Props {
isExpanded: boolean; isExpanded: boolean;
} }
@ -9,9 +8,27 @@ 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"
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> </svg>
); );
} }

@ -4,20 +4,10 @@ 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() {
@ -45,9 +35,7 @@ export default function Support() {
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 +
`/heruvym/subscribe?Authorization=${token}`,
onNewData: updateTickets, onNewData: updateTickets,
onDisconnect: () => { onDisconnect: () => {
clearMessageState(); clearMessageState();
@ -83,21 +71,12 @@ export default function Support() {
> >
{!upMd && ( {!upMd && (
<Collapse headerText="Тикеты"> <Collapse headerText="Тикеты">
{(closeCollapse) => ( {(closeCollapse) => <TicketList closeCollapse={closeCollapse} setActiveUserId={setActiveUserId} />}
<TicketList
closeCollapse={closeCollapse}
setActiveUserId={setActiveUserId}
/>
)}
</Collapse> </Collapse>
)} )}
<Chat /> <Chat />
{upMd && <TicketList setActiveUserId={setActiveUserId} />} {upMd && <TicketList setActiveUserId={setActiveUserId} />}
<ModalUser <ModalUser open={openUserModal} onClose={() => setOpenUserModal(false)} userId={activeUserId} />
open={openUserModal}
onClose={() => setOpenUserModal(false)}
userId={activeUserId}
/>
</Box> </Box>
); );
} }

@ -1,26 +1,25 @@
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 () => { const CloseTicket = async () => {
try { try {
const ticketCloseResponse = await makeRequest<unknown, unknown>({ const ticketCloseResponse = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/heruvym/close" , url: process.env.REACT_APP_DOMAIN + "/heruvym/close",
method: "post", method: "post",
useToken: true, useToken: true,
body: { body: {
"ticket": ticketId ticket: ticketId,
}, },
}); });
@ -30,7 +29,7 @@ export default function CloseTicketModal({ticketId, openModal, setOpenModal}: Pr
return [null, `Не удалось закрыть тикет. ${error}`]; return [null, `Не удалось закрыть тикет. ${error}`];
} }
} };
return ( return (
<Modal <Modal
@ -66,22 +65,19 @@ export default function CloseTicketModal({ticketId, openModal, setOpenModal}: Pr
}} }}
> >
<Button <Button
onClick={async ()=>{ onClick={async () => {
CloseTicket() CloseTicket();
setOpenModal(false) setOpenModal(false);
}} }}
sx={{width: "40px", height: "25px"}} sx={{ width: "40px", height: "25px" }}
> >
Да Да
</Button> </Button>
<Button <Button onClick={() => setOpenModal(false)} sx={{ width: "40px", height: "25px" }}>
onClick={() => setOpenModal(false)}
sx={{width: "40px", height: "25px"}}
>
Нет Нет
</Button> </Button>
</Box> </Box>
</Box> </Box>
</Modal> </Modal>
) );
} }

@ -1,14 +1,5 @@
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";
@ -76,9 +67,7 @@ export default function TicketItem({ ticket, setActiveUserId }: Props) {
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 <Box
sx={{ sx={{
...flexCenterSx, ...flexCenterSx,

@ -3,12 +3,12 @@ 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 = {
@ -16,17 +16,14 @@ type TicketListProps = {
setActiveUserId: (id: string) => void; setActiveUserId: (id: string) => void;
}; };
export default function TicketList({ export default function TicketList({ closeCollapse, setActiveUserId }: TicketListProps) {
closeCollapse,
setActiveUserId,
}: TicketListProps) {
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 ticketsFetchState = useTicketStore((state) => state.ticketsFetchState); const ticketsFetchState = useTicketStore((state) => state.ticketsFetchState);
const ticketsBoxRef = useRef<HTMLDivElement>(null); const ticketsBoxRef = useRef<HTMLDivElement>(null);
const ticketId = useParams().ticketId; const ticketId = useParams().ticketId;
const [openModal, setOpenModal] = useState(false) const [openModal, setOpenModal] = useState(false);
useEffect( useEffect(
function updateCurrentPageOnScroll() { function updateCurrentPageOnScroll() {
@ -34,15 +31,8 @@ export default function TicketList({
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);
@ -55,9 +45,7 @@ export default function TicketList({
[ticketsFetchState] [ticketsFetchState]
); );
const sortedTickets = tickets const sortedTickets = tickets.sort(sortTicketsByUpdateTime).sort(sortTicketsByUnread);
.sort(sortTicketsByUpdateTime)
.sort(sortTicketsByUnread);
return ( return (
<Box <Box
@ -97,7 +85,7 @@ export default function TicketList({
<SearchOutlinedIcon /> <SearchOutlinedIcon />
</Button> </Button>
<Button <Button
onClick={()=> setOpenModal(true)} onClick={() => setOpenModal(true)}
variant="text" variant="text"
sx={{ sx={{
width: "100%", width: "100%",
@ -116,7 +104,7 @@ export default function TicketList({
ЗАКРЫТЬ ТИКЕТ ЗАКРЫТЬ ТИКЕТ
<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}

@ -2,9 +2,8 @@ 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 }) => {
@ -19,63 +18,68 @@ const Contractor: React.FC<MWProps> = ({ openModal }) => {
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
sx={{
marginTop: "35px", marginTop: "35px",
display: "grid", display: "grid",
gridTemplateColumns: "repeat(2, 1fr)", gridTemplateColumns: "repeat(2, 1fr)",
gridGap: "20px", gridGap: "20px",
marginBottom: "120px", marginBottom: "120px",
}}> }}
>
<Button <Button
variant = "contained" variant="contained"
onClick={ () => openModal(3, 1) } onClick={() => openModal(3, 1)}
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
padding: '11px 65px', padding: "11px 65px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
} },
}}> }}
>
Создать тариф <br /> на аналитику время Создать тариф <br /> на аналитику время
</Button> </Button>
<Button <Button
variant = "contained" variant="contained"
onClick={ () => openModal(3, 1) } onClick={() => openModal(3, 1)}
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
padding: '11px 65px', padding: "11px 65px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
} },
}}> }}
>
Создать тариф <br /> на a/b тесты время Создать тариф <br /> на a/b тесты время
</Button> </Button>
<Button <Button
variant = "contained" variant="contained"
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
padding: '11px 65px', padding: "11px 65px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
} },
}}> }}
>
Изменить тариф Изменить тариф
</Button> </Button>
</Box> </Box>
</React.Fragment> </React.Fragment>
); );
} };
export default Contractor; export default Contractor;

@ -7,28 +7,26 @@ import {
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() {
@ -40,16 +38,15 @@ export default function CreateTariff() {
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 = { const initialValues: Values = {
@ -59,13 +56,10 @@ export default function CreateTariff() {
customPriceField: "", customPriceField: "",
privilegeIdField: "", privilegeIdField: "",
orderField: 0, orderField: 0,
privilege: null privilege: null,
}; };
const createTariffBackend = async ( const createTariffBackend = async (values: Values, formikHelpers: FormikHelpers<Values>) => {
values: Values,
formikHelpers: FormikHelpers<Values>
) => {
if (values.privilege !== null) { if (values.privilege !== null) {
const [, createdTariffError] = await createTariff({ const [, createdTariffError] = await createTariff({
name: values.nameField, name: values.nameField,
@ -111,13 +105,9 @@ export default function CreateTariff() {
// } // }
return ( return (
<Formik <Formik initialValues={initialValues} validate={checkFulledFields} onSubmit={createTariffBackend}>
initialValues={initialValues}
validate={checkFulledFields}
onSubmit={createTariffBackend}
>
{(props) => ( {(props) => (
<Form style={{ width: "100%" }} > <Form style={{ width: "100%" }}>
<Container <Container
sx={{ sx={{
p: "20px", p: "20px",
@ -126,7 +116,6 @@ export default function CreateTariff() {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "12px", gap: "12px",
}} }}
> >
<Typography variant="h6" sx={{ textAlign: "center", mb: "16px" }}> <Typography variant="h6" sx={{ textAlign: "center", mb: "16px" }}>
@ -162,13 +151,13 @@ export default function CreateTariff() {
label="Привилегия" label="Привилегия"
error={props.touched.privilegeIdField && !!props.errors.privilegeIdField} error={props.touched.privilegeIdField && !!props.errors.privilegeIdField}
onChange={(e) => { onChange={(e) => {
console.log(e.target.value) console.log(e.target.value);
console.log(findPrivilegeById(e.target.value)) console.log(findPrivilegeById(e.target.value));
if (findPrivilegeById(e.target.value) === null) { if (findPrivilegeById(e.target.value) === null) {
return enqueueSnackbar("Привилегия не найдена"); return enqueueSnackbar("Привилегия не найдена");
} }
props.setFieldValue("privilegeIdField", e.target.value) props.setFieldValue("privilegeIdField", e.target.value);
props.setFieldValue("privilege", findPrivilegeById(e.target.value)) props.setFieldValue("privilege", findPrivilegeById(e.target.value));
}} }}
onBlur={props.handleBlur} onBlur={props.handleBlur}
sx={{ sx={{
@ -213,7 +202,8 @@ export default function CreateTariff() {
Единица: <span>{props.values.privilege.type}</span> Единица: <span>{props.values.privilege.type}</span>
</Typography> </Typography>
<Typography> <Typography>
Стандартная цена за единицу: <span>{currencyFormatter.format(props.values.privilege.price / 100)}</span> Стандартная цена за единицу:{" "}
<span>{currencyFormatter.format(props.values.privilege.price / 100)}</span>
</Typography> </Typography>
</Box> </Box>
)} )}
@ -225,21 +215,17 @@ export default function CreateTariff() {
label="Название тарифа" label="Название тарифа"
type="text" type="text"
error={props.touched.nameField && !!props.errors.nameField} error={props.touched.nameField && !!props.errors.nameField}
helperText={ helperText={<Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.nameField}</Typography>}
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.nameField}
</Typography>
}
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,
} },
}} }}
/> />
<TextField <TextField
@ -247,27 +233,23 @@ export default function CreateTariff() {
name="amountField" name="amountField"
variant="filled" variant="filled"
onChange={(e) => { onChange={(e) => {
props.setFieldValue("amountField", e.target.value.replace(/[^\d]/g, '')) props.setFieldValue("amountField", e.target.value.replace(/[^\d]/g, ""));
}} }}
value={props.values.amountField} value={props.values.amountField}
onBlur={props.handleBlur} onBlur={props.handleBlur}
label="Кол-во единиц привилегии" label="Кол-во единиц привилегии"
error={props.touched.amountField && !!props.errors.amountField} error={props.touched.amountField && !!props.errors.amountField}
helperText={ helperText={<Typography sx={{ fontSize: "12px", width: "200px" }}>{props.errors.amountField}</Typography>}
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.amountField}
</Typography>
}
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,
} },
}} }}
/> />
<TextField <TextField
@ -275,7 +257,7 @@ export default function CreateTariff() {
name="customPriceField" name="customPriceField"
variant="filled" variant="filled"
onChange={(e) => { onChange={(e) => {
props.setFieldValue("customPriceField", e.target.value.replace(/[^\d]/g, '')) props.setFieldValue("customPriceField", e.target.value.replace(/[^\d]/g, ""));
}} }}
value={props.values.customPriceField} value={props.values.customPriceField}
onBlur={props.handleBlur} onBlur={props.handleBlur}
@ -284,12 +266,12 @@ export default function CreateTariff() {
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,
} },
}} }}
/> />
<TextField <TextField
@ -297,7 +279,7 @@ export default function CreateTariff() {
name="descriptionField" name="descriptionField"
variant="filled" variant="filled"
onChange={(e) => { onChange={(e) => {
props.setFieldValue("descriptionField", e.target.value) props.setFieldValue("descriptionField", e.target.value);
}} }}
value={props.values.descriptionField} value={props.values.descriptionField}
onBlur={props.handleBlur} onBlur={props.handleBlur}
@ -307,12 +289,12 @@ export default function CreateTariff() {
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,
} },
}} }}
/> />
<TextField <TextField
@ -320,7 +302,7 @@ export default function CreateTariff() {
name="orderField" name="orderField"
variant="filled" variant="filled"
onChange={(e) => { onChange={(e) => {
props.setFieldValue("orderField", e.target.value) props.setFieldValue("orderField", e.target.value);
}} }}
value={props.values.orderField} value={props.values.orderField}
onBlur={props.handleBlur} onBlur={props.handleBlur}
@ -329,26 +311,21 @@ export default function CreateTariff() {
style: { style: {
backgroundColor: theme.palette.content.main, backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
} },
}} }}
type={'number'} type={"number"}
InputLabelProps={{ InputLabelProps={{
style: { style: {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
} },
}} }}
/> />
<Button <Button className="btn_createTariffBackend" type="submit" disabled={props.isSubmitting}>
className="btn_createTariffBackend"
type="submit"
disabled={props.isSubmitting}
>
Создать Создать
</Button> </Button>
</Container> </Container>
</Form> </Form>
)} )}
</Formik> </Formik>
); );
} }

@ -1,7 +1,6 @@
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;
@ -17,11 +16,11 @@ export default function CustomButton({ onClick, children, sx }: Props) {
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,
}} }}

@ -1,7 +1,6 @@
import { Typography, useTheme } from "@mui/material"; import { Typography, useTheme } from "@mui/material";
import { ReactNode } from "react"; import { ReactNode } from "react";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
} }
@ -19,7 +18,7 @@ export default function CustomHeader({ children }: Props) {
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
color: theme.palette.secondary.main color: theme.palette.secondary.main,
}} }}
> >
{children} {children}

@ -8,14 +8,11 @@ import { enqueueSnackbar } from "notistack";
import { deleteTariff } from "@root/api/tariffs"; import { deleteTariff } from "@root/api/tariffs";
import { devlog } from "@frontend/kitui"; import { devlog } from "@frontend/kitui";
export default function DeleteModal() { export default function DeleteModal() {
const deleteTariffIds = useTariffStore(state => state.deleteTariffIds); const deleteTariffIds = useTariffStore((state) => state.deleteTariffIds);
async function deleteManyTariffs(tariffIds: string[]) { async function deleteManyTariffs(tariffIds: string[]) {
const results = await Promise.allSettled( const results = await Promise.allSettled(tariffIds.map((tariffId) => deleteTariff(tariffId)));
tariffIds.map((tariffId) => deleteTariff(tariffId))
);
let deletedCount = 0; let deletedCount = 0;
let errorCount = 0; let errorCount = 0;
@ -42,7 +39,7 @@ export default function DeleteModal() {
closeDeleteTariffDialog(); closeDeleteTariffDialog();
requestTariffs(); requestTariffs();
}; }
return ( return (
<Modal <Modal
@ -77,10 +74,7 @@ export default function DeleteModal() {
alignItems: "center", alignItems: "center",
}} }}
> >
<Button <Button onClick={() => handleTariffDeleteClick()} sx={{ width: "40px", height: "25px" }}>
onClick={() => handleTariffDeleteClick()}
sx={{ width: "40px", height: "25px" }}
>
Да Да
</Button> </Button>
{/* <Typography>Тариф:</Typography> {/* <Typography>Тариф:</Typography>

@ -36,8 +36,7 @@ export default function EditModal() {
const price = parseFloat(priceField); const price = parseFloat(priceField);
if (!isFinite(price)) if (!isFinite(price)) return enqueueSnackbar('Поле "Цена за единицу" не число');
return enqueueSnackbar('Поле "Цена за единицу" не число');
if (!nameField) return enqueueSnackbar('Поле "Имя" пустое'); if (!nameField) return enqueueSnackbar('Поле "Имя" пустое');
const updatedTariff = structuredClone(tariff); const updatedTariff = structuredClone(tariff);
@ -78,12 +77,7 @@ export default function EditModal() {
p: 4, p: 4,
}} }}
> >
<Typography <Typography id="modal-modal-title" variant="h6" component="h2" sx={{ whiteSpace: "nowrap" }}>
id="modal-modal-title"
variant="h6"
component="h2"
sx={{ whiteSpace: "nowrap" }}
>
Редактирование тариффа Редактирование тариффа
</Typography> </Typography>
@ -97,14 +91,10 @@ export default function EditModal() {
value={nameField} value={nameField}
sx={{ marginBottom: "10px" }} sx={{ marginBottom: "10px" }}
/> />
<Typography> <Typography>Цена: {Math.trunc((tariff.price ?? 0) / 100)}</Typography>
Цена: {Math.trunc((tariff.price ?? 0) / 100)}
</Typography>
<TextField <TextField
type="number" type="number"
onChange={({ target }) => onChange={({ target }) => setPriceField(String(+target.value * 100))}
setPriceField(String(+target.value * 100))
}
label="Цена" label="Цена"
name="price" name="price"
value={Math.trunc(Number(priceField) / 100)} value={Math.trunc(Number(priceField) / 100)}

@ -2,9 +2,8 @@ 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 Quiz: React.FC<MWProps> = ({ openModal }) => { const Quiz: React.FC<MWProps> = ({ openModal }) => {
@ -19,63 +18,68 @@ const Quiz: React.FC<MWProps> = ({ openModal }) => {
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
sx={{
marginTop: "35px", marginTop: "35px",
display: "grid", display: "grid",
gridTemplateColumns: "repeat(2, 1fr)", gridTemplateColumns: "repeat(2, 1fr)",
gridGap: "20px", gridGap: "20px",
marginBottom: "120px", marginBottom: "120px",
}}> }}
>
<Button <Button
variant = "contained" variant="contained"
onClick={ () => openModal(2, 1) } onClick={() => openModal(2, 1)}
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
padding: "11px 43px", padding: "11px 43px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
} },
}}> }}
>
Создать тариф на время Создать тариф на время
</Button> </Button>
<Button <Button
variant = "contained" variant="contained"
onClick={ () => openModal(2, 0) } onClick={() => openModal(2, 0)}
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
padding: '11px 43px', padding: "11px 43px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
} },
}}> }}
>
Создать тариф на объем Создать тариф на объем
</Button> </Button>
<Button <Button
variant = "contained" variant="contained"
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
padding: '11px 43px', padding: "11px 43px",
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
} },
}}> }}
>
Изменить тариф Изменить тариф
</Button> </Button>
</Box> </Box>
</React.Fragment> </React.Fragment>
); );
} };
export default Quiz; export default Quiz;

@ -4,9 +4,7 @@ import TariffsDG from "./tariffsDG";
import { Suspense } from "react"; import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
export default function TariffsInfo() { export default function TariffsInfo() {
return ( return (
<> <>
<Typography variant="h6" mt="20px"> <Typography variant="h6" mt="20px">

@ -2,9 +2,8 @@ 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 Templater: React.FC<MWProps> = ({ openModal }) => { const Templater: React.FC<MWProps> = ({ openModal }) => {
@ -19,77 +18,83 @@ const Templater: React.FC<MWProps> = ({ openModal }) => {
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
sx={{
marginTop: "35px", marginTop: "35px",
display: "grid", display: "grid",
gridTemplateColumns: "repeat(2, 1fr)", gridTemplateColumns: "repeat(2, 1fr)",
gridGap: "20px", gridGap: "20px",
marginBottom: "120px", marginBottom: "120px",
}}> }}
>
<Button <Button
variant = "contained" variant="contained"
onClick={ () => openModal(1, 1) } onClick={() => openModal(1, 1)}
sx={{ sx={{
backgroundColor: theme.palette.menu.main, backgroundColor: theme.palette.menu.main,
fontWeight: "normal", fontWeight: "normal",
fontSize: "17px", fontSize: "17px",
padding: '11px 25px', padding: "11px 25px",
"&:hover": { "&:hover": {
backgroundColor: theme.palette.grayMedium.main backgroundColor: theme.palette.grayMedium.main,
} },
}}> }}
>
Создать тариф на время Создать тариф на время
</Button> </Button>
<Button <Button
variant = "contained" variant="contained"
onClick={ () => openModal(1, 0) } onClick={() => openModal(1, 0)}
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,
} },
}}> }}
>
Создать тариф на объем Создать тариф на объем
</Button> </Button>
<Button <Button
variant = "contained" variant="contained"
onClick={ () => openModal(1, 2) } onClick={() => openModal(1, 2)}
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,
} },
}}> }}
>
Создать тариф на гигабайты Создать тариф на гигабайты
</Button> </Button>
<Button <Button
variant = "contained" variant="contained"
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,
} },
}}> }}
>
Изменить тариф Изменить тариф
</Button> </Button>
</Box> </Box>
</React.Fragment> </React.Fragment>
); );
} };
export default Templater; export default Templater;

@ -7,12 +7,16 @@ import DeleteModal from "@root/pages/dashboard/Content/Tariffs/DeleteModal";
import EditModal from "./EditModal"; import EditModal from "./EditModal";
import AutorenewIcon from "@mui/icons-material/Autorenew"; import AutorenewIcon from "@mui/icons-material/Autorenew";
import { requestTariffs } from "@root/services/tariffs.service"; import { requestTariffs } from "@root/services/tariffs.service";
import { openDeleteTariffDialog, openEditTariffDialog, setSelectedTariffIds, useTariffStore } from "@root/stores/tariffs"; import {
openDeleteTariffDialog,
openEditTariffDialog,
setSelectedTariffIds,
useTariffStore,
} from "@root/stores/tariffs";
import { Tariff } from "@frontend/kitui"; import { Tariff } from "@frontend/kitui";
import { getTariffPrice } from "@root/utils/tariffPrice"; import { getTariffPrice } from "@root/utils/tariffPrice";
import { currencyFormatter } from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
const columns: GridColDef<Tariff, string | number>[] = [ const columns: GridColDef<Tariff, string | number>[] = [
{ field: "_id", headerName: "ID", width: 100, valueGetter: ({ row }) => row.order || 1 }, { field: "_id", headerName: "ID", width: 100, valueGetter: ({ row }) => row.order || 1 },
{ {
@ -32,9 +36,24 @@ const columns: GridColDef<Tariff, string | number>[] = [
{ field: "serviceName", headerName: "Сервис", width: 150, valueGetter: ({ row }) => row.privileges[0].serviceKey }, { field: "serviceName", headerName: "Сервис", width: 150, valueGetter: ({ row }) => row.privileges[0].serviceKey },
{ field: "privilegeName", headerName: "Привилегия", width: 150, valueGetter: ({ row }) => row.privileges[0].name }, { field: "privilegeName", headerName: "Привилегия", width: 150, valueGetter: ({ row }) => row.privileges[0].name },
{ field: "type", headerName: "Единица", width: 100, valueGetter: ({ row }) => row.privileges[0].type }, { field: "type", headerName: "Единица", width: 100, valueGetter: ({ row }) => row.privileges[0].type },
{ field: "pricePerUnit", headerName: "Цена за ед.", width: 100, valueGetter: ({ row }) => currencyFormatter.format(row.privileges[0].price / 100) }, {
{ field: "isCustom", headerName: "Кастомная цена", width: 130, valueGetter: ({ row }) => row.isCustom ? "Да" : "Нет" }, field: "pricePerUnit",
{ field: "total", headerName: "Сумма", width: 100, valueGetter: ({ row }) => currencyFormatter.format(getTariffPrice(row) / 100) }, headerName: "Цена за ед.",
width: 100,
valueGetter: ({ row }) => currencyFormatter.format(row.privileges[0].price / 100),
},
{
field: "isCustom",
headerName: "Кастомная цена",
width: 130,
valueGetter: ({ row }) => (row.isCustom ? "Да" : "Нет"),
},
{
field: "total",
headerName: "Сумма",
width: 100,
valueGetter: ({ row }) => currencyFormatter.format(getTariffPrice(row) / 100),
},
{ {
field: "delete", field: "delete",
headerName: "Удаление", headerName: "Удаление",
@ -51,7 +70,7 @@ const columns: GridColDef<Tariff, string | number>[] = [
export default function TariffsDG() { export default function TariffsDG() {
const tariffs = useTariffStore((state) => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const selectedTariffIds = useTariffStore(state => state.selectedTariffIds); const selectedTariffIds = useTariffStore((state) => state.selectedTariffIds);
return ( return (
<> <>
@ -84,7 +103,7 @@ export default function TariffsDG() {
}} }}
> >
<Button <Button
onClick={() => openDeleteTariffDialog(selectedTariffIds.map(s => s.toString()))} onClick={() => openDeleteTariffDialog(selectedTariffIds.map((s) => s.toString()))}
sx={{ mr: "20px", zIndex: "10000" }} sx={{ mr: "20px", zIndex: "10000" }}
> >
Удалить выделенные Удалить выделенные

@ -89,18 +89,9 @@ const Users: React.FC = () => {
const [activeUserId, setActiveUserId] = useState<string>(""); const [activeUserId, setActiveUserId] = useState<string>("");
const { userId } = useParams(); const { userId } = useParams();
const { data: adminData, adminPages } = useAdmins( const { data: adminData, adminPages } = useAdmins(page.adminPage + 1, pageSize.adminPageSize);
page.adminPage + 1, const { data: managerData, managerPages } = useManagers(page.managerPage + 1, pageSize.managerPageSize);
pageSize.adminPageSize const { data: userData, userPagesCount } = useUsers(page.userPage + 1, pageSize.userPageSize);
);
const { data: managerData, managerPages } = useManagers(
page.managerPage + 1,
pageSize.managerPageSize
);
const { data: userData, userPagesCount } = useUsers(
page.userPage + 1,
pageSize.userPageSize
);
useEffect(() => { useEffect(() => {
handleChangeData(); handleChangeData();
@ -126,9 +117,7 @@ const Users: React.FC = () => {
}); });
}, [selectedValue]); }, [selectedValue]);
const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>( const [selectedTariffs, setSelectedTariffs] = useState<GridSelectionModel>([]);
[]
);
return ( return (
<React.Fragment> <React.Fragment>
{/* <Button {/* <Button
@ -163,9 +152,7 @@ const Users: React.FC = () => {
<AccordionSummary <AccordionSummary
sx={{ display: "flex" }} sx={{ display: "flex" }}
onClick={handleToggleAccordion} onClick={handleToggleAccordion}
expandIcon={ expandIcon={<ExpandMoreIcon sx={{ color: theme.palette.secondary.main }} />}
<ExpandMoreIcon sx={{ color: theme.palette.secondary.main }} />
}
aria-controls="panel1a-content" aria-controls="panel1a-content"
id="panel1a-header" id="panel1a-header"
> >
@ -178,7 +165,7 @@ const Users: React.FC = () => {
{accordionText} {accordionText}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{overflowX: "auto"}}> <AccordionDetails sx={{ overflowX: "auto" }}>
<Table <Table
sx={{ sx={{
width: "100%", width: "100%",
@ -417,45 +404,33 @@ const Users: React.FC = () => {
<ServiceUsersDG <ServiceUsersDG
users={adminData?.users.length ? adminData.users : []} users={adminData?.users.length ? adminData.users : []}
page={page.adminPage} page={page.adminPage}
setPage={(adminPage) => setPage={(adminPage) => setPage((pages) => ({ ...pages, adminPage }))}
setPage((pages) => ({ ...pages, adminPage }))
}
pagesCount={adminPages} pagesCount={adminPages}
pageSize={pageSize.adminPageSize} pageSize={pageSize.adminPageSize}
handleSelectionChange={setSelectedTariffs} handleSelectionChange={setSelectedTariffs}
onPageSizeChange={(adminPageSize) => onPageSizeChange={(adminPageSize) => setPageSize((pageSize) => ({ ...pageSize, adminPageSize }))}
setPageSize((pageSize) => ({ ...pageSize, adminPageSize }))
}
/> />
} }
childrenManager={ childrenManager={
<ServiceUsersDG <ServiceUsersDG
users={managerData?.users.length ? managerData.users : []} users={managerData?.users.length ? managerData.users : []}
page={page.managerPage} page={page.managerPage}
setPage={(managerPage) => setPage={(managerPage) => setPage((pages) => ({ ...pages, managerPage }))}
setPage((pages) => ({ ...pages, managerPage }))
}
pagesCount={managerPages} pagesCount={managerPages}
pageSize={pageSize.managerPageSize} pageSize={pageSize.managerPageSize}
handleSelectionChange={setSelectedTariffs} handleSelectionChange={setSelectedTariffs}
onPageSizeChange={(managerPageSize) => onPageSizeChange={(managerPageSize) => setPageSize((pageSize) => ({ ...pageSize, managerPageSize }))}
setPageSize((pageSize) => ({ ...pageSize, managerPageSize }))
}
/> />
} }
childrenUser={ childrenUser={
<ServiceUsersDG <ServiceUsersDG
users={userData?.users.length ? userData.users : []} users={userData?.users.length ? userData.users : []}
page={page.userPage} page={page.userPage}
setPage={(userPage) => setPage={(userPage) => setPage((pages) => ({ ...pages, userPage }))}
setPage((pages) => ({ ...pages, userPage }))
}
pagesCount={userPagesCount} pagesCount={userPagesCount}
pageSize={pageSize.userPageSize} pageSize={pageSize.userPageSize}
handleSelectionChange={setSelectedTariffs} handleSelectionChange={setSelectedTariffs}
onPageSizeChange={(userPageSize) => onPageSizeChange={(userPageSize) => setPageSize((pageSize) => ({ ...pageSize, userPageSize }))}
setPageSize((pageSize) => ({ ...pageSize, userPageSize }))
}
/> />
} }
/> />

@ -4,17 +4,17 @@ import { Box, Modal, Fade, Backdrop, Typography } from "@mui/material";
import theme from "../../../theme"; import theme from "../../../theme";
export interface MWProps { export interface MWProps {
open: boolean open: boolean;
} }
const ModalAdmin = ({open}: MWProps ) => { const ModalAdmin = ({ open }: MWProps) => {
return ( return (
<React.Fragment> <React.Fragment>
<Modal <Modal
aria-labelledby="transition-modal-title" aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description" aria-describedby="transition-modal-description"
open={ open } open={open}
onClose={ useLinkClickHandler('/users') } onClose={useLinkClickHandler("/users")}
closeAfterTransition closeAfterTransition
BackdropComponent={Backdrop} BackdropComponent={Backdrop}
BackdropProps={{ BackdropProps={{
@ -22,8 +22,9 @@ const ModalAdmin = ({open}: MWProps ) => {
}} }}
> >
<Fade in={open}> <Fade in={open}>
<Box sx={{ <Box
position: "absolute" as "absolute", sx={{
position: "absolute" as const,
top: "50%", top: "50%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
@ -35,21 +36,28 @@ const ModalAdmin = ({open}: MWProps ) => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
}}> }}
>
<Typography id="transition-modal-title" variant="caption"> <Typography id="transition-modal-title" variant="caption">
Администратор сервиса Администратор сервиса
</Typography> </Typography>
<Box sx={{ <Box
sx={{
width: "100%", width: "100%",
marginTop: "15px", marginTop: "15px",
display: "flex" display: "flex",
}}> }}
<Box sx={{ >
<Box
sx={{
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
width: "155px" width: "155px",
}}> }}
<Typography variant="h4" sx={{ >
<Typography
variant="h4"
sx={{
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
width: "100%", width: "100%",
height: "55px", //205px height: "55px", //205px
@ -58,10 +66,13 @@ const ModalAdmin = ({open}: MWProps ) => {
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
cursor: "pointer", cursor: "pointer",
}}> }}
>
ТЕКСТ ТЕКСТ
</Typography> </Typography>
<Typography variant="h4" sx={{ <Typography
variant="h4"
sx={{
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
color: theme.palette.grayDisabled.main, color: theme.palette.grayDisabled.main,
width: "100%", width: "100%",
@ -71,11 +82,14 @@ const ModalAdmin = ({open}: MWProps ) => {
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
cursor: "pointer", cursor: "pointer",
textAlign: "center" textAlign: "center",
}}> }}
>
ТЕКСТ ТЕКСТ
</Typography> </Typography>
<Typography variant="h4" sx={{ <Typography
variant="h4"
sx={{
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
color: theme.palette.grayDisabled.main, color: theme.palette.grayDisabled.main,
width: "100%", width: "100%",
@ -85,11 +99,14 @@ const ModalAdmin = ({open}: MWProps ) => {
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
cursor: "pointer", cursor: "pointer",
textAlign: "center" textAlign: "center",
}}> }}
>
ТЕКСТ ТЕКСТ
</Typography> </Typography>
<Typography variant="h4" sx={{ <Typography
variant="h4"
sx={{
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
color: theme.palette.grayDisabled.main, color: theme.palette.grayDisabled.main,
width: "100%", width: "100%",
@ -99,23 +116,25 @@ const ModalAdmin = ({open}: MWProps ) => {
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
cursor: "pointer", cursor: "pointer",
textAlign: "center" textAlign: "center",
}}> }}
>
ТЕКСТ ТЕКСТ
</Typography> </Typography>
</Box> </Box>
<Box sx={{ <Box
sx={{
backgroundColor: theme.palette.grayMedium.main, backgroundColor: theme.palette.grayMedium.main,
width: "calc(100% - 155px)", width: "calc(100% - 155px)",
height: "55px", height: "55px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
alignItems: "center" alignItems: "center",
}}> }}
Long text Long text Long text Long text Long text >
Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long
Long text Long text Long text text Long text Long text
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -123,7 +142,6 @@ const ModalAdmin = ({open}: MWProps ) => {
</Modal> </Modal>
</React.Fragment> </React.Fragment>
); );
} };
export default ModalAdmin; export default ModalAdmin;

@ -3,63 +3,63 @@ import { useLinkClickHandler } from "react-router-dom";
import { Box, Modal, Fade, Backdrop, Typography } from "@mui/material"; import { Box, Modal, Fade, Backdrop, Typography } from "@mui/material";
import Tabs from "@mui/material/Tabs"; import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab"; import Tab from "@mui/material/Tab";
import { DataGrid, GridColDef } from '@mui/x-data-grid'; import { DataGrid, GridColDef } from "@mui/x-data-grid";
import theme from "../../../theme"; import theme from "../../../theme";
export interface MWProps { export interface MWProps {
open: boolean open: boolean;
} }
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ {
field: 'id', field: "id",
headerName: 'ID', headerName: "ID",
width: 30, width: 30,
sortable: false, sortable: false,
}, },
{ {
field: 'dateTime', field: "dateTime",
headerName: 'Дата / время', headerName: "Дата / время",
width: 150, width: 150,
sortable: false, sortable: false,
}, },
{ {
field: 'email', field: "email",
headerName: 'Почта', headerName: "Почта",
width: 110,
sortable: false,
},{
field: 'summa',
headerName: 'Сумма',
type: 'number',
width: 110, width: 110,
sortable: false, sortable: false,
}, },
{ {
field: 'idLong', field: "summa",
headerName: 'ID long', headerName: "Сумма",
type: 'number', type: "number",
width: 110, width: 110,
sortable: false, sortable: false,
}, },
{ {
field: 'paymentStatus', field: "idLong",
headerName: 'Статус платежа', headerName: "ID long",
type: "number",
width: 110,
sortable: false,
},
{
field: "paymentStatus",
headerName: "Статус платежа",
width: 160, width: 160,
sortable: false, sortable: false,
}, },
]; ];
const rows = [ const rows = [
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" }, { id: 1, dateTime: "22.09.22 12:15", email: "asd@mail.ru", summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" }, { id: 1, dateTime: "22.09.22 12:15", email: "asd@mail.ru", summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" }, { id: 1, dateTime: "22.09.22 12:15", email: "asd@mail.ru", summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" }, { id: 1, dateTime: "22.09.22 12:15", email: "asd@mail.ru", summa: 100500, idLong: 123, paymentStatus: "В обработке" },
{ id: 1, dateTime: '22.09.22 12:15', email: 'asd@mail.ru', summa: 100500, idLong: 123, paymentStatus: "В обработке" }, { id: 1, dateTime: "22.09.22 12:15", email: "asd@mail.ru", summa: 100500, idLong: 123, paymentStatus: "В обработке" },
]; ];
const ModalEntities = ({open}: MWProps ) => { const ModalEntities = ({ open }: MWProps) => {
const [value, setValue] = React.useState(0); const [value, setValue] = React.useState(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => { const handleChange = (event: React.SyntheticEvent, newValue: number) => {
@ -71,8 +71,8 @@ const ModalEntities = ({open}: MWProps ) => {
<Modal <Modal
aria-labelledby="transition-modal-title" aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description" aria-describedby="transition-modal-description"
open={ open } open={open}
onClose={ useLinkClickHandler("/entities") } onClose={useLinkClickHandler("/entities")}
closeAfterTransition closeAfterTransition
BackdropComponent={Backdrop} BackdropComponent={Backdrop}
BackdropProps={{ BackdropProps={{
@ -80,8 +80,9 @@ const ModalEntities = ({open}: MWProps ) => {
}} }}
> >
<Fade in={open}> <Fade in={open}>
<Box sx={{ <Box
position: "absolute" as "absolute", sx={{
position: "absolute" as const,
top: "50%", top: "50%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
@ -93,17 +94,19 @@ const ModalEntities = ({open}: MWProps ) => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
}}> }}
>
<Typography id="transition-modal-title" variant="caption"> <Typography id="transition-modal-title" variant="caption">
Юридическое лицо Юридическое лицо
</Typography> </Typography>
<Box sx={{ <Box
sx={{
width: "100%", width: "100%",
marginTop: "50px", marginTop: "50px",
display: "flex" display: "flex",
}}> }}
>
<Tabs <Tabs
orientation="vertical" orientation="vertical"
variant="scrollable" variant="scrollable"
@ -114,36 +117,42 @@ const ModalEntities = ({open}: MWProps ) => {
borderRight: 1, borderRight: 1,
borderColor: theme.palette.secondary.main, borderColor: theme.palette.secondary.main,
"& .MuiTab-root.Mui-selected": { "& .MuiTab-root.Mui-selected": {
color: theme.palette.secondary.main color: theme.palette.secondary.main,
} },
}}
TabIndicatorProps={{
style: {
background: theme.palette.secondary.main,
},
}} }}
TabIndicatorProps={{style: {
background: theme.palette.secondary.main
}}}
> >
<Tab sx={{ <Tab
sx={{
color: theme.palette.grayDisabled.main, color: theme.palette.grayDisabled.main,
width: "180px", width: "180px",
fontSize: "15px" fontSize: "15px",
}} }}
label="Загруженные документы" /> label="Загруженные документы"
<Tab sx={{ />
<Tab
sx={{
color: theme.palette.grayDisabled.main, color: theme.palette.grayDisabled.main,
width: "180px", width: "180px",
fontSize: "15px" fontSize: "15px",
}} }}
label="История транзакций" /> label="История транзакций"
/>
</Tabs> </Tabs>
{ value == 0 && ( {value == 0 && (
<Box sx={{ marginLeft: "20px" }}> <Box sx={{ marginLeft: "20px" }}>
<Typography>Id: 1</Typography> <Typography>Id: 1</Typography>
<Typography>Email: 2</Typography> <Typography>Email: 2</Typography>
<Typography>Номер телефона: 3</Typography> <Typography>Номер телефона: 3</Typography>
</Box> </Box>
) } )}
{ value == 1 && ( {value == 1 && (
<Box sx={{ marginLeft: "20px" }}> <Box sx={{ marginLeft: "20px" }}>
<DataGrid <DataGrid
rows={rows} rows={rows}
@ -158,26 +167,24 @@ const ModalEntities = ({open}: MWProps ) => {
overflowY: "auto", overflowY: "auto",
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,
} },
}} }}
/> />
</Box> </Box>
) } )}
</Box> </Box>
</Box> </Box>
</Fade> </Fade>
</Modal> </Modal>
</React.Fragment> </React.Fragment>
); );
} };
export default ModalEntities; export default ModalEntities;

@ -109,9 +109,7 @@ export const PurchaseTab = ({ userId }: PurchaseTabProps) => {
}; };
const addScrollEvent = () => { const addScrollEvent = () => {
const grid = gridContainer.current?.querySelector( const grid = gridContainer.current?.querySelector(".MuiDataGrid-virtualScroller");
".MuiDataGrid-virtualScroller"
);
if (grid) { if (grid) {
grid.addEventListener("scroll", handleScroll); grid.addEventListener("scroll", handleScroll);
@ -125,18 +123,14 @@ export const PurchaseTab = ({ userId }: PurchaseTabProps) => {
addScrollEvent(); addScrollEvent();
return () => { return () => {
const grid = gridContainer.current?.querySelector( const grid = gridContainer.current?.querySelector(".MuiDataGrid-virtualScroller");
".MuiDataGrid-virtualScroller"
);
grid?.removeEventListener("scroll", handleScroll); grid?.removeEventListener("scroll", handleScroll);
}; };
}, []); }, []);
const scrollDataGrid = (toStart = false) => { const scrollDataGrid = (toStart = false) => {
const grid = gridContainer.current?.querySelector( const grid = gridContainer.current?.querySelector(".MuiDataGrid-virtualScroller");
".MuiDataGrid-virtualScroller"
);
if (grid) { if (grid) {
scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth }); scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth });
@ -224,11 +218,7 @@ export const PurchaseTab = ({ userId }: PurchaseTabProps) => {
}} }}
onClick={() => scrollDataGrid(true)} onClick={() => scrollDataGrid(true)}
> >
<img <img src={forwardIcon} alt="forward" style={{ transform: "rotate(180deg)" }} />
src={forwardIcon}
alt="forward"
style={{ transform: "rotate(180deg)" }}
/>
</Box> </Box>
)} )}
{canScrollToRight && ( {canScrollToRight && (
@ -264,8 +254,7 @@ const a = {
{ Key: "id", Value: "65e4f1881747c1eea8007d3b" }, { Key: "id", Value: "65e4f1881747c1eea8007d3b" },
{ {
Key: "name", Key: "name",
Value: Value: "Количество Заявок, Скрытие шильдика в опроснике, 2024-03-03T21:54:16.434Z",
"Количество Заявок, Скрытие шильдика в опроснике, 2024-03-03T21:54:16.434Z",
}, },
{ Key: "price", Value: 0 }, { Key: "price", Value: 0 },
{ Key: "iscustom", Value: true }, { Key: "iscustom", Value: true },

@ -1,5 +1,5 @@
import {Box, Button, TextField, Typography} from "@mui/material"; import { Box, Button, TextField, Typography } from "@mui/material";
import {ChangeEvent, useState} from "react"; import { ChangeEvent, useState } from "react";
import makeRequest from "@root/api/makeRequest"; import makeRequest from "@root/api/makeRequest";
type QuizTabProps = { type QuizTabProps = {
@ -7,8 +7,8 @@ type QuizTabProps = {
}; };
export default function QuizTab({ userId }: QuizTabProps) { export default function QuizTab({ userId }: QuizTabProps) {
const [quizId, setQuizId] = useState<string>("") const [quizId, setQuizId] = useState<string>("");
return( return (
<Box sx={{ padding: "25px" }}> <Box sx={{ padding: "25px" }}>
<Typography <Typography
sx={{ sx={{
@ -18,30 +18,28 @@ export default function QuizTab({ userId }: QuizTabProps) {
> >
Передача Квиза Передача Квиза
</Typography> </Typography>
<Box sx={{display: "flex", gap: "15px"}}> <Box sx={{ display: "flex", gap: "15px" }}>
<TextField <TextField
placeholder={"Ссылка на квиз"} placeholder={"Ссылка на квиз"}
onChange={(event: ChangeEvent<HTMLTextAreaElement>)=>{ onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {
setQuizId(event.target.value.split("link/")[1]) setQuizId(event.target.value.split("link/")[1]);
}} }}
/> />
<Button <Button
variant="text" variant="text"
sx={{ background: "#9A9AAF" }} sx={{ background: "#9A9AAF" }}
onClick={async() => { onClick={async () => {
await makeRequest({ await makeRequest({
method: "post", method: "post",
//useToken: true, //useToken: true,
url: process.env.REACT_APP_DOMAIN + "/squiz/quiz/move", url: process.env.REACT_APP_DOMAIN + "/squiz/quiz/move",
body: {Qid: quizId, AccountID: userId} body: { Qid: quizId, AccountID: userId },
}); });
}} }}
> >
Ок Ок
</Button> </Button>
</Box> </Box>
</Box> </Box>
) );
} }

@ -117,9 +117,7 @@ export const TransactionsTab = () => {
}; };
const addScrollEvent = () => { const addScrollEvent = () => {
const grid = gridContainer.current?.querySelector( const grid = gridContainer.current?.querySelector(".MuiDataGrid-virtualScroller");
".MuiDataGrid-virtualScroller"
);
if (grid) { if (grid) {
grid.addEventListener("scroll", handleScroll); grid.addEventListener("scroll", handleScroll);
@ -133,18 +131,14 @@ export const TransactionsTab = () => {
addScrollEvent(); addScrollEvent();
return () => { return () => {
const grid = gridContainer.current?.querySelector( const grid = gridContainer.current?.querySelector(".MuiDataGrid-virtualScroller");
".MuiDataGrid-virtualScroller"
);
grid?.removeEventListener("scroll", handleScroll); grid?.removeEventListener("scroll", handleScroll);
}; };
}, []); }, []);
const scrollDataGrid = (toStart = false) => { const scrollDataGrid = (toStart = false) => {
const grid = gridContainer.current?.querySelector( const grid = gridContainer.current?.querySelector(".MuiDataGrid-virtualScroller");
".MuiDataGrid-virtualScroller"
);
if (grid) { if (grid) {
scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth }); scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth });
@ -221,11 +215,7 @@ export const TransactionsTab = () => {
}} }}
onClick={() => scrollDataGrid(true)} onClick={() => scrollDataGrid(true)}
> >
<img <img src={forwardIcon} alt="forward" style={{ transform: "rotate(180deg)" }} />
src={forwardIcon}
alt="forward"
style={{ transform: "rotate(180deg)" }}
/>
</Box> </Box>
)} )}
{canScrollToRight && ( {canScrollToRight && (

@ -35,9 +35,7 @@ export const UserTab = ({ userId }: UserTabProps) => {
<Box sx={{ maxWidth: "300px", width: "100%" }}> <Box sx={{ maxWidth: "300px", width: "100%" }}>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>ID</Typography> <Typography sx={{ lineHeight: "20px" }}>ID</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>{user?._id}</Typography>
{user?._id}
</Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Дата регистрации</Typography> <Typography sx={{ lineHeight: "20px" }}>Дата регистрации</Typography>
@ -47,15 +45,11 @@ export const UserTab = ({ userId }: UserTabProps) => {
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Email</Typography> <Typography sx={{ lineHeight: "20px" }}>Email</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>{user?.email || user?.login}</Typography>
{user?.email || user?.login}
</Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Телефон</Typography> <Typography sx={{ lineHeight: "20px" }}>Телефон</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>{user?.phoneNumber}</Typography>
{user?.phoneNumber}
</Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Тип:</Typography> <Typography sx={{ lineHeight: "20px" }}>Тип:</Typography>
@ -70,19 +64,13 @@ export const UserTab = ({ userId }: UserTabProps) => {
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>ФИО:</Typography> <Typography sx={{ lineHeight: "20px" }}>ФИО:</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{`${account?.name.secondname || ""} ${ {`${account?.name.secondname || ""} ${account?.name.firstname || ""} ${account?.name.middlename || ""}`}
account?.name.firstname || ""
} ${account?.name.middlename || ""}`}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}> <Typography sx={{ lineHeight: "20px" }}>Внутренний кошелек</Typography>
Внутренний кошелек
</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{`${account ? account.wallet.money / 100 : 0} ${ {`${account ? account.wallet.money / 100 : 0} ${account?.wallet.currency || "RUB"}.`}
account?.wallet.currency || "RUB"
}.`}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>

@ -21,9 +21,7 @@ export const VerificationTab = ({ userId }: VerificationTabProps) => {
const requestVefification = async () => { const requestVefification = async () => {
setIsLoading(true); setIsLoading(true);
const [verificationResponse, verificationError] = await verification( const [verificationResponse, verificationError] = await verification(userId);
userId
);
setIsLoading(false); setIsLoading(false);
@ -34,7 +32,7 @@ export const VerificationTab = ({ userId }: VerificationTabProps) => {
if (verificationResponse) { if (verificationResponse) {
setVerificationInfo(verificationResponse); setVerificationInfo(verificationResponse);
setComment(verificationResponse.comment); setComment(verificationResponse.comment);
setINN(verificationResponse.taxnumber) setINN(verificationResponse.taxnumber);
} }
}; };
@ -51,7 +49,6 @@ export const VerificationTab = ({ userId }: VerificationTabProps) => {
}); });
}, 2000); }, 2000);
useEffect(() => { useEffect(() => {
requestVefification(); requestVefification();
}, []); }, []);
@ -69,11 +66,12 @@ export const VerificationTab = ({ userId }: VerificationTabProps) => {
status: verificationInfo.status, status: verificationInfo.status,
}); });
if (accepted && _ === "OK") await makeRequest({ if (accepted && _ === "OK")
await makeRequest({
method: "patch", method: "patch",
useToken: true, useToken: true,
url: process.env.REACT_APP_DOMAIN + `/customer/account/${userId}`, url: process.env.REACT_APP_DOMAIN + `/customer/account/${userId}`,
body: {status: verificationInfo.status} body: { status: verificationInfo.status },
}); });
if (patchVerificationError) { if (patchVerificationError) {
@ -95,13 +93,8 @@ export const VerificationTab = ({ userId }: VerificationTabProps) => {
{verificationInfo?.accepted ? "Верификация пройдена" : "Не верифицирован"} {verificationInfo?.accepted ? "Верификация пройдена" : "Не верифицирован"}
</Typography> </Typography>
{isLoading ? ( {isLoading ? (
<Typography <Typography sx={{ fontWeight: "bold", fontSize: "18px", marginBottom: "25px" }}>Загрузка данных...</Typography>
sx={{ fontWeight: "bold", fontSize: "18px", marginBottom: "25px" }} ) : verificationInfo && verificationInfo.files.length > 0 ? (
>
Загрузка данных...
</Typography>
) :
verificationInfo && verificationInfo.files.length > 0 ? (
verificationInfo.files.map(({ name, url }, index) => ( verificationInfo.files.map(({ name, url }, index) => (
<Box sx={{ marginBottom: "25px" }} key={name + url}> <Box sx={{ marginBottom: "25px" }} key={name + url}>
<Typography sx={{ fontWeight: "bold", fontSize: "18px" }}> <Typography sx={{ fontWeight: "bold", fontSize: "18px" }}>
@ -130,29 +123,24 @@ export const VerificationTab = ({ userId }: VerificationTabProps) => {
</Box> </Box>
)) ))
) : ( ) : (
<Typography <Typography sx={{ fontWeight: "bold", fontSize: "18px", marginBottom: "25px" }}>
sx={{ fontWeight: "bold", fontSize: "18px", marginBottom: "25px" }}
>
Пользователь не загружал данные Пользователь не загружал данные
</Typography> </Typography>
)} )}
<TextField <TextField
value={INN} value={INN}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>{ onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {
if (!verificationInfo) { if (!verificationInfo) {
return; return;
} }
setINN(event.target.value.replace(/[^0-9]/g,"")) setINN(event.target.value.replace(/[^0-9]/g, ""));
debouncedINNHC(event.target.value.replace(/[^0-9]/g,""))} debouncedINNHC(event.target.value.replace(/[^0-9]/g, ""));
} }}
placeholder="ИНН" placeholder="ИНН"
/> />
{verificationInfo?.comment && ( {verificationInfo?.comment && (
<Box sx={{ marginBottom: "15px" }}> <Box sx={{ marginBottom: "15px" }}>
<Typography <Typography component="span" sx={{ fontWeight: "bold", marginBottom: "10px" }}>
component="span"
sx={{ fontWeight: "bold", marginBottom: "10px" }}
>
Комментарий: Комментарий:
</Typography> </Typography>
<Typography component="span"> {verificationInfo.comment}</Typography> <Typography component="span"> {verificationInfo.comment}</Typography>
@ -169,23 +157,13 @@ export const VerificationTab = ({ userId }: VerificationTabProps) => {
maxWidth: "500px", maxWidth: "500px",
marginBottom: "10px", marginBottom: "10px",
}} }}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setComment(event.target.value)}
setComment(event.target.value)
}
/> />
<Box sx={{ display: "flex", columnGap: "10px" }}> <Box sx={{ display: "flex", columnGap: "10px" }}>
<Button <Button variant="text" sx={{ background: "#9A9AAF" }} onClick={() => verify(false)}>
variant="text"
sx={{ background: "#9A9AAF" }}
onClick={() => verify(false)}
>
Отклонить Отклонить
</Button> </Button>
<Button <Button variant="text" sx={{ background: "#9A9AAF" }} onClick={() => verify(true)}>
variant="text"
sx={{ background: "#9A9AAF" }}
onClick={() => verify(true)}
>
Подтвердить Подтвердить
</Button> </Button>
</Box> </Box>

@ -1,15 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import { Box, Modal, Fade, Backdrop, Typography, Tabs, Tab, useTheme, useMediaQuery } from "@mui/material";
Box,
Modal,
Fade,
Backdrop,
Typography,
Tabs,
Tab,
useTheme,
useMediaQuery,
} from "@mui/material";
import { UserTab } from "./UserTab"; import { UserTab } from "./UserTab";
import { PurchaseTab } from "./PurchaseTab"; import { PurchaseTab } from "./PurchaseTab";
@ -21,7 +11,7 @@ import { ReactComponent as PackageIcon } from "@root/assets/icons/package.svg";
import { ReactComponent as TransactionsIcon } from "@root/assets/icons/transactions.svg"; import { ReactComponent as TransactionsIcon } from "@root/assets/icons/transactions.svg";
import { ReactComponent as CheckIcon } from "@root/assets/icons/check.svg"; import { ReactComponent as CheckIcon } from "@root/assets/icons/check.svg";
import { ReactComponent as CloseIcon } from "@root/assets/icons/close.svg"; import { ReactComponent as CloseIcon } from "@root/assets/icons/close.svg";
import QuizIcon from '@mui/icons-material/Quiz'; import QuizIcon from "@mui/icons-material/Quiz";
import forwardIcon from "@root/assets/icons/forward.svg"; import forwardIcon from "@root/assets/icons/forward.svg";
import type { SyntheticEvent } from "react"; import type { SyntheticEvent } from "react";
@ -91,10 +81,7 @@ const ModalUser = ({ open, onClose, userId }: ModalUserProps) => {
}} }}
> >
{mobile && ( {mobile && (
<Box <Box onClick={onClose} sx={{ position: "absolute", top: "10px", right: "5px" }}>
onClick={onClose}
sx={{ position: "absolute", top: "10px", right: "5px" }}
>
<CloseIcon /> <CloseIcon />
</Box> </Box>
)} )}
@ -118,11 +105,7 @@ const ModalUser = ({ open, onClose, userId }: ModalUserProps) => {
sx={{ sx={{
width: "100%", width: "100%",
height: "100%", height: "100%",
maxWidth: mobile maxWidth: mobile ? (openNavigation ? "276px" : "68px") : "276px",
? openNavigation
? "276px"
: "68px"
: "276px",
}} }}
> >
{mobile && ( {mobile && (
@ -143,9 +126,7 @@ const ModalUser = ({ open, onClose, userId }: ModalUserProps) => {
orientation="vertical" orientation="vertical"
variant="scrollable" variant="scrollable"
value={value} value={value}
onChange={(event: SyntheticEvent, newValue: number) => onChange={(event: SyntheticEvent, newValue: number) => setValue(newValue)}
setValue(newValue)
}
aria-label="Vertical tabs example" aria-label="Vertical tabs example"
sx={{ sx={{
padding: mobile ? "16px" : "10px", padding: mobile ? "16px" : "10px",
@ -188,8 +169,7 @@ const ModalUser = ({ open, onClose, userId }: ModalUserProps) => {
position: "relative", position: "relative",
width: "100%", width: "100%",
color: theme.palette.common.black, color: theme.palette.common.black,
boxShadow: boxShadow: "inset 30px 0px 40px 0px rgba(210, 208, 225, 0.2)",
"inset 30px 0px 40px 0px rgba(210, 208, 225, 0.2)",
}} }}
> >
{value === 0 && <UserTab userId={userId} />} {value === 0 && <UserTab userId={userId} />}

@ -5,7 +5,7 @@ import type { CustomPrivilege } from "@frontend/kitui";
const mutatePrivileges = (privileges: CustomPrivilege[]) => { const mutatePrivileges = (privileges: CustomPrivilege[]) => {
let extracted: CustomPrivilege[] = []; let extracted: CustomPrivilege[] = [];
for (let serviceKey in privileges) { for (const serviceKey in privileges) {
//Приходит объект. В его значениях массивы привилегий для разных сервисов. Высыпаем в общую кучу и обновляем стор //Приходит объект. В его значениях массивы привилегий для разных сервисов. Высыпаем в общую кучу и обновляем стор
extracted = extracted.concat(privileges[serviceKey]); extracted = extracted.concat(privileges[serviceKey]);
} }
@ -14,8 +14,7 @@ const mutatePrivileges = (privileges: CustomPrivilege[]) => {
}; };
export const requestPrivileges = async () => { export const requestPrivileges = async () => {
const [privilegesResponse, privilegesError] = const [privilegesResponse, privilegesError] = await requestServicePrivileges();
await requestServicePrivileges();
if (privilegesError) { if (privilegesError) {
return console.error(privilegesError); return console.error(privilegesError);
@ -24,7 +23,8 @@ export const requestPrivileges = async () => {
let allPrivileges: CustomPrivilege[] = []; let allPrivileges: CustomPrivilege[] = [];
if (privilegesResponse) { if (privilegesResponse) {
if (privilegesResponse.templategen !== undefined) allPrivileges = allPrivileges.concat(privilegesResponse.templategen); if (privilegesResponse.templategen !== undefined)
allPrivileges = allPrivileges.concat(privilegesResponse.templategen);
if (privilegesResponse.squiz !== undefined) allPrivileges = allPrivileges.concat(privilegesResponse.squiz); if (privilegesResponse.squiz !== undefined) allPrivileges = allPrivileges.concat(privilegesResponse.squiz);
mutatePrivileges(allPrivileges); mutatePrivileges(allPrivileges);
} }

@ -9,13 +9,8 @@ const mutateTariffs = (tariffs: Tariff[]) => {
updateTariffs(nonDeletedTariffs); updateTariffs(nonDeletedTariffs);
}; };
export const requestTariffs = async ( export const requestTariffs = async (page: number = 1, existingTariffs: Tariff[] = []): Promise<void> => {
page: number = 1, const [tariffsResponse, tariffsResponseError] = await requestTariffsRequest(page);
existingTariffs: Tariff[] = []
): Promise<void> => {
const [tariffsResponse, tariffsResponseError] = await requestTariffsRequest(
page
);
if (tariffsResponseError) { if (tariffsResponseError) {
console.error(tariffsResponseError); console.error(tariffsResponseError);
@ -25,10 +20,7 @@ export const requestTariffs = async (
if (tariffsResponse) { if (tariffsResponse) {
if (page < tariffsResponse.totalPages) { if (page < tariffsResponse.totalPages) {
return requestTariffs(page + 1, [ return requestTariffs(page + 1, [...existingTariffs, ...tariffsResponse.tariffs]);
...existingTariffs,
...tariffsResponse.tariffs,
]);
} }
mutateTariffs([...existingTariffs, ...tariffsResponse.tariffs]); mutateTariffs([...existingTariffs, ...tariffsResponse.tariffs]);

@ -2,7 +2,6 @@ import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
import { CartData } from "@frontend/kitui"; import { CartData } from "@frontend/kitui";
interface CartStore { interface CartStore {
cartData: CartData | null; cartData: CartData | null;
} }
@ -12,13 +11,10 @@ const initialState: CartStore = {
}; };
export const useCartStore = create<CartStore>()( export const useCartStore = create<CartStore>()(
devtools( devtools((set, get) => initialState, {
(set, get) => initialState,
{
name: "Cart", name: "Cart",
enabled: process.env.NODE_ENV === "development", enabled: process.env.NODE_ENV === "development",
} })
)
); );
export const setCartData = (cartData: CartStore["cartData"]) => useCartStore.setState({ cartData }); export const setCartData = (cartData: CartStore["cartData"]) => useCartStore.setState({ cartData });

@ -4,10 +4,9 @@ import { devtools } from "zustand/middleware";
import { produce } from "immer"; import { produce } from "immer";
import { Discount } from "@frontend/kitui"; import { Discount } from "@frontend/kitui";
interface DiscountStore { interface DiscountStore {
discounts: Discount[]; discounts: Discount[];
selectedDiscountIds: GridSelectionModel, selectedDiscountIds: GridSelectionModel;
editDiscountId: string | null; editDiscountId: string | null;
} }
@ -27,23 +26,26 @@ export const useDiscountStore = create<DiscountStore>()(
export const setDiscounts = (discounts: DiscountStore["discounts"]) => useDiscountStore.setState({ discounts }); export const setDiscounts = (discounts: DiscountStore["discounts"]) => useDiscountStore.setState({ discounts });
export const findDiscountsById = (discountId: string): (Discount | null) => useDiscountStore.getState().discounts.find(discount => discount.ID === discountId) ?? null; export const findDiscountsById = (discountId: string): Discount | null =>
useDiscountStore.getState().discounts.find((discount) => discount.ID === discountId) ?? null;
export const addDiscount = (discount: DiscountStore["discounts"][number]) => useDiscountStore.setState( export const addDiscount = (discount: DiscountStore["discounts"][number]) =>
state => ({ discounts: [...state.discounts, discount] }) useDiscountStore.setState((state) => ({ discounts: [...state.discounts, discount] }));
);
export const updateDiscount = (updatedDiscount: DiscountStore["discounts"][number]) => useDiscountStore.setState( export const updateDiscount = (updatedDiscount: DiscountStore["discounts"][number]) =>
produce<DiscountStore>(state => { useDiscountStore.setState(
const discountIndex = state.discounts.findIndex(discount => discount.ID === updatedDiscount.ID); produce<DiscountStore>((state) => {
const discountIndex = state.discounts.findIndex((discount) => discount.ID === updatedDiscount.ID);
if (discountIndex === -1) throw new Error(`Discount not found when updating: ${updatedDiscount.ID}`); if (discountIndex === -1) throw new Error(`Discount not found when updating: ${updatedDiscount.ID}`);
state.discounts.splice(discountIndex, 1, updatedDiscount); state.discounts.splice(discountIndex, 1, updatedDiscount);
}) })
); );
export const setSelectedDiscountIds = (selectedDiscountIds: DiscountStore["selectedDiscountIds"]) => useDiscountStore.setState({ selectedDiscountIds }); export const setSelectedDiscountIds = (selectedDiscountIds: DiscountStore["selectedDiscountIds"]) =>
useDiscountStore.setState({ selectedDiscountIds });
export const openEditDiscountDialog = (editDiscountId: DiscountStore["editDiscountId"]) => useDiscountStore.setState({ editDiscountId }); export const openEditDiscountDialog = (editDiscountId: DiscountStore["editDiscountId"]) =>
useDiscountStore.setState({ editDiscountId });
export const closeEditDiscountDialog = () => useDiscountStore.setState({ editDiscountId: null }); export const closeEditDiscountDialog = () => useDiscountStore.setState({ editDiscountId: null });

@ -3,7 +3,6 @@ import { TicketMessage } from "@root/model/ticket";
import { create } from "zustand"; import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
interface MessageStore { interface MessageStore {
messages: TicketMessage[]; messages: TicketMessage[];
fetchState: "idle" | "fetching" | "all fetched"; fetchState: "idle" | "fetching" | "all fetched";
@ -34,9 +33,9 @@ export const useMessageStore = create<MessageStore>()(
export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => { export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
const state = useMessageStore.getState(); const state = useMessageStore.getState();
const messageIdToMessageMap: { [messageId: string]: TicketMessage; } = {}; const messageIdToMessageMap: { [messageId: string]: TicketMessage } = {};
[...state.messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message); [...state.messages, ...receivedMessages].forEach((message) => (messageIdToMessageMap[message.id] = message));
const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime); const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime);
@ -46,14 +45,16 @@ export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
}); });
}; };
export const clearMessageState = () => useMessageStore.setState({ export const clearMessageState = () =>
useMessageStore.setState({
messages: [], messages: [],
apiPage: 0, apiPage: 0,
lastMessageId: undefined, lastMessageId: undefined,
fetchState: "idle", fetchState: "idle",
}); });
export const setMessageFetchState = (fetchState: MessageStore["fetchState"]) => useMessageStore.setState({ fetchState }); export const setMessageFetchState = (fetchState: MessageStore["fetchState"]) =>
useMessageStore.setState({ fetchState });
export const incrementMessageApiPage = () => { export const incrementMessageApiPage = () => {
const state = useMessageStore.getState(); const state = useMessageStore.getState();
@ -61,9 +62,11 @@ export const incrementMessageApiPage = () => {
useMessageStore.setState({ apiPage: state.apiPage + 1 }); useMessageStore.setState({ apiPage: state.apiPage + 1 });
}; };
export const setIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useMessageStore.setState({ isPreventAutoscroll }); export const setIsPreventAutoscroll = (isPreventAutoscroll: boolean) =>
useMessageStore.setState({ isPreventAutoscroll });
export const setTicketMessagesFetchState = (ticketMessagesFetchState: FetchState) => useMessageStore.setState({ ticketMessagesFetchState }); export const setTicketMessagesFetchState = (ticketMessagesFetchState: FetchState) =>
useMessageStore.setState({ ticketMessagesFetchState });
function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) { function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) {
const date1 = new Date(ticket1.created_at).getTime(); const date1 = new Date(ticket1.created_at).getTime();

@ -1,49 +1,48 @@
import { Ticket } from "@root/model/ticket"; import { Ticket } from "@root/model/ticket";
export const testTickets: Ticket[] = [ export const testTickets: Ticket[] = [
{ {
"id": "cg5irh4vc9g7b3n3tcrg", id: "cg5irh4vc9g7b3n3tcrg",
"user": "6407625ed01874dcffa8b008", user: "6407625ed01874dcffa8b008",
"sess": "6407625ed01874dcffa8b008", sess: "6407625ed01874dcffa8b008",
"ans": "", ans: "",
"state": "open", state: "open",
"top_message": { top_message: {
"id": "cg5irh4vc9g7b3n3tcs0", id: "cg5irh4vc9g7b3n3tcs0",
"ticket_id": "cg5irh4vc9g7b3n3tcrg", ticket_id: "cg5irh4vc9g7b3n3tcrg",
"user_id": "6407625ed01874dcffa8b008", user_id: "6407625ed01874dcffa8b008",
"session_id": "6407625ed01874dcffa8b008", session_id: "6407625ed01874dcffa8b008",
"message": "text", message: "text",
"files": [], files: [],
"shown": {}, shown: {},
"request_screenshot": "", request_screenshot: "",
"created_at": "2023-03-10T13:16:52.73Z" created_at: "2023-03-10T13:16:52.73Z",
}, },
"title": "textual ticket", title: "textual ticket",
"created_at": "2023-03-10T13:16:52.73Z", created_at: "2023-03-10T13:16:52.73Z",
"updated_at": "2023-03-10T13:16:52.73Z", updated_at: "2023-03-10T13:16:52.73Z",
"rate": -1 rate: -1,
}, },
{ {
"id": "cg55nssvc9g7gddpnsug", id: "cg55nssvc9g7gddpnsug",
"user": "", user: "",
"sess": "", sess: "",
"ans": "", ans: "",
"state": "open", state: "open",
"top_message": { top_message: {
"id": "cg55nssvc9g7gddpnsv0", id: "cg55nssvc9g7gddpnsv0",
"ticket_id": "cg55nssvc9g7gddpnsug", ticket_id: "cg55nssvc9g7gddpnsug",
"user_id": "", user_id: "",
"session_id": "", session_id: "",
"message": "text", message: "text",
"files": [], files: [],
"shown": {}, shown: {},
"request_screenshot": "", request_screenshot: "",
"created_at": "2023-03-09T22:21:39.822Z" created_at: "2023-03-09T22:21:39.822Z",
},
title: "textual ticket",
created_at: "2023-03-09T22:21:39.822Z",
updated_at: "2023-03-09T22:21:39.822Z",
rate: -1,
}, },
"title": "textual ticket",
"created_at": "2023-03-09T22:21:39.822Z",
"updated_at": "2023-03-09T22:21:39.822Z",
"rate": -1
}
]; ];

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