Merge branch 'dev' into 'staging'

Dev

See merge request frontend/squiz!390
This commit is contained in:
Nastya 2025-01-18 13:09:32 +00:00
commit cca1877021
12 changed files with 401 additions and 43 deletions

@ -23,6 +23,7 @@ import { InfoPrivilege } from "./pages/InfoPrivilege";
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
import Landing from "./pages/Landing/Landing";
import Main from "./pages/main";
import { ErrorBoundary } from "react-error-boundary";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));

@ -8,6 +8,7 @@ import { clearUserData } from "@root/user";
import { clearQuizData } from "@root/quizes/store";
import type { AxiosResponse } from "axios";
import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils";
interface MakeRequest {
method?: Method | undefined;
@ -32,6 +33,11 @@ export const makeRequest = async <TRequest = unknown, TResponse = unknown>(
} catch (nativeError) {
const error = nativeError as AxiosError;
selectSendingMethod({
messageField: `status: ${error.response?.status}. Message ${(error.response?.data as ExtendedAxiosResponse)?.message}`,
isSnackbar: false,
systemError: true
});
if (
error.response?.status === 400 &&
(error.response?.data as ExtendedAxiosResponse)?.message ===

@ -12,7 +12,7 @@ export const getTariffs = async (
try {
const tariffs = await makeRequest<never, GetTariffsResponse>({
method: "GET",
url: `${API_URL}?page=${page}&limit=100`,
url: `${API_URL}/getList?page=${page}&limit=100`,
});
return [tariffs];
} catch (nativeError) {

@ -18,6 +18,7 @@ const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`;
export const sendTicketMessage = async (
ticketId: string,
message: string,
systemError: boolean
): Promise<[null, string?]> => {
try {
const sendTicketMessageResponse = await makeRequest<
@ -27,7 +28,8 @@ export const sendTicketMessage = async (
url: `${API_URL}/send`,
method: "POST",
useToken: true,
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
body: { ticket: ticketId, message: message, lang: "ru", files: [], system: systemError },
});
return [sendTicketMessageResponse];
@ -82,11 +84,12 @@ export const sendFile = async (
export const createTicket = async (
message: string,
useToken: boolean,
systemError: boolean
): Promise<[CreateTicketResponse | null, string?]> => {
try {
const createdTicket = await createTicketRequest({
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/create`,
body: { Title: "Unauth title", Message: message },
body: { Title: "Unauth title", Message: message, system: systemError },
useToken,
});

@ -17,6 +17,8 @@ import CloseIcon from "@icons/CloseBold";
import type { SnackbarKey } from "notistack";
import { CheckFastlink } from "@ui_kit/CheckFastlink";
import { ErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "./utils/handleComponentError";
moment.locale("ru");
polyfillCountryFlagEmojis();
@ -36,6 +38,8 @@ const snackbarAction = (snackbarId: SnackbarKey) => (
</Button>
);
const ApologyPage = () => <div><p>Что-то пошло не так</p></div>
const root = createRoot(document.getElementById("root")!);
root.render(
@ -60,7 +64,13 @@ root.render(
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<App />
<ErrorBoundary
FallbackComponent={ApologyPage}
onError={handleComponentError}
>
<App />
</ErrorBoundary>
<CheckFastlink />
</SnackbarProvider>
</BrowserRouter>

@ -128,10 +128,6 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que
})
}
console.log("got questions")
console.log(gottenQuestions)
console.log("settingsResponse")
console.log(settingsResponse.FieldsRule)
if (key === "Contact") {
const MAP = settingsResponse.FieldsRule[key as QuestionKeys].ContactRuleMap

@ -74,7 +74,7 @@ function TariffPage() {
const tariffsList: Tariff[] = [];
let page = 2
const [tariffsResponse, tariffsResponseError] = await getTariffs(page - 1);
console.log(tariffsResponse)
if (tariffsResponseError || !tariffsResponse) {
return tariffsList;
}
@ -182,7 +182,7 @@ function TariffPage() {
inCart();
};
const filteredTariffs = tariffs.filter((tariff) => {
const filteredTariffs = tariffs.filter((tariff, i) => {
return (
tariff.privileges[0].serviceKey === "squiz" &&
!tariff.isDeleted &&

@ -17,6 +17,8 @@ export const createTariffElements = (
cc?: boolean,
icon?: ReactNode
) => {
console.log("start work createTariffElements")
console.log("filteredTariffs ", filteredTariffs)
const tariffElements = filteredTariffs
.filter((tariff) => tariff.privileges.length > 0)
.map((tariff, index) => {

@ -22,6 +22,7 @@ import {
import { enqueueSnackbar } from "notistack";
import { parseAxiosError } from "@utils/parse-error";
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
import { selectSendingMethod } from "./utils";
type ModalWarningType =
| "errorType"
@ -47,10 +48,9 @@ const ACCEPT_SEND_FILE_TYPES_MAP = [
export default () => {
const user = useUserStore((state) => state.user?._id);
const ticket = useTicketStore(
(state) => state[user ? "authData" : "unauthData"],
);
const ticket = useTicketStore(state => state[user ? "authData" : "unauthData"]);
//Запись SEE в учётник
const { isActiveSSETab, updateSSEValue } = useSSETab<TicketMessage[]>(
"ticket",
addOrUpdateUnauthMessages,
@ -192,40 +192,13 @@ export default () => {
const sendMessage = async (messageField: string) => {
if (!messageField || ticket.isMessageSending) return false;
setSseEnabled(true);
let successful = false;
setIsMessageSending(true);
if (!ticket.sessionData?.ticketId) {
const [data, createError] = await createTicket(
messageField,
Boolean(user),
);
if (createError || !data) {
successful = false;
enqueueSnackbar(createError);
} else {
successful = true;
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
}
setIsMessageSending(false);
} else {
const [_, sendTicketMessageError] = await sendTicketMessage(
ticket.sessionData?.ticketId,
messageField,
);
successful = true;
if (sendTicketMessageError) {
successful = false;
enqueueSnackbar(sendTicketMessageError);
}
setIsMessageSending(false);
}
let successful = await selectSendingMethod({messageField});
setIsMessageSending(false);
return successful;
};
const sendFile = async (file: File) => {

@ -0,0 +1,266 @@
import { sendTicketMessage, shownMessage } from "@/api/ticket";
import { useSSETab } from "@/utils/hooks/useSSETab";
import { parseAxiosError } from "@/utils/parse-error";
import { TicketMessage, createTicket, useSSESubscription, useTicketMessages, useTicketsFetcher, sendFile as sf } from "@frontend/kitui";
import {
addOrUpdateUnauthMessages,
cleanAuthTicketData,
cleanUnauthTicketData,
setIsMessageSending,
setTicketData,
setUnauthIsPreventAutoscroll,
setUnauthTicketMessageFetchState,
useTicketStore,
} from "@root/ticket";
import { enqueueSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useState } from "react";
interface Props {
userId?: string;
}
type ModalWarningType =
| "errorType"
| "errorSize"
| "picture"
| "video"
| "audio"
| "document"
| null;
const MAX_FILE_SIZE = 419430400;
const ACCEPT_SEND_FILE_TYPES_MAP = [
".jpeg",
".jpg",
".png",
".mp4",
".doc",
".docx",
".pdf",
".txt",
".xlsx",
".csv",
] as const;
export default ({ userId }:Props) => {
const ticket = useTicketStore((state) => state[userId ? "authData" : "unauthData"]);
const { isActiveSSETab, updateSSEValue } = useSSETab<TicketMessage[]>(
"ticket",
addOrUpdateUnauthMessages,
);
const [modalWarningType, setModalWarningType] =
useState<ModalWarningType>(null);
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
const [sseEnabled, setSseEnabled] = useState(true);
const handleChatClickOpen = () => {
setIsChatOpened(true);
};
const handleChatClickClose = () => {
setIsChatOpened(false);
};
const handleChatClickSwitch = () => {
setIsChatOpened((state) => !state);
};
const getGreetingMessage: TicketMessage = useMemo(() => {
const workingHoursMessage =
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
const offHoursMessage =
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
const date = new Date();
const currentHourUTC = date.getUTCHours();
const MscTime = 3; // Москва UTC+3;
const moscowHour = (currentHourUTC + MscTime) % 24;
const greetingMessage =
moscowHour >= 3 && moscowHour < 10
? offHoursMessage
: workingHoursMessage;
return {
created_at: new Date().toISOString(),
files: [],
id: "111",
message: greetingMessage,
request_screenshot: "",
session_id: "greetingMessage",
shown: { me: 1 },
ticket_id: "111",
user_id: "greetingMessage",
};
}, [isChatOpened]);
useTicketsFetcher({
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
ticketsPerPage: 10,
ticketApiPage: 0,
onSuccess: (result) => {
if (result.data?.length) {
const currentTicket = result.data.find(
({ origin }) => !origin.includes("/support"),
);
if (!currentTicket) {
return;
}
setTicketData({
ticketId: currentTicket.id,
sessionId: currentTicket.sess,
});
}
},
onError: (error: Error) => {
const message = parseAxiosError(error);
if (message) enqueueSnackbar(message);
},
onFetchStateChange: () => {},
enabled: Boolean(userId),
});
useTicketMessages({
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
isUnauth: true,
ticketId: ticket.sessionData?.ticketId,
messagesPerPage: ticket.messagesPerPage,
messageApiPage: ticket.apiPage,
onSuccess: useCallback((messages) => {
addOrUpdateUnauthMessages(messages);
}, []),
onError: useCallback((error: Error) => {
if (error.name === "CanceledError") {
return;
}
const [message] = parseAxiosError(error);
if (message) enqueueSnackbar(message);
}, []),
onFetchStateChange: setUnauthTicketMessageFetchState,
});
useSSESubscription<TicketMessage>({
enabled:
sseEnabled && isActiveSSETab && Boolean(ticket.sessionData?.sessionId),
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/ticket?ticket=${ticket.sessionData?.ticketId}&s=${ticket.sessionData?.sessionId}`,
onNewData: (ticketMessages) => {
const isTicketClosed = ticketMessages.some(
(message) => message.session_id === "close",
);
if (isTicketClosed) {
cleanAuthTicketData();
addOrUpdateUnauthMessages([getGreetingMessage]);
if (!userId) {
cleanUnauthTicketData();
localStorage.removeItem("unauth-ticket");
}
return;
}
updateSSEValue(ticketMessages);
addOrUpdateUnauthMessages(ticketMessages);
},
onDisconnect: useCallback(() => {
setUnauthIsPreventAutoscroll(false);
setSseEnabled(false);
}, []),
marker: "ticket",
});
useEffect(() => {
cleanAuthTicketData();
setSseEnabled(true);
}, [userId]);
useEffect(() => {
if (isChatOpened) {
const newMessages = ticket.messages.filter(
({ shown }) => shown?.me !== 1,
);
newMessages.map(async ({ id }) => {
await shownMessage(id);
});
}
}, [isChatOpened, ticket.messages]);
const sendMessage = async (messageField: string) => {
if (!messageField || ticket.isMessageSending) return false;
setSseEnabled(true);
let successful = false;
setIsMessageSending(true);
if (!ticket.sessionData?.ticketId) {
const [data, createError] = await createTicket(
messageField,
Boolean(userId),
);
if (createError || !data) {
successful = false;
enqueueSnackbar(createError);
} else {
successful = true;
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
}
setIsMessageSending(false);
} else {
const [_, sendTicketMessageError] = await sendTicketMessage(
ticket.sessionData?.ticketId,
messageField,
);
successful = true;
if (sendTicketMessageError) {
successful = false;
enqueueSnackbar(sendTicketMessageError);
}
setIsMessageSending(false);
}
return successful;
};
const sendFile = async (file: File) => {
if (file === undefined) return true;
let ticketId = ticket.sessionData?.ticketId;
if (!ticket.sessionData?.ticketId) {
const [data, createError] = await createTicket("", Boolean(userId));
ticketId = data?.Ticket;
if (createError || !data) {
enqueueSnackbar(createError);
} else {
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
}
setIsMessageSending(false);
}
if (ticketId !== undefined) {
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
const [_, sendFileError] = await sf(ticketId, file);
if (sendFileError) {
enqueueSnackbar(sendFileError);
}
return true;
}
};
return {
isChatOpened,
handleChatClickOpen,
handleChatClickClose,
handleChatClickSwitch,
sendMessage,
sendFile,
modalWarningType,
setModalWarningType,
getGreetingMessage
};
};

@ -0,0 +1,54 @@
import { sendTicketMessage } from "@/api/ticket";
import { setTicketData, useTicketStore } from "@/stores/ticket";
import { useUserStore } from "@root/user";
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
import { enqueueSnackbar } from "notistack";
interface SelectSendingMethod {
messageField: string;
isSnackbar?: boolean;
systemError?: boolean;
}
export const selectSendingMethod = async ({messageField, isSnackbar = true, systemError = false}: SelectSendingMethod) => {
console.log("click")
const user = useUserStore.getState().user?._id;
const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"];
console.log(ticket)
console.log("click 2")
let successful = false;
if (!ticket.sessionData?.ticketId) {
console.log("autorisated 2")
const [data, createError] = await createTicket(
messageField,
Boolean(user),
systemError,
);
if (createError || !data) {
successful = false;
if (isSnackbar) enqueueSnackbar(createError);
} else {
successful = true;
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
}
} else {
const [_, sendTicketMessageError] = await sendTicketMessage(
ticket.sessionData?.ticketId,
messageField,
systemError,
);
successful = true;
if (sendTicketMessageError) {
successful = false;
if (isSnackbar) enqueueSnackbar(sendTicketMessageError);
}
}
return successful;
}

@ -0,0 +1,47 @@
import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils";
import { ErrorInfo } from "react";
interface ComponentError {
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,
};
queueErrorRequest(componentError);
}
let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout>;
function queueErrorRequest(error: ComponentError) {
errorsQueue.push(error);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
sendErrorsToServer();
}, 1000);
}
async function sendErrorsToServer() {
// makeRequest({
// url: "",
// method: "POST",
// body: errorsQueue,
// useToken: true,
// });
selectSendingMethod({
messageField: `Fake-sending ${errorsQueue.length} errors to server ${JSON.stringify(errorsQueue)}`,
isSnackbar: false,
systemError: true
});
// errorsQueue = [];
}