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"
extends: .deploy_template
deploy-to-prod:
tags:
- front

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

@ -10,8 +10,8 @@ module.exports = {
// plugin does not take it from tsconfig
baseUrl: "./src",
// 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",
"version": "0.1.0",
"private": true,
"type": "module",
"dependencies": {
"@date-io/dayjs": "^2.15.0",
"@emotion/react": "^11.10.4",
@ -57,13 +58,8 @@
"test:cypress": "start-server-and-test start http://localhost:3000 cypress",
"cypress": "cypress open",
"eject": "craco eject",
"format": "prettier . --write"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"format": "prettier . --write",
"lint": "eslint ./src --fix"
},
"browserslist": {
"production": [
@ -78,7 +74,10 @@
]
},
"devDependencies": {
"@eslint/js": "^9.3.0",
"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-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">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="stylesheet" href="fonts.css" />
<!--

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

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

@ -30,11 +30,9 @@ export type Account = {
wallet: Wallet;
};
const baseUrl = process.env.REACT_APP_DOMAIN + "/customer"
const baseUrl = process.env.REACT_APP_DOMAIN + "/customer";
export const getAccountInfo = async (
id: string
): Promise<[Account | null, string?]> => {
export const getAccountInfo = async (id: string): Promise<[Account | null, string?]> => {
try {
const accountInfoResponse = await makeRequest<never, Account>({
url: `${baseUrl}/account/${id}`,

@ -2,18 +2,11 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error";
import type {
LoginRequest,
RegisterRequest,
RegisterResponse,
} from "@frontend/kitui";
import type { 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 (
login: string,
password: string
): Promise<[RegisterResponse | null, string?]> => {
export const signin = async (login: string, password: string): Promise<[RegisterResponse | null, string?]> => {
try {
const signinResponse = await makeRequest<LoginRequest, RegisterResponse>({
url: baseUrl + "/login",
@ -24,7 +17,7 @@ export const signin = async (
return [signinResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
console.error(error)
console.error(error);
return [null, `Ошибка авторизации. ${error}`];
}
};
@ -35,10 +28,7 @@ export const register = async (
phoneNumber: string = "--"
): Promise<[RegisterResponse | null, string?]> => {
try {
const registerResponse = await makeRequest<
RegisterRequest,
RegisterResponse
>({
const registerResponse = await makeRequest<RegisterRequest, RegisterResponse>({
url: baseUrl + "/register",
body: { login, password, phoneNumber },
useToken: false,

@ -3,15 +3,11 @@ import makeRequest from "@root/api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error";
import type { Discount } from "@frontend/kitui";
import type {
CreateDiscountBody,
DiscountType,
GetDiscountResponse,
} from "@root/model/discount";
import type { CreateDiscountBody, DiscountType, GetDiscountResponse } from "@root/model/discount";
import useSWR from "swr";
import { enqueueSnackbar } from "notistack";
const baseUrl = process.env.REACT_APP_DOMAIN + "/price"
const baseUrl = process.env.REACT_APP_DOMAIN + "/price";
interface CreateDiscountParams {
purchasesAmount: number;
@ -109,10 +105,7 @@ export function createDiscountObject({
return discount;
}
export const changeDiscount = async (
discountId: string,
discount: Discount
): Promise<[unknown, string?]> => {
export const changeDiscount = async (discountId: string, discount: Discount): Promise<[unknown, string?]> => {
try {
const changeDiscountResponse = await makeRequest<Discount, unknown>({
url: baseUrl + "/discount/" + discountId,
@ -129,16 +122,11 @@ export const changeDiscount = async (
}
};
export const createDiscount = async (
discountParams: CreateDiscountParams
): Promise<[Discount | null, string?]> => {
export const createDiscount = async (discountParams: CreateDiscountParams): Promise<[Discount | null, string?]> => {
const discount = createDiscountObject(discountParams);
try {
const createdDiscountResponse = await makeRequest<
CreateDiscountBody,
Discount
>({
const createdDiscountResponse = await makeRequest<CreateDiscountBody, Discount>({
url: baseUrl + "/discount",
method: "post",
useToken: true,
@ -153,9 +141,7 @@ export const createDiscount = async (
}
};
export const deleteDiscount = async (
discountId: string
): Promise<[Discount | null, string?]> => {
export const deleteDiscount = async (discountId: string): Promise<[Discount | null, string?]> => {
try {
const deleteDiscountResponse = await makeRequest<never, Discount>({
url: baseUrl + "/discount/" + discountId,
@ -178,10 +164,7 @@ export const patchDiscount = async (
const discount = createDiscountObject(discountParams);
try {
const patchDiscountResponse = await makeRequest<
CreateDiscountBody,
Discount
>({
const patchDiscountResponse = await makeRequest<CreateDiscountBody, Discount>({
url: baseUrl + "/discount/" + discountId,
method: "patch",
useToken: true,
@ -196,9 +179,7 @@ export const patchDiscount = async (
}
};
export const requestDiscounts = async (): Promise<
[GetDiscountResponse | null, string?]
> => {
export const requestDiscounts = async (): Promise<[GetDiscountResponse | null, string?]> => {
try {
const discountsResponse = await makeRequest<never, GetDiscountResponse>({
url: baseUrl + "/discounts",
@ -238,7 +219,7 @@ export function useDiscounts() {
if (!(error instanceof Error)) return;
enqueueSnackbar(error.message, { variant: "error" });
}
},
});
return data;

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

@ -10,10 +10,7 @@ export function useHistory(accountId: string) {
const swrResponse = useSWRInfinite(
() => `history-${currentPage}`,
async () => {
const [historyResponse, error] = await historyApi.getUserHistory(
accountId,
currentPage
);
const [historyResponse, error] = await historyApi.getUserHistory(accountId, currentPage);
if (error) {
throw new Error(error);

@ -3,21 +3,30 @@ import { Method, ResponseType, AxiosError } from "axios";
import { clearAuthToken } from "@frontend/kitui";
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 {
const response = await KIT.makeRequest<unknown>(data)
const response = await KIT.makeRequest<unknown>(data);
return response as TResponse
return response as TResponse;
} catch (e) {
const error = e as AxiosError;
//@ts-ignore
if (error.response?.status === 400 && error.response?.data?.message === "refreshToken is empty") {
clearAuthToken()
clearAuthToken();
redirect("/");
}
throw e
};
};
throw e;
}
}
export default makeRequest;

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

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

@ -3,18 +3,9 @@ import useSwr, { mutate } from "swr";
import { enqueueSnackbar } from "notistack";
import { promocodeApi } from "./requests";
import type {
CreatePromocodeBody,
PromocodeList,
} from "@root/model/promocodes";
import type { CreatePromocodeBody, PromocodeList } from "@root/model/promocodes";
export function usePromocodes(
page: number,
pageSize: number,
promocodeId: string,
to: number,
from: number
) {
export function usePromocodes(page: number, pageSize: number, promocodeId: string, to: number, from: number) {
const promocodesCountRef = useRef<number>(0);
const swrResponse = useSwr(
["promocodes", page, pageSize],
@ -47,8 +38,7 @@ export function usePromocodes(
mutate(["promocodes", page, pageSize]);
} catch (error) {
console.error("Error creating promocode", error);
if (error instanceof Error)
enqueueSnackbar(error.message, { variant: "error" });
if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
}
},
[page, pageSize]
@ -82,8 +72,7 @@ export function usePromocodes(
);
} catch (error) {
console.error("Error deleting promocode", error);
if (error instanceof Error)
enqueueSnackbar(error.message, { variant: "error" });
if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
}
},
[page, pageSize]
@ -96,8 +85,7 @@ export function usePromocodes(
return null;
}
const promocodeStatisticsResponse =
await promocodeApi.getPromocodeStatistics(id, from, to);
const promocodeStatisticsResponse = await promocodeApi.getPromocodeStatistics(id, from, to);
return promocodeStatisticsResponse;
},
@ -118,8 +106,7 @@ export function usePromocodes(
mutate(["promocodes", page, pageSize]);
} catch (error) {
console.error("Error creating fast link", error);
if (error instanceof Error)
enqueueSnackbar(error.message, { variant: "error" });
if (error instanceof Error) enqueueSnackbar(error.message, { variant: "error" });
}
},
[page, pageSize]

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

@ -35,7 +35,7 @@ export type UserType = {
updatedAt: string;
};
const baseUrl =process.env.REACT_APP_DOMAIN + "/role"
const baseUrl = process.env.REACT_APP_DOMAIN + "/role";
export const getRoles_mock = (): Promise<TMockData> => {
return new Promise((resolve) => {

@ -20,19 +20,15 @@ type GetTariffsResponse = {
tariffs: Tariff[];
};
const baseUrl =process.env.REACT_APP_DOMAIN + "/strator"
const baseUrl = process.env.REACT_APP_DOMAIN + "/strator";
export const createTariff = async (
body: CreateTariffBackendRequest
): Promise<[unknown, string?]> => {
export const createTariff = async (body: CreateTariffBackendRequest): Promise<[unknown, string?]> => {
try {
const createdTariffResponse = await makeRequest<CreateTariffBackendRequest>(
{
const createdTariffResponse = await makeRequest<CreateTariffBackendRequest>({
url: baseUrl + "/tariff/",
method: "post",
body,
}
);
});
return [createdTariffResponse];
} catch (nativeError) {
@ -65,9 +61,7 @@ export const putTariff = async (tariff: Tariff): Promise<[null, string?]> => {
}
};
export const deleteTariff = async (
tariffId: string
): Promise<[Tariff | null, string?]> => {
export const deleteTariff = async (tariffId: string): Promise<[Tariff | null, string?]> => {
try {
const deletedTariffResponse = await makeRequest<{ id: string }, Tariff>({
method: "delete",
@ -83,9 +77,7 @@ export const deleteTariff = async (
}
};
export const requestTariffs = async (
page: number
): Promise<[GetTariffsResponse | null, string?]> => {
export const requestTariffs = async (page: number): Promise<[GetTariffsResponse | null, string?]> => {
try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
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";
const baseUrl = process.env.REACT_APP_DOMAIN + "/heruvym"
const baseUrl = process.env.REACT_APP_DOMAIN + "/heruvym";
export const sendTicketMessage = async (
body: SendTicketMessageRequest
): Promise<[unknown, string?]> => {
export const sendTicketMessage = async (body: SendTicketMessageRequest): Promise<[unknown, string?]> => {
try {
const sendTicketMessageResponse = await makeRequest<
SendTicketMessageRequest,
unknown
>({
const sendTicketMessageResponse = await makeRequest<SendTicketMessageRequest, unknown>({
url: `${baseUrl}/send`,
method: "POST",
useToken: true,

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

@ -10,10 +10,7 @@ export function useAdmins(page: number, pageSize: number) {
const swrResponse = useSwr(
["admin", page, pageSize],
async ([_, page, pageSize]) => {
const [adminResponse, error] = await userApi.getManagerList(
page,
pageSize
);
const [adminResponse, error] = await userApi.getManagerList(page, pageSize);
if (error) {
throw new Error(error);
@ -44,10 +41,7 @@ export function useManagers(page: number, pageSize: number) {
const swrResponse = useSwr(
["manager", page, pageSize],
async ([_, page, pageSize]) => {
const [managerResponse, error] = await userApi.getManagerList(
page,
pageSize
);
const [managerResponse, error] = await userApi.getManagerList(page, pageSize);
if (error) {
throw new Error(error);

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

@ -1,4 +1,6 @@
@font-face {
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,
Typography,
useMediaQuery,
useTheme
useTheme,
} from "@mui/material";
import { useDiscounts } from "@root/api/discounts";
import { useAllPromocodes } from "@root/api/promocode/swr";
@ -27,42 +27,40 @@ import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useState } from "react";
import CartItemRow from "./CartItemRow";
export default function Cart() {
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(400));
const discounts = useDiscounts();
const promocodes = useAllPromocodes();
const cartData = useCartStore((store) => store.cartData);
const tariffs = useTariffStore(state => state.tariffs);
const tariffs = useTariffStore((state) => state.tariffs);
const [couponField, setCouponField] = useState<string>("");
const [loyaltyField, setLoyaltyField] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isNonCommercial, setIsNonCommercial] = useState<boolean>(false);
const selectedTariffIds = useTariffStore(state => state.selectedTariffIds);
const selectedTariffIds = useTariffStore((state) => state.selectedTariffIds);
async function handleCalcCartClick() {
await requestPrivileges();
await requestDiscounts();
const cartTariffs = tariffs.filter(tariff => selectedTariffIds.includes(tariff._id));
const cartTariffs = tariffs.filter((tariff) => selectedTariffIds.includes(tariff._id));
let loyaltyValue = parseInt(loyaltyField);
if (!isFinite(loyaltyValue)) loyaltyValue = 0;
const promocode = promocodes.find(promocode => {
if (promocode.dueTo < (Date.now() / 1000)) return false;
const promocode = promocodes.find((promocode) => {
if (promocode.dueTo < Date.now() / 1000) return false;
return promocode.codeword === couponField.trim();
});
const userId = crypto.randomUUID();
const discountsWithPromocodeDiscount = promocode ? [
...discounts,
createDiscountFromPromocode(promocode, userId),
] : discounts;
const discountsWithPromocodeDiscount = promocode
? [...discounts, createDiscountFromPromocode(promocode, userId)]
: discounts;
try {
const cartData = calcCart(cartTariffs, discountsWithPromocodeDiscount, loyaltyValue, userId);
@ -96,7 +94,7 @@ export default function Cart() {
alignItems: "center",
justifyContent: "space-between",
gap: "20px",
flexDirection: mobile ? "column" : undefined
flexDirection: mobile ? "column" : undefined,
}}
>
<FormControlLabel
@ -201,44 +199,33 @@ export default function Cart() {
}}
>
<TableCell>
<Typography
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
<Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
Имя
</Typography>
</TableCell>
<TableCell>
<Typography
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
<Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
Описание
</Typography>
</TableCell>
<TableCell>
<Typography
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
<Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
Скидки
</Typography>
</TableCell>
<TableCell>
<Typography
variant="h4"
sx={{ color: theme.palette.secondary.main }}
>
<Typography variant="h4" sx={{ color: theme.palette.secondary.main }}>
стоимость
</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{cartData.services.flatMap(service => service.tariffs.map(tariffCartData => {
const appliedDiscounts = tariffCartData.privileges.flatMap(
privilege => Array.from(privilege.appliedDiscounts)
).sort((a, b) => a.Layer - b.Layer);
{cartData.services.flatMap((service) =>
service.tariffs.map((tariffCartData) => {
const appliedDiscounts = tariffCartData.privileges
.flatMap((privilege) => Array.from(privilege.appliedDiscounts))
.sort((a, b) => a.Layer - b.Layer);
return (
<CartItemRow
@ -247,7 +234,8 @@ export default function Cart() {
appliedDiscounts={appliedDiscounts}
/>
);
}))}
})
)}
</TableBody>
</Table>

@ -4,7 +4,6 @@ import { Discount, TariffCartData } from "@frontend/kitui";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { useEffect } from "react";
interface Props {
tariffCartData: TariffCartData;
appliedDiscounts: Discount[];
@ -27,28 +26,20 @@ export default function CartItemRow({ tariffCartData, appliedDiscounts }: Props)
borderColor: theme.palette.grayLight.main,
".MuiTableCell-root": {
color: theme.palette.secondary.main,
}
},
}}
>
<TableCell>
{tariffCartData.name}
</TableCell>
<TableCell>
{tariffCartData.privileges[0].description}
</TableCell>
<TableCell>{tariffCartData.name}</TableCell>
<TableCell>{tariffCartData.privileges[0].description}</TableCell>
<TableCell>
{appliedDiscounts.map((discount, index, arr) => (
<span key={discount.ID}>
<DiscountTooltip discount={discount} />
{index < arr.length - 1 && (
<span>,&nbsp;</span>
)}
{index < arr.length - 1 && <span>,&nbsp;</span>}
</span>
))}
</TableCell>
<TableCell>
{currencyFormatter.format(tariffCartData.price / 100)}
</TableCell>
<TableCell>{currencyFormatter.format(tariffCartData.price / 100)}</TableCell>
</TableRow>
);
}

@ -2,7 +2,6 @@ import { Tooltip, Typography } from "@mui/material";
import { Discount, findDiscountFactor } from "@frontend/kitui";
import { formatDiscountFactor } from "@root/utils/formatDiscountFactor";
interface Props {
discount: Discount;
}

@ -1,14 +1,23 @@
import { SxProps, TextField, Theme, useTheme } from "@mui/material";
import { HTMLInputTypeAttribute, ChangeEvent } from "react";
import {InputBaseProps} from "@mui/material/InputBase";
import { InputBaseProps } from "@mui/material/InputBase";
export function CustomTextField({ id, label, value, name, onBlur,error, type, sx, onChange: setValue }: {
export function CustomTextField({
id,
label,
value,
name,
onBlur,
error,
type,
sx,
onChange: setValue,
}: {
id: string;
label: string;
value: number | string | null;
name?: string;
onBlur?: InputBaseProps['onBlur'];
onBlur?: InputBaseProps["onBlur"];
error?: boolean;
type?: HTMLInputTypeAttribute;
sx?: SxProps<Theme>;
@ -32,12 +41,12 @@ export function CustomTextField({ id, label, value, name, onBlur,error, type, sx
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
},
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
color: theme.palette.secondary.main,
},
}}
value={value ? value : ""}
onChange={setValue}

@ -1,26 +1,21 @@
import { styled } from "@mui/material/styles";
import { Button, Skeleton} from "@mui/material";
import { Button, Skeleton } from "@mui/material";
const BeautifulButton = styled(Button)(({ theme }) => ({
width: "250px",
margin: "15px auto",
padding: "20px 30px",
fontSize: 18
fontSize: 18,
}));
interface Props {
isReady: boolean
text:string
type?: "button" | "reset" | "submit"
isReady: boolean;
text: string;
type?: "button" | "reset" | "submit";
}
export default ({
isReady = true,
text,
type = "button"
}:Props) => {
export default ({ isReady = true, text, type = "button" }: Props) => {
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 { styled } from "@mui/material/styles";
export default styled(DataGrid)(({ theme }) => ({
width: "100%",
minHeight: "400px",
margin: "10px 0",
color: theme.palette.secondary.main,
"& .MuiDataGrid-iconSeparator": {
display: "none"
display: "none",
},
"& .css-levciy-MuiTablePagination-displayedRows": {
color: theme.palette.secondary.main
color: theme.palette.secondary.main,
},
"& .MuiSvgIcon-root": {
color: theme.palette.secondary.main
color: theme.palette.secondary.main,
},
"& .MuiTablePagination-selectLabel": {
color: theme.palette.secondary.main
color: theme.palette.secondary.main,
},
"& .MuiInputBase-root": {
color: theme.palette.secondary.main
color: theme.palette.secondary.main,
},
"& .MuiButton-text": {
color: theme.palette.secondary.main
color: theme.palette.secondary.main,
},
})) as typeof DataGrid;

@ -1,4 +1,4 @@
import {TextField} from "@mui/material";
import { TextField } from "@mui/material";
import { styled } from "@mui/material/styles";
export default styled(TextField)(({ theme }) => ({
variant: "outlined",
@ -12,5 +12,5 @@ export default styled(TextField)(({ theme }) => ({
},
"& .Mui-focused": {
color: theme.palette.secondary.main,
}
},
}));

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

@ -2,7 +2,6 @@ import { useLocation, Navigate } from "react-router-dom";
import { useToken } from "@frontend/kitui";
interface Props {
children: JSX.Element;
}

@ -1,15 +1,14 @@
import { Discount } from "@frontend/kitui";
export type GetDiscountResponse = {
Discounts: Discount[];
};
export const discountTypes = {
"purchasesAmount": "Лояльность",
"cartPurchasesAmount": "Корзина",
"service": "Сервис",
"privilege": "Товар",
purchasesAmount: "Лояльность",
cartPurchasesAmount: "Корзина",
service: "Сервис",
privilege: "Товар",
} as const;
export type DiscountType = keyof typeof discountTypes;
@ -41,10 +40,12 @@ export type CreateDiscountBody = {
TargetScope: "Sum" | "Group" | "Each";
Overhelm: boolean;
TargetGroup: string;
Products: [{
Products: [
{
ID: string;
Factor: number;
Overhelm: false;
}];
},
];
};
};

@ -11,6 +11,6 @@ export const SERVICE_LIST = [
serviceKey: "dwarfener",
displayName: "Аналитика сокращателя",
},
] as const;
] as const;
export type ServiceType = (typeof SERVICE_LIST)[number]["serviceKey"];

@ -1,20 +1,18 @@
export interface CreateTicketRequest {
Title: string;
Message: string;
};
}
export interface CreateTicketResponse {
Ticket: string;
};
}
export interface SendTicketMessageRequest {
message: string;
ticket: string;
lang: string;
files: string[];
};
}
export type TicketStatus = "open";
@ -29,16 +27,16 @@ export interface Ticket {
created_at: string;
updated_at: string;
rate: number;
};
}
export interface TicketMessage {
id: string;
ticket_id: string;
user_id: string,
user_id: string;
session_id: string;
message: string;
files: string[],
shown: { [key: string]: number; },
request_screenshot: string,
files: string[];
shown: { [key: string]: number };
request_screenshot: string;
created_at: string;
};
}

@ -3,13 +3,7 @@ import { enqueueSnackbar } from "notistack";
import { useTheme } from "@mui/material/styles";
import { Formik, Field, Form, FormikHelpers } from "formik";
import { Link } from "react-router-dom";
import {
Box,
Checkbox,
Typography,
FormControlLabel,
Button, useMediaQuery,
} from "@mui/material";
import { Box, Checkbox, Typography, FormControlLabel, Button, useMediaQuery } from "@mui/material";
import Logo from "@pages/Logo";
import OutlinedInput from "@kitUI/outlinedInput";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
@ -50,10 +44,7 @@ const SigninForm = () => {
password: "",
};
const onSignFormSubmit = async (
values: Values,
formikHelpers: FormikHelpers<Values>
) => {
const onSignFormSubmit = async (values: Values, formikHelpers: FormikHelpers<Values>) => {
formikHelpers.setSubmitting(true);
const [_, signinError] = await signin(values.email, values.password);
@ -68,11 +59,7 @@ const SigninForm = () => {
};
return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={onSignFormSubmit}
>
<Formik initialValues={initialValues} validate={validate} onSubmit={onSignFormSubmit}>
{(props) => (
<Form>
<Box
@ -99,7 +86,7 @@ const SigninForm = () => {
"> *": {
marginTop: "15px",
},
padding: isMobile ? "0 16px" : undefined
padding: isMobile ? "0 16px" : undefined,
}}
>
<Logo />
@ -185,9 +172,7 @@ const SigninForm = () => {
/>
</Box>
<Link to="/restore" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}>
Забыли пароль?
</Typography>
<Typography color={theme.palette.golden.main}>Забыли пароль?</Typography>
</Link>
<Button
type="submit"
@ -206,13 +191,9 @@ const SigninForm = () => {
display: "flex",
}}
>
<Typography color={theme.palette.secondary.main}>
У вас нет аккаунта?&nbsp;
</Typography>
<Typography color={theme.palette.secondary.main}>У вас нет аккаунта?&nbsp;</Typography>
<Link to="/signup" style={{ textDecoration: "none" }}>
<Typography color={theme.palette.golden.main}>
Зарегестрируйтесь
</Typography>
<Typography color={theme.palette.golden.main}>Зарегестрируйтесь</Typography>
</Link>
</Box>
</Box>

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

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

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

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

@ -1,13 +1,12 @@
import { KeyboardEvent, useRef, useState } from "react";
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 { CustomPrivilege } from "@frontend/kitui";
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";
interface CardPrivilege {
privilege: CustomPrivilege;
}
@ -26,7 +25,6 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
};
const putPrivileges = async () => {
const [, putedPrivilegeError] = await putPrivilege({
name: privilege.name,
privilegeId: privilege.privilegeId,
@ -68,7 +66,7 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
if (!inputValue) return;
putPrivileges();
}
}
return (
<Box
@ -123,7 +121,17 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
</IconButton>
</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 ? (
<TextField
type="number"
@ -154,7 +162,7 @@ export const СardPrivilege = ({ privilege }: CardPrivilege) => {
<IconButton onClick={handleSavePrice}>
<SaveIcon />
</IconButton>
)
),
}}
/>
) : (

@ -8,7 +8,9 @@ import {
MenuItem,
Select,
SelectChangeEvent,
TextField, useMediaQuery, useTheme,
TextField,
useMediaQuery,
useTheme,
} from "@mui/material";
import { MOCK_DATA_USERS } from "@root/api/roles";

@ -44,10 +44,7 @@ export default function DeleteForm() {
return (
<>
<Button
onClick={() => rolesDelete(roleId)}
sx={{ mr: "5px", bgcolor: "#fe9903", color: "white" }}
>
<Button onClick={() => rolesDelete(roleId)} sx={{ mr: "5px", bgcolor: "#fe9903", color: "white" }}>
Удалить
</Button>
<TextField
@ -83,10 +80,7 @@ export default function DeleteForm() {
>
{MOCK_DATA_USERS.map(({ name, id }) => (
<MenuItem key={id} value={name}>
<Checkbox
onClick={() => setRoleId(id)}
checked={personName.indexOf(name) > -1}
/>
<Checkbox onClick={() => setRoleId(id)} checked={personName.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}

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

@ -7,7 +7,7 @@ import {
TableRow,
Typography,
useMediaQuery,
useTheme
useTheme,
} from "@mui/material";
import { CustomWrapper } from "@root/kitUI/CustomWrapper";
@ -22,8 +22,7 @@ export const SettingRoles = (): JSX.Element => {
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(600));
return (
<AccordionDetails sx={{ maxWidth: "890px",
width: "100%", }}>
<AccordionDetails sx={{ maxWidth: "890px", width: "100%" }}>
<CustomWrapper
text="Роли"
children={
@ -69,7 +68,7 @@ export const SettingRoles = (): JSX.Element => {
height: mobile ? undefined : "100px",
cursor: "pointer",
flexDirection: mobile ? "column" : "row",
gap: "5px"
gap: "5px",
}}
>
<FormCreateRoles />
@ -118,7 +117,7 @@ export const SettingRoles = (): JSX.Element => {
height: mobile ? undefined : "100px",
cursor: "pointer",
flexDirection: mobile ? "column" : "row",
gap: "5px"
gap: "5px",
}}
>
<FormDeleteRoles />
@ -129,8 +128,7 @@ export const SettingRoles = (): JSX.Element => {
}
/>
<PrivilegesWrapper text="Привилегии" sx={{ mt: "50px", maxWidth: "890px",
width: "100%", }} />
<PrivilegesWrapper text="Привилегии" sx={{ mt: "50px", maxWidth: "890px", width: "100%" }} />
</AccordionDetails>
);
};

@ -1,6 +1,6 @@
import { enqueueSnackbar } from "notistack";
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 { findDiscountsById } from "@root/stores/discounts";
@ -56,7 +56,9 @@ export default function DiscountDataGrid({ selectedRows }: Props) {
display: "flex",
justifyContent: "space-between",
flexDirection: mobile ? "column" : undefined,
gap: "10px"}}>
gap: "10px",
}}
>
<Button onClick={() => changeData(false)}>Активировать</Button>
<Button onClick={() => changeData(true)}>Деактивировать</Button>
</Box>

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

@ -1,11 +1,6 @@
import { useEffect } from "react";
import { Box, IconButton, useTheme, Tooltip } from "@mui/material";
import {
DataGrid,
GridColDef,
GridRowsProp,
GridToolbar,
} from "@mui/x-data-grid";
import { DataGrid, GridColDef, GridRowsProp, GridToolbar } from "@mui/x-data-grid";
import {
openEditDiscountDialog,
setSelectedDiscountIds,
@ -33,9 +28,7 @@ const columns: GridColDef[] = [
headerName: "Название скидки",
width: 150,
sortable: false,
renderCell: ({ row, value }) => (
<Box color={row.deleted && "#ff4545"}>{value}</Box>
),
renderCell: ({ row, value }) => <Box color={row.deleted && "#ff4545"}>{value}</Box>,
},
{
field: "description",
@ -108,21 +101,13 @@ const columns: GridColDef[] = [
];
const layerTranslate = ["", "Товар", "Сервис", "корзина", "лояльность"];
const layerValue = [
"",
"Term",
"PriceFrom",
"CartPurchasesAmount",
"PurchasesAmount",
];
const layerValue = ["", "Term", "PriceFrom", "CartPurchasesAmount", "PurchasesAmount"];
interface Props {
selectedRowsHC: (array: GridSelectionModel) => void;
}
export default function DiscountDataGrid({ selectedRowsHC }: Props) {
const theme = useTheme();
const selectedDiscountIds = useDiscountStore(
(state) => state.selectedDiscountIds
);
const selectedDiscountIds = useDiscountStore((state) => state.selectedDiscountIds);
const realDiscounts = useDiscountStore((state) => state.discounts);
useEffect(() => {
@ -138,26 +123,19 @@ export default function DiscountDataGrid({ selectedRowsHC }: Props) {
description: discount.Description,
conditionType: layerTranslate[discount.Layer],
factor: formatDiscountFactor(discount.Target.Factor),
value: (discount.Layer === 1) ?
discount.Condition[
layerValue[discount.Layer] as keyof typeof discount.Condition
]: Number(discount.Condition[
layerValue[discount.Layer] as keyof typeof discount.Condition
])/100,
value:
discount.Layer === 1
? discount.Condition[layerValue[discount.Layer] as keyof typeof discount.Condition]
: Number(discount.Condition[layerValue[discount.Layer] as keyof typeof discount.Condition]) / 100,
active: discount.Deprecated ? "🚫" : "✅",
deleted: discount.Audit.Deleted,
};
});
return (
<Box
sx={{ width: "100%", marginTop: "55px", p: "16px", maxWidth: "1000px" }}
>
<Box sx={{ width: "100%", marginTop: "55px", p: "16px", maxWidth: "1000px" }}>
<Tooltip title="обновить список привилегий">
<IconButton
onClick={requestDiscounts}
style={{ display: "block", margin: "0 auto" }}
>
<IconButton onClick={requestDiscounts} style={{ display: "block", margin: "0 auto" }}>
<AutorenewIcon sx={{ color: "white" }} />
</IconButton>
</Tooltip>

@ -8,13 +8,12 @@ import ControlPanel from "./ControlPanel";
import { useState } from "react";
import { GridSelectionModel } from "@mui/x-data-grid";
const DiscountManagement: React.FC = () => {
const theme = useTheme();
const [selectedRows, setSelectedRows] = useState<GridSelectionModel>([])
const selectedRowsHC = (array:GridSelectionModel) => {
setSelectedRows(array)
}
const [selectedRows, setSelectedRows] = useState<GridSelectionModel>([]);
const selectedRowsHC = (array: GridSelectionModel) => {
setSelectedRows(array);
};
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
@ -27,19 +26,17 @@ const DiscountManagement: React.FC = () => {
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
color: theme.palette.secondary.main
}}>
color: theme.palette.secondary.main,
}}
>
СКИДКИ
</Typography>
<CreateDiscount />
<DiscountDataGrid
selectedRowsHC={selectedRowsHC}
/>
<DiscountDataGrid selectedRowsHC={selectedRowsHC} />
<EditDiscountDialog />
<ControlPanel selectedRows={selectedRows}/>
<ControlPanel selectedRows={selectedRows} />
</LocalizationProvider>
);
};
export default DiscountManagement;

@ -18,15 +18,8 @@ import { patchDiscount } from "@root/api/discounts";
import { CustomTextField } from "@root/kitUI/CustomTextField";
import { DiscountType, discountTypes } from "@root/model/discount";
import { ServiceType, SERVICE_LIST } from "@root/model/tariff";
import {
closeEditDiscountDialog,
updateDiscount,
useDiscountStore,
} from "@root/stores/discounts";
import {
resetPrivilegeArray,
usePrivilegeStore,
} from "@root/stores/privilegesStore";
import { closeEditDiscountDialog, updateDiscount, useDiscountStore } from "@root/stores/discounts";
import { resetPrivilegeArray, usePrivilegeStore } from "@root/stores/privilegesStore";
import { getDiscountTypeFromLayer } from "@root/utils/discount";
import usePrivileges from "@root/utils/hooks/usePrivileges";
import { enqueueSnackbar } from "notistack";
@ -39,18 +32,14 @@ export default function EditDiscountDialog() {
const discounts = useDiscountStore((state) => state.discounts);
const privileges = usePrivilegeStore((state) => state.privileges);
const [serviceType, setServiceType] = useState<string>("templategen");
const [discountType, setDiscountType] =
useState<DiscountType>("purchasesAmount");
const [discountType, setDiscountType] = useState<DiscountType>("purchasesAmount");
const [discountNameField, setDiscountNameField] = useState<string>("");
const [discountDescriptionField, setDiscountDescriptionField] =
useState<string>("");
const [discountDescriptionField, setDiscountDescriptionField] = useState<string>("");
const [privilegeIdField, setPrivilegeIdField] = useState<string | "">("");
const [discountFactorField, setDiscountFactorField] = useState<string>("0");
const [purchasesAmountField, setPurchasesAmountField] = useState<string>("0");
const [cartPurchasesAmountField, setCartPurchasesAmountField] =
useState<string>("0");
const [discountMinValueField, setDiscountMinValueField] =
useState<string>("0");
const [cartPurchasesAmountField, setCartPurchasesAmountField] = useState<string>("0");
const [discountMinValueField, setDiscountMinValueField] = useState<string>("0");
const discount = discounts.find((discount) => discount.ID === editDiscountId);
@ -67,17 +56,13 @@ export default function EditDiscountDialog() {
setPrivilegeIdField(discount.Condition.Product ?? "");
setDiscountFactorField(((1 - discount.Target.Factor) * 100).toFixed(2));
setPurchasesAmountField(discount.Condition.PurchasesAmount ?? "");
setCartPurchasesAmountField(
discount.Condition.CartPurchasesAmount ?? ""
);
setCartPurchasesAmountField(discount.Condition.CartPurchasesAmount ?? "");
setDiscountMinValueField(discount.Condition.PriceFrom ?? "");
},
[discount]
);
const handleDiscountTypeChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const handleDiscountTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setDiscountType(event.target.value as DiscountType);
};
@ -89,34 +74,20 @@ export default function EditDiscountDialog() {
if (!discount) return;
const purchasesAmount = parseFloat(purchasesAmountField.replace(",", "."));
const discountFactor =
(100 - parseFloat(discountFactorField.replace(",", "."))) / 100;
const cartPurchasesAmount = parseFloat(
cartPurchasesAmountField.replace(",", ".")
);
const discountMinValue = parseFloat(
discountMinValueField.replace(",", ".")
);
const discountFactor = (100 - parseFloat(discountFactorField.replace(",", "."))) / 100;
const cartPurchasesAmount = parseFloat(cartPurchasesAmountField.replace(",", "."));
const discountMinValue = parseFloat(discountMinValueField.replace(",", "."));
if (!isFinite(purchasesAmount))
return enqueueSnackbar("Поле purchasesAmount не число");
if (!isFinite(discountFactor))
return enqueueSnackbar("Поле discountFactor не число");
if (!isFinite(cartPurchasesAmount))
return enqueueSnackbar("Поле cartPurchasesAmount не число");
if (!isFinite(discountMinValue))
return enqueueSnackbar("Поле discountMinValue не число");
if (discountType === "privilege" && !privilegeIdField)
return enqueueSnackbar("Привилегия не выбрана");
if (!isFinite(purchasesAmount)) return enqueueSnackbar("Поле purchasesAmount не число");
if (!isFinite(discountFactor)) return enqueueSnackbar("Поле discountFactor не число");
if (!isFinite(cartPurchasesAmount)) return enqueueSnackbar("Поле cartPurchasesAmount не число");
if (!isFinite(discountMinValue)) return enqueueSnackbar("Поле discountMinValue не число");
if (discountType === "privilege" && !privilegeIdField) return enqueueSnackbar("Привилегия не выбрана");
if (!discountNameField) return enqueueSnackbar('Поле "Имя" пустое');
if (!discountDescriptionField)
return enqueueSnackbar('Поле "Описание" пустое');
if (discountFactor < 0)
return enqueueSnackbar("Процент скидки не может быть больше 100");
if (!discountDescriptionField) return enqueueSnackbar('Поле "Описание" пустое');
if (discountFactor < 0) return enqueueSnackbar("Процент скидки не может быть больше 100");
const [patchedDiscountResponse, patchedDiscountError] = await patchDiscount(
discount.ID,
{
const [patchedDiscountResponse, patchedDiscountError] = await patchDiscount(discount.ID, {
cartPurchasesAmount,
discountFactor,
discountMinValue,
@ -128,8 +99,7 @@ export default function EditDiscountDialog() {
serviceType,
discountType,
privilegeId: privilegeIdField,
}
);
});
if (patchedDiscountError) {
console.error("Error patching discount", patchedDiscountError);

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

@ -1,13 +1,4 @@
import {
Button,
FormControlLabel,
MenuItem,
Radio,
RadioGroup,
Select,
TextField,
Typography,
} from "@mui/material";
import { Button, FormControlLabel, MenuItem, Radio, RadioGroup, Select, TextField, Typography } from "@mui/material";
import { DesktopDatePicker } from "@mui/x-date-pickers/DesktopDatePicker";
import { Field, Form, Formik } from "formik";
import { useEffect, useState } from "react";
@ -75,16 +66,11 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}, []);
const submitForm = (values: FormValues) => {
const currentPrivilege = privileges.find(
(item) => item.privilegeId === values.privilegeId
);
const currentPrivilege = privileges.find((item) => item.privilegeId === values.privilegeId);
const body = { ...values };
if (
(body.layer === 1 && bonusType === "discount") ||
bonusType === "privilege"
) {
if ((body.layer === 1 && bonusType === "discount") || bonusType === "privilege") {
if (currentPrivilege === undefined) {
enqueueSnackbar("Привилегия не выбрана");
@ -138,24 +124,9 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
padding: "0 10px",
}}
>
<CustomTextField
name="codeword"
label="Кодовое слово"
required
onChange={handleChange}
/>
<CustomTextField
name="description"
label="Описание"
required
onChange={handleChange}
/>
<CustomTextField
name="greetings"
label="Приветственное сообщение"
required
onChange={handleChange}
/>
<CustomTextField name="codeword" label="Кодовое слово" required onChange={handleChange} />
<CustomTextField name="description" label="Описание" required onChange={handleChange} />
<CustomTextField name="greetings" label="Приветственное сообщение" required onChange={handleChange} />
<Typography
variant="h4"
sx={{
@ -190,12 +161,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
name="activationCount"
label="Количество активаций промокода"
required
onChange={({ target }) =>
setFieldValue(
"activationCount",
Number(target.value.replace(/\D/g, ""))
)
}
onChange={({ target }) => setFieldValue("activationCount", Number(target.value.replace(/\D/g, "")))}
/>
<RadioGroup
row
@ -207,16 +173,8 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}}
onBlur={handleBlur}
>
<FormControlLabel
value="discount"
control={<Radio color="secondary" />}
label="Скидка"
/>
<FormControlLabel
value="privilege"
control={<Radio color="secondary" />}
label="Привилегия"
/>
<FormControlLabel value="discount" control={<Radio color="secondary" />} label="Скидка" />
<FormControlLabel value="privilege" control={<Radio color="secondary" />} label="Привилегия" />
</RadioGroup>
{bonusType === "discount" && (
<>
@ -231,26 +189,15 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}}
onBlur={handleBlur}
>
<FormControlLabel
value="1"
control={<Radio color="secondary" />}
label="Привилегия"
/>
<FormControlLabel
value="2"
control={<Radio color="secondary" />}
label="Сервис"
/>
<FormControlLabel value="1" control={<Radio color="secondary" />} label="Привилегия" />
<FormControlLabel value="2" control={<Radio color="secondary" />} label="Сервис" />
</RadioGroup>
<CustomTextField
name="factor"
label="Процент скидки"
required
onChange={({ target }) => {
setFieldValue(
"factor",
Number(target.value.replace(/\D/g, ""))
);
setFieldValue("factor", Number(target.value.replace(/\D/g, "")));
}}
/>
<Typography
@ -319,12 +266,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
<CustomTextField
name="threshold"
label="При каком значении применяется скидка"
onChange={({ target }) =>
setFieldValue(
"threshold",
Number(target.value.replace(/\D/g, ""))
)
}
onChange={({ target }) => setFieldValue("threshold", Number(target.value.replace(/\D/g, "")))}
/>
</>
)}
@ -366,12 +308,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
name="amount"
label="Количество"
required
onChange={({ target }) =>
setFieldValue(
"amount",
Number(target.value.replace(/\D/g, ""))
)
}
onChange={({ target }) => setFieldValue("amount", Number(target.value.replace(/\D/g, "")))}
/>
</>
)}
@ -403,12 +340,7 @@ type CustomTextFieldProps = {
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
};
const CustomTextField = ({
name,
label,
required = false,
onChange,
}: CustomTextFieldProps) => (
const CustomTextField = ({ name, label, required = false, onChange }: CustomTextFieldProps) => (
<Field
name={name}
label={label}

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

@ -1,14 +1,5 @@
import { useEffect, useState } from "react";
import {
Box,
Button,
Typography,
Modal,
TextField,
useTheme,
useMediaQuery,
IconButton,
} from "@mui/material";
import { Box, Button, Typography, Modal, TextField, useTheme, useMediaQuery, IconButton } from "@mui/material";
import { DataGrid, GridLoadingOverlay, GridToolbar } from "@mui/x-data-grid";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
@ -21,7 +12,7 @@ import type { GridColDef } from "@mui/x-data-grid";
import type { Promocode, PromocodeStatistics } from "@root/model/promocodes";
const host = window.location.hostname;
let isTest = host.includes("s");
const isTest = host.includes("s");
type StatisticsModalProps = {
id: string;
@ -51,7 +42,9 @@ const COLUMNS: GridColDef<Row, string>[] = [
renderCell: (params) => {
return (
<IconButton
onClick={() => navigator.clipboard.writeText(`https://${isTest ? "s" : ""}quiz.pena.digital/?fl=${params.row.link}`)}
onClick={() =>
navigator.clipboard.writeText(`https://${isTest ? "s" : ""}quiz.pena.digital/?fl=${params.row.link}`)
}
>
<ContentCopyIcon />
</IconButton>
@ -64,8 +57,7 @@ const COLUMNS: GridColDef<Row, string>[] = [
width: 320,
sortable: false,
valueGetter: ({ row }) => row.link,
renderCell: ({ value }) =>
value?.split("|").map((link) => <Typography>{link}</Typography>),
renderCell: ({ value }) => value?.split("|").map((link) => <Typography>{link}</Typography>),
},
{
field: "useCount",
@ -100,11 +92,8 @@ export const StatisticsModal = ({
const isMobile = useMediaQuery(theme.breakpoints.down(550));
const [rows, setRows] = useState<Row[]>([]);
const { privileges } = usePrivilegeStore();
const currentPrivilegeId = promocodes.find((promocode) => promocode.id === id)
?.bonus.privilege.privilegeID;
const privilege = privileges.find(
(item) => item.privilegeId === currentPrivilegeId
);
const currentPrivilegeId = promocodes.find((promocode) => promocode.id === id)?.bonus.privilege.privilegeID;
const privilege = privileges.find((item) => item.privilegeId === currentPrivilegeId);
const promocode = promocodes.find((item) => item.id === id);
const createFastlink = async () => {
await createFastLink(id);
@ -193,19 +182,12 @@ export const StatisticsModal = ({
</Box>
<Box sx={{ minWidth: "200px" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}>
от
</Typography>
<Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}>от</Typography>
<DatePicker
inputFormat="DD/MM/YYYY"
value={startDate}
onChange={(date) => date && setStartDate(date)}
renderInput={(params) => (
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
InputProps={{
sx: {
height: "40px",
@ -227,19 +209,12 @@ export const StatisticsModal = ({
marginTop: "10px",
}}
>
<Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}>
до
</Typography>
<Typography sx={{ minWidth: "20px", color: "#FFFFFF" }}>до</Typography>
<DatePicker
inputFormat="DD/MM/YYYY"
value={endDate}
onChange={(date) => date && setEndDate(date)}
renderInput={(params) => (
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
renderInput={(params) => <TextField {...params} sx={{ background: "#1F2126", borderRadius: "5px" }} />}
InputProps={{
sx: {
height: "40px",
@ -311,24 +286,15 @@ export const StatisticsModal = ({
>
<Typography>название привилегии: {privilege.name}</Typography>
<Typography>
{promocode?.activationCount} активаций из{" "}
{promocode?.activationLimit}
{promocode?.activationCount} активаций из {promocode?.activationLimit}
</Typography>
<Typography>приветствие: "{promocode?.greetings}"</Typography>
{promocode?.bonus?.discount?.factor !== undefined && (
<Typography>
скидка: {100 - promocode?.bonus?.discount?.factor * 100}%
</Typography>
<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 && (
<Typography>
действует до: {new Date(promocode.dueTo).toLocaleString()}
</Typography>
<Typography>действует до: {new Date(promocode.dueTo).toLocaleString()}</Typography>
)}
</Box>
)}

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

@ -6,10 +6,7 @@ import { useMemo, useState } from "react";
import { BarChart, Delete } from "@mui/icons-material";
import { promocodeApi } from "@root/api/promocode/requests";
export function usePromocodeGridColDef(
setStatistics: (id: string) => void,
deletePromocode: (id: string) => void
) {
export function usePromocodeGridColDef(setStatistics: (id: string) => void, deletePromocode: (id: string) => void) {
const validity = (value: string | number) => {
if (value === 0) {
return "неоганичен";
@ -38,8 +35,7 @@ export function usePromocodeGridColDef(
headerName: "Коэф. скидки",
width: 120,
sortable: false,
valueGetter: ({ row }) =>
Math.round(row.bonus.discount.factor * 1000) / 1000,
valueGetter: ({ row }) => Math.round(row.bonus.discount.factor * 1000) / 1000,
},
{
field: "activationCount",

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

@ -1,14 +1,6 @@
import { useState } from "react";
import moment from "moment";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Button,
useTheme,
} from "@mui/material";
import { Table, TableBody, TableCell, TableHead, TableRow, Button, useTheme } from "@mui/material";
import { DateFilter } from "./DateFilter";

@ -1,14 +1,6 @@
import { useState } from "react";
import moment from "moment";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
useTheme,
} from "@mui/material";
import { Table, TableBody, TableCell, TableHead, TableRow, Typography, useTheme } from "@mui/material";
import { DateFilter } from "./DateFilter";
@ -18,9 +10,7 @@ import { usePromocodeStatistics } from "@root/utils/hooks/usePromocodeStatistics
import type { Moment } from "moment";
export const StatisticsPromocode = () => {
const [from, setFrom] = useState<Moment | null>(
moment(moment().subtract(4, "weeks"))
);
const [from, setFrom] = useState<Moment | null>(moment(moment().subtract(4, "weeks")));
const [to, setTo] = useState<Moment | null>(moment());
const promocodes = useAllPromocodes();
const promocodeStatistics = usePromocodeStatistics({ to, from });

@ -55,14 +55,14 @@ export const StatisticsSchild = () => {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [from, setFrom] = useState<Moment | null>(
moment(moment().subtract(4, "weeks"))
);
const [from, setFrom] = useState<Moment | null>(moment(moment().subtract(4, "weeks")));
const [to, setTo] = useState<Moment | null>(moment());
const theme = useTheme();
const statistics = useSchildStatistics(from, to)
.map((obj) => ({...obj, Money: Number((obj.Money / 100).toFixed(2))}));
const statistics = useSchildStatistics(from, to).map((obj) => ({
...obj,
Money: Number((obj.Money / 100).toFixed(2)),
}));
useEffect(() => {
if (!openUserModal) {
@ -81,20 +81,14 @@ export const StatisticsSchild = () => {
}, [activeUserId]);
const copyQuizLink = (quizId: string) => {
navigator.clipboard.writeText(
`https://${
window.location.href.includes("/admin.") ? "" : "s."
}hbpn.link/${quizId}`
);
navigator.clipboard.writeText(`https://${window.location.href.includes("/admin.") ? "" : "s."}hbpn.link/${quizId}`);
enqueueSnackbar("Ссылка успешно скопирована");
};
return (
<>
<Typography sx={{ mt: "20px", mb: "20px" }}>
Статистика переходов с шильдика
</Typography>
<Typography sx={{ mt: "20px", mb: "20px" }}>Статистика переходов с шильдика</Typography>
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
<DataGrid
sx={{ marginTop: "30px", width: "80%" }}
@ -110,9 +104,7 @@ export const StatisticsSchild = () => {
pageSize={pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
onCellClick={({ id, field }) =>
field === "user" && setActiveUserId(String(id))
}
onCellClick={({ id, field }) => field === "user" && setActiveUserId(String(id))}
getRowHeight={() => "auto"}
columns={[
...COLUMNS,
@ -166,9 +158,7 @@ export const StatisticsSchild = () => {
{row.Quizes.map(({ QuizID, Regs, Money }) => (
<TableRow key={QuizID}>
<TableCell sx={{ color: "inherit" }} align="center">
<Button onClick={() => copyQuizLink(QuizID)}>
{QuizID}
</Button>
<Button onClick={() => copyQuizLink(QuizID)}>{QuizID}</Button>
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
{Regs}
@ -186,11 +176,7 @@ export const StatisticsSchild = () => {
},
]}
/>
<ModalUser
open={openUserModal}
onClose={() => setOpenUserModal(false)}
userId={activeUserId}
/>
<ModalUser open={openUserModal} onClose={() => setOpenUserModal(false)} userId={activeUserId} />
</>
);
};

@ -1,5 +1,12 @@
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 SendIcon from "@mui/icons-material/Send";
import AttachFileIcon from "@mui/icons-material/AttachFile";
@ -9,7 +16,14 @@ import { TicketMessage } from "@root/model/ticket";
import { sendTicketMessage } from "@root/api/tickets";
import { enqueueSnackbar } from "notistack";
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 ChatImage from "./ChatImage";
import ChatDocument from "./ChatDocument";
@ -17,57 +31,57 @@ import ChatVideo from "./ChatVideo";
import ChatMessage from "./ChatMessage";
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) => {
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 type = extension.toLowerCase();
switch (type) {
case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find(name => name === type):
if (file.size > MAX_FILE_SIZE) return tooLarge
return ""
case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find((name) => name === type):
if (file.size > MAX_FILE_SIZE) return tooLarge;
return "";
case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find(name => name === type):
if (file.size > MAX_PHOTO_SIZE) return tooLarge
return ""
case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find((name) => name === type):
if (file.size > MAX_PHOTO_SIZE) return tooLarge;
return "";
case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find(name => name === type):
if (file.size > MAX_VIDEO_SIZE) return tooLarge
return ""
case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find((name) => name === type):
if (file.size > MAX_VIDEO_SIZE) return tooLarge;
return "";
default:
return "Не удалось отправить файл. Недопустимый тип"
}
return "Не удалось отправить файл. Недопустимый тип";
}
};
export default function Chat() {
const token = useToken();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const tickets = useTicketStore(state => state.tickets);
const messages = useMessageStore(state => state.messages);
const tickets = useTicketStore((state) => state.tickets);
const messages = useMessageStore((state) => state.messages);
const [messageField, setMessageField] = useState<string>("");
const ticketId = useParams().ticketId;
const chatBoxRef = useRef<HTMLDivElement>(null);
const messageApiPage = useMessageStore(state => state.apiPage);
const messagesPerPage = useMessageStore(state => state.messagesPerPage);
const isPreventAutoscroll = useMessageStore(state => state.isPreventAutoscroll);
const fetchState = useMessageStore(state => state.ticketMessagesFetchState);
const lastMessageId = useMessageStore(state => state.lastMessageId);
const messageApiPage = useMessageStore((state) => state.apiPage);
const messagesPerPage = useMessageStore((state) => state.messagesPerPage);
const isPreventAutoscroll = useMessageStore((state) => state.isPreventAutoscroll);
const fetchState = useMessageStore((state) => state.ticketMessagesFetchState);
const lastMessageId = useMessageStore((state) => state.lastMessageId);
const fileInputRef = useRef<HTMLInputElement>(null);
const [disableFileButton, setDisableFileButton] = useState(false);
const ticket = tickets.find(ticket => ticket.id === ticketId);
const ticket = tickets.find((ticket) => ticket.id === ticketId);
useTicketMessages({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages",
ticketId,
messagesPerPage,
messageApiPage,
onSuccess: messages => {
onSuccess: (messages) => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
addOrUpdateMessages(messages);
},
@ -86,10 +100,12 @@ export default function Chat() {
clearMessageState();
setIsPreventAutoscroll(false);
},
marker: "ticket message"
marker: "ticket message",
});
const throttledScrollHandler = useMemo(() => throttle(() => {
const throttledScrollHandler = useMemo(
() =>
throttle(() => {
const chatBox = chatBoxRef.current;
if (!chatBox) return;
@ -102,11 +118,14 @@ export default function Chat() {
if (chatBox.scrollTop < chatBox.clientHeight) {
incrementMessageApiPage();
}
}, 200), [fetchState]);
}, 200),
[fetchState]
);
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
useEffect(function scrollOnNewMessage() {
useEffect(
function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) {
@ -115,7 +134,9 @@ export default function Chat() {
}, 50);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessageId]);
},
[lastMessageId]
);
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
@ -145,7 +166,7 @@ export default function Chat() {
let data;
const ticketId = ticket?.id
const ticketId = ticket?.id;
if (ticketId !== undefined) {
try {
const body = new FormData();
@ -165,14 +186,14 @@ export default function Chat() {
}
};
const sendFileHC = async (file: File) => {
const check = checkAcceptableMediaType(file)
const check = checkAcceptableMediaType(file);
if (check.length > 0) {
enqueueSnackbar(check)
return
enqueueSnackbar(check);
return;
}
setDisableFileButton(true)
await sendFile(file)
setDisableFileButton(false)
setDisableFileButton(true);
await sendFile(file);
setDisableFileButton(false);
};
function handleTextfieldKeyPress(e: KeyboardEvent) {
@ -183,7 +204,8 @@ export default function Chat() {
}
return (
<Box sx={{
<Box
sx={{
border: "1px solid",
borderColor: theme.palette.grayDark.main,
height: "600px",
@ -195,7 +217,8 @@ export default function Chat() {
justifyContent: "center",
alignItems: "center",
gap: "8px",
}}>
}}
>
<Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
<Box
ref={chatBoxRef}
@ -215,67 +238,73 @@ export default function Chat() {
messages.map((message) => {
const isFileVideo = () => {
if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType),
))
return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
const isFileImage = () => {
if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType),
))
return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
const isFileDocument = () => {
if (message.files) {
return (ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType),
))
return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
if (message.files !== null && message.files.length > 0 && isFileImage()) {
return <ChatImage
return (
<ChatImage
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
if (message.files !== null && message.files.length > 0 && isFileVideo()) {
return <ChatVideo
return (
<ChatVideo
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
if (message.files !== null && message.files.length > 0 && isFileDocument()) {
return <ChatDocument
return (
<ChatDocument
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
return <ChatMessage
return (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
})
}
);
})}
</Box>
{ticket &&
{ticket && (
<TextField
value={messageField}
onChange={e => setMessageField(e.target.value)}
onChange={(e) => setMessageField(e.target.value)}
onKeyPress={handleTextfieldKeyPress}
id="message-input"
placeholder="Написать сообщение"
@ -301,7 +330,7 @@ export default function Chat() {
</IconButton>
<IconButton
onClick={() => {
if (!disableFileButton) fileInputRef.current?.click()
if (!disableFileButton) fileInputRef.current?.click();
}}
sx={{
height: "45px",
@ -321,15 +350,15 @@ export default function Chat() {
<AttachFileIcon sx={{ color: theme.palette.golden.main }} />
</IconButton>
</InputAdornment>
)
),
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main,
}
},
}}
/>
}
)}
</Box>
);
}

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

@ -1,11 +1,4 @@
import {
Box,
ButtonBase,
Link,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Box, ButtonBase, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
interface Props {
@ -15,17 +8,11 @@ interface Props {
createdAt: string;
}
export default function ChatImage({
unAuthenticated = false,
isSelf,
file,
createdAt,
}: Props) {
export default function ChatImage({ unAuthenticated = false, isSelf, file, createdAt }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
return (
<Box
sx={{
@ -35,11 +22,13 @@ export default function ChatImage({
justifyContent: isSelf ? "end" : "start",
}}
>
<Typography sx={{
<Typography
sx={{
fontSize: "12px",
alignSelf: "end",
}}>
{new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
}}
>
{new Date(createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</Typography>
<Box
sx={{

@ -7,17 +7,10 @@ interface Props {
createdAt: string;
}
export default function ChatMessage({
unAuthenticated = false,
isSelf,
text,
createdAt,
}: Props) {
export default function ChatMessage({ unAuthenticated = false, isSelf, text, createdAt }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return (
<Box
sx={{
@ -27,11 +20,13 @@ export default function ChatMessage({
justifyContent: isSelf ? "end" : "start",
}}
>
<Typography sx={{
<Typography
sx={{
fontSize: "12px",
alignSelf: "end",
}}>
{new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
}}
>
{new Date(createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</Typography>
<Box
sx={{

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

@ -1,7 +1,6 @@
import { Box, Typography, useTheme } from "@mui/material";
import { TicketMessage } from "@root/model/ticket";
interface Props {
message: TicketMessage;
isSelf?: boolean;
@ -11,21 +10,26 @@ export default function Message({ message, isSelf }: Props) {
const theme = useTheme();
const time = (
<Typography sx={{
<Typography
sx={{
fontSize: "12px",
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>
);
return (
<Box sx={{
<Box
sx={{
display: "flex",
justifyContent: isSelf ? "end" : "start",
gap: "6px",
}}>
}}
>
{isSelf && time}
<Box sx={{
<Box
sx={{
backgroundColor: "#2a2b2c",
p: "12px",
border: `1px solid ${theme.palette.golden.main}`,
@ -33,7 +37,8 @@ export default function Message({ message, isSelf }: Props) {
borderTopLeftRadius: isSelf ? "20px" : 0,
borderTopRightRadius: isSelf ? 0 : "20px",
maxWidth: "90%",
}}>
}}
>
<Typography fontSize="14px" sx={{ wordBreak: "break-word" }}>
{message.message}
</Typography>

@ -6,4 +6,4 @@ export const ACCEPT_SEND_MEDIA_TYPES_MAP = {
picture: ["jpg", "png"],
video: ["mp4"],
document: ["doc", "docx", "pdf", "txt", "xlsx", "csv"],
} as const;
} as const;

@ -1,6 +1,5 @@
import { useTheme } from "@mui/material";
interface Props {
isExpanded: boolean;
}
@ -9,9 +8,27 @@ export default function ExpandIcon({ isExpanded }: Props) {
const theme = useTheme();
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">
<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
style={{ transform: isExpanded ? "rotate(180deg)" : undefined }}
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>
);
}

@ -4,20 +4,10 @@ import Chat from "./Chat/Chat";
import Collapse from "./Collapse";
import TicketList from "./TicketList/TicketList";
import { Ticket } from "@root/model/ticket";
import {
clearTickets,
setTicketsFetchState,
updateTickets,
useTicketStore,
} from "@root/stores/tickets";
import { clearTickets, setTicketsFetchState, updateTickets, useTicketStore } from "@root/stores/tickets";
import { enqueueSnackbar } from "notistack";
import { clearMessageState } from "@root/stores/messages";
import {
getMessageFromFetchError,
useSSESubscription,
useTicketsFetcher,
useToken,
} from "@frontend/kitui";
import { getMessageFromFetchError, useSSESubscription, useTicketsFetcher, useToken } from "@frontend/kitui";
import ModalUser from "@root/pages/dashboard/ModalUser";
export default function Support() {
@ -45,9 +35,7 @@ export default function Support() {
useSSESubscription<Ticket>({
enabled: Boolean(token),
url:
process.env.REACT_APP_DOMAIN +
`/heruvym/subscribe?Authorization=${token}`,
url: process.env.REACT_APP_DOMAIN + `/heruvym/subscribe?Authorization=${token}`,
onNewData: updateTickets,
onDisconnect: () => {
clearMessageState();
@ -83,21 +71,12 @@ export default function Support() {
>
{!upMd && (
<Collapse headerText="Тикеты">
{(closeCollapse) => (
<TicketList
closeCollapse={closeCollapse}
setActiveUserId={setActiveUserId}
/>
)}
{(closeCollapse) => <TicketList closeCollapse={closeCollapse} setActiveUserId={setActiveUserId} />}
</Collapse>
)}
<Chat />
{upMd && <TicketList setActiveUserId={setActiveUserId} />}
<ModalUser
open={openUserModal}
onClose={() => setOpenUserModal(false)}
userId={activeUserId}
/>
<ModalUser open={openUserModal} onClose={() => setOpenUserModal(false)} userId={activeUserId} />
</Box>
);
}

@ -1,26 +1,25 @@
import Modal from "@mui/material/Modal";
import {closeDeleteTariffDialog} from "@stores/tariffs";
import { closeDeleteTariffDialog } from "@stores/tariffs";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import makeRequest from "@root/api/makeRequest";
import {parseAxiosError} from "@root/utils/parse-error";
interface Props{
ticketId: string | undefined,
openModal: boolean,
setOpenModal: (a: boolean) => void
import { parseAxiosError } from "@root/utils/parse-error";
interface Props {
ticketId: string | undefined;
openModal: boolean;
setOpenModal: (a: boolean) => void;
}
export default function CloseTicketModal({ticketId, openModal, setOpenModal}: Props) {
export default function CloseTicketModal({ ticketId, openModal, setOpenModal }: Props) {
const CloseTicket = async () => {
try {
const ticketCloseResponse = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/heruvym/close" ,
url: process.env.REACT_APP_DOMAIN + "/heruvym/close",
method: "post",
useToken: true,
body: {
"ticket": ticketId
ticket: ticketId,
},
});
@ -30,7 +29,7 @@ export default function CloseTicketModal({ticketId, openModal, setOpenModal}: Pr
return [null, `Не удалось закрыть тикет. ${error}`];
}
}
};
return (
<Modal
@ -66,22 +65,19 @@ export default function CloseTicketModal({ticketId, openModal, setOpenModal}: Pr
}}
>
<Button
onClick={async ()=>{
CloseTicket()
setOpenModal(false)
onClick={async () => {
CloseTicket();
setOpenModal(false);
}}
sx={{width: "40px", height: "25px"}}
sx={{ width: "40px", height: "25px" }}
>
Да
</Button>
<Button
onClick={() => setOpenModal(false)}
sx={{width: "40px", height: "25px"}}
>
<Button onClick={() => setOpenModal(false)} sx={{ width: "40px", height: "25px" }}>
Нет
</Button>
</Box>
</Box>
</Modal>
)
);
}

@ -1,14 +1,5 @@
import CircleIcon from "@mui/icons-material/Circle";
import {
Box,
Card,
CardActionArea,
CardContent,
CardHeader,
Divider,
Typography,
useTheme,
} from "@mui/material";
import { Box, Card, CardActionArea, CardContent, CardHeader, Divider, Typography, useTheme } from "@mui/material";
import { green } from "@mui/material/colors";
import { Ticket } from "@root/model/ticket";
import { useNavigate, useParams } from "react-router-dom";
@ -76,9 +67,7 @@ export default function TicketItem({ ticket, setActiveUserId }: Props) {
p: 0,
}}
>
<Box sx={flexCenterSx}>
{new Date(ticket.top_message.created_at).toLocaleDateString()}
</Box>
<Box sx={flexCenterSx}>{new Date(ticket.top_message.created_at).toLocaleDateString()}</Box>
<Box
sx={{
...flexCenterSx,

@ -3,12 +3,12 @@ import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined";
import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { Ticket } from "@root/model/ticket";
import { incrementTicketsApiPage, useTicketStore } from "@root/stores/tickets";
import {useEffect, useRef, useState} from "react";
import { useEffect, useRef, useState } from "react";
import TicketItem from "./TicketItem";
import { throttle } from "@frontend/kitui";
import makeRequest from "@root/api/makeRequest";
import {parseAxiosError} from "@root/utils/parse-error";
import {useParams} from "react-router-dom";
import { parseAxiosError } from "@root/utils/parse-error";
import { useParams } from "react-router-dom";
import CloseTicketModal from "@pages/dashboard/Content/Support/TicketList/CloseTicketModal";
type TicketListProps = {
@ -16,17 +16,14 @@ type TicketListProps = {
setActiveUserId: (id: string) => void;
};
export default function TicketList({
closeCollapse,
setActiveUserId,
}: TicketListProps) {
export default function TicketList({ closeCollapse, setActiveUserId }: TicketListProps) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const tickets = useTicketStore((state) => state.tickets);
const ticketsFetchState = useTicketStore((state) => state.ticketsFetchState);
const ticketsBoxRef = useRef<HTMLDivElement>(null);
const ticketId = useParams().ticketId;
const [openModal, setOpenModal] = useState(false)
const [openModal, setOpenModal] = useState(false);
useEffect(
function updateCurrentPageOnScroll() {
@ -34,15 +31,8 @@ export default function TicketList({
const ticketsBox = ticketsBoxRef.current;
const scrollHandler = () => {
const scrollBottom =
ticketsBox.scrollHeight -
ticketsBox.scrollTop -
ticketsBox.clientHeight;
if (
scrollBottom < ticketsBox.clientHeight &&
ticketsFetchState === "idle"
)
incrementTicketsApiPage();
const scrollBottom = ticketsBox.scrollHeight - ticketsBox.scrollTop - ticketsBox.clientHeight;
if (scrollBottom < ticketsBox.clientHeight && ticketsFetchState === "idle") incrementTicketsApiPage();
};
const throttledScrollHandler = throttle(scrollHandler, 200);
@ -55,9 +45,7 @@ export default function TicketList({
[ticketsFetchState]
);
const sortedTickets = tickets
.sort(sortTicketsByUpdateTime)
.sort(sortTicketsByUnread);
const sortedTickets = tickets.sort(sortTicketsByUpdateTime).sort(sortTicketsByUnread);
return (
<Box
@ -97,7 +85,7 @@ export default function TicketList({
<SearchOutlinedIcon />
</Button>
<Button
onClick={()=> setOpenModal(true)}
onClick={() => setOpenModal(true)}
variant="text"
sx={{
width: "100%",
@ -116,7 +104,7 @@ export default function TicketList({
ЗАКРЫТЬ ТИКЕТ
<HighlightOffOutlinedIcon />
</Button>
<CloseTicketModal openModal={openModal} setOpenModal={setOpenModal} ticketId={ticketId}/>
<CloseTicketModal openModal={openModal} setOpenModal={setOpenModal} ticketId={ticketId} />
</Box>
<Box
ref={ticketsBoxRef}

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

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

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

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

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

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

@ -2,9 +2,8 @@ import * as React from "react";
import { Box, Typography, Button } from "@mui/material";
import theme from "../../../../../theme";
export interface MWProps {
openModal: (type:number, num: number) => void
openModal: (type: number, num: number) => void;
}
const Quiz: React.FC<MWProps> = ({ openModal }) => {
@ -19,63 +18,68 @@ const Quiz: React.FC<MWProps> = ({ openModal }) => {
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
color: theme.palette.secondary.main
}}>
color: theme.palette.secondary.main,
}}
>
Опросник
</Typography>
<Box sx={{
<Box
sx={{
marginTop: "35px",
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gridGap: "20px",
marginBottom: "120px",
}}>
}}
>
<Button
variant = "contained"
onClick={ () => openModal(2, 1) }
variant="contained"
onClick={() => openModal(2, 1)}
sx={{
backgroundColor: theme.palette.menu.main,
padding: "11px 43px",
fontWeight: "normal",
fontSize: "17px",
"&:hover": {
backgroundColor: theme.palette.grayMedium.main
}
}}>
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Создать тариф на время
</Button>
<Button
variant = "contained"
onClick={ () => openModal(2, 0) }
variant="contained"
onClick={() => openModal(2, 0)}
sx={{
backgroundColor: theme.palette.menu.main,
padding: '11px 43px',
padding: "11px 43px",
fontWeight: "normal",
fontSize: "17px",
"&:hover": {
backgroundColor: theme.palette.grayMedium.main
}
}}>
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Создать тариф на объем
</Button>
<Button
variant = "contained"
variant="contained"
sx={{
backgroundColor: theme.palette.menu.main,
padding: '11px 43px',
padding: "11px 43px",
fontWeight: "normal",
fontSize: "17px",
"&:hover": {
backgroundColor: theme.palette.grayMedium.main
}
}}>
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Изменить тариф
</Button>
</Box>
</React.Fragment>
);
}
};
export default Quiz;

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

@ -2,9 +2,8 @@ import * as React from "react";
import { Box, Typography, Button } from "@mui/material";
import theme from "../../../../../theme";
export interface MWProps {
openModal: (type:number, num: number) => void
openModal: (type: number, num: number) => void;
}
const Templater: React.FC<MWProps> = ({ openModal }) => {
@ -19,77 +18,83 @@ const Templater: React.FC<MWProps> = ({ openModal }) => {
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
color: theme.palette.secondary.main
}}>
color: theme.palette.secondary.main,
}}
>
Шаблонизатор документов
</Typography>
<Box sx={{
<Box
sx={{
marginTop: "35px",
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gridGap: "20px",
marginBottom: "120px",
}}>
}}
>
<Button
variant = "contained"
onClick={ () => openModal(1, 1) }
variant="contained"
onClick={() => openModal(1, 1)}
sx={{
backgroundColor: theme.palette.menu.main,
fontWeight: "normal",
fontSize: "17px",
padding: '11px 25px',
padding: "11px 25px",
"&:hover": {
backgroundColor: theme.palette.grayMedium.main
}
}}>
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Создать тариф на время
</Button>
<Button
variant = "contained"
onClick={ () => openModal(1, 0) }
variant="contained"
onClick={() => openModal(1, 0)}
sx={{
backgroundColor: theme.palette.menu.main,
padding: '11px 25px',
padding: "11px 25px",
fontWeight: "normal",
fontSize: "17px",
"&:hover": {
backgroundColor: theme.palette.grayMedium.main
}
}}>
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Создать тариф на объем
</Button>
<Button
variant = "contained"
onClick={ () => openModal(1, 2) }
variant="contained"
onClick={() => openModal(1, 2)}
sx={{
backgroundColor: theme.palette.menu.main,
padding: '11px 25px',
padding: "11px 25px",
fontWeight: "normal",
fontSize: "17px",
"&:hover": {
backgroundColor: theme.palette.grayMedium.main
}
}}>
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Создать тариф на гигабайты
</Button>
<Button
variant = "contained"
variant="contained"
sx={{
backgroundColor: theme.palette.menu.main,
padding: '11px 25px',
padding: "11px 25px",
fontWeight: "normal",
fontSize: "17px",
"&:hover": {
backgroundColor: theme.palette.grayMedium.main
}
}}>
backgroundColor: theme.palette.grayMedium.main,
},
}}
>
Изменить тариф
</Button>
</Box>
</React.Fragment>
);
}
};
export default Templater;

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -4,10 +4,9 @@ import { devtools } from "zustand/middleware";
import { produce } from "immer";
import { Discount } from "@frontend/kitui";
interface DiscountStore {
discounts: Discount[];
selectedDiscountIds: GridSelectionModel,
selectedDiscountIds: GridSelectionModel;
editDiscountId: string | null;
}
@ -27,23 +26,26 @@ export const useDiscountStore = create<DiscountStore>()(
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(
state => ({ discounts: [...state.discounts, discount] })
);
export const addDiscount = (discount: DiscountStore["discounts"][number]) =>
useDiscountStore.setState((state) => ({ discounts: [...state.discounts, discount] }));
export const updateDiscount = (updatedDiscount: DiscountStore["discounts"][number]) => useDiscountStore.setState(
produce<DiscountStore>(state => {
const discountIndex = state.discounts.findIndex(discount => discount.ID === updatedDiscount.ID);
export const updateDiscount = (updatedDiscount: DiscountStore["discounts"][number]) =>
useDiscountStore.setState(
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}`);
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 });

@ -3,7 +3,6 @@ import { TicketMessage } from "@root/model/ticket";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface MessageStore {
messages: TicketMessage[];
fetchState: "idle" | "fetching" | "all fetched";
@ -34,9 +33,9 @@ export const useMessageStore = create<MessageStore>()(
export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
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);
@ -46,14 +45,16 @@ export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
});
};
export const clearMessageState = () => useMessageStore.setState({
export const clearMessageState = () =>
useMessageStore.setState({
messages: [],
apiPage: 0,
lastMessageId: undefined,
fetchState: "idle",
});
});
export const setMessageFetchState = (fetchState: MessageStore["fetchState"]) => useMessageStore.setState({ fetchState });
export const setMessageFetchState = (fetchState: MessageStore["fetchState"]) =>
useMessageStore.setState({ fetchState });
export const incrementMessageApiPage = () => {
const state = useMessageStore.getState();
@ -61,9 +62,11 @@ export const incrementMessageApiPage = () => {
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) {
const date1 = new Date(ticket1.created_at).getTime();

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