refactor: makeRequests decomposed

This commit is contained in:
IlyaDoronin 2024-05-27 18:43:38 +03:00
parent 8e79f16640
commit 9261f3e797
21 changed files with 1586 additions and 1415 deletions

@ -9,7 +9,7 @@ import type {
RegisterResponse,
} from "@frontend/kitui";
const apiUrl = process.env.REACT_APP_DOMAIN + "/auth";
const API_URL = `${process.env.REACT_APP_DOMAIN}/auth`;
export async function register(
login: string,
@ -21,7 +21,7 @@ export async function register(
RegisterRequest,
RegisterResponse
>({
url: apiUrl + "/register",
url: `${API_URL}/register`,
body: { login, password, phoneNumber },
useToken: false,
withCredentials: true,
@ -41,7 +41,7 @@ export async function login(
): Promise<[LoginResponse | null, string?]> {
try {
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
url: apiUrl + "/login",
url: `${API_URL}/login`,
body: { login, password },
useToken: false,
withCredentials: true,
@ -60,14 +60,20 @@ export async function recover(
): Promise<[unknown | null, string?]> {
try {
const formData = new FormData();
formData.append("email", email);
formData.append("RedirectionURL", process.env.REACT_APP_DOMAIN + "/changepwd")
formData.append(
"RedirectionURL",
`${process.env.REACT_APP_DOMAIN}/changepwd`
);
const recoverResponse = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/codeword/recover",
url: `${process.env.REACT_APP_DOMAIN}/codeword/recover`,
body: formData,
useToken: false,
withCredentials: true,
});
return [recoverResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
@ -79,7 +85,7 @@ export async function recover(
export async function logout(): Promise<[unknown, string?]> {
try {
const logoutResponse = await makeRequest<never, void>({
url: apiUrl + "/logout",
url: `${API_URL}/logout`,
method: "POST",
useToken: true,
withCredentials: true,

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

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

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

@ -5,7 +5,7 @@ import { parseAxiosError } from "@root/utils/parse-error";
import { enqueueSnackbar } from "notistack";
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) {
if (userId === null) {
@ -14,7 +14,7 @@ export async function getDiscounts(userId: string | null) {
try {
const discountsResponse = await makeRequest<never, GetDiscountsResponse>({
url: `${apiUrl}/discount/user/${userId}`,
url: `${API_URL}/discount/user/${userId}`,
method: "get",
useToken: true,
});

@ -1,28 +1,19 @@
import makeRequest from "@api/makeRequest"
import makeRequest from "@api/makeRequest";
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) {
try {
const response = await makeRequest<
| {
codeword: string;
}
| {
fastLink: string;
},
{
greetings: string;
}
{ codeword: string } | { fastLink: string },
{ greetings: string }
>({
url: apiUrl + "/activate",
url: `${API_URL}/activate`,
method: "POST",
contentType: true,
body: {
codeword: promocode,
},
body: { codeword: promocode },
});
return response.greetings;

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

@ -1,124 +1,136 @@
import makeRequest from "@api/makeRequest"
import makeRequest from "@api/makeRequest";
import { Tariff } from "@frontend/kitui";
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 { removeTariffFromCart } from "@root/stores/user";
const apiUrl = process.env.REACT_APP_DOMAIN + "/strator"
const API_URL = `${process.env.REACT_APP_DOMAIN}/strator`;
export async function getTariffs(
apiPage: number,
tariffsPerPage: number,
signal: AbortSignal | undefined
apiPage: number,
tariffsPerPage: number,
signal: AbortSignal | undefined
): Promise<[GetTariffsResponse | null, string?]> {
try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
url: apiUrl + `/tariff?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal,
});
try {
const tariffsResponse = await makeRequest<never, GetTariffsResponse>({
url: `${API_URL}/tariff?page=${apiPage}&limit=${tariffsPerPage}`,
method: "get",
useToken: true,
signal,
});
return [tariffsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [tariffsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список тарифов. ${error}`];
}
return [null, `Не удалось получить список тарифов. ${error}`];
}
}
interface CreateTariffBody {
name: string;
price?: number;
isCustom: boolean;
privileges: PrivilegeWithoutPrice[];
name: string;
price?: number;
isCustom: boolean;
privileges: PrivilegeWithoutPrice[];
}
export async function createTariff(tariff: CreateTariffBody): Promise<[Tariff | null, string?]> {
try {
const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({
url: `${apiUrl}/tariff`,
method: "post",
useToken: true,
body: tariff,
});
export async function createTariff(
tariff: CreateTariffBody
): Promise<[Tariff | null, string?]> {
try {
const createTariffResponse = await makeRequest<CreateTariffBody, Tariff>({
url: `${API_URL}/tariff`,
method: "post",
useToken: true,
body: tariff,
});
return [createTariffResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [createTariffResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось создать тариф. ${error}`];
}
return [null, `Не удалось создать тариф. ${error}`];
}
}
export async function getTariffById(tariffId: string): Promise<[Tariff | null, string?, number?]> {
try {
const getTariffByIdResponse = await makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`,
method: "get",
useToken: true,
});
export async function getTariffById(
tariffId: string
): Promise<[Tariff | null, string?, number?]> {
try {
const getTariffByIdResponse = await makeRequest<never, Tariff>({
url: `${API_URL}/tariff/${tariffId}`,
method: "get",
useToken: true,
});
return [getTariffByIdResponse];
} catch (nativeError) {
const [error, status] = parseAxiosError(nativeError);
return [getTariffByIdResponse];
} catch (nativeError) {
const [error, status] = parseAxiosError(nativeError);
return [null, `Не удалось получить тарифы. ${error}`, status];
}
return [null, `Не удалось получить тарифы. ${error}`, status];
}
}
export async function getCustomTariffs(
signal: AbortSignal | undefined
signal: AbortSignal | undefined
): Promise<[ServiceKeyToPrivilegesMap | null, string?]> {
try {
const customTariffsResponse = await makeRequest<null, ServiceKeyToPrivilegesMap>({
url: apiUrl + "/privilege/service",
signal,
method: "get",
useToken: true,
});
try {
const customTariffsResponse = await makeRequest<
null,
ServiceKeyToPrivilegesMap
>({
url: `${API_URL}/privilege/service`,
signal,
method: "get",
useToken: true,
});
const tempCustomTariffsResponse = {
...customTariffsResponse,
squiz: customTariffsResponse.squiz
};
const tempCustomTariffsResponse = {
...customTariffsResponse,
squiz: customTariffsResponse.squiz,
};
return [tempCustomTariffsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [tempCustomTariffsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить мои тарифы. ${error}`];
}
return [null, `Не удалось получить мои тарифы. ${error}`];
}
}
export async function getTariffArray(tariffIds: string[] | undefined) {
if (!tariffIds) return null;
if (!tariffIds) return null;
const responses = await Promise.allSettled(tariffIds.map(tariffId =>
makeRequest<never, Tariff>({
url: `${apiUrl}/tariff/${tariffId}`,
method: "get",
useToken: true,
})
));
const responses = await Promise.allSettled(
tariffIds.map((tariffId) =>
makeRequest<never, Tariff>({
url: `${API_URL}/tariff/${tariffId}`,
method: "get",
useToken: true,
})
)
);
const tariffs: Tariff[] = [];
const tariffs: Tariff[] = [];
responses.forEach((response, index) => {
switch (response.status) {
case "fulfilled": {
tariffs.push(response.value);
break;
}
case "rejected": {
const [, status] = parseAxiosError(response.reason);
if (status === 404) removeTariffFromCart(tariffIds[index]);
break;
}
}
});
responses.forEach((response, index) => {
switch (response.status) {
case "fulfilled": {
tariffs.push(response.value);
break;
}
case "rejected": {
const [, status] = parseAxiosError(response.reason);
if (status === 404) removeTariffFromCart(tariffIds[index]);
break;
}
}
});
return tariffs;
return tariffs;
}

@ -1,9 +1,13 @@
import makeRequest from "@api/makeRequest"
import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@root/utils/parse-error";
import { SendTicketMessageRequest } from "@frontend/kitui";
const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym";
type SendFileResponse = {
message: string;
};
const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym`;
export async function sendTicketMessage(
ticketId: string,
@ -14,7 +18,7 @@ export async function sendTicketMessage(
SendTicketMessageRequest,
null
>({
url: `${apiUrl}/send`,
url: `${API_URL}/send`,
method: "POST",
useToken: true,
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
@ -31,7 +35,7 @@ export async function sendTicketMessage(
export async function shownMessage(id: string): Promise<[null, string?]> {
try {
const shownMessageResponse = await makeRequest<{ id: string }, null>({
url: apiUrl + "/shown",
url: `${API_URL}/shown`,
method: "POST",
useToken: true,
body: { id },
@ -44,3 +48,27 @@ export async function shownMessage(id: string): Promise<[null, string?]> {
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: `${process.env.REACT_APP_DOMAIN}/sendFiles`,
body,
});
return [sendResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отправить файл. ${error}`];
}
};

@ -1,27 +1,27 @@
import { User } from "@frontend/kitui"
import makeRequest from "@api/makeRequest"
import { PatchUserRequest } from "@root/model/user"
import { parseAxiosError } from "@root/utils/parse-error"
import { User } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { PatchUserRequest } from "@root/model/user";
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(
user: PatchUserRequest
user: PatchUserRequest
): Promise<[User | null, string?]> {
try {
const patchUserResponse = await makeRequest<PatchUserRequest, User>({
url: apiUrl+"/",
contentType: true,
method: "PATCH",
useToken: true,
withCredentials: false,
body: user,
})
try {
const patchUserResponse = await makeRequest<PatchUserRequest, User>({
url: `${API_URL}/`,
contentType: true,
method: "PATCH",
useToken: true,
withCredentials: false,
body: user,
});
return [patchUserResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [patchUserResponse];
} catch (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 { parseAxiosError } from "@root/utils/parse-error"
import { jsonToFormdata } from "@root/utils/jsonToFormdata";
import { parseAxiosError } from "@root/utils/parse-error";
import type {
Verification,
SendDocumentsArgs,
UpdateDocumentsArgs,
} from "@root/model/auth"
import { AxiosError } from "axios"
Verification,
SendDocumentsArgs,
UpdateDocumentsArgs,
} from "@root/model/auth";
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(
userId: string
userId: string
): Promise<[Verification | null, string?]> {
try {
const verificationResponse = await makeRequest<never, Verification>({
url: apiUrl + "/verification/" + userId,
method: "GET",
useToken: true,
withCredentials: true,
})
try {
const verificationResponse = await makeRequest<never, Verification>({
url: `${API_URL}/${userId}`,
method: "GET",
useToken: true,
withCredentials: true,
});
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() || "")
return 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() || ""
);
return obj;
});
return [verificationResponse]
} catch (nativeError) {
const err = nativeError as AxiosError
if (err.response?.status === 404) {
return [null, `нет данных`]
}
const [error] = parseAxiosError(nativeError)
return [verificationResponse];
} catch (nativeError) {
const [error, status] = parseAxiosError(nativeError);
return [null, `Ошибка запроса верификации. ${error}`]
}
if (status === 404) {
return [null, "нет данных"];
}
return [null, `Ошибка запроса верификации. ${error}`];
}
}
export async function sendDocuments(
documents: SendDocumentsArgs
documents: SendDocumentsArgs
): Promise<[Verification | "OK" | null, string?]> {
try {
const sendDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification",
method: "POST",
useToken: true,
withCredentials: true,
body: jsonToFormdata({ ...documents, egrule: documents.inn }),
})
try {
const sendDocumentsResponse = await makeRequest<FormData, Verification>({
url: API_URL,
method: "POST",
useToken: true,
withCredentials: true,
body: jsonToFormdata({ ...documents, egrule: documents.inn }),
});
return [sendDocumentsResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [sendDocumentsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка отправки документов. ${error}`]
}
return [null, `Ошибка отправки документов. ${error}`];
}
}
export async function updateDocuments(
documents: UpdateDocumentsArgs
): Promise<[Verification | "OK" | null, string? ]> {
try {
const updateDocumentsResponse = await makeRequest<FormData, Verification>({
url: apiUrl + "/verification/file",
method: "PATCH",
useToken: true,
withCredentials: true,
body: jsonToFormdata(
documents.inn ? { ...documents, egrule: documents.inn } : documents
),
})
documents: UpdateDocumentsArgs
): Promise<[Verification | "OK" | null, string?]> {
try {
const updateDocumentsResponse = await makeRequest<FormData, Verification>({
url: `${API_URL}/file`,
method: "PATCH",
useToken: true,
withCredentials: true,
body: jsonToFormdata(
documents.inn ? { ...documents, egrule: documents.inn } : documents
),
});
return [updateDocumentsResponse]
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
return [updateDocumentsResponse];
} catch (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>({
url: API_URL,
method: "PATCH",
body,
useToken: true,
withCredentials: true,
});
return [updateDocumentResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Ошибка обновления документа. ${error}`];
}
};

@ -10,7 +10,6 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import makeRequest from "@api/makeRequest"
import {
createTicket,
getMessageFromFetchError,
@ -28,13 +27,17 @@ import {
useMemo,
useRef,
useState,
WheelEvent
WheelEvent,
} from "react";
import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon";
import ArrowLeft from "@root/assets/Icons/arrowLeft";
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 {
ACCEPT_SEND_MEDIA_TYPES_MAP,
@ -115,7 +118,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
? offHoursMessage
: workingHoursMessage;
return ({
return {
created_at: new Date().toISOString(),
files: [],
id: "111",
@ -125,8 +128,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
shown: { me: 1 },
ticket_id: "111",
user_id: "greetingMessage",
});
};
}, [open]);
useTicketMessages({
@ -182,7 +184,8 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
onSuccess: (result) => {
if (result.data?.length) {
const currentTicket = result.data.find(
({ origin, state }) => !origin.includes("/support") && state !== "close"
({ origin, state }) =>
!origin.includes("/support") && state !== "close"
);
if (!currentTicket) {
@ -199,7 +202,7 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
},
onFetchStateChange: () => { },
onFetchStateChange: () => {},
enabled: Boolean(user),
});
@ -228,7 +231,6 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
scrollToBottom();
}, [open]);
useEffect(
function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
@ -353,19 +355,15 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
const ticketId = ticket.sessionData?.ticketId || data?.Ticket;
if (ticketId !== undefined) {
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
try {
const body = new FormData();
body.append(file.name, file);
body.append("ticket", ticketId);
await makeRequest({
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles",
body: body,
method: "POST",
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
const [, sendFileError] = await sendFileRequest(
ticketId,
file
);
if(sendFileError) {
enqueueSnackbar(sendFileError)
}
}
return true;
}
@ -547,8 +545,13 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) {
);
})}
{!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>
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
<InputBase

@ -1,135 +1,134 @@
import axios from "axios"
import { Box, IconButton, SxProps, Theme, Typography, useTheme } from "@mui/material"
import { Document, Page } from "react-pdf"
import { Buffer } from "buffer"
import { downloadFileToDevice } from "@root/utils/downloadFileToDevice"
import EditIcon from "@mui/icons-material/Edit"
import { ChangeEvent, useRef } from "react"
import { SendDocumentsArgs, Verification } from "@root/model/auth"
import makeRequest from "@api/makeRequest"
import { jsonToFormdata } from "@utils/jsonToFormdata"
import { parseAxiosError } from "@utils/parse-error"
import { readFile } from "@root/utils/readFile"
import { enqueueSnackbar } from "notistack"
import axios from "axios";
import {
Box,
IconButton,
SxProps,
Theme,
Typography,
useTheme,
} from "@mui/material";
import { Document, Page } from "react-pdf";
import { Buffer } from "buffer";
import { downloadFileToDevice } from "@root/utils/downloadFileToDevice";
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 =
"inn" |
"rule" |
"certificate"
type KeyNames = "inn" | "rule" | "certificate";
interface Props {
text: string;
documentUrl: string;
sx?: SxProps<Theme>;
keyName: KeyNames
text: string;
documentUrl: string;
sx?: SxProps<Theme>;
keyName: KeyNames;
}
export default function DocumentItem({
text,
documentUrl = "",
sx,
keyName,
}: Props) {
const theme = useTheme();
export default function DocumentItem({ text, documentUrl = "", sx, keyName }: Props) {
const theme = useTheme()
const fileInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null)
function handleChooseFileClick() {
fileInputRef.current?.click();
}
function handleChooseFileClick() {
fileInputRef.current?.click()
}
const downloadFile = async () => {
const { data } = await axios.get<ArrayBuffer>(documentUrl, {
responseType: "arraybuffer",
});
const downloadFile = async () => {
const { data } = await axios.get<ArrayBuffer>(documentUrl, {
responseType: "arraybuffer",
})
if (!data) {
return;
}
if (!data) {
return
}
downloadFileToDevice(
`${documentUrl.split("/").pop()?.split(".")?.[0] || "document"}.pdf`,
Buffer.from(data)
);
downloadFileToDevice(
`${documentUrl.split("/").pop()?.split(".")?.[0] || "document"}.pdf`,
Buffer.from(data)
)
return;
};
return
}
async function sendDocument(e: ChangeEvent<HTMLInputElement>) {
const target = e.target as HTMLInputElement;
const file = target?.files?.[0] || null;
if (file !== null) {
const readedFile = await readFile(file, "binary");
const [, updateDocumentError] = await updateDocument(
jsonToFormdata({ [keyName]: readedFile })
);
async function sendDocument(
e: ChangeEvent<HTMLInputElement>
) {
const target = e.target as HTMLInputElement;
const file = target?.files?.[0] || null;
if (file !== null) {
const readedFile = await readFile(file, "binary")
try {
await makeRequest<FormData, Verification>({
url: `${process.env.REACT_APP_DOMAIN}/verification/v1.0.0/verification`,
method: "PATCH",
useToken: true,
withCredentials: true,
body: jsonToFormdata({ [keyName]: readedFile }),
})
if (updateDocumentError) {
return enqueueSnackbar(
`Ошибка отправки документов. ${updateDocumentError}`
);
}
enqueueSnackbar("Данные обновлены")
} catch (nativeError) {
const [error] = parseAxiosError(nativeError)
enqueueSnackbar("Данные обновлены");
}
}
enqueueSnackbar(`Ошибка отправки документов. ${error}`)
}
}
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "10px",
...sx,
}}
>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
}}
>
{text}
</Typography>
{documentUrl && (
<>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
sx={{ color: theme.palette.purple.main, cursor: "pointer" }}
onClick={downloadFile}
>
{documentUrl.split("/").pop()?.split(".")?.[0]}
</Typography>
<IconButton onClick={handleChooseFileClick}>
<EditIcon sx={{ color: theme.palette.purple.main }} />
</IconButton>
<input
ref={fileInputRef}
style={{ display: "none" }}
onChange={sendDocument}
type="file"
id="image-file"
multiple
accept={"application/pdf"}
/>
</Box>
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "10px",
...sx,
}}
>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
}}
>
{text}
</Typography>
{documentUrl && (
<>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
sx={{ color: theme.palette.purple.main, cursor: "pointer" }}
onClick={downloadFile}
>
{documentUrl.split("/").pop()?.split(".")?.[0]}
</Typography>
<IconButton
onClick={handleChooseFileClick}
>
<EditIcon sx={{ color: theme.palette.purple.main }} />
</IconButton>
<input
ref={fileInputRef}
style={{ display: "none" }}
onChange={sendDocument}
type="file"
id="image-file"
multiple
accept={"application/pdf"}
/>
</Box>
<Document file={documentUrl}>
<Page
pageNumber={1}
width={200}
renderTextLayer={false}
renderAnnotationLayer={false}
/>
</Document>
</>
)}
</Box>
)
<Document file={documentUrl}>
<Page
pageNumber={1}
width={200}
renderTextLayer={false}
renderAnnotationLayer={false}
/>
</Document>
</>
)}
</Box>
);
}

@ -1,21 +1,19 @@
import {
Box,
IconButton,
Typography,
useMediaQuery,
useTheme
} from "@mui/material"
import CustomAccordion from "@components/CustomAccordion"
import File from "@components/icons/File"
import {getDeclension} from "@utils/declension"
import {enqueueSnackbar} from "notistack"
import {addTariffToCart, useUserStore} from "@root/stores/user"
import ForwardToInboxOutlinedIcon
from "@mui/icons-material/ForwardToInboxOutlined";
import {makeRequest} from "@frontend/kitui";
import {KeyValue, RawDetails} from "@api/history";
import {useNavigate} from "react-router-dom"
import {VerificationStatus} from "@root/model/account"
Box,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import CustomAccordion from "@components/CustomAccordion";
import File from "@components/icons/File";
import { getDeclension } from "@utils/declension";
import { enqueueSnackbar } from "notistack";
import { addTariffToCart, useUserStore } from "@root/stores/user";
import ForwardToInboxOutlinedIcon from "@mui/icons-material/ForwardToInboxOutlined";
import { KeyValue, RawDetails, sendReport } from "@api/history";
import { useNavigate } from "react-router-dom";
import { VerificationStatus } from "@root/model/account";
export type History = {
title: string;
@ -31,296 +29,322 @@ interface AccordionWrapperProps {
last?: boolean;
first?: boolean;
createdAt: string;
onClickMail?: any
mainId: string
onClickMail?: any;
mainId: string;
}
export default function AccordionWrapper({ content, last, first, createdAt, 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 verificationStatus = useUserStore((state) => state.verificationStatus)
const OrgName = useUserStore((state) => state.userAccount?.name.orgname)
export default function AccordionWrapper({
content,
last,
first,
createdAt,
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 verificationStatus = useUserStore((state) => state.verificationStatus);
const OrgName = useUserStore((state) => state.userAccount?.name.orgname);
const valuesByKey: any = {}
if (Array.isArray(content[0].Value) && Array.isArray(content[0].Value[0])) {
(content[0].Value[0] as KeyValue[]).forEach((item: KeyValue) => {
valuesByKey[item.Key] = item.Value;
});
}
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/)
return dateMatch ? dateMatch[0] : null
}
const valuesByKey: any = {};
if (Array.isArray(content[0].Value) && Array.isArray(content[0].Value[0])) {
(content[0].Value[0] as KeyValue[]).forEach((item: KeyValue) => {
valuesByKey[item.Key] = item.Value;
});
}
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/);
return dateMatch ? dateMatch[0] : null;
};
async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId);
if (patchCartError) {
enqueueSnackbar(patchCartError);
} else {
enqueueSnackbar("Тариф добавлен в корзину");
}
}
async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId)
if (patchCartError) {
enqueueSnackbar(patchCartError)
} else {
enqueueSnackbar("Тариф добавлен в корзину")
}
}
async function sendBillByEmail(logId: string) {
if (verificationStatus === VerificationStatus.VERIFICATED && OrgName) {
const [, sendReportError] = await sendReport(logId);
async function sendBillByEmail(logId: string) {
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("Извините, произошла ошибка");
}
}
navigate("/settings")
if(verificationStatus !== VerificationStatus.VERIFICATED && !OrgName){
enqueueSnackbar("Пройдите верификацию и заполните название организации");
} else if(!OrgName){
enqueueSnackbar("Заполните поле название организации");
}else if(verificationStatus !== VerificationStatus.VERIFICATED) {
enqueueSnackbar("Пройдите верификацию");
}
}
enqueueSnackbar("Извините, произошла ошибка");
}
navigate("/settings");
if (verificationStatus !== VerificationStatus.VERIFICATED && !OrgName) {
enqueueSnackbar("Пройдите верификацию и заполните название организации");
} else if (!OrgName) {
enqueueSnackbar("Заполните поле название организации");
} else if (verificationStatus !== VerificationStatus.VERIFICATED) {
enqueueSnackbar("Пройдите верификацию");
}
}
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<CustomAccordion
last={last}
first={first}
divide
text={valuesByKey.privileges.map((e:KeyValue[]) => (
<Typography
key={valuesByKey.id}
>
{e[1].Value} - {e[5].Value} {getDeclension(Number(e[5].Value), e[7].Value.toString())}
</Typography>)
)}
header={
<>
<Box
sx={{
width: "100%",
height: upMd ? "72px" : undefined,
padding: "20px 20px 20px 0",
display: "flex",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
gap: "20px",
alignItems: upSm ? "center" : undefined,
flexDirection: upSm ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: upSm ? "center" : undefined,
justifyContent: "space-between",
flexDirection: upSm ? undefined : "column",
gap: upMd ? "51px" : "10px",
}}
>
<Typography
sx={{
width: "110px",
fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.text.secondary,
px: 0,
whiteSpace: "nowrap",
}}
>
{createdAt}
</Typography>
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<CustomAccordion
last={last}
first={first}
divide
text={valuesByKey.privileges.map((e: KeyValue[]) => (
<Typography key={valuesByKey.id}>
{e[1].Value} - {e[5].Value}{" "}
{getDeclension(Number(e[5].Value), e[7].Value.toString())}
</Typography>
))}
header={
<>
<Box
sx={{
width: "100%",
height: upMd ? "72px" : undefined,
padding: "20px 20px 20px 0",
display: "flex",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
gap: "20px",
alignItems: upSm ? "center" : undefined,
flexDirection: upSm ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: upSm ? "center" : undefined,
justifyContent: "space-between",
flexDirection: upSm ? undefined : "column",
gap: upMd ? "51px" : "10px",
}}
>
<Typography
sx={{
width: "110px",
fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.text.secondary,
px: 0,
whiteSpace: "nowrap",
}}
>
{createdAt}
</Typography>
<Typography
title={valuesByKey.iscustom ? "Мой тариф" : valuesByKey.name}
sx={{
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
px: 0,
width: "200px",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{valuesByKey.iscustom ? "Мой тариф" : valuesByKey.name}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexFlow: "1",
flexBasis: "60%",
}}
>
<Box display="flex" width="100%" justifyContent="space-between">
<Typography
sx={{
display: upMd ? undefined : "none",
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
px: 0,
}}
title={`>Способ оплаты: ${valuesByKey.payMethod}</Typography>}`}
>
{valuesByKey.payMethod && <Typography
sx={{
maxWidth: "300px",
width: "300px",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>Способ оплаты: {valuesByKey.payMethod}</Typography>}
</Typography>
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
width: "100%",
maxWidth: isTablet ? null : "160px",
}}
>
<Typography
sx={{
marginLeft: isTablet ? (isMobile ? null : "auto") : null,
color: valuesByKey.expired ? theme.palette.text.disabled : theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
textAlign: "left",
}}
>
{Number(content[1].Value) / 100 ? Number(content[1].Value) / 100 : "nodata"} руб.
</Typography>
</Box>
</Box>
{!isMobile &&
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
ml: "20px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
}
}}
>
<ForwardToInboxOutlinedIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation()
handleTariffItemClick(valuesByKey.id)
}}
sx={{
ml: "20px",
bgcolor:"#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor:"#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor:"black",
stroke: "white",
}
}}
>
<File></File>
</IconButton>
</>
}
</Box>
</Box>
{isMobile &&
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
m: "0 10px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
}
}}
>
<ForwardToInboxOutlinedIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation()
handleTariffItemClick(valuesByKey.id)
}}
sx={{
mr: "10px",
bgcolor:"#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor:"#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor:"black",
stroke: "white",
}
}}
>
<File></File>
</IconButton>
</>
}
</>
}
/>
</Box>
)
<Typography
title={valuesByKey.iscustom ? "Мой тариф" : valuesByKey.name}
sx={{
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.gray.dark,
px: 0,
width: "200px",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{valuesByKey.iscustom ? "Мой тариф" : valuesByKey.name}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexFlow: "1",
flexBasis: "60%",
}}
>
<Box display="flex" width="100%" justifyContent="space-between">
<Typography
sx={{
display: upMd ? undefined : "none",
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 400,
color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.gray.dark,
px: 0,
}}
title={`>Способ оплаты: ${valuesByKey.payMethod}</Typography>}`}
>
{valuesByKey.payMethod && (
<Typography
sx={{
maxWidth: "300px",
width: "300px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
Способ оплаты: {valuesByKey.payMethod}
</Typography>
)}
</Typography>
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
width: "100%",
maxWidth: isTablet ? null : "160px",
}}
>
<Typography
sx={{
marginLeft: isTablet
? isMobile
? null
: "auto"
: null,
color: valuesByKey.expired
? theme.palette.text.disabled
: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
textAlign: "left",
}}
>
{Number(content[1].Value) / 100
? Number(content[1].Value) / 100
: "nodata"}{" "}
руб.
</Typography>
</Box>
</Box>
{!isMobile && (
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
ml: "20px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
},
}}
>
<ForwardToInboxOutlinedIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation();
handleTariffItemClick(valuesByKey.id);
}}
sx={{
ml: "20px",
bgcolor: "#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor: "black",
stroke: "white",
},
}}
>
<File></File>
</IconButton>
</>
)}
</Box>
</Box>
{isMobile && (
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
m: "0 10px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
},
}}
>
<ForwardToInboxOutlinedIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation();
handleTariffItemClick(valuesByKey.id);
}}
sx={{
mr: "10px",
bgcolor: "#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor: "black",
stroke: "white",
},
}}
>
<File></File>
</IconButton>
</>
)}
</>
}
/>
</Box>
);
}

@ -1,160 +1,175 @@
import {
Box,
IconButton,
Typography,
useMediaQuery,
useTheme
Box,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import CustomAccordion from "@components/CustomAccordion";
import File from "@components/icons/File";
import {getDeclension} from "@utils/declension";
import {enqueueSnackbar} from "notistack";
import {addTariffToCart, useUserStore} from "@root/stores/user"
import {makeRequest, Tariff} from "@frontend/kitui";
import {currencyFormatter} from "@root/utils/currencyFormatter";
import ForwardToInboxIcon from '@mui/icons-material/ForwardToInbox';
import {VerificationStatus} from "@root/model/account"
import {useNavigate} from "react-router-dom"
import { getDeclension } from "@utils/declension";
import { enqueueSnackbar } from "notistack";
import { addTariffToCart, useUserStore } from "@root/stores/user";
import { makeRequest, Tariff } from "@frontend/kitui";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import ForwardToInboxIcon from "@mui/icons-material/ForwardToInbox";
import { VerificationStatus } from "@root/model/account";
import { useNavigate } from "react-router-dom";
import { sendReport } from "@api/history";
export type History = {
title: string;
date: string;
info: string;
description: string;
payMethod?: string;
expired?: boolean;
title: string;
date: string;
info: string;
description: string;
payMethod?: string;
expired?: boolean;
};
interface AccordionWrapperProps {
tariff: Tariff;
price: number;
last?: boolean;
first?: boolean;
createdAt: string;
mainId: string
tariff: Tariff;
price: number;
last?: boolean;
first?: boolean;
createdAt: string;
mainId: string;
}
export default function AccordionWrapper2({ tariff, price, last, first, createdAt, 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 verificationStatus = useUserStore((state) => state.verificationStatus)
const OrgName = useUserStore((state) => state.userAccount?.name.orgname)
async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId);
if (patchCartError) {
enqueueSnackbar(patchCartError);
} else {
enqueueSnackbar("Тариф добавлен в корзину");
}
export default function AccordionWrapper2({
tariff,
price,
last,
first,
createdAt,
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 verificationStatus = useUserStore((state) => state.verificationStatus);
const OrgName = useUserStore((state) => state.userAccount?.name.orgname);
async function handleTariffItemClick(tariffId: string) {
const { patchCartError } = await addTariffToCart(tariffId);
if (patchCartError) {
enqueueSnackbar(patchCartError);
} else {
enqueueSnackbar("Тариф добавлен в корзину");
}
}
async function sendBillByEmail(logId: string) {
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("Извините, произошла ошибка");
}
}
navigate("/settings")
if(verificationStatus !== VerificationStatus.VERIFICATED && !OrgName){
enqueueSnackbar("Пройдите верификацию и заполните название организации");
} else if(!OrgName){
enqueueSnackbar("Заполните поле название организации");
}else if(verificationStatus !== VerificationStatus.VERIFICATED) {
enqueueSnackbar("Пройдите верификацию");
}
async function sendBillByEmail(logId: string) {
if (verificationStatus === VerificationStatus.VERIFICATED && OrgName) {
const [, sendReportError] = await sendReport(logId);
if (!sendReportError) {
return enqueueSnackbar(
"Акт будет отправлен на почту, указанную при регистрации"
);
}
enqueueSnackbar("Извините, произошла ошибка");
}
navigate("/settings");
if (verificationStatus !== VerificationStatus.VERIFICATED && !OrgName) {
enqueueSnackbar("Пройдите верификацию и заполните название организации");
} else if (!OrgName) {
enqueueSnackbar("Заполните поле название организации");
} else if (verificationStatus !== VerificationStatus.VERIFICATED) {
enqueueSnackbar("Пройдите верификацию");
}
}
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<CustomAccordion
last={last}
first={first}
divide
text={tariff.privileges.map(privilege => (
`${privilege.description} - ${privilege.amount} ${getDeclension(Number(privilege.serviceKey), privilege.value)} `
))}
header={
<>
<Box
sx={{
width: "100%",
height: upMd ? "72px" : undefined,
padding: "20px 20px 20px 0",
display: "flex",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
gap: "20px",
alignItems: upSm ? "center" : undefined,
flexDirection: upSm ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: upSm ? "center" : undefined,
justifyContent: "space-between",
flexDirection: upSm ? undefined : "column",
gap: upMd ? "51px" : "10px",
}}
>
<Typography
sx={{
width: "110px",
fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.text.secondary,
px: 0,
whiteSpace: "nowrap",
}}
>
{createdAt}
</Typography>
return (
<Box
sx={{
borderRadius: "12px",
}}
>
<CustomAccordion
last={last}
first={first}
divide
text={tariff.privileges.map(
(privilege) =>
`${privilege.description} - ${privilege.amount} ${getDeclension(
Number(privilege.serviceKey),
privilege.value
)} `
)}
header={
<>
<Box
sx={{
width: "100%",
height: upMd ? "72px" : undefined,
padding: "20px 20px 20px 0",
display: "flex",
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
gap: "20px",
alignItems: upSm ? "center" : undefined,
flexDirection: upSm ? undefined : "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: upSm ? "center" : undefined,
justifyContent: "space-between",
flexDirection: upSm ? undefined : "column",
gap: upMd ? "51px" : "10px",
}}
>
<Typography
sx={{
width: "110px",
fontSize: upMd ? "20px" : "18px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: /* valuesByKey.expired */ false
? theme.palette.text.disabled
: theme.palette.text.secondary,
px: 0,
whiteSpace: "nowrap",
}}
>
{createdAt}
</Typography>
<Typography
title={tariff.isCustom ? "Мой тариф" : tariff.name}
sx={{
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.gray.dark,
px: 0,
width: upMd? "200px" : "auto",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{tariff.isCustom ? "Мой тариф" : tariff.name}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexFlow: "1",
flexBasis: "60%",
}}
>
<Box display="flex" width="100%" justifyContent="space-between">
{/* <Typography
<Typography
title={tariff.isCustom ? "Мой тариф" : tariff.name}
sx={{
fontSize: upMd ? "18px" : "16px",
lineHeight: upMd ? undefined : "19px",
fontWeight: 500,
color: /* valuesByKey.expired */ false
? theme.palette.text.disabled
: theme.palette.gray.dark,
px: 0,
width: upMd ? "200px" : "auto",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{tariff.isCustom ? "Мой тариф" : tariff.name}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexFlow: "1",
flexBasis: "60%",
}}
>
<Box display="flex" width="100%" justifyContent="space-between">
{/* <Typography
sx={{
display: upMd ? undefined : "none",
fontSize: upMd ? "18px" : "16px",
@ -173,134 +188,144 @@ export default function AccordionWrapper2({ tariff, price, last, first, createdA
}}
>Способ оплаты: {valuesByKey.payMethod}</Typography>}
</Typography> */}
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
width: "100%",
maxWidth: isTablet ? null : "160px",
}}
>
<Typography
sx={{
marginLeft: isTablet ? (isMobile ? null : "auto") : null,
color: /* valuesByKey.expired */ false ? theme.palette.text.disabled : theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
textAlign: "left",
}}
>
{currencyFormatter.format(price)}
</Typography>
</Box>
</Box>
{!isMobile &&
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
ml: "20px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
}
}}
>
<ForwardToInboxIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation();
handleTariffItemClick(tariff._id);
}}
sx={{
ml: "20px",
bgcolor: "#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor: "black",
stroke: "white",
}
}}
>
<File></File>
</IconButton>
</>
}
</Box>
</Box>
{isMobile &&
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
m: "0 10px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
}
}}
>
<ForwardToInboxIcon fontSize={"medium"} sx={{ opacity: 0.9 }}/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation();
handleTariffItemClick(tariff._id);
}}
sx={{
mr: "10px",
bgcolor: "#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor: "black",
stroke: "white",
}
}}
>
<File></File>
</IconButton>
</>
}
</>
}
/>
</Box>
);
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
gap: upSm ? "111px" : "17px",
width: "100%",
maxWidth: isTablet ? null : "160px",
}}
>
<Typography
sx={{
marginLeft: isTablet
? isMobile
? null
: "auto"
: null,
color: /* valuesByKey.expired */ false
? theme.palette.text.disabled
: theme.palette.gray.dark,
fontSize: upSm ? "20px" : "16px",
fontWeight: 500,
textAlign: "left",
}}
>
{currencyFormatter.format(price)}
</Typography>
</Box>
</Box>
{!isMobile && (
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
ml: "20px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
},
}}
>
<ForwardToInboxIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation();
handleTariffItemClick(tariff._id);
}}
sx={{
ml: "20px",
bgcolor: "#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor: "black",
stroke: "white",
},
}}
>
<File></File>
</IconButton>
</>
)}
</Box>
</Box>
{isMobile && (
<>
<IconButton
onClick={(e) => {
e.stopPropagation();
sendBillByEmail(mainId);
}}
sx={{
m: "0 10px",
bgcolor: "#EEE4FC",
color: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
color: "white",
},
"&:active": {
bgcolor: "black",
color: "white",
},
}}
>
<ForwardToInboxIcon
fontSize={"medium"}
sx={{ opacity: 0.9 }}
/>
</IconButton>
<IconButton
title="Добавить в корзину тариф"
onClick={(e) => {
e.stopPropagation();
handleTariffItemClick(tariff._id);
}}
sx={{
mr: "10px",
bgcolor: "#EEE4FC",
stroke: "#7E2AEA",
borderRadius: 2,
"&:hover": {
bgcolor: "#7E2AEA",
stroke: "white",
},
"&:active": {
bgcolor: "black",
stroke: "white",
},
}}
>
<File></File>
</IconButton>
</>
)}
</>
}
/>
</Box>
);
}

@ -1,154 +1,168 @@
import {useState} from "react";
import { useState } from "react";
import {
Box,
IconButton,
Typography,
useMediaQuery,
useTheme
Box,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import SectionWrapper from "@root/components/SectionWrapper";
import {Select} from "@root/components/Select";
import {Tabs} from "@root/components/Tabs";
import { Select } from "@root/components/Select";
import { Tabs } from "@root/components/Tabs";
import AccordionWrapper from "./AccordionWrapper";
import {useHistoryTracker} from "@root/utils/hooks/useHistoryTracker";
import {ErrorBoundary} from "react-error-boundary";
import {handleComponentError} from "@root/utils/handleComponentError";
import {useHistoryStore} from "@root/stores/history";
import {enqueueSnackbar} from "notistack";
import {makeRequest} from "@frontend/kitui";
import {HistoryRecord, HistoryRecord2} from "@root/api/history";
import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker";
import { ErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
import { useHistoryStore } from "@root/stores/history";
import { enqueueSnackbar } from "notistack";
import {
HistoryRecord,
HistoryRecord2,
sendReportById,
} from "@root/api/history";
import AccordionWrapper2 from "./AccordionWrapper2";
const subPages = ["Платежи"];
// const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]
export default function History() {
const [selectedItem, setSelectedItem] = useState<number>(0);
const [selectedItem, setSelectedItem] = useState<number>(0);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const historyData = useHistoryStore(state => state.history);
const handleCustomBackNavigation = useHistoryTracker();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const historyData = useHistoryStore((state) => state.history);
const handleCustomBackNavigation = useHistoryTracker();
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/);
return dateMatch ? dateMatch[0] : "";
};
const extractDateFromString = (tariffName: string) => {
const dateMatch = tariffName.match(/\d{4}-\d{2}-\d{2}/);
return dateMatch ? dateMatch[0] : "";
};
async function handleHistoryResponse(tariffId: string) {
try {
await makeRequest(
{
url: process.env.REACT_APP_DOMAIN + `/customer/sendReport/${tariffId}`,
method: "POST",
}
);
enqueueSnackbar("Запрос отправлен");
} catch (e) {
enqueueSnackbar("извините, произошла ошибка");
}
async function handleHistoryResponse(tariffId: string) {
const [, sendReportError] = await sendReportById(tariffId);
if (sendReportError) {
return enqueueSnackbar(sendReportError);
}
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
px: isTablet ? (isTablet ? "18px" : "40px") : "20px",
}}
enqueueSnackbar("Запрос отправлен");
}
return (
<SectionWrapper
maxWidth="lg"
sx={{
mt: upMd ? "25px" : "20px",
mb: upMd ? "70px" : "37px",
px: isTablet ? (isTablet ? "18px" : "40px") : "20px",
}}
>
<Box
sx={{
mt: "20px",
mb: isTablet ? "38px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton
onClick={handleCustomBackNavigation}
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
<Box
sx={{
mt: "20px",
mb: isTablet ? "38px" : "20px",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile && (
<IconButton onClick={handleCustomBackNavigation} sx={{ p: 0, height: "28px", width: "28px", color: "black" }}>
<ArrowBackIcon />
</IconButton>
)}
<Typography
sx={{
fontSize: isMobile ? "24px" : "36px",
fontWeight: "500",
}}
>
История
</Typography>
</Box>
{isMobile ? (
<Select items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
) : (
<Tabs items={subPages} selectedItem={selectedItem} setSelectedItem={setSelectedItem} />
)}
<ErrorBoundary
fallback={
<Typography mt="8px">Ошибка загрузки истории</Typography>
}
onError={handleComponentError}
>
{historyData?.length === 0 && <Typography textAlign="center">Нет данных</Typography>}
{/* Для ненормального rawDetails */}
{historyData?.filter((e): e is HistoryRecord => {
e.createdAt = extractDateFromString(e.createdAt);
return (
!e.isDeleted
&& e.key === "payCart"
&& Array.isArray(e.rawDetails)
&& Array.isArray(e.rawDetails[0].Value)
);
}).map((e, index) => {
return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper
first={index === 0}
last={index === historyData?.length - 1}
content={(e as HistoryRecord).rawDetails}
mainId={(e as HistoryRecord).id}
key={e.id}
createdAt={e.createdAt}
onClickMail={(event: any) => {
event.stopPropagation();
handleHistoryResponse(e.id);
}}
/>
</Box>
);
})}
{/* Для нормального rawDetails */}
{historyData?.filter((e): e is HistoryRecord2 => {
e.createdAt = extractDateFromString(e.createdAt);
return (
!e.isDeleted
&& e.key === "payCart"
&& !Array.isArray(e.rawDetails)
&& !!e.rawDetails.tariffs[0]
);
}).map((e, index) => {
return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper2
key={e.id}
first={index === 0}
last={index === historyData?.length - 1}
mainId={(e as HistoryRecord2).id}
createdAt={e.createdAt}
tariff={e.rawDetails.tariffs[0]}
price={e.rawDetails.price/100}
/>
</Box>
);
})}
</ErrorBoundary>
</SectionWrapper>
);
История
</Typography>
</Box>
{isMobile ? (
<Select
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
) : (
<Tabs
items={subPages}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
)}
<ErrorBoundary
fallback={<Typography mt="8px">Ошибка загрузки истории</Typography>}
onError={handleComponentError}
>
{historyData?.length === 0 && (
<Typography textAlign="center">Нет данных</Typography>
)}
{/* Для ненормального rawDetails */}
{historyData
?.filter((e): e is HistoryRecord => {
e.createdAt = extractDateFromString(e.createdAt);
return (
!e.isDeleted &&
e.key === "payCart" &&
Array.isArray(e.rawDetails) &&
Array.isArray(e.rawDetails[0].Value)
);
})
.map((e, index) => {
return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper
first={index === 0}
last={index === historyData?.length - 1}
content={(e as HistoryRecord).rawDetails}
mainId={(e as HistoryRecord).id}
key={e.id}
createdAt={e.createdAt}
onClickMail={(event: any) => {
event.stopPropagation();
handleHistoryResponse(e.id);
}}
/>
</Box>
);
})}
{/* Для нормального rawDetails */}
{historyData
?.filter((e): e is HistoryRecord2 => {
e.createdAt = extractDateFromString(e.createdAt);
return (
!e.isDeleted &&
e.key === "payCart" &&
!Array.isArray(e.rawDetails) &&
!!e.rawDetails.tariffs[0]
);
})
.map((e, index) => {
return (
<Box key={index} sx={{ mt: index === 0 ? "27px" : "0px" }}>
<AccordionWrapper2
key={e.id}
first={index === 0}
last={index === historyData?.length - 1}
mainId={(e as HistoryRecord2).id}
createdAt={e.createdAt}
tariff={e.rawDetails.tariffs[0]}
price={e.rawDetails.price / 100}
/>
</Box>
);
})}
</ErrorBoundary>
</SectionWrapper>
);
}

@ -1,5 +1,4 @@
import { useEffect, useState } from "react";
import axios, { AxiosResponse } from "axios";
import { ApologyPage } from "../ApologyPage";
import { useNavigate } from "react-router-dom";
import {
@ -22,19 +21,6 @@ import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
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 action = params.get("action");
const dif = params.get("dif");

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

@ -21,7 +21,6 @@ import { setUserId, useUserStore } from "@root/stores/user";
import { cardShadow } from "@root/utils/theme";
import AmoButton from "./AmoButton";
import { recover } from "@root/api/auth";
import {AxiosError} from "axios"
interface Values {
email: string;

@ -1,12 +1,12 @@
import {
Box,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
Button,
Box,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
Button,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { useLocation, useNavigate } from "react-router-dom";
@ -19,175 +19,180 @@ import { useEffect, useState } from "react";
import { useUserStore } from "@root/stores/user";
import { cardShadow } from "@root/utils/theme";
import makeRequest from "@api/makeRequest"
import { setAuthToken } from "@frontend/kitui"
import { patchUser } from "@api/user";
import { setAuthToken } from "@frontend/kitui";
interface Values {
password: string;
password: string;
}
const initialValues: Values = {
password: "",
password: "",
};
const validationSchema = object({
password: string()
.min(8, "Минимум 8 символов")
.matches(/^[.,:;\-_+!&()*<>\[\]\{\}`@"#$\%\^\=?\d\w]+$/, "Некорректные символы")
.required("Поле обязательно"),
password: string()
.min(8, "Минимум 8 символов")
.matches(
/^[.,:;\-_+!&()*<>\[\]\{\}`@"#$\%\^\=?\d\w]+$/,
"Некорректные символы"
)
.required("Поле обязательно"),
});
export default function RecoverPassword() {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true);
const [tokenUser, setTokenUser] = useState<string | null>("");
const user = useUserStore((state) => state.user);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const location = useLocation();
const formik = useFormik<Values>({
initialValues,
validationSchema,
onSubmit: async (values, formikHelpers) => {
if (tokenUser) {
setAuthToken(tokenUser || "")
try {
const response = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/user/",
method: "PATCH",
body: {password: values.password},
});
setIsDialogOpen(false)
navigate("/")
enqueueSnackbar("Пароль успешно сменён")
} catch (error) {
setAuthToken("")
enqueueSnackbar("Извините, произошла ошибка, попробуйте повторить позже")}
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true);
const [tokenUser, setTokenUser] = useState<string | null>("");
const user = useUserStore((state) => state.user);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const location = useLocation();
const formik = useFormik<Values>({
initialValues,
validationSchema,
onSubmit: async (values, formikHelpers) => {
if (tokenUser) {
setAuthToken(tokenUser || "");
const [, patchUserError] = await patchUser({
password: values.password,
});
} else {
enqueueSnackbar("Неверный url-адрес")
}
},
});
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const authToken = params.get("auth")
setTokenUser(authToken)
if (!patchUserError) {
setIsDialogOpen(false);
navigate("/");
enqueueSnackbar("Пароль успешно сменён");
} else {
setAuthToken("");
enqueueSnackbar(
"Извините, произошла ошибка, попробуйте повторить позже"
);
}
} else {
enqueueSnackbar("Неверный url-адрес");
}
},
});
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const authToken = params.get("auth");
setTokenUser(authToken);
history.pushState(null, document.title, "/changepwd");
return () => {setAuthToken("")}
}, []);
history.pushState(null, document.title, "/changepwd");
return () => {
setAuthToken("");
};
}, []);
function handleClose() {
setIsDialogOpen(false);
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
}
function handleClose() {
setIsDialogOpen(false);
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
}
return (
<Dialog
open={isDialogOpen}
onClose={handleClose}
PaperProps={{
sx: {
width: "600px",
maxWidth: "600px",
},
}}
slotProps={{
backdrop: {
style: {
backgroundColor: "rgb(0 0 0 / 0.7)",
},
},
}}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: cardShadow,
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled":
{
position: "absolute",
top: "46px",
margin: "0",
},
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton>
<Box>
<PenaLogo width={upMd ? 233 : 196} color="black" />
</Box>
<Typography
sx={{
color: theme.palette.gray.dark,
mt: "5px",
mb: upMd ? "10px" : "33px",
}}
>
Введите новый пароль
</Typography>
<InputTextfield
TextfieldProps={{
value: formik.values.password,
placeholder: "введите пароль",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
}}
onChange={formik.handleChange}
color="#F2F3F7"
id="password"
label="Новый пароль"
gap={upMd ? "10px" : "10px"}
/>
<Button
variant="pena-contained-dark"
fullWidth
type="submit"
disabled={formik.isSubmitting}
sx={{
py: "12px",
"&:hover": {
backgroundColor: theme.palette.purple.dark,
},
"&:active": {
color: "white",
backgroundColor: "black",
},
}}
>
Восстановить
</Button>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
mt: "auto",
}}
>
</Box>
</Box>
</Dialog>
);
return (
<Dialog
open={isDialogOpen}
onClose={handleClose}
PaperProps={{
sx: {
width: "600px",
maxWidth: "600px",
},
}}
slotProps={{
backdrop: {
style: {
backgroundColor: "rgb(0 0 0 / 0.7)",
},
},
}}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: cardShadow,
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled":
{
position: "absolute",
top: "46px",
margin: "0",
},
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton>
<Box>
<PenaLogo width={upMd ? 233 : 196} color="black" />
</Box>
<Typography
sx={{
color: theme.palette.gray.dark,
mt: "5px",
mb: upMd ? "10px" : "33px",
}}
>
Введите новый пароль
</Typography>
<InputTextfield
TextfieldProps={{
value: formik.values.password,
placeholder: "введите пароль",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
}}
onChange={formik.handleChange}
color="#F2F3F7"
id="password"
label="Новый пароль"
gap={upMd ? "10px" : "10px"}
/>
<Button
variant="pena-contained-dark"
fullWidth
type="submit"
disabled={formik.isSubmitting}
sx={{
py: "12px",
"&:hover": {
backgroundColor: theme.palette.purple.dark,
},
"&:active": {
color: "white",
backgroundColor: "black",
},
}}
>
Восстановить
</Button>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
mt: "auto",
}}
></Box>
</Box>
</Dialog>
);
}

@ -1,42 +1,35 @@
import { ErrorInfo } from "react"
import { ErrorInfo } from "react";
interface ComponentError {
timestamp: number;
message: string;
callStack: string | undefined;
componentStack: string | null | undefined;
timestamp: number;
message: string;
callStack: string | undefined;
componentStack: string | null | undefined;
}
export function handleComponentError(error: Error, info: ErrorInfo) {
const componentError: ComponentError = {
timestamp: Math.floor(Date.now() / 1000),
message: error.message,
callStack: error.stack,
componentStack: info.componentStack,
}
const componentError: ComponentError = {
timestamp: Math.floor(Date.now() / 1000),
message: error.message,
callStack: error.stack,
componentStack: info.componentStack,
};
queueErrorRequest(componentError)
queueErrorRequest(componentError);
}
let errorsQueue: ComponentError[] = []
let timeoutId: ReturnType<typeof setTimeout>
let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout>;
function queueErrorRequest(error: ComponentError) {
errorsQueue.push(error)
errorsQueue.push(error);
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
sendErrorsToServer()
}, 1000)
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
sendErrorsToServer();
}, 1000);
}
async function sendErrorsToServer() {
// makeRequest({
// url: "",
// method: "POST",
// body: errorsQueue,
// useToken: true,
// });
errorsQueue = []
errorsQueue = [];
}