Merge branch 'dev' into 'staging'

Dev

See merge request frontend/marketplace!204
This commit is contained in:
Nastya 2024-06-01 00:19:49 +00:00
commit da57531f38
32 changed files with 2222 additions and 1956 deletions

@ -9,19 +9,23 @@ import type {
RegisterResponse, RegisterResponse,
} from "@frontend/kitui"; } from "@frontend/kitui";
const apiUrl = process.env.REACT_APP_DOMAIN + "/auth"; type RecoverResponse = {
message: string;
};
export async function register( const API_URL = `${process.env.REACT_APP_DOMAIN}/auth`;
export const register = async (
login: string, login: string,
password: string, password: string,
phoneNumber: string phoneNumber: string
): Promise<[RegisterResponse | null, string?]> { ): Promise<[RegisterResponse | null, string?]> => {
try { try {
const registerResponse = await makeRequest< const registerResponse = await makeRequest<
RegisterRequest, RegisterRequest,
RegisterResponse RegisterResponse
>({ >({
url: apiUrl + "/register", url: `${API_URL}/register`,
body: { login, password, phoneNumber }, body: { login, password, phoneNumber },
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,
@ -33,15 +37,15 @@ export async function register(
return [null, `Не удалось зарегестрировать аккаунт. ${error}`]; return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
} }
} };
export async function login( export const login = async (
login: string, login: string,
password: string password: string
): Promise<[LoginResponse | null, string?]> { ): Promise<[LoginResponse | null, string?]> => {
try { try {
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({ const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
url: apiUrl + "/login", url: `${API_URL}/login`,
body: { login, password }, body: { login, password },
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,
@ -53,34 +57,40 @@ export async function login(
return [null, `Не удалось войти. ${error}`]; return [null, `Не удалось войти. ${error}`];
} }
} };
export async function recover( export const recover = async (
email: string email: string
): Promise<[unknown | null, string?]> { ): Promise<[RecoverResponse | null, string?]> => {
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append("email", email); formData.append("email", email);
formData.append("RedirectionURL", process.env.REACT_APP_DOMAIN + "/changepwd") formData.append(
const recoverResponse = await makeRequest<unknown, unknown>({ "RedirectionURL",
url: process.env.REACT_APP_DOMAIN + "/codeword/recover", `${process.env.REACT_APP_DOMAIN}/changepwd`
);
const recoverResponse = await makeRequest<FormData, RecoverResponse>({
url: `${process.env.REACT_APP_DOMAIN}/codeword/recover`,
body: formData, body: formData,
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,
}); });
return [recoverResponse]; return [recoverResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return [null, `Не удалось восстановить пароль. ${error}`]; return [null, `Не удалось восстановить пароль. ${error}`];
} }
} };
export async function logout(): Promise<[unknown, string?]> { export const logout = async (): Promise<[void | null, string?]> => {
try { try {
const logoutResponse = await makeRequest<never, void>({ const logoutResponse = await makeRequest<never, void>({
url: apiUrl + "/logout",
method: "POST", method: "POST",
url: `${API_URL}/logout`,
useToken: true, useToken: true,
withCredentials: true, withCredentials: true,
}); });
@ -91,4 +101,4 @@ export async function logout(): Promise<[unknown, string?]> {
return [null, `Не удалось выйти. ${error}`]; return [null, `Не удалось выйти. ${error}`];
} }
} };

@ -1,83 +1,82 @@
import { UserAccount } from "@frontend/kitui" import { UserAccount } from "@frontend/kitui";
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error" import { parseAxiosError } from "@root/utils/parse-error";
const apiUrl = process.env.REACT_APP_DOMAIN + "/customer" const API_URL = `${process.env.REACT_APP_DOMAIN}/customer`;
export async function patchCart( export const patchCart = async (
tariffId: string tariffId: string
): Promise<[string[], string?]> { ): Promise<[string[], string?]> => {
try { try {
const patchCartResponse = await makeRequest<never, UserAccount>({ const patchCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "PATCH", method: "PATCH",
url: `${API_URL}/cart?id=${tariffId}`,
useToken: true, useToken: true,
}) });
return [patchCartResponse.cart] return [patchCartResponse.cart];
} catch (nativeError) { } catch (nativeError) {
let [error, status] = parseAxiosError(nativeError) let [error, status] = parseAxiosError(nativeError);
if (status === 400 && error.indexOf("invalid id") !== -1) error = "Данный тариф более недоступен" if (status === 400 && error.indexOf("invalid id") !== -1)
error = "Данный тариф более недоступен";
return [[], `Не удалось добавить товар в корзину. ${error}`] return [[], `Не удалось добавить товар в корзину. ${error}`];
} }
} };
export async function deleteCart( export const deleteCart = async (
tariffId: string tariffId: string
): Promise<[string[], string?]> { ): Promise<[string[], string?]> => {
try { try {
const deleteCartResponse = await makeRequest<never, UserAccount>({ const deleteCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "DELETE", method: "DELETE",
url: `${API_URL}/cart?id=${tariffId}`,
useToken: true, useToken: true,
}) });
return [deleteCartResponse.cart] return [deleteCartResponse.cart];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [[], `Не удалось удалить товар из корзины. ${error}`] return [[], `Не удалось удалить товар из корзины. ${error}`];
} }
} };
export async function payCart(): Promise<[UserAccount | null, string?]> { export const payCart = async (): Promise<[UserAccount | null, string?]> => {
try { try {
const payCartResponse = await makeRequest<never, UserAccount>({ const payCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + "/cart/pay",
method: "POST", method: "POST",
url: `${API_URL}/cart/pay`,
useToken: true, useToken: true,
}) });
return [payCartResponse] return [payCartResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Не удалось оплатить товар из корзины. ${error}`] return [null, `Не удалось оплатить товар из корзины. ${error}`];
} }
} };
export async function patchCurrency( export const patchCurrency = async (
currency: string currency: string
): Promise<[UserAccount | null, string?]> { ): Promise<[UserAccount | null, string?]> => {
try { try {
const patchCurrencyResponse = await makeRequest< const patchCurrencyResponse = await makeRequest<
{ currency: string }, { currency: string },
UserAccount UserAccount
>({ >({
url: apiUrl + "/wallet",
method: "PATCH", method: "PATCH",
url: `${API_URL}/wallet`,
useToken: true, useToken: true,
body: { body: { currency },
currency, });
},
})
return [patchCurrencyResponse] return [patchCurrencyResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Не удалось изменить валюту. ${error}`] return [null, `Не удалось изменить валюту. ${error}`];
} }
} };

@ -1,6 +1,6 @@
import {Tariff} from "@frontend/kitui" import { Tariff } from "@frontend/kitui";
import {parseAxiosError} from "@root/utils/parse-error" import { parseAxiosError } from "@root/utils/parse-error";
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
export interface GetHistoryResponse { export interface GetHistoryResponse {
totalPages: number; totalPages: number;
@ -37,48 +37,87 @@ export type HistoryRecord2 = {
userId: string; userId: string;
}; };
export type KeyValue = { Key: string; Value: string | number }; export type KeyValue = { Key: string; Value: string | number };
export type RawDetails = { export type RawDetails = {
Key: "tariffs" | "price"; Key: "tariffs" | "price";
Value: string | number | KeyValue[][]; Value: string | number | KeyValue[][];
} };
export async function getHistory(): Promise<[GetHistoryResponse | GetHistoryResponse2 | null, string?]> { const API_URL = `${process.env.REACT_APP_DOMAIN}/customer`;
export const getHistory = async (): Promise<
[GetHistoryResponse | GetHistoryResponse2 | null, string?]
> => {
try { try {
const historyResponse = await makeRequest<never, GetHistoryResponse|GetHistoryResponse2>({ const historyResponse = await makeRequest<
url: process.env.REACT_APP_DOMAIN + "/customer/history?page=1&limit=100&type=payCart", never,
method: "get", GetHistoryResponse | GetHistoryResponse2
>({
method: "GET",
url: `${API_URL}/history?page=1&limit=100&type=payCart`,
useToken: true, useToken: true,
}) });
if (!Array.isArray(historyResponse.records[0]?.rawDetails)) { if (!Array.isArray(historyResponse.records[0]?.rawDetails)) {
return [historyResponse] as [GetHistoryResponse2] return [historyResponse] as [GetHistoryResponse2];
} }
const checked = historyResponse.records.map((data) => { const checked = historyResponse.records.map((data) => {
//const buffer:RawDetails[] = [];
/*(data.rawDetails as HistoryRecord["rawDetails"]).forEach((slot) => {
const index = regList[slot.Key]
buffer[index] = { ...slot }
})*/
//Чистим дыры с помощью .filter(() => true)
//@ts-ignore
//data.rawDetails = buffer
const checkedRowDetails = [ const checkedRowDetails = [
(data.rawDetails as HistoryRecord["rawDetails"]).find((details) => details.Key === "tariffs") as RawDetails, (data.rawDetails as HistoryRecord["rawDetails"]).find(
(data.rawDetails as HistoryRecord["rawDetails"]).find((details) => details.Key === "price") as KeyValue (details) => details.Key === "tariffs"
] ) as RawDetails,
return {...data, rawDetails: checkedRowDetails} as HistoryRecord (data.rawDetails as HistoryRecord["rawDetails"]).find(
}) (details) => details.Key === "price"
) as KeyValue,
];
return { ...data, rawDetails: checkedRowDetails } as HistoryRecord;
});
historyResponse.records = checked || [] historyResponse.records = checked || [];
return [historyResponse] return [historyResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить историю. ${error}`] return [null, `Не удалось получить историю. ${error}`];
} }
} };
export const sendReport = async (
id: string
): Promise<[void | null, string?]> => {
debugger;
try {
const sendReportResponse = await makeRequest<{ id: string }, void>({
method: "POST",
url: `${API_URL}/sendReport`,
body: { id },
});
return [sendReportResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить отчёт. ${error}`];
}
};
export const sendReportById = async (
tariffId: string
): Promise<[void | null, string?]> => {
debugger;
try {
const sendReportResponse = await makeRequest<never, void>({
method: "POST",
url: `${API_URL}/sendReport/${tariffId}`,
});
return [sendReportResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить отчёт. ${error}`];
}
};

@ -5,30 +5,44 @@ import { clearUserData } from "@root/stores/user";
import { clearCustomTariffs } from "@root/stores/customTariffs"; import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets"; import { clearTickets } from "@root/stores/tickets";
import { redirect } from "react-router-dom"; import { redirect } from "react-router-dom";
import {setNotEnoughMoneyAmount} from "@stores/cart" import { setNotEnoughMoneyAmount } from "@stores/cart";
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;
}
interface ErrorResponseData { interface ErrorResponseData {
message?: string; message?: string;
} }
async function makeRequest<TRequest = unknown, TResponse = unknown>(data: MakeRequest): Promise<TResponse> { async function makeRequest<TRequest = unknown, TResponse = unknown>(
data: MakeRequest
): Promise<TResponse> {
try { try {
const response = await KIT.makeRequest<unknown>(data) const response = await KIT.makeRequest<unknown>(data);
return response as TResponse return response as TResponse;
} catch (e) { } catch (e) {
const error = e as AxiosError; const error = e as AxiosError;
if (error.response?.status === 400 && (error.response?.data as ErrorResponseData)?.message === "refreshToken is empty") { if (
error.response?.status === 400 &&
(error.response?.data as ErrorResponseData)?.message ===
"refreshToken is empty"
) {
clearAuthToken(); clearAuthToken();
clearUserData(); clearUserData();
clearCustomTariffs(); clearCustomTariffs();
clearTickets(); clearTickets();
setNotEnoughMoneyAmount(0) setNotEnoughMoneyAmount(0);
redirect("/"); redirect("/");
} }
throw e throw e;
}; }
}; }
export default makeRequest; export default makeRequest;

@ -1,21 +1,21 @@
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import type { GetDiscountsResponse } from "@root/model/discount"; import type { GetDiscountsResponse } from "@root/model/discount";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import useSWR from "swr"; import useSWR from "swr";
const apiUrl = process.env.REACT_APP_DOMAIN + "/price"; const API_URL = `${process.env.REACT_APP_DOMAIN}/price`;
export async function getDiscounts(userId: string | null) { export const getDiscounts = async (userId: string | null) => {
if (userId === null) { if (userId === null) {
return; return;
} }
try { try {
const discountsResponse = await makeRequest<never, GetDiscountsResponse>({ const discountsResponse = await makeRequest<never, GetDiscountsResponse>({
url: `${apiUrl}/discount/user/${userId}`, method: "GET",
method: "get", url: `${API_URL}/discount/user/${userId}`,
useToken: true, useToken: true,
}); });
@ -25,9 +25,9 @@ export async function getDiscounts(userId: string | null) {
throw new Error(`Ошибка получения списка скидок. ${error}`); throw new Error(`Ошибка получения списка скидок. ${error}`);
} }
} };
export function useDiscounts(userId: string | null) { export const useDiscounts = (userId: string | null) => {
const { data } = useSWR("discounts", () => getDiscounts(userId), { const { data } = useSWR("discounts", () => getDiscounts(userId), {
keepPreviousData: true, keepPreviousData: true,
onError: (error) => { onError: (error) => {
@ -38,4 +38,4 @@ export function useDiscounts(userId: string | null) {
}); });
return data; return data;
} };

@ -1,34 +1,27 @@
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
const apiUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode"; const API_URL = `${process.env.REACT_APP_DOMAIN}/codeword/promocode`;
export async function activatePromocode(promocode: string) { export const activatePromocode = async (
promocode: string
): Promise<[string | null, string?]> => {
try { try {
const response = await makeRequest< const response = await makeRequest<
| { { codeword: string } | { fastLink: string },
codeword: string; { greetings: string }
}
| {
fastLink: string;
},
{
greetings: string;
}
>({ >({
url: apiUrl + "/activate",
method: "POST", method: "POST",
url: `${API_URL}/activate`,
body: { codeword: promocode },
contentType: true, contentType: true,
body: {
codeword: promocode,
},
}); });
return response.greetings; return [response.greetings];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
throw new Error(error); return [null, error];
} }
} };

@ -1,17 +1,30 @@
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error" import { parseAxiosError } from "@root/utils/parse-error";
export async function getRecentlyPurchasedTariffs(): Promise<[any | null, string?]> { const API_URL = `${process.env.REACT_APP_DOMAIN}/customer`;
type GetRecentlyPurchasedTariffsResponse = {
id: string;
};
export const getRecentlyPurchasedTariffs = async (): Promise<
[GetRecentlyPurchasedTariffsResponse[] | null, string?]
> => {
try { try {
const recentlyPurchased = await makeRequest<never, any>({ debugger;
url: process.env.REACT_APP_DOMAIN + "/customer/recent", const recentlyPurchased = await makeRequest<
method: "get", never,
GetRecentlyPurchasedTariffsResponse[]
>({
method: "GET",
url: `${API_URL}/recent`,
useToken: true, useToken: true,
}) });
return [recentlyPurchased]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [null, `Не удалось получить историю. ${error}`] return [recentlyPurchased];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить историю. ${error}`];
} }
} };

@ -1,22 +1,32 @@
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { Tariff } from "@frontend/kitui"; import { Tariff } from "@frontend/kitui";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
import type { PrivilegeWithoutPrice, ServiceKeyToPrivilegesMap } from "@root/model/privilege"; import type {
PrivilegeWithoutPrice,
ServiceKeyToPrivilegesMap,
} from "@root/model/privilege";
import type { GetTariffsResponse } from "@root/model/tariff"; import type { GetTariffsResponse } from "@root/model/tariff";
import { removeTariffFromCart } from "@root/stores/user"; import { removeTariffFromCart } from "@root/stores/user";
const apiUrl = process.env.REACT_APP_DOMAIN + "/strator" interface CreateTariffBody {
name: string;
price?: number;
isCustom: boolean;
privileges: PrivilegeWithoutPrice[];
}
export async function getTariffs( const API_URL = `${process.env.REACT_APP_DOMAIN}/strator`;
export const getTariffs = async (
apiPage: number, apiPage: number,
tariffsPerPage: number, tariffsPerPage: number,
signal: AbortSignal | undefined signal: AbortSignal | undefined
): Promise<[GetTariffsResponse | null, string?]> { ): Promise<[GetTariffsResponse | null, string?]> => {
try { try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({ const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
url: apiUrl + `/tariff?page=${apiPage}&limit=${tariffsPerPage}`, method: "GET",
method: "get", url: `${API_URL}/tariff?page=${apiPage}&limit=${tariffsPerPage}`,
useToken: true, useToken: true,
signal, signal,
}); });
@ -27,22 +37,17 @@ export async function getTariffs(
return [null, `Не удалось получить список тарифов. ${error}`]; return [null, `Не удалось получить список тарифов. ${error}`];
} }
} };
interface CreateTariffBody { export const createTariff = async (
name: string; tariff: CreateTariffBody
price?: number; ): Promise<[Tariff | null, string?]> => {
isCustom: boolean;
privileges: PrivilegeWithoutPrice[];
}
export async function createTariff(tariff: CreateTariffBody): Promise<[Tariff | null, string?]> {
try { try {
const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({ const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({
url: `${apiUrl}/tariff`, method: "POST",
method: "post", url: `${API_URL}/tariff`,
useToken: true,
body: tariff, body: tariff,
useToken: true,
}); });
return [createTariffResponse]; return [createTariffResponse];
@ -51,13 +56,15 @@ export async function createTariff(tariff: CreateTariffBody): Promise<[Tariff |
return [null, `Не удалось создать тариф. ${error}`]; return [null, `Не удалось создать тариф. ${error}`];
} }
} };
export async function getTariffById(tariffId: string): Promise<[Tariff | null, string?, number?]> { export const getTariffById = async (
tariffId: string
): Promise<[Tariff | null, string?, number?]> => {
try { try {
const getTariffByIdResponse = await makeRequest<never, Tariff>({ const getTariffByIdResponse = await makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`, method: "GET",
method: "get", url: `${API_URL}/tariff/${tariffId}`,
useToken: true, useToken: true,
}); });
@ -67,22 +74,25 @@ export async function getTariffById(tariffId: string): Promise<[Tariff | null, s
return [null, `Не удалось получить тарифы. ${error}`, status]; return [null, `Не удалось получить тарифы. ${error}`, status];
} }
} };
export async function getCustomTariffs( export const getCustomTariffs = async (
signal: AbortSignal | undefined signal: AbortSignal | undefined
): Promise<[ServiceKeyToPrivilegesMap | null, string?]> { ): Promise<[ServiceKeyToPrivilegesMap | null, string?]> => {
try { try {
const customTariffsResponse = await makeRequest<null, ServiceKeyToPrivilegesMap>({ const customTariffsResponse = await makeRequest<
url: apiUrl + "/privilege/service", null,
ServiceKeyToPrivilegesMap
>({
method: "GET",
url: `${API_URL}/privilege/service`,
signal, signal,
method: "get",
useToken: true, useToken: true,
}); });
const tempCustomTariffsResponse = { const tempCustomTariffsResponse = {
...customTariffsResponse, ...customTariffsResponse,
squiz: customTariffsResponse.squiz squiz: customTariffsResponse.squiz,
}; };
return [tempCustomTariffsResponse]; return [tempCustomTariffsResponse];
@ -91,18 +101,20 @@ export async function getCustomTariffs(
return [null, `Не удалось получить мои тарифы. ${error}`]; return [null, `Не удалось получить мои тарифы. ${error}`];
} }
} };
export async function getTariffArray(tariffIds: string[] | undefined) { export const getTariffArray = async (tariffIds: string[] | undefined) => {
if (!tariffIds) return null; if (!tariffIds) return null;
const responses = await Promise.allSettled(tariffIds.map(tariffId => const responses = await Promise.allSettled(
tariffIds.map((tariffId) =>
makeRequest<never, Tariff>({ makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`, method: "GET",
method: "get", url: `${API_URL}/tariff/${tariffId}`,
useToken: true, useToken: true,
}) })
)); )
);
const tariffs: Tariff[] = []; const tariffs: Tariff[] = [];
@ -121,4 +133,4 @@ export async function getTariffArray(tariffIds: string[] | undefined) {
}); });
return tariffs; return tariffs;
} };

@ -1,23 +1,29 @@
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
import { createTicket as createTicketRequest } from "@frontend/kitui";
import type { CreateTicketResponse } from "@frontend/kitui";
import { SendTicketMessageRequest } from "@frontend/kitui"; import { SendTicketMessageRequest } from "@frontend/kitui";
const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym"; type SendFileResponse = {
message: string;
};
export async function sendTicketMessage( const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`;
export const sendTicketMessage = async (
ticketId: string, ticketId: string,
message: string message: string
): Promise<[null, string?]> { ): Promise<[null, string?]> => {
try { try {
const sendTicketMessageResponse = await makeRequest< const sendTicketMessageResponse = await makeRequest<
SendTicketMessageRequest, SendTicketMessageRequest,
null null
>({ >({
url: `${apiUrl}/send`,
method: "POST", method: "POST",
useToken: true, url: `${API_URL}/send`,
body: { ticket: ticketId, message: message, lang: "ru", files: [] }, body: { ticket: ticketId, message: message, lang: "ru", files: [] },
useToken: true,
}); });
return [sendTicketMessageResponse]; return [sendTicketMessageResponse];
@ -26,15 +32,15 @@ export async function sendTicketMessage(
return [null, `Не удалось отправить сообщение. ${error}`]; return [null, `Не удалось отправить сообщение. ${error}`];
} }
} };
export async function shownMessage(id: string): Promise<[null, string?]> { export const shownMessage = async (id: string): Promise<[null, string?]> => {
try { try {
const shownMessageResponse = await makeRequest<{ id: string }, null>({ const shownMessageResponse = await makeRequest<{ id: string }, null>({
url: apiUrl + "/shown",
method: "POST", method: "POST",
useToken: true, url: `${API_URL}/shown`,
body: { id }, body: { id },
useToken: true,
}); });
return [shownMessageResponse]; return [shownMessageResponse];
@ -43,4 +49,46 @@ export async function shownMessage(id: string): Promise<[null, string?]> {
return [null, `Не удалось прочесть сообщение. ${error}`]; return [null, `Не удалось прочесть сообщение. ${error}`];
} }
} };
export const sendFile = async (
ticketId: string,
file: File
): Promise<[SendFileResponse | null, string?]> => {
try {
const body = new FormData();
body.append(file.name, file);
body.append("ticket", ticketId);
const sendResponse = await makeRequest<FormData, SendFileResponse>({
method: "POST",
url: `${API_URL}/sendFiles`,
body,
});
return [sendResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить файл. ${error}`];
}
};
export const createTicket = async (
ticketNameField: string,
ticketBodyField: string
): Promise<[CreateTicketResponse | null, string?]> => {
try {
const createTicketResponse = await createTicketRequest({
url: `${API_URL}/create`,
body: { Title: ticketNameField, Message: ticketBodyField },
});
return [createTicketResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить файл. ${error}`];
}
};

@ -1,27 +1,27 @@
import { User } from "@frontend/kitui" import { User } from "@frontend/kitui";
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { PatchUserRequest } from "@root/model/user" import { PatchUserRequest } from "@root/model/user";
import { parseAxiosError } from "@root/utils/parse-error" import { parseAxiosError } from "@root/utils/parse-error";
const apiUrl = process.env.REACT_APP_DOMAIN + "/user" const API_URL = `${process.env.REACT_APP_DOMAIN}/user`;
export async function patchUser( export const patchUser = async (
user: PatchUserRequest user: PatchUserRequest
): Promise<[User | null, string?]> { ): Promise<[User | null, string?]> => {
try { try {
const patchUserResponse = await makeRequest<PatchUserRequest, User>({ const patchUserResponse = await makeRequest<PatchUserRequest, User>({
url: apiUrl+"/",
contentType: true,
method: "PATCH", method: "PATCH",
url: `${API_URL}/`,
body: user,
contentType: true,
useToken: true, useToken: true,
withCredentials: false, withCredentials: false,
body: user, });
})
return [patchUserResponse] return [patchUserResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Не удалось изменить пользователя. ${error}`] return [null, `Не удалось изменить пользователя. ${error}`];
} }
} };

@ -1,83 +1,110 @@
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { jsonToFormdata } from "@root/utils/jsonToFormdata" import { jsonToFormdata } from "@root/utils/jsonToFormdata";
import { parseAxiosError } from "@root/utils/parse-error" import { parseAxiosError } from "@root/utils/parse-error";
import type { import type {
Verification, Verification,
SendDocumentsArgs, SendDocumentsArgs,
UpdateDocumentsArgs, UpdateDocumentsArgs,
} from "@root/model/auth" } from "@root/model/auth";
import { AxiosError } from "axios"
const apiUrl = process.env.REACT_APP_DOMAIN + "/verification/v1.0.0" const API_URL = `${process.env.REACT_APP_DOMAIN}/verification/v1.0.0/verification`;
export async function verification( export const verification = async (
userId: string userId: string
): Promise<[Verification | null, string?]> { ): Promise<[Verification | null, string?]> => {
try { try {
const verificationResponse = await makeRequest<never, Verification>({ const verificationResponse = await makeRequest<never, Verification>({
url: apiUrl + "/verification/" + userId,
method: "GET", method: "GET",
url: `${API_URL}/${userId}`,
useToken: true, useToken: true,
withCredentials: true, withCredentials: true,
}) });
verificationResponse.files = verificationResponse.files.map((obj) => { verificationResponse.files = verificationResponse.files.map((obj) => {
obj.url = obj.url.replace("https://hub.pena.digital", process.env.REACT_APP_DOMAIN?.toString() || "").replace("https://shub.pena.digital", process.env.REACT_APP_DOMAIN?.toString() || "") obj.url = obj.url
return obj .replace(
}) "https://hub.pena.digital",
process.env.REACT_APP_DOMAIN?.toString() || ""
)
.replace(
"https://shub.pena.digital",
process.env.REACT_APP_DOMAIN?.toString() || ""
);
return obj;
});
return [verificationResponse] return [verificationResponse];
} catch (nativeError) { } catch (nativeError) {
const err = nativeError as AxiosError const [error, status] = parseAxiosError(nativeError);
if (err.response?.status === 404) {
return [null, `нет данных`]
}
const [error] = parseAxiosError(nativeError)
return [null, `Ошибка запроса верификации. ${error}`] if (status === 404) {
return [null, "нет данных"];
} }
}
export async function sendDocuments( return [null, `Ошибка запроса верификации. ${error}`];
}
};
export const sendDocuments = async (
documents: SendDocumentsArgs documents: SendDocumentsArgs
): Promise<[Verification | "OK" | null, string?]> { ): Promise<[Verification | "OK" | null, string?]> => {
try { try {
const sendDocumentsResponse = await makeRequest<FormData, Verification>({ const sendDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification",
method: "POST", method: "POST",
url: API_URL,
body: jsonToFormdata({ ...documents, egrule: documents.inn }),
useToken: true, useToken: true,
withCredentials: true, withCredentials: true,
body: jsonToFormdata({ ...documents, egrule: documents.inn }), });
})
return [sendDocumentsResponse] return [sendDocumentsResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Ошибка отправки документов. ${error}`] return [null, `Ошибка отправки документов. ${error}`];
} }
} };
export async function updateDocuments( export const updateDocuments = async (
documents: UpdateDocumentsArgs documents: UpdateDocumentsArgs
): Promise<[Verification | "OK" | null, string? ]> { ): Promise<[Verification | "OK" | null, string?]> => {
try { try {
const updateDocumentsResponse = await makeRequest<FormData, Verification>({ const updateDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification/file",
method: "PATCH", method: "PATCH",
useToken: true, url: `${API_URL}/file`,
withCredentials: true,
body: jsonToFormdata( body: jsonToFormdata(
documents.inn ? { ...documents, egrule: documents.inn } : documents documents.inn ? { ...documents, egrule: documents.inn } : documents
), ),
}) useToken: true,
withCredentials: true,
});
return [updateDocumentsResponse] return [updateDocumentsResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError) const [error] = parseAxiosError(nativeError);
return [null, `Ошибка обновления документов. ${error}`] return [null, `Ошибка обновления документов. ${error}`];
} }
} };
export const updateDocument = async (
body: FormData
): Promise<[Verification | "OK" | null, string?]> => {
try {
const updateDocumentResponse = await makeRequest<FormData, Verification>({
method: "PATCH",
url: API_URL,
body,
useToken: true,
withCredentials: true,
});
return [updateDocumentResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка обновления документа. ${error}`];
}
};

@ -1,4 +1,4 @@
import makeRequest from "@api/makeRequest" import makeRequest from "@api/makeRequest";
import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet"; import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet";
import { parseAxiosError } from "@root/utils/parse-error"; import { parseAxiosError } from "@root/utils/parse-error";
@ -7,14 +7,14 @@ const isStaging = (() => {
return host.includes("s") ? "s" : ""; return host.includes("s") ? "s" : "";
})(); })();
const apiUrl = process.env.REACT_APP_DOMAIN + "/customer"; const API_URL = `${process.env.REACT_APP_DOMAIN}/customer`;
interface PaymentBody { interface PaymentBody {
type: string; type: string;
amount: number; amount: number;
} }
export async function sendPayment({ export const sendPayment = async ({
userId, userId,
body, body,
fromSquiz, fromSquiz,
@ -24,18 +24,8 @@ export async function sendPayment({
body: PaymentBody; body: PaymentBody;
fromSquiz: boolean; fromSquiz: boolean;
paymentPurpose: "paycart" | "replenishwallet"; paymentPurpose: "paycart" | "replenishwallet";
}): Promise<[SendPaymentResponse | null, string?]> { }): Promise<[SendPaymentResponse | null, string?]> => {
try { const reqeustBody = {
const sendPaymentResponse = await makeRequest<
SendPaymentRequest,
SendPaymentResponse
>({
url: apiUrl + "/wallet",
contentType: true,
method: "POST",
useToken: true,
withCredentials: false,
body: {
currency: "RUB", currency: "RUB",
bankCard: { bankCard: {
number: "RUB", number: "RUB",
@ -50,7 +40,19 @@ export async function sendPayment({
fromSquiz ? "quiz" : "hub" fromSquiz ? "quiz" : "hub"
}&purpose=${paymentPurpose}&userid=${userId}`, }&purpose=${paymentPurpose}&userid=${userId}`,
...body, ...body,
}, };
try {
const sendPaymentResponse = await makeRequest<
SendPaymentRequest,
SendPaymentResponse
>({
method: "POST",
url: `${API_URL}/wallet`,
body: reqeustBody,
contentType: true,
useToken: true,
withCredentials: false,
}); });
return [sendPaymentResponse]; return [sendPaymentResponse];
@ -59,22 +61,24 @@ export async function sendPayment({
return [null, `Ошибка оплаты. ${error}`]; return [null, `Ошибка оплаты. ${error}`];
} }
} };
export const sendRSPayment = async (money: number): Promise<string | null> => { export const sendRSPayment = async (
money: number
): Promise<[string | null, string?]> => {
try { try {
await makeRequest<unknown, string>({ const sendRSPaymentResponse = await makeRequest<{ money: number }, string>({
url: apiUrl + "/wallet/rspay",
method: "POST", method: "POST",
url: `${API_URL}/wallet/rspay`,
body: { money },
useToken: true, useToken: true,
body: { money: money },
withCredentials: false, withCredentials: false,
}); });
return null; return [sendRSPaymentResponse];
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(nativeError); const [error] = parseAxiosError(nativeError);
return `Ошибка оплаты. ${error}`; return [null, `Ошибка оплаты. ${error}`];
} }
}; };

@ -10,9 +10,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import makeRequest from "@api/makeRequest"
import { import {
createTicket,
getMessageFromFetchError, getMessageFromFetchError,
throttle, throttle,
TicketMessage, TicketMessage,
@ -28,13 +26,17 @@ import {
useMemo, useMemo,
useRef, useRef,
useState, useState,
WheelEvent WheelEvent,
} from "react"; } from "react";
import ChatMessage from "../ChatMessage"; import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon"; import SendIcon from "../icons/SendIcon";
import ArrowLeft from "@root/assets/Icons/arrowLeft"; import ArrowLeft from "@root/assets/Icons/arrowLeft";
import UserCircleIcon from "./UserCircleIcon"; import UserCircleIcon from "./UserCircleIcon";
import { sendTicketMessage, shownMessage } from "@root/api/ticket"; import {
sendTicketMessage,
shownMessage,
sendFile as sendFileRequest,
} from "@root/api/ticket";
import { useSSETab } from "@root/utils/hooks/useSSETab"; import { useSSETab } from "@root/utils/hooks/useSSETab";
import { import {
ACCEPT_SEND_MEDIA_TYPES_MAP, ACCEPT_SEND_MEDIA_TYPES_MAP,
@ -56,6 +58,7 @@ import AttachFileIcon from "@mui/icons-material/AttachFile";
import ChatDocument from "./ChatDocument"; import ChatDocument from "./ChatDocument";
import ChatImage from "./ChatImage"; import ChatImage from "./ChatImage";
import ChatVideo from "./ChatVideo"; import ChatVideo from "./ChatVideo";
import { createTicket } from "@api/ticket";
type ModalWarningType = type ModalWarningType =
| "errorType" | "errorType"
@ -76,7 +79,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(800)); const isMobile = useMediaQuery(theme.breakpoints.down(800));
const [messageField, setMessageField] = useState<string>(""); const [messageField, setMessageField] = useState<string>("");
const [disableFileButton, setDisableFileButton] = useState(false); const [disableFileButton, setDisableFileButton] = useState<boolean>(false);
const [sseEnabled, setSseEnabled] = useState(true); const [sseEnabled, setSseEnabled] = useState(true);
const [modalWarningType, setModalWarningType] = const [modalWarningType, setModalWarningType] =
useState<ModalWarningType>(null); useState<ModalWarningType>(null);
@ -115,7 +118,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
? offHoursMessage ? offHoursMessage
: workingHoursMessage; : workingHoursMessage;
return ({ return {
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
files: [], files: [],
id: "111", id: "111",
@ -125,8 +128,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
shown: { me: 1 }, shown: { me: 1 },
ticket_id: "111", ticket_id: "111",
user_id: "greetingMessage", user_id: "greetingMessage",
}); };
}, [open]); }, [open]);
useTicketMessages({ useTicketMessages({
@ -182,7 +184,8 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
onSuccess: (result) => { onSuccess: (result) => {
if (result.data?.length) { if (result.data?.length) {
const currentTicket = result.data.find( const currentTicket = result.data.find(
({ origin, state }) => !origin.includes("/support") && state !== "close" ({ origin, state }) =>
!origin.includes("/support") && state !== "close"
); );
if (!currentTicket) { if (!currentTicket) {
@ -199,7 +202,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
const message = getMessageFromFetchError(error); const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message); if (message) enqueueSnackbar(message);
}, },
onFetchStateChange: () => { }, onFetchStateChange: () => {},
enabled: Boolean(user), enabled: Boolean(user),
}); });
@ -228,7 +231,6 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
scrollToBottom(); scrollToBottom();
}, [open]); }, [open]);
useEffect( useEffect(
function scrollOnNewMessage() { function scrollOnNewMessage() {
if (!chatBoxRef.current) return; if (!chatBoxRef.current) return;
@ -265,29 +267,24 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
if (!sessionData?.ticketId) { if (!sessionData?.ticketId) {
setIsMessageSending(true); setIsMessageSending(true);
createTicket({
url: process.env.REACT_APP_DOMAIN + "/heruvym/create", const [createTicketresult, createTicketerror] = await createTicket(
body: { "Unauth title",
Title: "Unauth title", messageField
Message: messageField, );
},
useToken: Boolean(user), if (createTicketerror) {
}) enqueueSnackbar(createTicketerror);
.then((response) => { } else if (createTicketresult) {
setTicketData({ setTicketData({
ticketId: response.Ticket, ticketId: createTicketresult.Ticket,
sessionId: response.sess, sessionId: createTicketresult.sess,
}); });
setSseEnabled(true); setSseEnabled(true);
}) }
.catch((error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
})
.finally(() => {
setMessageField(""); setMessageField("");
setIsMessageSending(false); setIsMessageSending(false);
});
} else { } else {
setIsMessageSending(true); setIsMessageSending(true);
@ -331,18 +328,18 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
let data; let data;
if (!ticket.sessionData?.ticketId) { if (!ticket.sessionData?.ticketId) {
try { try {
data = await createTicket({ const [createTicketresult] = await createTicket("Unauth title", "");
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
body: { if (createTicketresult) {
Title: "Unauth title", data = createTicketresult;
Message: "", }
},
useToken: Boolean(user), if (data) {
});
setTicketData({ setTicketData({
ticketId: data.Ticket, ticketId: data.Ticket,
sessionId: data.sess, sessionId: data.sess,
}); });
}
} catch (error: any) { } catch (error: any) {
const errorMessage = getMessageFromFetchError(error); const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage); if (errorMessage) enqueueSnackbar(errorMessage);
@ -353,20 +350,13 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
const ticketId = ticket.sessionData?.ticketId || data?.Ticket; const ticketId = ticket.sessionData?.ticketId || data?.Ticket;
if (ticketId !== undefined) { if (ticketId !== undefined) {
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize"); if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
try {
const body = new FormData();
body.append(file.name, file); const [, sendFileError] = await sendFileRequest(ticketId, file);
body.append("ticket", ticketId);
await makeRequest({ if (sendFileError) {
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles", enqueueSnackbar(sendFileError);
body: body,
method: "POST",
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
} }
return true; return true;
} }
}; };
@ -377,6 +367,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
enqueueSnackbar(check); enqueueSnackbar(check);
return; return;
} }
setDisableFileButton(true); setDisableFileButton(true);
await sendFile(file); await sendFile(file);
setDisableFileButton(false); setDisableFileButton(false);
@ -547,8 +538,13 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
); );
})} })}
{!ticket.sessionData?.ticketId && ( {!ticket.sessionData?.ticketId && (
<ChatMessage unAuthenticated text={getGreetingMessage.message} createdAt={getGreetingMessage.created_at} isSelf={false} />) <ChatMessage
} unAuthenticated
text={getGreetingMessage.message}
createdAt={getGreetingMessage.created_at}
isSelf={false}
/>
)}
</Box> </Box>
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}> <FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
<InputBase <InputBase

@ -98,7 +98,7 @@ export default function ChatDocument({
</svg> </svg>
<Link <Link
download="" download=""
href={`https://storage.yandexcloud.net/pair/${file}`} href={`https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/angesight/${file}`}
style={{ style={{
color: "#7E2AEA", color: "#7E2AEA",
display: "flex", display: "flex",

@ -110,7 +110,7 @@ export default function ChatImage({
height: "217px", height: "217px",
width: "217px", width: "217px",
}} }}
src={`https://storage.yandexcloud.net/pair/${file}`} src={`https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/angesight/${file}`}
/> />
</ButtonBase> </ButtonBase>
</Box> </Box>

@ -113,7 +113,9 @@ export default function ChatImage({
}} }}
controls controls
> >
<source src={`https://storage.yandexcloud.net/pair/${file}`} /> <source
src={`https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/angesight/${file}`}
/>
</Box> </Box>
</Box> </Box>
</Box> </Box>

@ -1,77 +1,79 @@
import axios from "axios" import axios from "axios";
import { Box, IconButton, SxProps, Theme, Typography, useTheme } from "@mui/material" import {
import { Document, Page } from "react-pdf" Box,
import { Buffer } from "buffer" IconButton,
import { downloadFileToDevice } from "@root/utils/downloadFileToDevice" SxProps,
import EditIcon from "@mui/icons-material/Edit" Theme,
import { ChangeEvent, useRef } from "react" Typography,
import { SendDocumentsArgs, Verification } from "@root/model/auth" useTheme,
import makeRequest from "@api/makeRequest" } from "@mui/material";
import { jsonToFormdata } from "@utils/jsonToFormdata" import { Document, Page } from "react-pdf";
import { parseAxiosError } from "@utils/parse-error" import { Buffer } from "buffer";
import { readFile } from "@root/utils/readFile" import { downloadFileToDevice } from "@root/utils/downloadFileToDevice";
import { enqueueSnackbar } from "notistack" import EditIcon from "@mui/icons-material/Edit";
import { ChangeEvent, useRef } from "react";
import { SendDocumentsArgs, Verification } from "@root/model/auth";
import { updateDocument } from "@api/verification";
import { jsonToFormdata } from "@utils/jsonToFormdata";
import { parseAxiosError } from "@utils/parse-error";
import { readFile } from "@root/utils/readFile";
import { enqueueSnackbar } from "notistack";
type KeyNames = type KeyNames = "inn" | "rule" | "certificate";
"inn" |
"rule" |
"certificate"
interface Props { interface Props {
text: string; text: string;
documentUrl: string; documentUrl: string;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
keyName: KeyNames keyName: KeyNames;
} }
export default function DocumentItem({
text,
documentUrl = "",
sx,
keyName,
}: Props) {
const theme = useTheme();
export default function DocumentItem({ text, documentUrl = "", sx, keyName }: Props) { const fileInputRef = useRef<HTMLInputElement>(null);
const theme = useTheme()
const fileInputRef = useRef<HTMLInputElement>(null)
function handleChooseFileClick() { function handleChooseFileClick() {
fileInputRef.current?.click() fileInputRef.current?.click();
} }
const downloadFile = async () => { const downloadFile = async () => {
const { data } = await axios.get<ArrayBuffer>(documentUrl, { const { data } = await axios.get<ArrayBuffer>(documentUrl, {
responseType: "arraybuffer", responseType: "arraybuffer",
}) });
if (!data) { if (!data) {
return return;
} }
downloadFileToDevice( downloadFileToDevice(
`${documentUrl.split("/").pop()?.split(".")?.[0] || "document"}.pdf`, `${documentUrl.split("/").pop()?.split(".")?.[0] || "document"}.pdf`,
Buffer.from(data) Buffer.from(data)
) );
return return;
} };
async function sendDocument( async function sendDocument(e: ChangeEvent<HTMLInputElement>) {
e: ChangeEvent<HTMLInputElement>
) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const file = target?.files?.[0] || null; const file = target?.files?.[0] || null;
if (file !== null) { if (file !== null) {
const readedFile = await readFile(file, "binary") const readedFile = await readFile(file, "binary");
try { const [, updateDocumentError] = await updateDocument(
await makeRequest<FormData, Verification>({ jsonToFormdata({ [keyName]: readedFile })
url: `${process.env.REACT_APP_DOMAIN}/verification/v1.0.0/verification`, );
method: "PATCH",
useToken: true,
withCredentials: true,
body: jsonToFormdata({ [keyName]: readedFile }),
})
enqueueSnackbar("Данные обновлены") if (updateDocumentError) {
} catch (nativeError) { return enqueueSnackbar(
const [error] = parseAxiosError(nativeError) `Ошибка отправки документов. ${updateDocumentError}`
);
enqueueSnackbar(`Ошибка отправки документов. ${error}`)
} }
enqueueSnackbar("Данные обновлены");
} }
} }
@ -103,9 +105,7 @@ export default function DocumentItem({ text, documentUrl = "", sx, keyName }: Pr
> >
{documentUrl.split("/").pop()?.split(".")?.[0]} {documentUrl.split("/").pop()?.split(".")?.[0]}
</Typography> </Typography>
<IconButton <IconButton onClick={handleChooseFileClick}>
onClick={handleChooseFileClick}
>
<EditIcon sx={{ color: theme.palette.purple.main }} /> <EditIcon sx={{ color: theme.palette.purple.main }} />
</IconButton> </IconButton>
<input <input
@ -120,7 +120,6 @@ export default function DocumentItem({ text, documentUrl = "", sx, keyName }: Pr
</Box> </Box>
<Document file={documentUrl}> <Document file={documentUrl}>
<Page <Page
pageNumber={1} pageNumber={1}
width={200} width={200}
@ -131,5 +130,5 @@ export default function DocumentItem({ text, documentUrl = "", sx, keyName }: Pr
</> </>
)} )}
</Box> </Box>
) );
} }

@ -3,19 +3,17 @@ import {
IconButton, IconButton,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme useTheme,
} from "@mui/material" } from "@mui/material";
import CustomAccordion from "@components/CustomAccordion" import CustomAccordion from "@components/CustomAccordion";
import File from "@components/icons/File" import File from "@components/icons/File";
import {getDeclension} from "@utils/declension" import { getDeclension } from "@utils/declension";
import {enqueueSnackbar} from "notistack" import { enqueueSnackbar } from "notistack";
import {addTariffToCart, useUserStore} from "@root/stores/user" import { addTariffToCart, useUserStore } from "@root/stores/user";
import ForwardToInboxOutlinedIcon import ForwardToInboxOutlinedIcon from "@mui/icons-material/ForwardToInboxOutlined";
from "@mui/icons-material/ForwardToInboxOutlined"; import { KeyValue, RawDetails, sendReport } from "@api/history";
import {makeRequest} from "@frontend/kitui"; import { useNavigate } from "react-router-dom";
import {KeyValue, RawDetails} from "@api/history"; import { VerificationStatus } from "@root/model/account";
import {useNavigate} from "react-router-dom"
import {VerificationStatus} from "@root/model/account"
export type History = { export type History = {
title: string; title: string;
@ -31,61 +29,65 @@ interface AccordionWrapperProps {
last?: boolean; last?: boolean;
first?: boolean; first?: boolean;
createdAt: string; createdAt: string;
onClickMail?: any onClickMail?: any;
mainId: string mainId: string;
} }
export default function AccordionWrapper({ content, last, first, createdAt, onClickMail, mainId }: AccordionWrapperProps) { export default function AccordionWrapper({
const theme = useTheme() content,
const upMd = useMediaQuery(theme.breakpoints.up("md")) last,
const upSm = useMediaQuery(theme.breakpoints.up("sm")) first,
const isTablet = useMediaQuery(theme.breakpoints.down(900)) createdAt,
const isMobile = useMediaQuery(theme.breakpoints.down(560)) onClickMail,
mainId,
}: AccordionWrapperProps) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isMobile = useMediaQuery(theme.breakpoints.down(560));
const navigate = useNavigate(); const navigate = useNavigate();
const verificationStatus = useUserStore((state) => state.verificationStatus) const verificationStatus = useUserStore((state) => state.verificationStatus);
const OrgName = useUserStore((state) => state.userAccount?.name.orgname) const OrgName = useUserStore((state) => state.userAccount?.name.orgname);
const valuesByKey: any = {} const valuesByKey: any = {};
if (Array.isArray(content[0].Value) && Array.isArray(content[0].Value[0])) { if (Array.isArray(content[0].Value) && Array.isArray(content[0].Value[0])) {
(content[0].Value[0] as KeyValue[]).forEach((item: KeyValue) => { (content[0].Value[0] as KeyValue[]).forEach((item: KeyValue) => {
valuesByKey[item.Key] = item.Value; valuesByKey[item.Key] = item.Value;
}); });
} }
const extractDateFromString = (tariffName: string) => { const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/) const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/);
return dateMatch ? dateMatch[0] : null return dateMatch ? dateMatch[0] : null;
} };
async function handleTariffItemClick(tariffId: string) { async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId) const { patchCartError } = await addTariffToCart(tariffId);
if (patchCartError) { if (patchCartError) {
enqueueSnackbar(patchCartError) enqueueSnackbar(patchCartError);
} else { } else {
enqueueSnackbar("Тариф добавлен в корзину") enqueueSnackbar("Тариф добавлен в корзину");
} }
} }
async function sendBillByEmail(logId: string) { async function sendBillByEmail(logId: string) {
if (verificationStatus === VerificationStatus.VERIFICATED && OrgName) {
const [, sendReportError] = await sendReport(logId);
if (!sendReportError) {
return enqueueSnackbar(
"Акт будет отправлен на почту, указанную при регистрации"
);
}
if(verificationStatus === VerificationStatus.VERIFICATED && OrgName){
try {
await makeRequest({
url: process.env.REACT_APP_DOMAIN + `/customer/sendReport`,
body: {id: logId},
method: "POST",
});
return enqueueSnackbar("Акт будет отправлен на почту, указанную при регистрации");
} catch (e) {
enqueueSnackbar("Извините, произошла ошибка"); enqueueSnackbar("Извините, произошла ошибка");
} }
} navigate("/settings");
navigate("/settings") if (verificationStatus !== VerificationStatus.VERIFICATED && !OrgName) {
if(verificationStatus !== VerificationStatus.VERIFICATED && !OrgName){
enqueueSnackbar("Пройдите верификацию и заполните название организации"); enqueueSnackbar("Пройдите верификацию и заполните название организации");
} else if(!OrgName){ } else if (!OrgName) {
enqueueSnackbar("Заполните поле название организации"); enqueueSnackbar("Заполните поле название организации");
}else if(verificationStatus !== VerificationStatus.VERIFICATED) { } else if (verificationStatus !== VerificationStatus.VERIFICATED) {
enqueueSnackbar("Пройдите верификацию"); enqueueSnackbar("Пройдите верификацию");
} }
} }
@ -100,13 +102,12 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
last={last} last={last}
first={first} first={first}
divide divide
text={valuesByKey.privileges.map((e:KeyValue[]) => ( text={valuesByKey.privileges.map((e: KeyValue[]) => (
<Typography <Typography key={valuesByKey.id}>
key={valuesByKey.id} {e[1].Value} - {e[5].Value}{" "}
> {getDeclension(Number(e[5].Value), e[7].Value.toString())}
{e[1].Value} - {e[5].Value} {getDeclension(Number(e[5].Value), e[7].Value.toString())} </Typography>
</Typography>) ))}
)}
header={ header={
<> <>
<Box <Box
@ -138,7 +139,9 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
fontSize: upMd ? "20px" : "18px", fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px", lineHeight: upMd ? undefined : "19px",
fontWeight: 500, fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.text.secondary, color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.text.secondary,
px: 0, px: 0,
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
@ -152,12 +155,14 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
fontSize: upMd ? "18px" : "16px", fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px", lineHeight: upMd ? undefined : "19px",
fontWeight: 500, fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark, color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.gray.dark,
px: 0, px: 0,
width: "200px", width: "200px",
maxWidth: "200px", maxWidth: "200px",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis" textOverflow: "ellipsis",
}} }}
> >
{valuesByKey.iscustom ? "Мой тариф" : valuesByKey.name} {valuesByKey.iscustom ? "Мой тариф" : valuesByKey.name}
@ -179,19 +184,25 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
fontSize: upMd ? "18px" : "16px", fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px", lineHeight: upMd ? undefined : "19px",
fontWeight: 400, fontWeight: 400,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark, color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.gray.dark,
px: 0, px: 0,
}} }}
title={`>Способ оплаты: ${valuesByKey.payMethod}</Typography>}`} title={`>Способ оплаты: ${valuesByKey.payMethod}</Typography>}`}
> >
{valuesByKey.payMethod && <Typography {valuesByKey.payMethod && (
<Typography
sx={{ sx={{
maxWidth: "300px", maxWidth: "300px",
width: "300px", width: "300px",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis" textOverflow: "ellipsis",
}} }}
>Способ оплаты: {valuesByKey.payMethod}</Typography>} >
Способ оплаты: {valuesByKey.payMethod}
</Typography>
)}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@ -205,18 +216,27 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
> >
<Typography <Typography
sx={{ sx={{
marginLeft: isTablet ? (isMobile ? null : "auto") : null, marginLeft: isTablet
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark, ? isMobile
? null
: "auto"
: null,
color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px", fontSize: upSm ? "20px" : "16px",
fontWeight: 500, fontWeight: 500,
textAlign: "left", textAlign: "left",
}} }}
> >
{Number(content[1].Value) / 100 ? Number(content[1].Value) / 100 : "nodata"} руб. {Number(content[1].Value) / 100
? Number(content[1].Value) / 100
: "nodata"}{" "}
руб.
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
{!isMobile && {!isMobile && (
<> <>
<IconButton <IconButton
onClick={(e) => { onClick={(e) => {
@ -235,40 +255,42 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
"&:active": { "&:active": {
bgcolor: "black", bgcolor: "black",
color: "white", color: "white",
} },
}} }}
> >
<ForwardToInboxOutlinedIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/> <ForwardToInboxOutlinedIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton> </IconButton>
<IconButton <IconButton
title="Добавить в корзину тариф" title="Добавить в корзину тариф"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
handleTariffItemClick(valuesByKey.id) handleTariffItemClick(valuesByKey.id);
}} }}
sx={{ sx={{
ml: "20px", ml: "20px",
bgcolor:"#EEE4FC", bgcolor: "#EEE4FC",
stroke: "#7E2AEA", stroke: "#7E2AEA",
borderRadius: 2, borderRadius: 2,
"&:hover": { "&:hover": {
bgcolor:"#7E2AEA", bgcolor: "#7E2AEA",
stroke: "white", stroke: "white",
}, },
"&:active": { "&:active": {
bgcolor:"black", bgcolor: "black",
stroke: "white", stroke: "white",
} },
}} }}
> >
<File></File> <File></File>
</IconButton> </IconButton>
</> </>
)}
}
</Box> </Box>
</Box> </Box>
{isMobile && {isMobile && (
<> <>
<IconButton <IconButton
onClick={(e) => { onClick={(e) => {
@ -287,40 +309,42 @@ export default function AccordionWrapper({ content, last, first, createdAt, onCl
"&:active": { "&:active": {
bgcolor: "black", bgcolor: "black",
color: "white", color: "white",
} },
}} }}
> >
<ForwardToInboxOutlinedIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/> <ForwardToInboxOutlinedIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton> </IconButton>
<IconButton <IconButton
title="Добавить в корзину тариф" title="Добавить в корзину тариф"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
handleTariffItemClick(valuesByKey.id) handleTariffItemClick(valuesByKey.id);
}} }}
sx={{ sx={{
mr: "10px", mr: "10px",
bgcolor:"#EEE4FC", bgcolor: "#EEE4FC",
stroke: "#7E2AEA", stroke: "#7E2AEA",
borderRadius: 2, borderRadius: 2,
"&:hover": { "&:hover": {
bgcolor:"#7E2AEA", bgcolor: "#7E2AEA",
stroke: "white", stroke: "white",
}, },
"&:active": { "&:active": {
bgcolor:"black", bgcolor: "black",
stroke: "white", stroke: "white",
} },
}} }}
> >
<File></File> <File></File>
</IconButton> </IconButton>
</> </>
)}
}
</> </>
} }
/> />
</Box> </Box>
) );
} }

@ -3,18 +3,19 @@ import {
IconButton, IconButton,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import CustomAccordion from "@components/CustomAccordion"; import CustomAccordion from "@components/CustomAccordion";
import File from "@components/icons/File"; import File from "@components/icons/File";
import {getDeclension} from "@utils/declension"; import { getDeclension } from "@utils/declension";
import {enqueueSnackbar} from "notistack"; import { enqueueSnackbar } from "notistack";
import {addTariffToCart, useUserStore} from "@root/stores/user" import { addTariffToCart, useUserStore } from "@root/stores/user";
import {makeRequest, Tariff} from "@frontend/kitui"; import { makeRequest, Tariff } from "@frontend/kitui";
import {currencyFormatter} from "@root/utils/currencyFormatter"; import { currencyFormatter } from "@root/utils/currencyFormatter";
import ForwardToInboxIcon from '@mui/icons-material/ForwardToInbox'; import ForwardToInboxIcon from "@mui/icons-material/ForwardToInbox";
import {VerificationStatus} from "@root/model/account" import { VerificationStatus } from "@root/model/account";
import {useNavigate} from "react-router-dom" import { useNavigate } from "react-router-dom";
import { sendReport } from "@api/history";
export type History = { export type History = {
title: string; title: string;
@ -31,18 +32,25 @@ interface AccordionWrapperProps {
last?: boolean; last?: boolean;
first?: boolean; first?: boolean;
createdAt: string; createdAt: string;
mainId: string mainId: string;
} }
export default function AccordionWrapper2({ tariff, price, last, first, createdAt, mainId }: AccordionWrapperProps) { export default function AccordionWrapper2({
tariff,
price,
last,
first,
createdAt,
mainId,
}: AccordionWrapperProps) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm")); const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const isTablet = useMediaQuery(theme.breakpoints.down(900)); const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isMobile = useMediaQuery(theme.breakpoints.down(560)); const isMobile = useMediaQuery(theme.breakpoints.down(560));
const navigate = useNavigate(); const navigate = useNavigate();
const verificationStatus = useUserStore((state) => state.verificationStatus) const verificationStatus = useUserStore((state) => state.verificationStatus);
const OrgName = useUserStore((state) => state.userAccount?.name.orgname) const OrgName = useUserStore((state) => state.userAccount?.name.orgname);
async function handleTariffItemClick(tariffId: string) { async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId); const { patchCartError } = await addTariffToCart(tariffId);
if (patchCartError) { if (patchCartError) {
@ -53,24 +61,23 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
} }
async function sendBillByEmail(logId: string) { async function sendBillByEmail(logId: string) {
if(verificationStatus === VerificationStatus.VERIFICATED && OrgName){ if (verificationStatus === VerificationStatus.VERIFICATED && OrgName) {
try { const [, sendReportError] = await sendReport(logId);
await makeRequest({
url: process.env.REACT_APP_DOMAIN + `/customer/sendReport`, if (!sendReportError) {
body: {id: logId}, return enqueueSnackbar(
method: "POST", "Акт будет отправлен на почту, указанную при регистрации"
}); );
return enqueueSnackbar("Акт будет отправлен на почту, указанную при регистрации"); }
} catch (e) {
enqueueSnackbar("Извините, произошла ошибка"); enqueueSnackbar("Извините, произошла ошибка");
} }
} navigate("/settings");
navigate("/settings") if (verificationStatus !== VerificationStatus.VERIFICATED && !OrgName) {
if(verificationStatus !== VerificationStatus.VERIFICATED && !OrgName){
enqueueSnackbar("Пройдите верификацию и заполните название организации"); enqueueSnackbar("Пройдите верификацию и заполните название организации");
} else if(!OrgName){ } else if (!OrgName) {
enqueueSnackbar("Заполните поле название организации"); enqueueSnackbar("Заполните поле название организации");
}else if(verificationStatus !== VerificationStatus.VERIFICATED) { } else if (verificationStatus !== VerificationStatus.VERIFICATED) {
enqueueSnackbar("Пройдите верификацию"); enqueueSnackbar("Пройдите верификацию");
} }
} }
@ -85,9 +92,13 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
last={last} last={last}
first={first} first={first}
divide divide
text={tariff.privileges.map(privilege => ( text={tariff.privileges.map(
`${privilege.description} - ${privilege.amount} ${getDeclension(Number(privilege.serviceKey), privilege.value)} ` (privilege) =>
))} `${privilege.description} - ${privilege.amount} ${getDeclension(
Number(privilege.serviceKey),
privilege.value
)} `
)}
header={ header={
<> <>
<Box <Box
@ -119,7 +130,9 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
fontSize: upMd ? "20px" : "18px", fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px", lineHeight: upMd ? undefined : "19px",
fontWeight: 500, fontWeight: 500,
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.text.secondary, color: /* valuesByKey.expired */ false
? theme.palette.text.disabled
: theme.palette.text.secondary,
px: 0, px: 0,
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
@ -133,9 +146,11 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
fontSize: upMd ? "18px" : "16px", fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px", lineHeight: upMd ? undefined : "19px",
fontWeight: 500, fontWeight: 500,
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.gray.dark, color: /* valuesByKey.expired */ false
? theme.palette.text.disabled
: theme.palette.gray.dark,
px: 0, px: 0,
width: upMd? "200px" : "auto", width: upMd ? "200px" : "auto",
maxWidth: "200px", maxWidth: "200px",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
@ -185,8 +200,14 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
> >
<Typography <Typography
sx={{ sx={{
marginLeft: isTablet ? (isMobile ? null : "auto") : null, marginLeft: isTablet
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.gray.dark, ? isMobile
? null
: "auto"
: null,
color: /* valuesByKey.expired */ false
? theme.palette.text.disabled
: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px", fontSize: upSm ? "20px" : "16px",
fontWeight: 500, fontWeight: 500,
textAlign: "left", textAlign: "left",
@ -196,7 +217,7 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
{!isMobile && {!isMobile && (
<> <>
<IconButton <IconButton
onClick={(e) => { onClick={(e) => {
@ -215,10 +236,13 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
"&:active": { "&:active": {
bgcolor: "black", bgcolor: "black",
color: "white", color: "white",
} },
}} }}
> >
<ForwardToInboxIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/> <ForwardToInboxIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton> </IconButton>
<IconButton <IconButton
title="Добавить в корзину тариф" title="Добавить в корзину тариф"
@ -238,17 +262,16 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
"&:active": { "&:active": {
bgcolor: "black", bgcolor: "black",
stroke: "white", stroke: "white",
} },
}} }}
> >
<File></File> <File></File>
</IconButton> </IconButton>
</> </>
)}
}
</Box> </Box>
</Box> </Box>
{isMobile && {isMobile && (
<> <>
<IconButton <IconButton
onClick={(e) => { onClick={(e) => {
@ -267,10 +290,13 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
"&:active": { "&:active": {
bgcolor: "black", bgcolor: "black",
color: "white", color: "white",
} },
}} }}
> >
<ForwardToInboxIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/> <ForwardToInboxIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton> </IconButton>
<IconButton <IconButton
title="Добавить в корзину тариф" title="Добавить в корзину тариф"
@ -290,14 +316,13 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
"&:active": { "&:active": {
bgcolor: "black", bgcolor: "black",
stroke: "white", stroke: "white",
} },
}} }}
> >
<File></File> <File></File>
</IconButton> </IconButton>
</> </>
)}
}
</> </>
} }
/> />

@ -1,25 +1,28 @@
import {useState} from "react"; import { useState } from "react";
import { import {
Box, Box,
IconButton, IconButton,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import SectionWrapper from "@root/components/SectionWrapper"; import SectionWrapper from "@root/components/SectionWrapper";
import {Select} from "@root/components/Select"; import { Select } from "@root/components/Select";
import {Tabs} from "@root/components/Tabs"; import { Tabs } from "@root/components/Tabs";
import AccordionWrapper from "./AccordionWrapper"; import AccordionWrapper from "./AccordionWrapper";
import {useHistoryTracker} from "@root/utils/hooks/useHistoryTracker"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import {ErrorBoundary} from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import {handleComponentError} from "@root/utils/handleComponentError"; import { handleComponentError } from "@root/utils/handleComponentError";
import {useHistoryStore} from "@root/stores/history"; import { useHistoryStore } from "@root/stores/history";
import {enqueueSnackbar} from "notistack"; import { enqueueSnackbar } from "notistack";
import {makeRequest} from "@frontend/kitui"; import {
import {HistoryRecord, HistoryRecord2} from "@root/api/history"; HistoryRecord,
HistoryRecord2,
sendReportById,
} from "@root/api/history";
import AccordionWrapper2 from "./AccordionWrapper2"; import AccordionWrapper2 from "./AccordionWrapper2";
const subPages = ["Платежи"]; const subPages = ["Платежи"];
@ -32,7 +35,7 @@ export default function History() {
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const historyData = useHistoryStore(state => state.history); const historyData = useHistoryStore((state) => state.history);
const handleCustomBackNavigation = useHistoryTracker(); const handleCustomBackNavigation = useHistoryTracker();
const extractDateFromString = (tariffName: string) => { const extractDateFromString = (tariffName: string) => {
@ -41,18 +44,15 @@ export default function History() {
}; };
async function handleHistoryResponse(tariffId: string) { async function handleHistoryResponse(tariffId: string) {
try { const [, sendReportError] = await sendReportById(tariffId);
await makeRequest(
{ if (sendReportError) {
url: process.env.REACT_APP_DOMAIN + `/customer/sendReport/${tariffId}`, return enqueueSnackbar(sendReportError);
method: "POST",
} }
);
enqueueSnackbar("Запрос отправлен"); enqueueSnackbar("Запрос отправлен");
} catch (e) {
enqueueSnackbar("извините, произошла ошибка");
}
} }
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
@ -72,7 +72,10 @@ export default function History() {
}} }}
> >
{isMobile && ( {isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}> <IconButton
onClick={handleCustomBackNavigation}
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
>
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
)} )}
@ -86,27 +89,37 @@ export default function History() {
</Typography> </Typography>
</Box> </Box>
{isMobile ? ( {isMobile ? (
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} /> <Select
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
) : ( ) : (
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} /> <Tabs
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
)} )}
<ErrorBoundary <ErrorBoundary
fallback={ fallback={<Typography mt="8px">Ошибка загрузки истории</Typography>}
<Typography mt="8px">Ошибка загрузки истории</Typography>
}
onError={handleComponentError} onError={handleComponentError}
> >
{historyData?.length === 0 && <Typography textAlign="center">Нет данных</Typography>} {historyData?.length === 0 && (
<Typography textAlign="center">Нет данных</Typography>
)}
{/* Для ненормального rawDetails */} {/* Для ненормального rawDetails */}
{historyData?.filter((e): e is HistoryRecord => { {historyData
?.filter((e): e is HistoryRecord => {
e.createdAt = extractDateFromString(e.createdAt); e.createdAt = extractDateFromString(e.createdAt);
return ( return (
!e.isDeleted !e.isDeleted &&
&& e.key === "payCart" e.key === "payCart" &&
&& Array.isArray(e.rawDetails) Array.isArray(e.rawDetails) &&
&& Array.isArray(e.rawDetails[0].Value) Array.isArray(e.rawDetails[0].Value)
); );
}).map((e, index) => { })
.map((e, index) => {
return ( return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}> <Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper <AccordionWrapper
@ -125,15 +138,17 @@ export default function History() {
); );
})} })}
{/* Для нормального rawDetails */} {/* Для нормального rawDetails */}
{historyData?.filter((e): e is HistoryRecord2 => { {historyData
?.filter((e): e is HistoryRecord2 => {
e.createdAt = extractDateFromString(e.createdAt); e.createdAt = extractDateFromString(e.createdAt);
return ( return (
!e.isDeleted !e.isDeleted &&
&& e.key === "payCart" e.key === "payCart" &&
&& !Array.isArray(e.rawDetails) !Array.isArray(e.rawDetails) &&
&& !!e.rawDetails.tariffs[0] !!e.rawDetails.tariffs[0]
); );
}).map((e, index) => { })
.map((e, index) => {
return ( return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}> <Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper2 <AccordionWrapper2
@ -143,7 +158,7 @@ export default function History() {
mainId={(e as HistoryRecord2).id} mainId={(e as HistoryRecord2).id}
createdAt={e.createdAt} createdAt={e.createdAt}
tariff={e.rawDetails.tariffs[0]} tariff={e.rawDetails.tariffs[0]}
price={e.rawDetails.price/100} price={e.rawDetails.price / 100}
/> />
</Box> </Box>
); );

@ -139,10 +139,12 @@ export default function Payment() {
return; return;
} }
const sendRSPaymentError = await sendRSPayment(Number(paymentValueField)); const [sendRSPaymentResponse] = await sendRSPayment(
Number(paymentValueField)
);
if (sendRSPaymentError) { if (sendRSPaymentResponse) {
return enqueueSnackbar(sendRSPaymentError); return enqueueSnackbar(sendRSPaymentResponse);
} }
enqueueSnackbar( enqueueSnackbar(
@ -153,18 +155,17 @@ export default function Payment() {
} }
} }
function handleApplyPromocode() { async function handleApplyPromocode() {
if (!promocodeField) return; if (!promocodeField) return;
activatePromocode(promocodeField) const [greeting, activateError] = await activatePromocode(promocodeField);
.then((response) => {
enqueueSnackbar(response); if (activateError) {
return enqueueSnackbar(activateError);
}
enqueueSnackbar(greeting);
mutate("discounts"); mutate("discounts");
})
.catch((error) => {
if (error.message !== "" && typeof error.message === "string")
enqueueSnackbar(error.message);
});
} }
return ( return (

@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import axios, { AxiosResponse } from "axios";
import { ApologyPage } from "../ApologyPage"; import { ApologyPage } from "../ApologyPage";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
@ -22,19 +21,6 @@ import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets"; import { clearTickets } from "@root/stores/tickets";
import {setNotEnoughMoneyAmount} from "@stores/cart" import {setNotEnoughMoneyAmount} from "@stores/cart"
function refresh(token: string) {
return axios<never, AxiosResponse<{ accessToken: string }>>(
process.env.REACT_APP_DOMAIN + "/auth/refresh",
{
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
},
method: "POST",
}
);
}
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const action = params.get("action"); const action = params.get("action");
const dif = params.get("dif"); const dif = params.get("dif");

@ -12,7 +12,7 @@ export default function ChatImageNewWindow() {
maxHeight: "100vh", maxHeight: "100vh",
maxWidth: "100vw", maxWidth: "100vw",
}} }}
src={`https://storage.yandexcloud.net/pair/${srcImage}`} src={`https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/angesight/${srcImage}`}
/> />
</> </>
); );

@ -1,32 +1,38 @@
import { Box, Typography, FormControl, InputBase, useMediaQuery, useTheme, Button } from "@mui/material" import {
import { useState } from "react" Box,
import { useNavigate } from "react-router-dom" Typography,
import { enqueueSnackbar } from "notistack" FormControl,
import { cardShadow } from "@root/utils/theme" InputBase,
import { createTicket } from "@frontend/kitui" useMediaQuery,
useTheme,
Button,
} from "@mui/material";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import { cardShadow } from "@root/utils/theme";
import { createTicket } from "@api/ticket";
export default function CreateTicket() { export default function CreateTicket() {
const theme = useTheme() const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")) const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate() const navigate = useNavigate();
const [ticketNameField, setTicketNameField] = useState<string>("") const [ticketNameField, setTicketNameField] = useState<string>("");
const [ticketBodyField, setTicketBodyField] = useState<string>("") const [ticketBodyField, setTicketBodyField] = useState<string>("");
async function handleCreateTicket() { async function handleCreateTicket() {
if (!ticketBodyField || !ticketNameField) return if (!ticketBodyField || !ticketNameField) return;
createTicket({ const [createTicketresult, createTicketerror] = await createTicket(
url: process.env.REACT_APP_DOMAIN + "/heruvym/create", ticketNameField,
body: { ticketBodyField
Title: ticketNameField, );
Message: ticketBodyField,
if (createTicketerror) {
return enqueueSnackbar(createTicketerror);
} }
}).then(result => {
navigate(`/support/${result.Ticket}`) navigate(`/support/${createTicketresult?.Ticket}`);
}).catch(error => {
enqueueSnackbar(error.message)
})
} }
return ( return (
@ -39,9 +45,7 @@ export default function CreateTicket() {
borderRadius: "12px", borderRadius: "12px",
p: upMd ? "20px" : undefined, p: upMd ? "20px" : undefined,
gap: upMd ? "40px" : "20px", gap: upMd ? "40px" : "20px",
boxShadow: upMd boxShadow: upMd ? cardShadow : undefined,
? cardShadow
: undefined,
}} }}
> >
<Box <Box
@ -125,8 +129,10 @@ export default function CreateTicket() {
variant="pena-contained-dark" variant="pena-contained-dark"
onClick={handleCreateTicket} onClick={handleCreateTicket}
disabled={!ticketBodyField || !ticketNameField} disabled={!ticketBodyField || !ticketNameField}
>Отправить</Button> >
Отправить
</Button>
</Box> </Box>
</Box> </Box>
) );
} }

@ -14,7 +14,6 @@ import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import SendIcon from "@components/icons/SendIcon"; import SendIcon from "@components/icons/SendIcon";
import makeRequest from "@api/makeRequest"
import { throttle, useToken } from "@frontend/kitui"; import { throttle, useToken } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useTicketStore } from "@root/stores/tickets"; import { useTicketStore } from "@root/stores/tickets";
@ -35,7 +34,11 @@ import {
useSSESubscription, useSSESubscription,
useTicketMessages, useTicketMessages,
} from "@frontend/kitui"; } from "@frontend/kitui";
import { shownMessage, sendTicketMessage } from "@root/api/ticket"; import {
shownMessage,
sendTicketMessage,
sendFile as sendFileRequest,
} from "@root/api/ticket";
import { withErrorBoundary } from "react-error-boundary"; import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError"; import { handleComponentError } from "@root/utils/handleComponentError";
import { useSSETab } from "@root/utils/hooks/useSSETab"; import { useSSETab } from "@root/utils/hooks/useSSETab";
@ -196,20 +199,12 @@ function SupportChat() {
return; return;
} }
try { const [, sendFileError] = await sendFileRequest(ticketId, file);
const body = new FormData();
body.append(file.name, file); if (sendFileError) {
body.append("ticket", ticketId); enqueueSnackbar(sendFileError);
await makeRequest({
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles",
body: body,
method: "POST",
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
} }
return true; return true;
} }
}; };

@ -1,7 +1,17 @@
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
import { Tariff, calcTariffPrice, getMessageFromFetchError } from "@frontend/kitui"; import {
Tariff,
calcTariffPrice,
getMessageFromFetchError,
} from "@frontend/kitui";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"; import {
Box,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import NumberIcon from "@root/components/NumberIcon"; import NumberIcon from "@root/components/NumberIcon";
import { useTariffStore } from "@root/stores/tariffs"; import { useTariffStore } from "@root/stores/tariffs";
import { addTariffToCart, useUserStore } from "@root/stores/user"; import { addTariffToCart, useUserStore } from "@root/stores/user";
@ -26,8 +36,6 @@ const StepperText: Record<string, string> = {
time: "Тарифы на время", time: "Тарифы на время",
}; };
function TariffPage() { function TariffPage() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
@ -36,10 +44,12 @@ function TariffPage() {
const location = useLocation(); const location = useLocation();
const tariffs = useTariffStore((state) => state.tariffs); const tariffs = useTariffStore((state) => state.tariffs);
const [selectedItem, setSelectedItem] = useState<number>(0); const [selectedItem, setSelectedItem] = useState<number>(0);
const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0; const purchasesAmount =
const userId = useUserStore(state => state.user?._id) ?? ""; useUserStore((state) => state.userAccount?.wallet.spent) ?? 0;
const userId = useUserStore((state) => state.user?._id) ?? "";
const discounts = useDiscounts(userId); const discounts = useDiscounts(userId);
const isUserNko = useUserStore((state) => state.userAccount?.status) === "nko"; const isUserNko =
useUserStore((state) => state.userAccount?.status) === "nko";
const currentTariffs = useCartTariffs(); const currentTariffs = useCartTariffs();
const handleCustomBackNavigation = usePrevLocation(location); const handleCustomBackNavigation = usePrevLocation(location);
@ -81,7 +91,10 @@ function TariffPage() {
return false; return false;
}); });
const createTariffElements = (filteredTariffs: Tariff[], addFreeTariff = false) => { const createTariffElements = (
filteredTariffs: Tariff[],
addFreeTariff = false
) => {
const tariffElements = filteredTariffs const tariffElements = filteredTariffs
.filter((tariff) => tariff.privileges.length > 0) .filter((tariff) => tariff.privileges.length > 0)
.map((tariff, index) => { .map((tariff, index) => {
@ -91,17 +104,28 @@ function TariffPage() {
purchasesAmount, purchasesAmount,
currentTariffs ?? [], currentTariffs ?? [],
isUserNko, isUserNko,
userId, userId
); );
return ( return (
<TariffCard <TariffCard
key={tariff._id} key={tariff._id}
discount={(priceBeforeDiscounts - priceAfterDiscounts) ? `${((priceBeforeDiscounts - priceAfterDiscounts) / (priceBeforeDiscounts / 100)).toFixed(0)}%` : ""} discount={
priceBeforeDiscounts - priceAfterDiscounts
? `${(
(priceBeforeDiscounts - priceAfterDiscounts) /
(priceBeforeDiscounts / 100)
).toFixed(0)}%`
: ""
}
icon={ icon={
<NumberIcon <NumberIcon
number={index + 1} number={index + 1}
color={unit === "time" ? theme.palette.purple.main : theme.palette.orange.main} color={
unit === "time"
? theme.palette.purple.main
: theme.palette.orange.main
}
backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"} backgroundColor={unit === "time" ? "#EEE4FC" : "#FEDFD0"}
/> />
} }
@ -114,9 +138,17 @@ function TariffPage() {
price={ price={
<> <>
{priceBeforeDiscounts !== priceAfterDiscounts && ( {priceBeforeDiscounts !== priceAfterDiscounts && (
<Typography variant="oldPrice">{currencyFormatter.format(Math.trunc(priceBeforeDiscounts) / 100)}</Typography> <Typography variant="oldPrice">
{currencyFormatter.format(
Math.trunc(priceBeforeDiscounts) / 100
)} )}
<Typography variant="price">{currencyFormatter.format(Math.trunc(priceAfterDiscounts) / 100)}</Typography> </Typography>
)}
<Typography variant="price">
{currencyFormatter.format(
Math.trunc(priceAfterDiscounts) / 100
)}
</Typography>
</> </>
} }
/> />
@ -124,8 +156,10 @@ function TariffPage() {
}); });
if (addFreeTariff) { if (addFreeTariff) {
if (tariffElements.length < 6) tariffElements.push(<FreeTariffCard key="free_tariff_card" />); if (tariffElements.length < 6)
else tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />); tariffElements.push(<FreeTariffCard key="free_tariff_card" />);
else
tariffElements.splice(5, 0, <FreeTariffCard key="free_tariff_card" />);
} }
return tariffElements; return tariffElements;
@ -146,7 +180,13 @@ function TariffPage() {
{isMobile && ( {isMobile && (
<IconButton <IconButton
onClick={handleCustomBackNavigation} onClick={handleCustomBackNavigation}
sx={{ p: 0, height: "28px", width: "28px", color: "black", marginRight: "10px" }} sx={{
p: 0,
height: "28px",
width: "28px",
color: "black",
marginRight: "10px",
}}
> >
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
@ -186,7 +226,9 @@ function TariffPage() {
mb: "30px", mb: "30px",
display: "grid", display: "grid",
gap: "40px", gap: "40px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"}))`, gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${
isTablet ? "436px" : "360px"
}))`,
}} }}
> >
{createTariffElements(filteredTariffs, true)} {createTariffElements(filteredTariffs, true)}

@ -21,7 +21,6 @@ import { setUserId, useUserStore } from "@root/stores/user";
import { cardShadow } from "@root/utils/theme"; import { cardShadow } from "@root/utils/theme";
import AmoButton from "./AmoButton"; import AmoButton from "./AmoButton";
import { recover } from "@root/api/auth"; import { recover } from "@root/api/auth";
import {AxiosError} from "axios"
interface Values { interface Values {
email: string; email: string;
@ -48,17 +47,15 @@ export default function RecoverDialog() {
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: async (values, formikHelpers) => { onSubmit: async (values, formikHelpers) => {
const [recoverResponse, recoverError] = await recover( const [, recoverError] = await recover(values.email.trim());
values.email.trim()
);
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
if (recoverError) { if (recoverError) {
enqueueSnackbar(recoverError); enqueueSnackbar(recoverError);
return return;
} }
navigate("/") navigate("/");
enqueueSnackbar("Письмо придёт Вам на почту") enqueueSnackbar("Письмо придёт Вам на почту");
}, },
}); });

@ -19,8 +19,8 @@ import { useEffect, useState } from "react";
import { useUserStore } from "@root/stores/user"; import { useUserStore } from "@root/stores/user";
import { cardShadow } from "@root/utils/theme"; import { cardShadow } from "@root/utils/theme";
import makeRequest from "@api/makeRequest" import { patchUser } from "@api/user";
import { setAuthToken } from "@frontend/kitui" import { setAuthToken } from "@frontend/kitui";
interface Values { interface Values {
password: string; password: string;
} }
@ -32,7 +32,10 @@ const initialValues: Values = {
const validationSchema = object({ const validationSchema = object({
password: string() password: string()
.min(8, "Минимум 8 символов") .min(8, "Минимум 8 символов")
.matches(/^[.,:;\-_+!&()*<>\[\]\{\}`@"#$\%\^\=?\d\w]+$/, "Некорректные символы") .matches(
/^[.,:;\-_+!&()*<>\[\]\{\}`@"#$\%\^\=?\d\w]+$/,
"Некорректные символы"
)
.required("Поле обязательно"), .required("Поле обязательно"),
}); });
@ -49,32 +52,35 @@ export default function RecoverPassword() {
validationSchema, validationSchema,
onSubmit: async (values, formikHelpers) => { onSubmit: async (values, formikHelpers) => {
if (tokenUser) { if (tokenUser) {
setAuthToken(tokenUser || "") setAuthToken(tokenUser || "");
try { const [, patchUserError] = await patchUser({
const response = await makeRequest<unknown, unknown>({ password: values.password,
url: process.env.REACT_APP_DOMAIN + "/user/",
method: "PATCH",
body: {password: values.password},
}); });
setIsDialogOpen(false)
navigate("/")
enqueueSnackbar("Пароль успешно сменён")
} catch (error) {
setAuthToken("")
enqueueSnackbar("Извините, произошла ошибка, попробуйте повторить позже")}
if (!patchUserError) {
setIsDialogOpen(false);
navigate("/");
enqueueSnackbar("Пароль успешно сменён");
} else { } else {
enqueueSnackbar("Неверный url-адрес") setAuthToken("");
enqueueSnackbar(
"Извините, произошла ошибка, попробуйте повторить позже"
);
}
} else {
enqueueSnackbar("Неверный url-адрес");
} }
}, },
}); });
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search);
const authToken = params.get("auth") const authToken = params.get("auth");
setTokenUser(authToken) setTokenUser(authToken);
history.pushState(null, document.title, "/changepwd"); history.pushState(null, document.title, "/changepwd");
return () => {setAuthToken("")} return () => {
setAuthToken("");
};
}, []); }, []);
function handleClose() { function handleClose() {
@ -185,8 +191,7 @@ export default function RecoverPassword() {
gap: "10px", gap: "10px",
mt: "auto", mt: "auto",
}} }}
> ></Box>
</Box>
</Box> </Box>
</Dialog> </Dialog>
); );

@ -1,5 +1,4 @@
import { ErrorInfo } from "react" import { ErrorInfo } from "react";
interface ComponentError { interface ComponentError {
timestamp: number; timestamp: number;
@ -14,29 +13,23 @@ export function handleComponentError(error: Error, info: ErrorInfo) {
message: error.message, message: error.message,
callStack: error.stack, callStack: error.stack,
componentStack: info.componentStack, componentStack: info.componentStack,
} };
queueErrorRequest(componentError) queueErrorRequest(componentError);
} }
let errorsQueue: ComponentError[] = [] let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout> let timeoutId: ReturnType<typeof setTimeout>;
function queueErrorRequest(error: ComponentError) { function queueErrorRequest(error: ComponentError) {
errorsQueue.push(error) errorsQueue.push(error);
clearTimeout(timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
sendErrorsToServer() sendErrorsToServer();
}, 1000) }, 1000);
} }
async function sendErrorsToServer() { async function sendErrorsToServer() {
// makeRequest({ errorsQueue = [];
// url: "",
// method: "POST",
// body: errorsQueue,
// useToken: true,
// });
errorsQueue = []
} }

@ -1,8 +1,8 @@
import { useEffect, useLayoutEffect, useRef } from "react" import { useEffect, useLayoutEffect, useRef } from "react";
import { devlog } from "@frontend/kitui" import { devlog } from "@frontend/kitui";
import { ServiceKeyToPrivilegesMap } from "@root/model/privilege" import { ServiceKeyToPrivilegesMap } from "@root/model/privilege";
import { getCustomTariffs } from "@root/api/tariff" import { getCustomTariffs } from "@root/api/tariff";
export function useCustomTariffs({ export function useCustomTariffs({
onError, onError,
@ -11,28 +11,35 @@ export function useCustomTariffs({
onNewUser: (response: ServiceKeyToPrivilegesMap) => void; onNewUser: (response: ServiceKeyToPrivilegesMap) => void;
onError: (error: any) => void; onError: (error: any) => void;
}) { }) {
const onNewUserRef = useRef(onNewUser) const onNewUserRef = useRef(onNewUser);
const onErrorRef = useRef(onError) const onErrorRef = useRef(onError);
useLayoutEffect(() => { useLayoutEffect(() => {
onNewUserRef.current = onNewUser onNewUserRef.current = onNewUser;
onErrorRef.current = onError onErrorRef.current = onError;
}) });
useEffect(() => { useEffect(() => {
const controller = new AbortController() const controller = new AbortController();
getCustomTariffs(controller.signal) const getCustomTariffsRequest = async () => {
.then(([customTariffs]) => { const [customTariffs, customTariffsError] = await getCustomTariffs(
if (customTariffs) { controller.signal
onNewUserRef.current(customTariffs) );
if (customTariffsError) {
devlog("Error fetching custom tariffs", customTariffsError);
onErrorRef.current(customTariffsError);
return;
} }
}) if (customTariffs) {
.catch(([_, error]) => { onNewUserRef.current(customTariffs);
devlog("Error fetching custom tariffs", error) }
onErrorRef.current(error) };
})
return () => controller.abort() getCustomTariffsRequest();
}, [])
return () => controller.abort();
}, []);
} }

@ -1,8 +1,8 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react" import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { getTariffs } from "@root/api/tariff" import { getTariffs } from "@root/api/tariff";
import type { Tariff } from "@frontend/kitui" import type { Tariff } from "@frontend/kitui";
export function useTariffFetcher({ export function useTariffFetcher({
tariffsPerPage, tariffsPerPage,
@ -16,32 +16,42 @@ export function useTariffFetcher({
onSuccess: (response: Tariff[]) => void; onSuccess: (response: Tariff[]) => void;
onError?: (error: Error) => void; onError?: (error: Error) => void;
}) { }) {
const [fetchState, setFetchState] = useState<"fetching" | "idle" | "all fetched">("idle") const [fetchState, setFetchState] = useState<
const onSuccessRef = useRef(onSuccess) "fetching" | "idle" | "all fetched"
const onErrorRef = useRef(onError) >("idle");
const onSuccessRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
useLayoutEffect(() => { useLayoutEffect(() => {
onSuccessRef.current = onSuccess onSuccessRef.current = onSuccess;
onErrorRef.current = onError onErrorRef.current = onError;
}, [onError, onSuccess]) }, [onError, onSuccess]);
useEffect(() => { useEffect(() => {
const controller = new AbortController() const controller = new AbortController();
setFetchState("fetching") const getTariffsRequest = async () => {
getTariffs(apiPage, tariffsPerPage, controller.signal) setFetchState("fetching");
.then(([result]) => { const [tariffs, tariffsError] = await getTariffs(
if (result && result.tariffs.length > 0) { apiPage,
onSuccessRef.current(result.tariffs) tariffsPerPage,
setFetchState("idle") controller.signal
} else setFetchState("all fetched") );
})
.catch(([_, error]) => {
onErrorRef.current?.(error)
})
return () => controller.abort() if (tariffsError) {
}, [apiPage, tariffsPerPage]) return onErrorRef.current?.(new Error(tariffsError));
}
return fetchState if (tariffs && tariffs.tariffs.length > 0) {
onSuccessRef.current(tariffs.tariffs);
setFetchState("idle");
} else setFetchState("all fetched");
};
getTariffsRequest();
return () => controller.abort();
}, [apiPage, tariffsPerPage]);
return fetchState;
} }

@ -9,10 +9,10 @@ export type ServerError = {
const translateMessage: Record<string, string> = { const translateMessage: Record<string, string> = {
"user not found": "Пользователь не найден", "user not found": "Пользователь не найден",
"invalid password": "Неправильный пароль", "invalid password": "Неправильный пароль",
"field <password> is empty": "Поле \"Пароль\" не заполнено", "field <password> is empty": 'Поле "Пароль" не заполнено',
"field <login> is empty": "Поле \"Логин\" не заполнено", "field <login> is empty": 'Поле "Логин" не заполнено',
"field <email> is empty": "Поле \"E-mail\" не заполнено", "field <email> is empty": 'Поле "E-mail" не заполнено',
"field <phoneNumber> is empty": "Поле \"Номер телефона\" не заполнено", "field <phoneNumber> is empty": 'Поле "Номер телефона" не заполнено',
"user with this email or login is exist": "Пользователь уже существует", "user with this email or login is exist": "Пользователь уже существует",
"user with this login is exist": "user with this login is exist":
"Пользователь с таким логином уже существует", "Пользователь с таким логином уже существует",
@ -29,6 +29,10 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
if (error.response?.data) { if (error.response?.data) {
const serverError = error.response.data as ServerError; const serverError = error.response.data as ServerError;
let SEMessage; let SEMessage;
if (typeof error.response?.data === "string") {
return [error.response?.data];
}
if ("statusCode" in (error.response?.data as ServerError)) { if ("statusCode" in (error.response?.data as ServerError)) {
SEMessage = serverError?.message.toLowerCase() || ""; SEMessage = serverError?.message.toLowerCase() || "";
} }