diff --git a/src/App.tsx b/src/App.tsx index 5626f416..d8daf4b4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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")); diff --git a/src/api/makeRequest.ts b/src/api/makeRequest.ts index 930a89b3..b969d870 100644 --- a/src/api/makeRequest.ts +++ b/src/api/makeRequest.ts @@ -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 ( } 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 === diff --git a/src/api/tariff.ts b/src/api/tariff.ts index a1b4ea3d..a0be6e70 100644 --- a/src/api/tariff.ts +++ b/src/api/tariff.ts @@ -12,7 +12,7 @@ export const getTariffs = async ( try { const tariffs = await makeRequest({ method: "GET", - url: `${API_URL}?page=${page}&limit=100`, + url: `${API_URL}/getList?page=${page}&limit=100`, }); return [tariffs]; } catch (nativeError) { diff --git a/src/api/ticket.ts b/src/api/ticket.ts index 48404ed3..5056dc64 100644 --- a/src/api/ticket.ts +++ b/src/api/ticket.ts @@ -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, }); diff --git a/src/index.tsx b/src/index.tsx index 3e3fe6e1..86b5737e 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -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) => ( ); +const ApologyPage = () =>

Что-то пошло не так

+ const root = createRoot(document.getElementById("root")!); root.render( @@ -60,7 +64,13 @@ root.render( style={{ backgroundColor: lightTheme.palette.brightPurple.main }} > - + + + + diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts b/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts index 6afafbd2..e420f921 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts +++ b/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts @@ -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 diff --git a/src/pages/Tariffs/Tariffs.tsx b/src/pages/Tariffs/Tariffs.tsx index 24f1bc0e..49b63c1e 100644 --- a/src/pages/Tariffs/Tariffs.tsx +++ b/src/pages/Tariffs/Tariffs.tsx @@ -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 && diff --git a/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx index 704dd138..7aad4952 100644 --- a/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx +++ b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx @@ -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) => { diff --git a/src/ui_kit/FloatingSupportChat/index.tsx b/src/ui_kit/FloatingSupportChat/index.tsx index 145ce8de..eb17a552 100644 --- a/src/ui_kit/FloatingSupportChat/index.tsx +++ b/src/ui_kit/FloatingSupportChat/index.tsx @@ -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( "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) => { diff --git a/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts b/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts new file mode 100644 index 00000000..21430c7e --- /dev/null +++ b/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts @@ -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( + "ticket", + addOrUpdateUnauthMessages, + ); + + const [modalWarningType, setModalWarningType] = + useState(null); + const [isChatOpened, setIsChatOpened] = useState(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({ + 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 + }; +}; \ No newline at end of file diff --git a/src/ui_kit/FloatingSupportChat/utils.ts b/src/ui_kit/FloatingSupportChat/utils.ts new file mode 100644 index 00000000..dab6eddd --- /dev/null +++ b/src/ui_kit/FloatingSupportChat/utils.ts @@ -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; +} \ No newline at end of file diff --git a/src/utils/handleComponentError.ts b/src/utils/handleComponentError.ts new file mode 100644 index 00000000..4ad62fe0 --- /dev/null +++ b/src/utils/handleComponentError.ts @@ -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; + +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 = []; +}