diff --git a/.gitea/workflows/deployProd.yml b/.gitea/workflows/deployProd.yml index ad853873..cd705f0c 100644 --- a/.gitea/workflows/deployProd.yml +++ b/.gitea/workflows/deployProd.yml @@ -2,9 +2,8 @@ name: Deploy run-name: ${{ gitea.actor }} build image and push to container registry on: - push: - branches: - - 'main' + registry_package: + types: [published] jobs: CreateImage: diff --git a/.gitea/workflows/deployStaging.yml b/.gitea/workflows/deployStaging.yml index 88de5f30..97c15694 100644 --- a/.gitea/workflows/deployStaging.yml +++ b/.gitea/workflows/deployStaging.yml @@ -2,25 +2,30 @@ name: Deploy run-name: ${{ gitea.actor }} build image and push to container registry on: - push: - branches: - - 'staging' + registry_package: + types: [published] jobs: - CreateImage: - runs-on: [skeris] - uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p - with: - runner: skeris - secrets: - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + # CreateImage: + # runs-on: [skeris] + # uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p + # with: + # runner: skeris + # secrets: + # REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + # REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} DeployService: + if: contains(github.event.package.name, 'staging') runs-on: [frontstaging] - needs: CreateImage - uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7 - with: - runner: frontstaging - actionid: ${{ gitea.run_id }} + container: + image: gitea.pena:3000/penadevops/container-images/node-compose:main + env: + GITHUB_RUN_NUMBER: "${{ inputs.actionid }}" + volumes: + - /run/user/1000/docker/docker.sock:/run/user/1000/docker/docker.sock + steps: + - name: Check out repository code + uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1 + - run: compose -f deployments/staging/docker-compose.yaml up -d diff --git a/Containerfile b/Containerfile deleted file mode 100644 index 95c38239..00000000 --- a/Containerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM gitea.pena/penadevops/container-images/node:main as build - -WORKDIR /usr/app -COPY . . - - -RUN npm install --force && yarn cache clean -RUN psstat.sh "npm run build" - -FROM gitea.pena/penadevops/container-images/nginx:main as result -WORKDIR /usr/share/nginx/html -COPY --from=build /usr/app/build/ /usr/share/nginx/html -COPY hub.conf /etc/nginx/conf.d/default.conf diff --git a/Dockerfile b/Dockerfile index e69de29b..95c38239 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM gitea.pena/penadevops/container-images/node:main as build + +WORKDIR /usr/app +COPY . . + + +RUN npm install --force && yarn cache clean +RUN psstat.sh "npm run build" + +FROM gitea.pena/penadevops/container-images/nginx:main as result +WORKDIR /usr/share/nginx/html +COPY --from=build /usr/app/build/ /usr/share/nginx/html +COPY hub.conf /etc/nginx/conf.d/default.conf diff --git a/deployments/staging/docker-compose.yaml b/deployments/staging/docker-compose.yaml index 77b80c51..e9efd2cc 100644 --- a/deployments/staging/docker-compose.yaml +++ b/deployments/staging/docker-compose.yaml @@ -2,6 +2,7 @@ services: squiz: container_name: squiz restart: unless-stopped - image: gitea.pena/squiz/frontpanel/staging:$GITHUB_RUN_NUMBER + image: gitea.pena/squiz/frontpanel/staging:latest hostname: squiz tty: true + pull_policy: always diff --git a/package.json b/package.json index 3f8736f3..e356838d 100755 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@craco/craco": "^7.0.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", - "@frontend/kitui": "^1.0.108", + "@frontend/kitui": "1.0.110", "@frontend/squzanswerer": "^1.0.57", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", @@ -69,6 +69,7 @@ "test": "craco test", "eject": "craco eject", "code:format": "prettier --write --ignore-unknown", + "deploy": "docker login gitea.pena && docker build -t gitea.pena/squiz/frontpanel/$(git branch --show-current):latest . && docker push gitea.pena/squiz/frontpanel/$(git branch --show-current):latest", "prepare": "husky install", "cypress:open": "cypress open", "cypress:run": "cypress run" diff --git a/public/index.html b/public/index.html index 0df06af1..005d6ef6 100755 --- a/public/index.html +++ b/public/index.html @@ -165,7 +165,7 @@ console.log(params.get("debug")) if (params.get("debug")) { console.log( - "mhgfhdhfjhffhfhjfghjgf" + "params.get(debug) is true" ) let scriptTag = document.createElement('script'); scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js"); diff --git a/src/App.tsx b/src/App.tsx index d6195646..78bf452b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { clearAuthToken, getMessageFromFetchError, UserAccount, useUserFetcher } from "@frontend/kitui"; +import { clearAuthToken, createMakeRequestConfig, getMessageFromFetchError, handleComponentError, UserAccount, useTicketsFetcher, useUserFetcher } from "@frontend/kitui"; import type { OriginalUserAccount } from "@root/user"; import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user"; import ContactFormModal from "@ui_kit/ContactForm"; @@ -8,7 +8,7 @@ import { useAfterPay } from "@utils/hooks/useAutoPay"; import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher"; import { enqueueSnackbar } from "notistack"; import type { SuspenseProps } from "react"; -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { lazily } from "react-lazily"; import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { useAmoAccount } from "./api/integration"; @@ -23,7 +23,11 @@ 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 Debug from "./pages/Debug"; +import { setTicketData, setTickets, useTicketStore } from "./stores/ticket"; +import { parseAxiosError } from "./utils/parse-error"; import { ErrorBoundary } from "react-error-boundary"; +import { handleLogoutClick } from "./utils/HandleLogoutClick"; const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull")); const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery")); @@ -39,6 +43,12 @@ const PersonalizationAI = lazy(() => import("./pages/PersonalizationAI/Personali let params = new URLSearchParams(document.location.search); const isTest = Boolean(params.get("test")) +createMakeRequestConfig( + handleLogoutClick, + (error, info, getTickets) => handleComponentError(error, info, getTickets()), + () => useTicketStore.getState().tickets +); + const routeslink = [ { path: "/edit", @@ -73,12 +83,16 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => ( }>{children} ); +const ApologyPage = () =>

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

+ export default function App() { window.LoadingObserver = false; const userId = useUserStore((state) => state.userId); const location = useLocation(); const navigate = useNavigate(); const { data: amoAccount } = useAmoAccount(); + const tickets = useTicketStore(store => store.tickets); + useUserFetcher({ url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`, @@ -97,8 +111,11 @@ export default function App() { useUserAccountFetcher({ url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`, userId, - onNewUserAccount: setCustomerAccount, + onNewUserAccount: (account) => { + setCustomerAccount(account); + }, onError: (error) => { + console.error("App: Error in customerAccount fetcher:", error); const errorMessage = getMessageFromFetchError(error); if (errorMessage) { enqueueSnackbar(errorMessage); @@ -124,6 +141,37 @@ export default function App() { }, }); + useTicketsFetcher({ + url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`, + ticketsPerPage: 10, + ticketApiPage: 0, + onSuccess: (result) => { + if (result.data?.length) { + // Записываем все тикеты в стор + setTickets(result.data); + + 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), + }); + useAfterPay(); if (location.state?.redirectTo) @@ -136,7 +184,10 @@ export default function App() { ); return ( - <> + handleComponentError(error, info, () => useTicketStore.getState().tickets)} + > {amoAccount && } @@ -259,6 +310,10 @@ export default function App() { path={"/image/:srcImage"} element={} /> + } + /> }> {routeslink.map((e, i) => ( - + + {/* Компонент отладки ошибок - доступен по Ctrl+Shift+D */} + + ); } diff --git a/src/api/auth.ts b/src/api/auth.ts index b47d6fbf..5dfd5833 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/cart.ts b/src/api/cart.ts index d9ec8a24..a08fb904 100644 --- a/src/api/cart.ts +++ b/src/api/cart.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/contactForm.ts b/src/api/contactForm.ts index 6ed0f71d..df81d211 100644 --- a/src/api/contactForm.ts +++ b/src/api/contactForm.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/integration.ts b/src/api/integration.ts index 40f91b44..177c5bf0 100644 --- a/src/api/integration.ts +++ b/src/api/integration.ts @@ -1,5 +1,5 @@ import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types"; -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { useToken } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; import useSWR from "swr"; diff --git a/src/api/makeRequest.ts b/src/api/makeRequest.ts deleted file mode 100644 index ebc554b4..00000000 --- a/src/api/makeRequest.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as KIT from "@frontend/kitui"; -import { Method, ResponseType, AxiosError } from "axios"; -import { redirect } from "react-router-dom"; -import { clearAuthToken } from "@frontend/kitui"; - -import { cleanAuthTicketData } from "@root/ticket"; -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; - url: string; - body?: unknown; - useToken?: boolean | undefined; - contentType?: boolean | undefined; - responseType?: ResponseType | undefined; - signal?: AbortSignal | undefined; - withCredentials?: boolean | undefined; -} - -type ExtendedAxiosResponse = AxiosResponse & { message: string }; - -export const makeRequest = async ( - data: MakeRequest, -): Promise => { - try { - const response = await KIT.makeRequest(data); - - return response; - } catch (nativeError) { - const error = nativeError as AxiosError; - - // if (window.location.hostname !== 'localhost') 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 === - "refreshToken is empty" - ) { - cleanAuthTicketData(); - clearAuthToken(); - clearUserData(); - clearQuizData(); - redirect("/"); - } - - throw nativeError; - } -}; diff --git a/src/api/promocode.ts b/src/api/promocode.ts index 87e90a01..f20a7305 100644 --- a/src/api/promocode.ts +++ b/src/api/promocode.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/question.ts b/src/api/question.ts index f15fcee6..cf4f902b 100644 --- a/src/api/question.ts +++ b/src/api/question.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/quiz.ts b/src/api/quiz.ts index 1bcea534..e189a7be 100644 --- a/src/api/quiz.ts +++ b/src/api/quiz.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { defaultQuizConfig } from "@model/quizSettings"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/result.ts b/src/api/result.ts index 74d0a5d8..c04edea0 100644 --- a/src/api/result.ts +++ b/src/api/result.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/statistic.ts b/src/api/statistic.ts index 1fd3c245..8310deae 100644 --- a/src/api/statistic.ts +++ b/src/api/statistic.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/tariff.ts b/src/api/tariff.ts index 48efae84..d43b056d 100644 --- a/src/api/tariff.ts +++ b/src/api/tariff.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; import type { GetTariffsResponse } from "@frontend/kitui"; diff --git a/src/api/ticket.ts b/src/api/ticket.ts index 5056dc64..f6f5a181 100644 --- a/src/api/ticket.ts +++ b/src/api/ticket.ts @@ -1,6 +1,6 @@ import { createTicket as createTicketRequest } from "@frontend/kitui"; -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; @@ -15,47 +15,7 @@ type SendFileResponse = { 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< - SendTicketMessageRequest, - null - >({ - url: `${API_URL}/send`, - method: "POST", - useToken: true, - body: { ticket: ticketId, message: message, lang: "ru", files: [], system: systemError }, - - }); - return [sendTicketMessageResponse]; - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - - return [null, `Не удалось отправить сообщение. ${error}`]; - } -}; - -export const shownMessage = async (id: string): Promise<[null, string?]> => { - try { - const shownMessageResponse = await makeRequest<{ id: string }, null>({ - url: `${API_URL}/shown`, - method: "POST", - useToken: true, - body: { id }, - }); - - return [shownMessageResponse]; - } catch (nativeError) { - const [error] = parseAxiosError(nativeError); - - return [null, `Не удалось прочесть сообщение. ${error}`]; - } -}; export const sendFile = async ( ticketId: string, diff --git a/src/api/user.ts b/src/api/user.ts index 14b072c3..4f4c0d07 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,4 +1,4 @@ -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/assets/icons/logo/Postback.tsx b/src/assets/icons/logo/Postback.tsx new file mode 100644 index 00000000..23956784 --- /dev/null +++ b/src/assets/icons/logo/Postback.tsx @@ -0,0 +1,15 @@ +import { Box, SxProps } from "@mui/material"; + +export default (sx: SxProps) => ( + + + +); \ No newline at end of file diff --git a/src/assets/icons/logo/PostbackPC.tsx b/src/assets/icons/logo/PostbackPC.tsx new file mode 100644 index 00000000..1dff7213 --- /dev/null +++ b/src/assets/icons/logo/PostbackPC.tsx @@ -0,0 +1,19 @@ +import { Box, SxProps } from "@mui/material"; + +export default (sx: SxProps) => ( + + + + + +); \ No newline at end of file diff --git a/src/assets/icons/logo/zapier.png b/src/assets/icons/logo/zapier.png new file mode 100644 index 00000000..53f20b39 Binary files /dev/null and b/src/assets/icons/logo/zapier.png differ diff --git a/src/index.tsx b/src/index.tsx index 86b5737e..436554ae 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -18,7 +18,7 @@ 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"; +import { handleComponentError } from "@frontend/kitui"; moment.locale("ru"); polyfillCountryFlagEmojis(); @@ -38,7 +38,6 @@ const snackbarAction = (snackbarId: SnackbarKey) => ( ); -const ApologyPage = () =>

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

const root = createRoot(document.getElementById("root")!); @@ -65,12 +64,7 @@ root.render( > - - diff --git a/src/model/quizSettings.ts b/src/model/quizSettings.ts index 50050301..f04e8151 100644 --- a/src/model/quizSettings.ts +++ b/src/model/quizSettings.ts @@ -69,6 +69,8 @@ export type QuizTheme = export enum QuizMetricType { yandex = "yandexMetricsNumber", vk = "vkMetricsNumber", + zapier = "zapierIntegration", + postback = "postbackIntegration", } export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address"; diff --git a/src/pages/Analytics/General.tsx b/src/pages/Analytics/General.tsx index be9b7b88..78ce3a9f 100644 --- a/src/pages/Analytics/General.tsx +++ b/src/pages/Analytics/General.tsx @@ -42,16 +42,9 @@ const GeneralItem = ({ ([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue), ); const days = data.map(([value]) => value); - const { time } = data.reduce( - (total, [_, value]) => ({ - defaultValue: value > 0 ? value : total.defaultValue, - time: [...total.time, value > 0 ? value : total.defaultValue], - }), - { defaultValue: 0, time: [] as number[] }, - ); const numberValue = calculateTime - ? time.reduce((total, value) => total + value, 0) / days.length + ? Object.values(general).reduce((total, value) => total + value, 0) / Object.values(general).length : conversionValue ? conversionValue : Object.values(general).reduce((total, item) => total + item, 0); @@ -82,17 +75,17 @@ const GeneralItem = ({ { const timestamp = Number(value); if (isNaN(timestamp)) return 'Invalid Date'; - return moment.unix(timestamp).format(statiscticsResult ? "DD/MM/YYYY" : "DD/MM/YYYY HH") + (statiscticsResult ? "" : "ч"); + return moment.unix(timestamp).format("DD/MM/YYYY"); }, }, ]} series={[ { - data: Object.values(statiscticsResult ? time : general), + data: Object.values(general), valueFormatter: (value) => calculateTime ? getCalculatedTime(value) @@ -131,11 +124,21 @@ export const General: FC = ({ data, day }) => { const generalResponse = Object.entries(data).reduce( (total, [fatherKey, values]) => { const value = Object.keys(values).reduce((totalValue, key) => { - if (Number(key) - currentDate < 0) { + const keyTimestamp = Number(key); + const todayStart = moment().startOf('day').unix(); + const todayEnd = moment().endOf('day').unix(); + + // Включаем данные за сегодня и прошлые дни, исключаем будущие дни + if (keyTimestamp >= todayStart && keyTimestamp <= todayEnd) { + // Сегодняшний день - включаем return { ...totalValue, [key]: values[key] }; + } else if (keyTimestamp < todayStart) { + // Прошлые дни - включаем + return { ...totalValue, [key]: values[key] }; + } else { + // Будущие дни - исключаем + return totalValue; } - - return totalValue; }, {}); return { ...total, [fatherKey]: value }; diff --git a/src/pages/Debug.tsx b/src/pages/Debug.tsx new file mode 100644 index 00000000..bd9a64c7 --- /dev/null +++ b/src/pages/Debug.tsx @@ -0,0 +1,149 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Button, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper +} from '@mui/material'; + +// Функции для создания тестовых ошибок +const createTestError = (message: string) => { + const error = new Error(message); + (error as any).__forceSend = true; + throw error; +}; + +const createReactComponentError = (message: string) => { + const error = new Error(message); + (error as any).__forceSend = true; + throw error; +}; + +/** + * Простая страница отладки системы обработки ошибок + * Активируется по Ctrl+Shift+F + */ +const Debug: React.FC = () => { + const [isVisible, setIsVisible] = useState(false); + + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.ctrlKey && event.shiftKey && event.code === 'KeyF') { + event.preventDefault(); + setIsVisible(!isVisible); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isVisible]); + + if (!isVisible) { + return null; + } + + const errorTests = [ + { + description: 'Простая ошибка', + action: () => { + createTestError('Тестовая ошибка отладки'); + } + }, + { + description: 'React ошибка компонента', + action: () => { + createReactComponentError('Тестовая ошибка React компонента'); + } + }, + { + description: 'Прямая ошибка в обработчике', + action: () => { + const error = new Error('Прямая ошибка в компоненте'); + (error as any).__forceSend = true; + throw error; + } + }, + { + description: 'Ошибка с длинным стеком', + action: () => { + const deepError = new Error('Глубокая ошибка с длинным стеком'); + deepError.stack = 'Error: Глубокая ошибка\n at level1 (debug.ts:10)\n at level2 (debug.ts:15)\n at level3 (debug.ts:20)\n at level4 (debug.ts:25)'; + (deepError as any).__forceSend = true; + throw deepError; + } + }, + { + description: 'Ошибка с undefined', + action: () => { + const obj: any = null; + try { + obj.nonExistentMethod(); + } catch (error) { + (error as any).__forceSend = true; + throw error; + } + } + } + ]; + + return ( + + + + 🛠️ Отладка ошибок + + + + Ctrl+Shift+F для закрытия + + + + + + + Описание ошибки + Действие + + + + {errorTests.map((test, index) => ( + + {test.description} + + + + + ))} + +
+
+
+
+ ); +}; + +export default Debug; \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx index e73e45ac..084fc813 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/AmoQuestions.tsx @@ -150,10 +150,6 @@ export const AmoQuestions: FC = ({ }, [activeScope]) const [blockButton, setBlockButton] = useState(false) - console.log("selectedQuestions") - console.log(selectedQuestions) - console.log("SCFworld") - console.log(SCFworld) return ( <> (accountInfo ? "accountInfo" : "amoLogin") const [openDelete, setOpenDelete] = useState(null); - console.log("--") - console.log(selectedQuestions) const startDeleteTagQuestion = (itemForDelete) => { setOpenDelete(itemForDelete) @@ -135,9 +133,6 @@ export const SwitchPages = ({ if (type === "tag") { setSelectedTags((prevState) => { - console.log(prevState) - console.log(scope) - console.log(id) return({ ...prevState, [scope]: [...prevState[scope as TagKeys], id], @@ -147,9 +142,6 @@ export const SwitchPages = ({ if (type === "question") { const q = questions.find(e => e.backendId === Number(id)) setSelectedQuestions((prevState) => { - console.log(prevState) - console.log(scope) - console.log(id) return ({ ...prevState, [scope]: [...prevState[scope as QuestionKeys], { diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts b/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts index 5a983e76..a5a106df 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts +++ b/src/pages/IntegrationsPage/IntegrationsModal/Amo/useAmoIntegration.ts @@ -353,7 +353,7 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que }; }; -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`; export const resetAmoTagsFields = async () => { diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx new file mode 100644 index 00000000..3b7af294 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx @@ -0,0 +1,75 @@ +import { FC } from "react"; +import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import { Quiz } from "@/model/quiz/quiz"; + +type PostbackModalProps = { + isModalOpen: boolean; + handleCloseModal: () => void; + companyName: string | null; + quiz: Quiz; +}; + +export const PostbackModal: FC = ({ + isModalOpen, + handleCloseModal, + companyName, + quiz +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + + return ( + + + + + Интеграция с {companyName ? companyName : "Postback"} + + + + + + + + Интеграция с Postback находится в разработке. + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx new file mode 100644 index 00000000..39483a96 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx @@ -0,0 +1,75 @@ +import { FC } from "react"; +import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import { Quiz } from "@/model/quiz/quiz"; + +type ZapierModalProps = { + isModalOpen: boolean; + handleCloseModal: () => void; + companyName: string | null; + quiz: Quiz; +}; + +export const ZapierModal: FC = ({ + isModalOpen, + handleCloseModal, + companyName, + quiz +}) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + + return ( + + + + + Интеграция с {companyName ? companyName : "Zapier"} + + + + + + + + Интеграция с Zapier находится в разработке. + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsPage.tsx b/src/pages/IntegrationsPage/IntegrationsPage.tsx index b74e3650..ce2af29a 100644 --- a/src/pages/IntegrationsPage/IntegrationsPage.tsx +++ b/src/pages/IntegrationsPage/IntegrationsPage.tsx @@ -27,6 +27,8 @@ export const IntegrationsPage = ({ >(null); const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState(false); + const [isZapierModalOpen, setIsZapierModalOpen] = useState(false); + const [isPostbackModalOpen, setIsPostbackModalOpen] = useState(false); useEffect(() => { if (editQuizId === null) navigate("/list"); @@ -44,6 +46,12 @@ export const IntegrationsPage = ({ const handleCloseAmoSRMModal = () => { setIsAmoCrmModalOpen(false); }; + const handleCloseZapierModal = () => { + setIsZapierModalOpen(false); + }; + const handleClosePostbackModal = () => { + setIsPostbackModalOpen(false); + }; return ( <> @@ -60,7 +68,7 @@ export const IntegrationsPage = ({ > Интеграции @@ -73,6 +81,12 @@ export const IntegrationsPage = ({ setIsAmoCrmModalOpen={setIsAmoCrmModalOpen} isAmoCrmModalOpen={isAmoCrmModalOpen} handleCloseAmoSRMModal={handleCloseAmoSRMModal} + setIsZapierModalOpen={setIsZapierModalOpen} + isZapierModalOpen={isZapierModalOpen} + handleCloseZapierModal={handleCloseZapierModal} + setIsPostbackModalOpen={setIsPostbackModalOpen} + isPostbackModalOpen={isPostbackModalOpen} + handleClosePostbackModal={handleClosePostbackModal} /> diff --git a/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx index 6612b444..6cb28b6d 100644 --- a/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx +++ b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx @@ -1,13 +1,13 @@ -import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { Box, Typography, useTheme } from "@mui/material"; import React, { FC, lazy, Suspense } from "react"; -import { ServiceButton } from "./ServiceButton/ServiceButton"; +import { ServiceButton } from "./buttons/ServiceButton"; +import { ZapierButton } from "./buttons/ZapierButton"; +import { PostbackButton } from "./buttons/PostbackButton"; import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo"; -// import AnalyticsModal from "./AnalyticsModal/AnalyticsModal"; import { VKPixelLogo } from "../mocks/VKPixelLogo"; import { QuizMetricType } from "@model/quizSettings"; import { AmoCRMLogo } from "../mocks/AmoCRMLogo"; import { useCurrentQuiz } from "@/stores/quizes/hooks"; -import { useUserStore } from "@/stores/user"; const AnalyticsModal = lazy(() => import("./AnalyticsModal/AnalyticsModal").then((module) => ({ @@ -21,6 +21,18 @@ const AmoCRMModal = lazy(() => })) ); +const ZapierModal = lazy(() => + import("../IntegrationsModal/Zapier").then((module) => ({ + default: module.ZapierModal, + })) +); + +const PostbackModal = lazy(() => + import("../IntegrationsModal/Postback").then((module) => ({ + default: module.PostbackModal, + })) +); + type PartnersBoardProps = { setIsModalOpen: (value: boolean) => void; companyName: keyof typeof QuizMetricType | null; @@ -30,6 +42,12 @@ type PartnersBoardProps = { setIsAmoCrmModalOpen: (value: boolean) => void; isAmoCrmModalOpen: boolean; handleCloseAmoSRMModal: () => void; + setIsZapierModalOpen: (value: boolean) => void; + isZapierModalOpen: boolean; + handleCloseZapierModal: () => void; + setIsPostbackModalOpen: (value: boolean) => void; + isPostbackModalOpen: boolean; + handleClosePostbackModal: () => void; }; export const PartnersBoard: FC = ({ @@ -41,13 +59,31 @@ export const PartnersBoard: FC = ({ setIsAmoCrmModalOpen, isAmoCrmModalOpen, handleCloseAmoSRMModal, + setIsZapierModalOpen, + isZapierModalOpen, + handleCloseZapierModal, + setIsPostbackModalOpen, + isPostbackModalOpen, + handleClosePostbackModal, }) => { const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(600)); - const quiz = useCurrentQuiz(); - const user = useUserStore(); + const sectionTitleStyles = { + textAlign: { xs: "start", sm: "start", md: "start" } as const, + lineHeight: "1", + marginBottom: "12px", + marginTop: "20px", + color: theme.palette.grey3.main, + fontSize: "18px", + }; + + const containerStyles = { + display: "flex", + flexWrap: "wrap", + justifyContent: { xs: "start", sm: "start", md: "start" }, + gap: { xs: "15px", md: "20px" }, + }; return ( = ({ }} > - <> - - CRM - - - } - setIsModalOpen={setIsAmoCrmModalOpen} - setCompanyName={setCompanyName} - name={"amoCRM"} - /> - - + CRM + + + } + setIsModalOpen={setIsAmoCrmModalOpen} + setCompanyName={setCompanyName} + name={"amoCRM"} + /> + + + Аналитика - + } setIsModalOpen={setIsModalOpen} @@ -114,9 +129,24 @@ export const PartnersBoard: FC = ({ name={"vk"} setIsModalOpen={setIsModalOpen} setCompanyName={setCompanyName} - > + /> + + + + Автоматизация + + + + + {companyName && ( = ({ isModalOpen={isAmoCrmModalOpen} handleCloseModal={handleCloseAmoSRMModal} companyName={companyName} - quiz={quiz} + quiz={quiz!} + /> + + )} + {companyName && isZapierModalOpen && ( + + + + )} + {companyName && isPostbackModalOpen && ( + + )} diff --git a/src/pages/IntegrationsPage/PartnersBoard/ServiceButton/ServiceButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/ServiceButton/ServiceButton.tsx deleted file mode 100644 index 4773e19f..00000000 --- a/src/pages/IntegrationsPage/PartnersBoard/ServiceButton/ServiceButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Box, Typography, useTheme } from "@mui/material"; -import { FC } from "react"; -import { QuizMetricType } from "@model/quizSettings"; - -type PartnerItemProps = { - setIsModalOpen: (value: boolean) => void; - setCompanyName: (value: keyof typeof QuizMetricType) => void; - logo?: JSX.Element; - title?: string; - name: string; -}; - -export const ServiceButton: FC = ({ - setIsModalOpen, - logo, - title, - name, - setCompanyName, -}) => { - const theme = useTheme(); - - const handleClick = () => { - setCompanyName(name as keyof typeof QuizMetricType); - setIsModalOpen(true); - }; - - return ( - <> - - {logo && logo} - - {title && title} - - - - ); -}; diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/IntegrationButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/IntegrationButton.tsx new file mode 100644 index 00000000..fe22cfd0 --- /dev/null +++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/IntegrationButton.tsx @@ -0,0 +1,53 @@ +import { Box } from "@mui/material"; +import { FC, useState, ReactNode } from "react"; + +type IntegrationButtonProps = { + children: ReactNode; + onClick: () => void; + padding?: string; +}; + +export const IntegrationButton: FC = ({ + children, + onClick, + padding = "0 20px", +}) => { + const [isPressed, setIsPressed] = useState(false); + + const handleMouseDown = () => setIsPressed(true); + const handleMouseUp = () => setIsPressed(false); + const handleMouseLeave = () => setIsPressed(false); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/PostbackButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/PostbackButton.tsx new file mode 100644 index 00000000..faf90d05 --- /dev/null +++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/PostbackButton.tsx @@ -0,0 +1,32 @@ +import { Box } from "@mui/material"; +import { FC } from "react"; +import { QuizMetricType } from "@model/quizSettings"; +import PostbackDefault from "@/assets/icons/logo/Postback"; +import PostbackPC from "@/assets/icons/logo/PostbackPC"; +import { IntegrationButton } from "./IntegrationButton"; + +type PostbackButtonProps = { + setIsModalOpen: (value: boolean) => void; + setCompanyName: (value: keyof typeof QuizMetricType) => void; +}; + +export const PostbackButton: FC = ({ + setIsModalOpen, + setCompanyName, +}) => { + const handleClick = () => { + setCompanyName("postback" as keyof typeof QuizMetricType); + setIsModalOpen(true); + }; + + return ( + + <> + {/* Иконка монитора */} + + {/* Текст Postback */} + + + + ); +}; \ No newline at end of file diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/ServiceButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/ServiceButton.tsx new file mode 100644 index 00000000..88a92fce --- /dev/null +++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/ServiceButton.tsx @@ -0,0 +1,40 @@ +import { Typography } from "@mui/material"; +import { FC } from "react"; +import { QuizMetricType } from "@model/quizSettings"; +import { IntegrationButton } from "./IntegrationButton"; + +type PartnerItemProps = { + setIsModalOpen: (value: boolean) => void; + setCompanyName: (value: keyof typeof QuizMetricType) => void; + logo?: JSX.Element; + title?: string; + name: string; +}; + +export const ServiceButton: FC = ({ + setIsModalOpen, + logo, + title, + name, + setCompanyName, +}) => { + const handleClick = () => { + setCompanyName(name as keyof typeof QuizMetricType); + setIsModalOpen(true); + }; + + return ( + + {logo && logo} + + {title && title} + + + ); +}; \ No newline at end of file diff --git a/src/pages/IntegrationsPage/PartnersBoard/buttons/ZapierButton.tsx b/src/pages/IntegrationsPage/PartnersBoard/buttons/ZapierButton.tsx new file mode 100644 index 00000000..8abcc70a --- /dev/null +++ b/src/pages/IntegrationsPage/PartnersBoard/buttons/ZapierButton.tsx @@ -0,0 +1,35 @@ +import { Box } from "@mui/material"; +import { FC } from "react"; +import { QuizMetricType } from "@model/quizSettings"; +import zapierLogo from "@/assets/icons/logo/zapier.png"; +import { IntegrationButton } from "./IntegrationButton"; + +type ZapierButtonProps = { + setIsModalOpen: (value: boolean) => void; + setCompanyName: (value: keyof typeof QuizMetricType) => void; +}; + +export const ZapierButton: FC = ({ + setIsModalOpen, + setCompanyName, +}) => { + const handleClick = () => { + setCompanyName("zapier" as keyof typeof QuizMetricType); + setIsModalOpen(true); + }; + + return ( + + + + ); +}; \ No newline at end of file diff --git a/src/pages/Payment/Payment.tsx b/src/pages/Payment/Payment.tsx new file mode 100644 index 00000000..5a410641 --- /dev/null +++ b/src/pages/Payment/Payment.tsx @@ -0,0 +1,12 @@ +import { useAuthRedirect } from "../../utils/hooks/useAuthRedirect"; + +export default function Payment() { + // Используем хук авторизации + const { isProcessing } = useAuthRedirect(); + + // Если идет обработка авторизации, показываем загрузку + if (isProcessing) { + return
Идёт загрузка...
; + } + + // ... existing component code ... \ No newline at end of file diff --git a/src/pages/PersonalizationAI/AuditoryList.tsx b/src/pages/PersonalizationAI/AuditoryList.tsx index a84c7e2a..1050535a 100644 --- a/src/pages/PersonalizationAI/AuditoryList.tsx +++ b/src/pages/PersonalizationAI/AuditoryList.tsx @@ -11,8 +11,6 @@ export const AuditoryList = ({utmParams, auditory, onDelete}:{utmParams:string,a const { isTestServer } = useDomainDefine(); const [linksOpen, setLinksOpen] = useState(true); - console.log("auditory-___---_auditory__---__-__auditory_------__---__-__---_------__---__-__---_------__---__-____--__") - console.log(auditory) return ( <> diff --git a/src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx b/src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx new file mode 100644 index 00000000..c92c20d1 --- /dev/null +++ b/src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx @@ -0,0 +1,112 @@ +import { Button, ClickAwayListener, Tooltip, useTheme } from "@mui/material"; +import { useState } from "react"; + +interface CreateButtonWithTooltipProps { + gender: string; + age: string; + ageError: boolean; + onClick: () => void; +} + +export default function CreateButtonWithTooltip({ + gender, + age, + ageError, + onClick +}: CreateButtonWithTooltipProps) { + const theme = useTheme(); + const [open, setOpen] = useState(false); + + const handleTooltipClose = () => { + setOpen(false); + }; + + const handleTooltipOpen = () => { + setOpen(true); + }; + + // Определяем причины неактивности кнопки + const getDisabledReasons = () => { + const reasons: string[] = []; + + if (!gender) { + reasons.push("выберите пол"); + } + + if (!age) { + reasons.push("заполните поле возраста"); + } + + if (ageError) { + reasons.push("исправьте ошибку в поле возраста"); + } + + return reasons; + }; + + const disabledReasons = getDisabledReasons(); + const isDisabled = !gender || !age || ageError; + const tooltipText = isDisabled + ? disabledReasons.length === 2 + ? disabledReasons.join(' и ') + : disabledReasons.join('\n') + : ''; + + return ( + +
+ {isDisabled && ( +
+ )} + + + +
+ + ); +} \ No newline at end of file diff --git a/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx b/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx index 720022f3..765afb3d 100644 --- a/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx +++ b/src/pages/PersonalizationAI/GenderAndAgeSelector.tsx @@ -1,7 +1,8 @@ -import { Box, FormControl, FormLabel, Checkbox, FormControlLabel, useTheme, Button, useMediaQuery } from "@mui/material"; +import { Box, FormControl, FormLabel, Checkbox, FormControlLabel, useTheme, useMediaQuery } from "@mui/material"; import CheckboxIcon from "@icons/Checkbox"; import AgeInputWithSelect from "./AgeInputWithSelect"; import { useState, useEffect } from "react"; +import CreateButtonWithTooltip from "./CreateButtonWithTooltip"; interface GenderAndAgeSelectorProps { gender: string; @@ -190,23 +191,12 @@ export default function GenderAndAgeSelector({ - + /> ); } \ No newline at end of file diff --git a/src/pages/PersonalizationAI/PersonalizationAI.tsx b/src/pages/PersonalizationAI/PersonalizationAI.tsx index d86fede9..a78e8e49 100644 --- a/src/pages/PersonalizationAI/PersonalizationAI.tsx +++ b/src/pages/PersonalizationAI/PersonalizationAI.tsx @@ -11,16 +11,17 @@ import { useSnackbar } from "notistack"; import { PayModal } from "./PayModal"; import { useUserStore } from "@/stores/user"; import { cartApi } from "@/api/cart"; -import { outCart } from "../Tariffs/Tariffs"; -import { inCart } from "../Tariffs/Tariffs"; +import { outCart } from "../Tariffs/utils"; +import { inCart } from "../Tariffs/utils"; import { isTestServer } from "@/utils/hooks/useDomainDefine"; import { useToken } from "@frontend/kitui"; import { useSWRConfig } from "swr"; -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { setUserAccount, setCustomerAccount } from "@/stores/user"; import { quizApi } from "@api/quiz"; import { setQuizes } from "@root/quizes/actions"; import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo"; +import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest"; const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19"; export default function PersonalizationAI() { @@ -92,7 +93,7 @@ export default function PersonalizationAI() { useToken: true, withCredentials: false, }).catch(error => { - console.log(error) + console.error(error) enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" }); return null; }), @@ -102,7 +103,7 @@ export default function PersonalizationAI() { useToken: true, withCredentials: false, }).catch(error => { - console.log(error) + console.error(error) enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" }); return null; }) @@ -115,7 +116,7 @@ export default function PersonalizationAI() { setCustomerAccount(customerAccountResult); } } catch (error) { - console.log(error) + console.error(error) enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" }); } } @@ -128,8 +129,6 @@ export default function PersonalizationAI() { (async () => { if (quiz?.backendId) { const [result, error] = await auditoryGet({ quizId: quiz.backendId }); - console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__") - console.log(result) if (result) { setAuditory(result); } @@ -209,8 +208,6 @@ export default function PersonalizationAI() { setUtmParams(paramString ? `&${paramString}` : ""); }; - console.log("______----giga_chat-----__--_---_--_----__--__-__--_--__--__--_---_______-quiz") - console.log(quiz?.giga_chat) const startCreate = async () => { if (quiz?.giga_chat) { createNewLink(); @@ -240,7 +237,15 @@ export default function PersonalizationAI() { //если денег не хватило if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) { var link = document.createElement("a"); - link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=50000&data=${token}&userid=${userId}&from=AI&wayback=ai_${quiz?.backendId}`; + link.href = generateHubWalletRequestURL({ + wayback: "personalization-ai", + action: "buy", + dif: "50000", + userid: userId, + additionalinformation: quiz?.backendId.toString(), + token + }); + //link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=50000&data=${token}&userid=${userId}&from=AI&wayback=ai_${quiz?.backendId}`; document.body.appendChild(link); link.click(); return; @@ -263,12 +268,9 @@ export default function PersonalizationAI() { // Обновляем данные квиза после успешной оплаты - console.log("Обновляем данные квиза после оплаты"); const [quizes, quizesError] = await quizApi.getList(); - console.log("Получены данные квизов:", quizes); if (!quizesError) { setQuizes(quizes); - console.log("Данные квизов обновлены в сторе"); } else { console.error("Ошибка при получении данных квизов:", quizesError); } @@ -289,7 +291,7 @@ export default function PersonalizationAI() { lineHeight: "21.4px" }}> Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам. -
Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже. +
Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже. - Так же вы можете обогатить свою ссылку UTM метками в поле "вставьте свою ссылку" и эти метки применятся ко всем вашим ссылкам. + Так же вы можете обогатить свою ссылку UTM метками в поле "вставьте свою ссылку" и эти метки применятся ко всем вашим ссылкам. import("./BranchingMap").then((module) => ({ default: module.BranchingMap })), ); @@ -55,31 +54,6 @@ export const QuestionSwitchWindowTool = ({ }} /> - // { - // console.log(e) - // Array.from(e.target.files).forEach((element, i) => { - // setTimeout(() => { - // console.log(i) - // console.log(Number(element.name.replace(/[^0-9,\s]/g, ""))) - // const q = questions.find((q) => q.page + 1 === Number(element.name.replace(/[^0-9,\s]/g, ""))) - // if (q !== undefined) { - // console.log(q) - // console.log("-----------------") - // uploadQuestionImage( - // q.id, - // quiz.qid, - // e.target.files[i], - // (question, url) => { - // question.content.back = url; - // question.content.originalBack = url; - // }, - // ); - // } - // }, 1000); - // }); - // }} - // /> } {openBranchingPage ? ( diff --git a/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx b/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx index 6bcfe86b..b424dd35 100644 --- a/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx +++ b/src/pages/Questions/DraggableList/QuestionPageCardTitle.tsx @@ -126,7 +126,6 @@ const QuestionPageCardTitle = memo(function ({ value={title} placeholder={"Заголовок вопроса"} onChange={({ target }) => { - console.log(target.value.length) if (target.value.length > maxLengthTextField) { enqueueSnackbar("Превышена длина вводимого текста") } else { diff --git a/src/pages/Questions/DraggableList/index.tsx b/src/pages/Questions/DraggableList/index.tsx index 4b9cf1f4..03346b3f 100644 --- a/src/pages/Questions/DraggableList/index.tsx +++ b/src/pages/Questions/DraggableList/index.tsx @@ -31,9 +31,6 @@ export const DraggableList = ({ createUntypedQuestion(Number(quiz.backendId)); } }, [quiz, filteredQuestions]); - console.log(quiz) - console.log(questions) - // if () {}uploadQuestionImage return ( diff --git a/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx b/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx index 15e3a3f9..6dd6c494 100644 --- a/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx +++ b/src/pages/Questions/Emoji/EmojiAnswerItem/VariantAdornment.tsx @@ -15,7 +15,6 @@ export default function VariantAdornment({ }) { const theme = useTheme(); - console.log("VariantAdornment extendedText", extendedText) return ( diff --git a/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx b/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx index 3535cfbb..92ba114a 100644 --- a/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx +++ b/src/pages/Questions/QuestionOptions/SliderOptions/SliderOptions.tsx @@ -44,11 +44,6 @@ export default function SliderOptions({ question, openBranchingPage, setOpenBran }); }, 5000); const updateStepsDebounced = useDebouncedCallback((value: string) => { - console.log("value") - console.log(value) - console.log(value.toString()) - console.log("ReplaceToNotStartZero(Number(value)) _____________________________________") - console.log(ReplaceToNotStartZero(Number(value))) updateQuestion(question.id, (question) => { question.content.step = ReplaceToNotStartZero(Number(value)); }); diff --git a/src/pages/Tariffs/Tabs.tsx b/src/pages/Tariffs/Tabs.tsx index bcfeec11..1cae19d1 100644 --- a/src/pages/Tariffs/Tabs.tsx +++ b/src/pages/Tariffs/Tabs.tsx @@ -1,11 +1,13 @@ import { Tabs as MuiTabs } from "@mui/material"; import { CustomTab } from "./CustomTab"; +import { TypePages } from "./types"; type TabsProps = { names: string[]; items: string[]; - selectedItem: "day" | "count" | "dop" | "hide" | "create"; - setSelectedItem: (num: "day" | "count" | "dop") => void; + selectedItem: TypePages; + setSelectedItem: (num: TypePages) => void; + toDop: () => void; }; export const Tabs = ({ @@ -18,7 +20,7 @@ export const Tabs = ({ sx={{ m: "25px" }} TabIndicatorProps={{ sx: { display: "none" } }} value={selectedItem} - onChange={(event, newValue: "day" | "count" | "dop") => { + onChange={(event, newValue: TypePages) => { setSelectedItem(newValue); }} variant="scrollable" diff --git a/src/pages/Tariffs/TariffCardDisplaySelector.tsx b/src/pages/Tariffs/TariffCardDisplaySelector.tsx new file mode 100644 index 00000000..617febfa --- /dev/null +++ b/src/pages/Tariffs/TariffCardDisplaySelector.tsx @@ -0,0 +1,147 @@ +import { Box, useMediaQuery, useTheme } from "@mui/material" +import { NavCard } from "./components/NavCard" +import { createTariffElements } from "./tariffsUtils/createTariffElements" +import SmallIconPena from "@/assets/icons/SmallIconPena" + +interface TariffCardDisplaySelectorProps { + content: { + title: string, + onClick: () => void + }[] + selectedItem: TypePages + tariffs: any[] + user: any + discounts: any[] + openModalHC: (tariffInfo: any) => void + userPrivilegies: any + startRequestCreate: () => void +} + +export const TariffCardDisplaySelector = ({ + content, + selectedItem, + tariffs, + user, + discounts, + openModalHC, + userPrivilegies, + startRequestCreate +}: TariffCardDisplaySelectorProps) => { + const theme = useTheme() + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + const sendRequest = userPrivilegies?.quizManual?.amount > 0 ? startRequestCreate : undefined + + switch (selectedItem) { + case "dop": + return + {content.map(data => )} + + + case "hide": + const filteredBadgeTariffs = tariffs.filter((tariff) => { + return ( + tariff.privileges[0].serviceKey === "squiz" && + !tariff.isDeleted && + !tariff.isCustom && + tariff.privileges[0].privilegeId === "squizHideBadge" && + tariff.privileges[0]?.type === "day" + ); + }); + return createTariffElements( + filteredBadgeTariffs, + false, + user, + discounts, + openModalHC, + ) + + case "create": + const filteredCreateTariffs = tariffs.filter((tariff) => { + return ( + tariff.privileges[0].serviceKey === "squiz" && + !tariff.isDeleted && + !tariff.isCustom && + tariff.privileges[0].privilegeId === "quizManual" && + tariff.privileges[0]?.type === "count" + ); + }); + return createTariffElements( + filteredCreateTariffs, + false, + user, + discounts, + openModalHC, + sendRequest, + true, + + ) + + case "premium": + const filteredPremiumTariffs = tariffs.filter((tariff) => { + return ( + tariff.privileges[0].serviceKey === "squiz" && + !tariff.isDeleted && + !tariff.isCustom && + tariff.privileges[0].privilegeId === "squizPremium" && + tariff.privileges[0]?.type === "day" + ); + }); + return createTariffElements( + filteredPremiumTariffs, + false, + user, + discounts, + openModalHC, + ) + + case "analytics": + const filteredAnalyticsTariffs = tariffs.filter((tariff) => { + return ( + tariff.privileges[0].serviceKey === "squiz" && + !tariff.isDeleted && + !tariff.isCustom && + tariff.privileges[0].privilegeId === "squizAnalytics" && + tariff.privileges[0]?.type === "count" + ); + }); + return createTariffElements( + filteredAnalyticsTariffs, + false, + user, + discounts, + openModalHC, + ) + + case "custom": + const filteredCustomTariffs = tariffs.filter((tariff) => { + return ( + tariff.privileges[0].serviceKey === "squiz" && + !tariff.isDeleted && + tariff.isCustom && + tariff.privileges[0]?.type === "day" + ); + }); + return createTariffElements( + filteredCustomTariffs, + false, + user, + discounts, + openModalHC, + ) + + default: + return + {content.map(data => )} + + } +} \ No newline at end of file diff --git a/src/pages/Tariffs/Tariffs.tsx b/src/pages/Tariffs/Tariffs.tsx index 5a886bb0..7ac558e4 100644 --- a/src/pages/Tariffs/Tariffs.tsx +++ b/src/pages/Tariffs/Tariffs.tsx @@ -1,39 +1,34 @@ import { activatePromocode } from "@api/promocode"; import { useToken } from "@frontend/kitui"; -import ArrowLeft from "@icons/questionsPage/arrowLeft"; import { Box, - Button, - Container, - IconButton, - Modal, - Paper, Typography, useMediaQuery, useTheme, } from "@mui/material"; import { useUserStore } from "@root/user"; -import { LogoutButton } from "@ui_kit/LogoutButton"; import { useDomainDefine } from "@utils/hooks/useDomainDefine"; import { enqueueSnackbar } from "notistack"; import { useEffect, useState } from "react"; import { withErrorBoundary } from "react-error-boundary"; -import { Link, useNavigate } from "react-router-dom"; -import Logotip from "../../pages/Landing/images/icons/QuizLogo"; +import { useNavigate } from "react-router-dom"; import CollapsiblePromocodeField from "./CollapsiblePromocodeField"; import { Tabs } from "./Tabs"; import { createTariffElements } from "./tariffsUtils/createTariffElements"; import { currencyFormatter } from "./tariffsUtils/currencyFormatter"; import { useWallet, setCash } from "@root/cash"; -import { handleLogoutClick } from "@utils/HandleLogoutClick"; import { cartApi } from "@api/cart"; -import { Other } from "./pages/Other"; +import { TariffCardDisplaySelector } from "./TariffCardDisplaySelector"; import { ModalRequestCreate } from "./ModalRequestCreate"; import { cancelCC, useCC } from "@/stores/cc"; import { NavSelect } from "./NavSelect"; import { useTariffs } from '@utils/hooks/useTariffs'; import { useDiscounts } from '@utils/hooks/useDiscounts'; +import { PaymentConfirmationModal } from "./components/PaymentConfirmationModal"; +import { TariffsHeader } from "./components/TariffsHeader"; +import { inCart, outCart } from "./utils"; +import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest"; const StepperText: Record = { day: "Тарифы на время", @@ -50,12 +45,17 @@ function TariffPage() { const userId = useUserStore((state) => state.userId); const navigate = useNavigate(); const user = useUserStore((state) => state.customerAccount); - const a = useUserStore((state) => state.customerAccount); //c wallet - console.log("________________34563875693785692576_____________USERRRRRRR") - console.log(a) - const { data: discounts } = useDiscounts(userId); + const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet + const userAccount = useUserStore((state) => state.userAccount); + // console.info("________________userWithWallet_____________USERRRRRRR") + // console.info(userWithWallet) + // console.info("________________userAccount_____________") + // console.info(userAccount) + // console.info("________________customerAccount_____________") + // console.info(user) + const { data: discounts, error: discountsError, isLoading: discountsLoading } = useDiscounts(userId); const [isRequestCreate, setIsRequestCreate] = useState(false); - const [openModal, setOpenModal] = useState({}); + const [openModal, setOpenModal] = useState<{ id?: string; price?: number }>({}); const { cashString, cashCop, cashRub } = useWallet(); const [selectedItem, setSelectedItem] = useState("day"); const { isTestServer } = useDomainDefine(); @@ -65,17 +65,14 @@ function TariffPage() { const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs(); -console.log("________34563875693785692576_____ TARIFFS") -console.log(tariffs) - useEffect(() => { - if (a) { + if (userWithWallet && user) { let cs = currencyFormatter.format(Number(user.wallet.cash) / 100); let cc = Number(user.wallet.cash); let cr = Number(user.wallet.cash) / 100; setCash(cs, cc, cr); } - }, [a]); + }, [userWithWallet, user]); useEffect(() => { if (cc) { @@ -83,7 +80,26 @@ console.log(tariffs) cancelCC() } }, []) - if (!user || !tariffs || !discounts) return ; + + // Проверяем, что все данные загружены и нет ошибок + const isDataLoading = tariffsLoading || (userId && discountsLoading); + const hasErrors = tariffsError || discountsError; + + // Если userId есть, но customerAccount еще не загружен, показываем загрузку + const isCustomerAccountLoading = userId && !user; + const hasAllData = user && tariffs && (userId ? discounts : true); + + if (isDataLoading || isCustomerAccountLoading) { + return ; + } + + if (hasErrors) { + return ; + } + + if (!hasAllData) { + return ; + } const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo); const tryBuy = async ({ id, price }: { id: string; price: number }) => { @@ -109,11 +125,20 @@ console.log(tariffs) //если денег не хватило if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) { let cashDif = Number(payError.split(":")[1]); - var link = document.createElement("a"); - link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`; - if (cc) link.href = link.href + "&cc=true"//после покупки тарифа и возвращения будем знать что надо открыть модалку - document.body.appendChild(link); - link.click(); + + if (!userId) { + enqueueSnackbar("Ошибка: ID пользователя не найден"); + return; + } + + const l = generateHubWalletRequestURL({ + action: cc ? "createquizcc" : "buy", + dif: cashDif.toString(), + userid: userId, + wayback: "list", + token + }); + window.location.href = l; return; } @@ -169,63 +194,10 @@ console.log(tariffs) setIsRequestCreate(true) } - if (!a) return null; + if (!userWithWallet) return null; return ( <> - - - - - navigate("/list")}> - - - - - - Мой баланс - - 9 ? "13px" : "16px") : "16px" - } - > - {cashString} - - - { - navigate("/"); - handleLogoutClick(); - }} - sx={{ - ml: "20px", - }} - /> - - + setSelectedItem("create") }, + { + title: "Премиум функции", + onClick: () => setSelectedItem("premium") + }, + { + title: "Расширенная аналитика", + onClick: () => setSelectedItem("analytics") + }, + { + title: "Кастомные тарифы", + onClick: () => setSelectedItem("custom") + }, ]} tariffs={tariffs} user={user} - discounts={discounts} + discounts={discounts || []} openModalHC={openModalHC} userPrivilegies={userPrivilegies} startRequestCreate={startRequestCreate} /> )} + {selectedItem === "dop" && ( + setSelectedItem("hide") + }, + { + title: "Создать квиз на заказ", + onClick: () => setSelectedItem("create") + }, + ] + : [ + { + title: `Убрать логотип "PenaQuiz"`, + onClick: () => setSelectedItem("hide") + }, + { + title: "Создать квиз на заказ", + onClick: () => setSelectedItem("create") + }, + { + title: "Премиум функции", + onClick: () => setSelectedItem("premium") + }, + { + title: "Расширенная аналитика", + onClick: () => setSelectedItem("analytics") + }, + { + title: "Кастомные тарифы", + onClick: () => setSelectedItem("custom") + }, + ] + } + + tariffs={tariffs} + user={user} + discounts={discounts || []} + openModalHC={openModalHC} + userPrivilegies={userPrivilegies} + startRequestCreate={startRequestCreate} + /> + )} - 0} onClose={() => setOpenModal({})} - > - - - Вы подтверждаете платёж в сумму{" "} - {openModal.price ? openModal.price.toFixed(2) : 0} ₽ - - - - + onConfirm={() => { + if (openModal.id && openModal.price !== undefined) { + tryBuy({ id: openModal.id, price: openModal.price }); + } + }} + price={openModal.price || 0} + /> setIsRequestCreate(false)} /> ); @@ -364,47 +374,3 @@ const LoadingPage = () => ( ); - -export const inCart = () => { - let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]"); - if (Array.isArray(saveCart)) { - saveCart.forEach(async (id: string) => { - const [_, addError] = await cartApi.add(id); - - if (addError) { - console.error(addError); - } else { - let index = saveCart.indexOf("green"); - - if (index !== -1) { - saveCart.splice(index, 1); - } - - localStorage.setItem("saveCart", JSON.stringify(saveCart)); - } - }); - } else { - localStorage.setItem("saveCart", "[]"); - } -}; -export const outCart = (cart: string[]) => { - //Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально - if (cart.length > 0) { - cart.forEach(async (id: string) => { - const [_, deleteError] = await cartApi.delete(id); - - if (deleteError) { - console.error(deleteError); - cancelCC()//мы хотели открыть модалку после покупки тарифа на создание квиза, но не вышло и модалку не откроем - - return; - } - - let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || []; - if (!Array.isArray(saveCart)) saveCart = [] - saveCart = saveCart.push(id); - localStorage.setItem("saveCart", JSON.stringify(saveCart)); - }); - } - -}; diff --git a/src/pages/Tariffs/components/PaymentConfirmationModal.tsx b/src/pages/Tariffs/components/PaymentConfirmationModal.tsx new file mode 100644 index 00000000..0d13eb46 --- /dev/null +++ b/src/pages/Tariffs/components/PaymentConfirmationModal.tsx @@ -0,0 +1,51 @@ +import { + Button, + Modal, + Paper, + Typography, +} from "@mui/material"; + +interface PaymentConfirmationModalProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; + price: number; +} + +export const PaymentConfirmationModal = ({ + open, + onClose, + onConfirm, + price, +}: PaymentConfirmationModalProps) => { + return ( + + + + Вы подтверждаете платёж в сумму{" "} + {price ? price.toFixed(2) : 0} ₽ + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/Tariffs/components/TariffsHeader.tsx b/src/pages/Tariffs/components/TariffsHeader.tsx new file mode 100644 index 00000000..63965510 --- /dev/null +++ b/src/pages/Tariffs/components/TariffsHeader.tsx @@ -0,0 +1,83 @@ +import { useToken } from "@frontend/kitui"; +import ArrowLeft from "@icons/questionsPage/arrowLeft"; +import { + Box, + Container, + IconButton, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useUserStore } from "@root/user"; +import { LogoutButton } from "@ui_kit/LogoutButton"; +import { handleLogoutClick } from "@utils/HandleLogoutClick"; +import { Link, useNavigate } from "react-router-dom"; +import Logotip from "../../../pages/Landing/images/icons/QuizLogo"; + +interface TariffsHeaderProps { + cashString: string; +} + +export const TariffsHeader = ({ cashString }: TariffsHeaderProps) => { + const theme = useTheme(); + const navigate = useNavigate(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + + return ( + + + + + navigate("/list")}> + + + + + + Мой баланс + + 9 ? "13px" : "16px") : "16px" + } + > + {cashString} + + + { + navigate("/"); + handleLogoutClick(); + }} + sx={{ + ml: "20px", + }} + /> + + + ); +}; \ No newline at end of file diff --git a/src/pages/Tariffs/pages/HideLogo.tsx b/src/pages/Tariffs/pages/HideLogo.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/Tariffs/pages/Other.tsx b/src/pages/Tariffs/pages/Other.tsx deleted file mode 100644 index 5373da51..00000000 --- a/src/pages/Tariffs/pages/Other.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Box, useMediaQuery, useTheme } from "@mui/material" -import { NavCard } from "../components/NavCard" -import { createTariffElements } from "../tariffsUtils/createTariffElements" -import SmallIconPena from "@/assets/icons/SmallIconPena" - -interface Props { - content: { - title: string, - onClick: () => void - }[] - selectedItem: TypePages -} - -export const Other = ({ - content, - selectedItem, - - tariffs, - user, - discounts, - openModalHC, - userPrivilegies, - startRequestCreate -}: any) => { - const theme = useTheme() - const isTablet = useMediaQuery(theme.breakpoints.down(1000)); -const sendRequest = userPrivilegies?.quizManual?.amount > 0 ? startRequestCreate : undefined - - switch (selectedItem) { - case "hide": - const filteredBadgeTariffs = tariffs.filter((tariff) => { - return ( - tariff.privileges[0].serviceKey === "squiz" && - !tariff.isDeleted && - !tariff.isCustom && - tariff.privileges[0].privilegeId === "squizHideBadge" && - tariff.privileges[0]?.type === "day" - ); - }); - return - {createTariffElements( - filteredBadgeTariffs, - false, - user, - discounts, - openModalHC, - )} - - case "create": - const filteredCreateTariffs = tariffs.filter((tariff) => { - return ( - tariff.privileges[0].serviceKey === "squiz" && - !tariff.isDeleted && - !tariff.isCustom && - tariff.privileges[0].privilegeId === "quizManual" && - tariff.privileges[0]?.type === "count" - ); - }); - return - {createTariffElements( - filteredCreateTariffs, - false, - user, - discounts, - openModalHC, - sendRequest, - true, - - )} - - default: - return - {content.map(data => )} - - } -} diff --git a/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx index 7999e51f..1511e49c 100644 --- a/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx +++ b/src/pages/Tariffs/tariffsUtils/createTariffElements.tsx @@ -17,10 +17,6 @@ export const createTariffElements = ( cc?: boolean, icon?: ReactNode ) => { - console.log("start work createTariffElements") - console.log("filteredTariffs ", filteredTariffs) - console.log("user ", user) - console.log("user.isUserNko, ", user.isUserNko) const tariffElements = filteredTariffs .filter((tariff) => tariff.privileges.length > 0) .map((tariff, index) => { diff --git a/src/pages/Tariffs/types.ts b/src/pages/Tariffs/types.ts index fd6c0dea..6db0e8d6 100644 --- a/src/pages/Tariffs/types.ts +++ b/src/pages/Tariffs/types.ts @@ -1 +1 @@ -type TypePages = "count" | "day" | "dop" | "hide" | "create" \ No newline at end of file +type TypePages = "count" | "day" | "dop" | "hide" | "create" | "premium" | "analytics" | "custom" \ No newline at end of file diff --git a/src/pages/Tariffs/utils.ts b/src/pages/Tariffs/utils.ts new file mode 100644 index 00000000..ca612a35 --- /dev/null +++ b/src/pages/Tariffs/utils.ts @@ -0,0 +1,46 @@ +import { cartApi } from "@api/cart"; +import { cancelCC } from "@/stores/cc"; + +export const inCart = () => { + let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]"); + if (Array.isArray(saveCart)) { + saveCart.forEach(async (id: string) => { + const [_, addError] = await cartApi.add(id); + + if (addError) { + console.error(addError); + } else { + let index = saveCart.indexOf("green"); + + if (index !== -1) { + saveCart.splice(index, 1); + } + + localStorage.setItem("saveCart", JSON.stringify(saveCart)); + } + }); + } else { + localStorage.setItem("saveCart", "[]"); + } +}; + +export const outCart = (cart: string[]) => { + //Сделаем муторно и подольше, зато при прерывании сессии данные потеряются минимально + if (cart.length > 0) { + cart.forEach(async (id: string) => { + const [_, deleteError] = await cartApi.delete(id); + + if (deleteError) { + console.error(deleteError); + cancelCC()//мы хотели открыть модалку после покупки тарифа на создание квиза, но не вышло и модалку не откроем + + return; + } + + let saveCart = JSON.parse(localStorage.getItem("saveCart") || "[]") || []; + if (!Array.isArray(saveCart)) saveCart = [] + saveCart = saveCart.push(id); + localStorage.setItem("saveCart", JSON.stringify(saveCart)); + }); + } +}; \ No newline at end of file diff --git a/src/pages/auth/RecoverPassword.tsx b/src/pages/auth/RecoverPassword.tsx index 8afd81ca..4849d9e4 100644 --- a/src/pages/auth/RecoverPassword.tsx +++ b/src/pages/auth/RecoverPassword.tsx @@ -18,7 +18,7 @@ import { object, string } from "yup"; import { useEffect, useState } from "react"; import { useUserStore } from "@root/user"; -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { setAuthToken } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; import { recoverUser } from "@api/user"; diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index 97cc5d12..204fbe88 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -242,7 +242,7 @@ export default function SignupDialog() { diff --git a/src/pages/createQuize/AvailablePrivilege.tsx b/src/pages/createQuize/AvailablePrivilege.tsx index 7b7cd5b0..dbca4762 100644 --- a/src/pages/createQuize/AvailablePrivilege.tsx +++ b/src/pages/createQuize/AvailablePrivilege.tsx @@ -53,8 +53,6 @@ export default function AvailablePrivilege() { } const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || ""); const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || ""); - console.log(userPrivileges) - console.log(quizUnlimTime) return ( ()( @@ -95,7 +98,6 @@ export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) => // if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.at(-1)?.id || ""; if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.[sortedMessages.length - 1]?.id || ""; - console.log("ticketStudy, ", sortedMessages) }); export const incrementUnauthMessage = () => @@ -156,9 +158,17 @@ export const updateTicket = ( }, ); -function setProducedState( +function setProducedState( recipe: (state: TicketStore) => void, action?: A, ) { useTicketStore.setState((state) => produce(state, recipe), false, action); } + +// Функция для записи тикетов в стор +export const setTickets = (tickets: Ticket[] | null) => { + useTicketStore.setState((state) => ({ + ...state, + tickets: tickets || [] + }), false); +}; diff --git a/src/stores/user.ts b/src/stores/user.ts index 20352de0..4de52d83 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -71,5 +71,6 @@ export const clearUserData = () => useUserStore.setState({ ...initialState }); export const setUserAccount = (userAccount: OriginalUserAccount) => useUserStore.setState({ userAccount }); -export const setCustomerAccount = (customerAccount: UserAccount) => +export const setCustomerAccount = (customerAccount: UserAccount) => { useUserStore.setState({ customerAccount }); +}; diff --git a/src/ui_kit/CheckFastlink.tsx b/src/ui_kit/CheckFastlink.tsx index ce7cbbaa..f90db289 100644 --- a/src/ui_kit/CheckFastlink.tsx +++ b/src/ui_kit/CheckFastlink.tsx @@ -3,7 +3,7 @@ import { Box, Button, Modal, Typography } from "@mui/material"; import { enqueueSnackbar } from "notistack"; import { mutate } from "swr"; -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { getDiscounts } from "@api/discounts"; import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user"; diff --git a/src/ui_kit/FloatingSupportChat/Chat.tsx b/src/ui_kit/FloatingSupportChat/Chat.tsx index 28e17676..5e6046d1 100644 --- a/src/ui_kit/FloatingSupportChat/Chat.tsx +++ b/src/ui_kit/FloatingSupportChat/Chat.tsx @@ -1,9 +1,6 @@ import { Box, - FormControl, IconButton, - InputAdornment, - InputBase, SxProps, Theme, Typography, @@ -17,46 +14,45 @@ import { useTicketStore, } from "@root/ticket"; import type { TouchEvent, WheelEvent } from "react"; -import * as React from "react"; -import { useEffect, useMemo, useRef, useState } from "react"; -import ChatMessage from "./ChatMessage"; -import ChatVideo from "./ChatVideo"; -import SendIcon from "@icons/SendIcon"; +import { useEffect, useMemo, useRef } from "react"; +import ChatMessageRenderer from "./ChatMessageRenderer"; +import ChatInput from "./ChatInput"; import UserCircleIcon from "./UserCircleIcon"; import { throttle, TicketMessage } from "@frontend/kitui"; import ArrowLeft from "@icons/questionsPage/arrowLeft"; import { useUserStore } from "@root/user"; -import AttachFileIcon from "@mui/icons-material/AttachFile"; -import ChatImage from "./ChatImage"; -import ChatDocument from "@ui_kit/FloatingSupportChat/ChatDocument"; -import { - ACCEPT_SEND_MEDIA_TYPES_MAP, - checkAcceptableMediaType, -} from "@utils/checkAcceptableMediaType"; -import { enqueueSnackbar } from "notistack"; interface Props { open: boolean; sx?: SxProps; onclickArrow?: () => void; sendMessage: (a: string) => Promise; - sendFile: (a: File | undefined) => Promise; - greetingMessage: TicketMessage; + sendFile: (a: File | undefined) => Promise; } +const greetingMessage: TicketMessage = { + id: "greeting", + ticket_id: "", + user_id: "system", + session_id: "", + message: "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут", + files: [], + shown: {}, + request_screenshot: "", + created_at: new Date().toISOString(), + system: false +}; + export default function Chat({ open = false, sx, onclickArrow, sendMessage, sendFile, - greetingMessage, }: Props) { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(800)); - const [messageField, setMessageField] = useState(""); - const [disableFileButton, setDisableFileButton] = useState(false); const user = useUserStore((state) => state.user?._id); const ticket = useTicketStore( @@ -72,31 +68,11 @@ export default function Chat({ const chatBoxRef = useRef(null); useEffect(() => { - addOrUpdateUnauthMessages([greetingMessage]); if (open) { scrollToBottom(); } }, [open]); - const sendMessageHC = async () => { - const successful = await sendMessage(messageField); - if (successful) { - setMessageField(""); - } - }; - const sendFileHC = async (file: File) => { - const check = checkAcceptableMediaType(file); - if (check.length > 0) { - enqueueSnackbar(check); - return; - } - setDisableFileButton(true); - await sendFile(file); - setDisableFileButton(false); - }; - - const fileInputRef = useRef(null); - const throttledScrollHandler = useMemo( () => throttle(() => { @@ -152,14 +128,6 @@ export default function Chat({ behavior, }); } - const handleTextfieldKeyPress: React.KeyboardEventHandler< - HTMLInputElement | HTMLTextAreaElement - > = (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - sendMessageHC(); - } - }; return ( <> @@ -240,164 +208,28 @@ export default function Chat({ > {ticket.sessionData?.ticketId && messages.map((message) => { - const isFileVideo = () => { - if (message.files) { - return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some( - (fileType) => - message.files[0].toLowerCase().endsWith(fileType), - ); - } - }; - const isFileImage = () => { - if (message.files) { - return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some( - (fileType) => - message.files[0].toLowerCase().endsWith(fileType), - ); - } - }; - const isFileDocument = () => { - if (message.files) { - return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some( - (fileType) => - message.files[0].toLowerCase().endsWith(fileType), - ); - } - }; - if (message.files.length > 0 && isFileImage()) { - return ( - - ); - } - if (message.files.length > 0 && isFileVideo()) { - return ( - - ); - } - if (message.files.length > 0 && isFileDocument()) { - return ( - - ); - } + const isSelf = (ticket.sessionData?.sessionId || user) === message.user_id; + return ( - ); })} {!ticket.sessionData?.ticketId && ( - )} - - setMessageField(e.target.value)} - endAdornment={ - - { - if (!disableFileButton) fileInputRef.current?.click(); - }} - > - - - { - if (e.target.files?.[0]) - sendFileHC(e.target.files?.[0]); - }} - style={{ display: "none" }} - type="file" - /> - - - - - } - /> - + )} diff --git a/src/ui_kit/FloatingSupportChat/ChatInput.tsx b/src/ui_kit/FloatingSupportChat/ChatInput.tsx new file mode 100644 index 00000000..ba17b903 --- /dev/null +++ b/src/ui_kit/FloatingSupportChat/ChatInput.tsx @@ -0,0 +1,137 @@ +import { useCallback, useRef, useState } from "react"; +import { + FormControl, + IconButton, + InputAdornment, + InputBase, + useMediaQuery, + useTheme, +} from "@mui/material"; +import SendIcon from "@icons/SendIcon"; +import AttachFileIcon from "@mui/icons-material/AttachFile"; +import { checkAcceptableMediaType } from "@utils/checkAcceptableMediaType"; +import { enqueueSnackbar } from "notistack"; + +interface ChatInputProps { + sendMessage: (message: string) => Promise; + sendFile: (file: File | undefined) => Promise; + isMessageSending: boolean; +} + +const ChatInput = ({ sendMessage, sendFile, isMessageSending }: ChatInputProps) => { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const [messageField, setMessageField] = useState(""); + const [disableFileButton, setDisableFileButton] = useState(false); + const fileInputRef = useRef(null); + + const handleSendMessage = useCallback(async () => { + const successful = await sendMessage(messageField); + if (successful) { + setMessageField(""); + } + }, [sendMessage, messageField]); + + const handleSendFile = useCallback(async (file: File) => { + const check = checkAcceptableMediaType(file); + if (check.length > 0) { + enqueueSnackbar(check); + return; + } + setDisableFileButton(true); + await sendFile(file); + setDisableFileButton(false); + }, [sendFile]); + + const handleFileInputChange = useCallback((e: React.ChangeEvent) => { + if (e.target.files?.[0]) { + handleSendFile(e.target.files[0]); + } + }, [handleSendFile]); + + const handleFileButtonClick = useCallback(() => { + if (!disableFileButton) { + fileInputRef.current?.click(); + } + }, [disableFileButton]); + + const handleTextfieldKeyPress: React.KeyboardEventHandler< + HTMLInputElement | HTMLTextAreaElement + > = useCallback((e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }, [handleSendMessage]); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setMessageField(e.target.value); + }, []); + + return ( + + + + + + + + + + + } + /> + + ); +}; + +export default ChatInput; \ No newline at end of file diff --git a/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx b/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx new file mode 100644 index 00000000..4955915e --- /dev/null +++ b/src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx @@ -0,0 +1,70 @@ +import { memo, useMemo } from "react"; +import { TicketMessage } from "@frontend/kitui"; +import ChatMessage from "./ChatMessage"; +import ChatImage from "./ChatImage"; +import ChatVideo from "./ChatVideo"; +import ChatDocument from "./ChatDocument"; +import { ACCEPT_SEND_MEDIA_TYPES_MAP } from "@utils/checkAcceptableMediaType"; + +interface ChatMessageRendererProps { + message: TicketMessage; + isSelf: boolean; +} + +const ChatMessageRenderer = memo(({ message, isSelf }: ChatMessageRendererProps) => { + const fileType = useMemo(() => { + if (!message.files?.length) return null; + + const fileName = message.files[0].toLowerCase(); + + if (ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(fileType => fileName.endsWith(fileType))) { + return 'video'; + } + + if (ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(fileType => fileName.endsWith(fileType))) { + return 'image'; + } + + if (ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(fileType => fileName.endsWith(fileType))) { + return 'document'; + } + + return null; + }, [message.files]); + + // Если есть файлы и определён тип + if (message.files?.length > 0 && fileType) { + const commonProps = { + unAuthenticated: true, + key: message.id, + file: message.files[0], + createdAt: message.created_at, + isSelf, + }; + + switch (fileType) { + case 'image': + return ; + case 'video': + return ; + case 'document': + return ; + default: + break; + } + } + + // Текстовое сообщение + return ( + + ); +}); + +ChatMessageRenderer.displayName = 'ChatMessageRenderer'; + +export default ChatMessageRenderer; \ No newline at end of file diff --git a/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx b/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx index c59aa418..e133464f 100644 --- a/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx +++ b/src/ui_kit/FloatingSupportChat/FloatingSupportChat.tsx @@ -1,5 +1,5 @@ import type { ReactNode, Ref } from "react"; -import { forwardRef, useEffect, useState } from "react"; +import { forwardRef, useEffect, useState, useMemo } from "react"; import { Badge, Box, @@ -44,10 +44,9 @@ interface Props { handleChatClickClose: () => void; handleChatClickSwitch: () => void; sendMessage: (a: string) => Promise; - sendFile: (a: File | undefined) => Promise; + sendFile: (a: File | undefined) => Promise; modalWarningType: string | null; setModalWarningType: any; - greetingMessage: TicketMessage; } export default function FloatingSupportChat({ @@ -59,7 +58,6 @@ export default function FloatingSupportChat({ sendFile, modalWarningType, setModalWarningType, - greetingMessage, }: Props) { const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">(""); const theme = useTheme(); @@ -72,6 +70,48 @@ export default function FloatingSupportChat({ (state) => state[user ? "authData" : "unauthData"], ); + const ticket = useTicketStore( + (state) => state[user ? "authData" : "unauthData"], + ); + + // Функция для подсчёта непрочитанных сообщений согласно новой логике + const unreadCount = useMemo(() => { + if (messages.length === 0) return 0; + + const currentUserId = user || ticket?.sessionData?.sessionId; + if (!currentUserId) return 0; + + // Если последнее сообщение моё - не показываем количество + const lastMessage = messages[messages.length - 1]; + if (lastMessage.user_id === currentUserId) { + return 0; + } + + // Если последнее сообщение не моё и оно не прочитано - считаем его как +1 + if (lastMessage.shown?.me !== 1) { + let count = 1; + + // Считаем все последующие сообщения до тех пор пока не воткнёмся в моё сообщение + for (let i = messages.length - 2; i >= 0; i--) { + const message = messages[i]; + + // Если встретили моё сообщение - останавливаемся + if (message.user_id === currentUserId) { + break; + } + + // Если сообщение не прочитано - добавляем к счётчику + if (message.shown?.me !== 1) { + count++; + } + } + + return count; + } + + return 0; + }, [messages, user, ticket?.sessionData?.sessionId]); + useEffect(() => { const onResize = () => { if (document.fullscreenElement) { @@ -108,7 +148,6 @@ export default function FloatingSupportChat({ sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }} sendMessage={sendMessage} sendFile={sendFile} - greetingMessage={greetingMessage} /> )} shown?.me !== 1).length || 0 - } + badgeContent={unreadCount} sx={{ "& .MuiBadge-badge": { display: isChatOpened ? "none" : "flex", diff --git a/src/ui_kit/FloatingSupportChat/index.tsx b/src/ui_kit/FloatingSupportChat/index.tsx index eb17a552..dc0830b5 100644 --- a/src/ui_kit/FloatingSupportChat/index.tsx +++ b/src/ui_kit/FloatingSupportChat/index.tsx @@ -1,13 +1,15 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; import { TicketMessage, + createTicket, + shownMessage, useSSESubscription, useTicketMessages, useTicketsFetcher, + sendFile as sf } from "@frontend/kitui"; import FloatingSupportChat from "./FloatingSupportChat"; import { useUserStore } from "@root/user"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { sendTicketMessage, shownMessage } from "../../api/ticket"; import { useSSETab } from "../../utils/hooks/useSSETab"; import { addOrUpdateUnauthMessages, @@ -21,7 +23,6 @@ import { } from "@root/ticket"; import { enqueueSnackbar } from "notistack"; import { parseAxiosError } from "@utils/parse-error"; -import { createTicket, sendFile as sendFileRequest } from "@api/ticket"; import { selectSendingMethod } from "./utils"; type ModalWarningType = @@ -71,60 +72,6 @@ export default () => { 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(user), - }); useTicketMessages({ url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`, @@ -156,7 +103,6 @@ export default () => { ); if (isTicketClosed) { cleanAuthTicketData(); - addOrUpdateUnauthMessages([getGreetingMessage]); if (!user) { cleanUnauthTicketData(); localStorage.removeItem("unauth-ticket"); @@ -184,33 +130,44 @@ export default () => { ({ shown }) => shown?.me !== 1, ); - newMessages.map(async ({ id }) => { - await shownMessage(id); - }); + // Находим последнее сообщение, которое не от пользователя + const lastNonUserMessage = newMessages + .filter(({ user_id }) => (ticket.sessionData?.sessionId || user) !== user_id) + .pop(); + + // Отправляем shown только на последнее сообщение, которое не от пользователя + if (lastNonUserMessage) { + shownMessage(lastNonUserMessage.id); + } } }, [isChatOpened, ticket.messages]); const sendMessage = async (messageField: string) => { if (!messageField || ticket.isMessageSending) return false; - + setSseEnabled(true); setIsMessageSending(true); - let successful = await selectSendingMethod({messageField}); + let successful = await selectSendingMethod({ messageField }); setIsMessageSending(false); return successful; }; - const sendFile = async (file: File) => { - if (file === undefined) return true; + const sendFile = async (file: File | undefined): Promise => { + if (file === undefined) return; let ticketId = ticket.sessionData?.ticketId; if (!ticket.sessionData?.ticketId) { - const [data, createError] = await createTicket("", Boolean(user)); + const [data, createError] = await createTicket({ + message: "", + useToken: Boolean(user), + systemError: false + }); ticketId = data?.Ticket; if (createError || !data) { - enqueueSnackbar(createError); + enqueueSnackbar(`Не удалось создать диалог ${parseAxiosError(createError)}`); + return; } else { setTicketData({ ticketId: data.Ticket, sessionId: data.sess }); } @@ -219,15 +176,16 @@ export default () => { } if (ticketId !== undefined) { - if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize"); - - const [_, sendFileError] = await sendFileRequest(ticketId, file); - - if (sendFileError) { - enqueueSnackbar(sendFileError); + if (file.size > MAX_FILE_SIZE) { + setModalWarningType("errorSize"); + return; } - return true; + const [_, sendFileError] = await sf({ticketId, file}); + + if (sendFileError) { + enqueueSnackbar(`Не удалось отправить файл ${parseAxiosError(sendFileError)}`); + } } }; @@ -241,7 +199,6 @@ export default () => { sendFile={sendFile} modalWarningType={modalWarningType} setModalWarningType={setModalWarningType} - greetingMessage={getGreetingMessage} /> ); }; diff --git a/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts b/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts deleted file mode 100644 index 21430c7e..00000000 --- a/src/ui_kit/FloatingSupportChat/useTechnicalSupport.ts +++ /dev/null @@ -1,266 +0,0 @@ -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 index a603e348..430030ac 100644 --- a/src/ui_kit/FloatingSupportChat/utils.ts +++ b/src/ui_kit/FloatingSupportChat/utils.ts @@ -1,55 +1,51 @@ -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"; +import { createTicket, sendTicketMessage } from "@frontend/kitui"; +import { parseAxiosError } from "@/utils/parse-error"; interface SelectSendingMethod { - messageField: string; - isSnackbar?: boolean; - systemError?: boolean; - - } -export const selectSendingMethod = async ({messageField, isSnackbar = true, systemError = false}: SelectSendingMethod) => { - console.log("click") + messageField: string; + isSnackbar?: boolean; + systemError?: boolean; + +} +export const selectSendingMethod = async ({ messageField, isSnackbar = true, systemError = false }: SelectSendingMethod) => { const user = useUserStore.getState().user?._id; const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"]; - console.log(ticket) - console.log("click 2") let successful = false; - if (!(window.location.hostname == 'localhost' && systemError )) { //предупреждать о системных ошибках вне локалхост - if (!ticket.sessionData?.ticketId) { - console.log("autorisated 2") - const [data, createError] = await createTicket( - messageField, - Boolean(user), - false, - ); + if (!(window.location.hostname == 'localhost' && systemError)) { //предупреждать о системных ошибках вне локалхост + if (!ticket.sessionData?.ticketId) { + const [data, createError] = await createTicket({ + message: messageField, + useToken: Boolean(user), + systemError: false, + }); - if (createError || !data) { - successful = false; - - if (isSnackbar) enqueueSnackbar(createError); - } else { - successful = true; - - setTicketData({ ticketId: data.Ticket, sessionId: data.sess }); - } + if (createError || !data) { + successful = false; + if (isSnackbar) enqueueSnackbar(`Не удалось открыть диалог ${parseAxiosError(createError)}`); } else { - const [_, sendTicketMessageError] = await sendTicketMessage( - ticket.sessionData?.ticketId, - messageField, - false, - ); successful = true; - if (sendTicketMessageError) { - successful = false; - if (isSnackbar) enqueueSnackbar(sendTicketMessageError); - } + setTicketData({ ticketId: data.Ticket, sessionId: data.sess }); } - } + + } else { + const [_, sendTicketMessageError] = await sendTicketMessage({ + ticketId: ticket.sessionData?.ticketId, + message: messageField, + systemError: false, + }); + successful = true; + + if (sendTicketMessageError) { + successful = false; + if (isSnackbar) enqueueSnackbar(sendTicketMessageError); + } + } + } return successful; } \ No newline at end of file diff --git a/src/ui_kit/Modal/CropModal/NavigationPanel.tsx b/src/ui_kit/Modal/CropModal/NavigationPanel.tsx index 580a8a0a..95872caa 100644 --- a/src/ui_kit/Modal/CropModal/NavigationPanel.tsx +++ b/src/ui_kit/Modal/CropModal/NavigationPanel.tsx @@ -29,9 +29,6 @@ export const NavigationPanel: FC = ({ const isMobile = useMediaQuery(theme.breakpoints.down(786)); const lastStep = currentStep + 1 === totalSteps; - console.log("nextStepName") - console.log(nextStepName) - const handlePrevStep = () => { if (currentStep === 0) return; setCurrentStep(currentStep - 1); diff --git a/src/ui_kit/Modal/CropModal/WorkSpace.tsx b/src/ui_kit/Modal/CropModal/WorkSpace.tsx index 057d5975..e9f1a483 100644 --- a/src/ui_kit/Modal/CropModal/WorkSpace.tsx +++ b/src/ui_kit/Modal/CropModal/WorkSpace.tsx @@ -46,8 +46,6 @@ export default function WorkSpace({ modalModels[currentStepName] ), [currentStepName]); - // console.log(" промежуточный рендер которому должно быть похуй") - return ( <> { const [open, setOpen] = useState(false); const quiz = useCurrentQuiz(); const account = useUserStore() - console.log(account.userId) if (account.userId === "6755b1ddd5802e9f13663f56") { return ( <> diff --git a/src/utils/generateHubWalletRequest.ts b/src/utils/generateHubWalletRequest.ts new file mode 100644 index 00000000..b7f6bfdb --- /dev/null +++ b/src/utils/generateHubWalletRequest.ts @@ -0,0 +1,42 @@ +import { isTestServer } from "./hooks/useDomainDefine"; + +export const generateHubWalletRequestURL = ({ + wayback, + action, + dif, + userid, + additionalinformation, + token +}:{ + wayback?: string; + action: "topupwallet" | "createquizcc" | "buy"; + dif: string; + userid: string; + additionalinformation?: string; + token: string; +}) => { + let currentDomain = window.location.host; + if (currentDomain === "localhost") currentDomain += ":3000"; + +console.log("Я здесь для отладки и спешу сообщить, что деплой был успешно завершен!") + + // Используем более надежный способ генерации URL + const baseUrl = `http://${isTestServer ? "s" : ""}hub.pena.digital/anyservicepayment`; + const params = new URLSearchParams({ + fromdomain: currentDomain, + action: action, + dif: dif, + userid: userid, + sec: token + }); + + if (additionalinformation) params.append('additionalinformation', additionalinformation); + if (wayback) params.append('wayback', wayback); + + let url = `${baseUrl}?${params.toString()}`; + + // Для продакшена раскомментировать эту строку: + // let url = `https://${isTestServer ? "s" : ""}hub.pena.digital/payment?${params.toString()}`; + + return url; +} \ No newline at end of file diff --git a/src/utils/handleComponentError.ts b/src/utils/handleComponentError.ts index 94369f24..2687d868 100644 --- a/src/utils/handleComponentError.ts +++ b/src/utils/handleComponentError.ts @@ -1,5 +1,8 @@ -import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils"; import { ErrorInfo } from "react"; +import { Ticket, createTicket, getAuthToken, sendTicketMessage } from ".."; + +let errorsQueue: ComponentError[] = []; +let timeoutId: ReturnType; interface ComponentError { timestamp: number; @@ -8,40 +11,99 @@ interface ComponentError { componentStack: string | null | undefined; } -export function handleComponentError(error: Error, info: ErrorInfo) { - const componentError: ComponentError = { +function isErrorReportingAllowed(error?: Error): boolean { + // Если ошибка помечена как debug-override — всегда отправлять + if (error && (error as any).__forceSend) return true; + // Проверяем домен + const currentDomain = window.location.hostname; + return currentDomain !== 'localhost'; +} + +// Новый API: getTickets — callback, возвращающий актуальные тикеты +export function handleComponentError(error: Error, info: ErrorInfo, getTickets: () => Ticket[]) { + //репортим только о авторизонышах + if (!getAuthToken()) return; + // Проверяем разрешение на отправку ошибок (по домену) + if (!isErrorReportingAllowed(error)) { + console.log('❌ Отправка ошибки заблокирована:', error.message); + return; + } + console.log(`✅ Обработка ошибки: ${error.message}`); + // Копируем __forceSend если есть + const componentError: ComponentError & { __forceSend?: boolean } = { timestamp: Math.floor(Date.now() / 1000), message: error.message, callStack: error.stack, componentStack: info.componentStack, + ...(error && (error as any).__forceSend ? { __forceSend: true } : {}) }; - - queueErrorRequest(componentError); + queueErrorRequest(componentError, getTickets); } -let errorsQueue: ComponentError[] = []; -let timeoutId: ReturnType; - -function queueErrorRequest(error: ComponentError) { +// Ставит ошибку в очередь для отправки, через 1 секунду вызывает sendErrorsToServer +export function queueErrorRequest(error: ComponentError, getTickets: () => Ticket[]) { errorsQueue.push(error); - clearTimeout(timeoutId); timeoutId = setTimeout(() => { - sendErrorsToServer(); + sendErrorsToServer(getTickets); }, 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 = []; +// Отправляет накопленные ошибки в тикеты, ищет существующий тикет с system: true или создает новый +export async function sendErrorsToServer(getTickets: () => Ticket[]) { + if (errorsQueue.length === 0) return; + // Проверяем разрешение на отправку ошибок (по домену и debug-override) + // Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё + const forceSend = errorsQueue.some(e => (e as any).__forceSend); + if (!forceSend && !isErrorReportingAllowed()) { + console.log('❌ Отправка ошибок заблокирована, очищаем очередь'); + errorsQueue = []; + return; + } + const tickets = getTickets(); + try { + // Формируем сообщение об ошибке + const errorMessage = errorsQueue.map(error => { + return `[${new Date(error.timestamp * 1000).toISOString()}] ${error.message}\n\nCall Stack:\n${error.callStack || 'N/A'}\n\nComponent Stack:\n${error.componentStack || 'N/A'}`; + }).join('\n\n---\n\n'); + // ВСЕГДА ищем тикет через API + const existingSystemTicket = await findSystemTicket(tickets); + if (existingSystemTicket) { + sendTicketMessage({ + ticketId: existingSystemTicket, + message: errorMessage, + systemError: true, + }); + } else { + // Создаем новый тикет для ошибки + createTicket({ + message: errorMessage, + useToken: true, + systemError: true, + }); + } + } catch (error) { + console.error('Error in sendErrorsToServer:', error); + } finally { + // Очищаем очередь ошибок + errorsQueue = []; + } } + +// Ищет существующий тикет с system: true +export async function findSystemTicket(tickets: Ticket[]) { + for (const ticket of tickets) { + console.log("[findSystemTicket] Проверяем тикет:", ticket); + if (!('messages' in ticket)) { + if (ticket.top_message && ticket.top_message.system === true) { + console.log("[findSystemTicket] Найден тикет по top_message.system:true:", ticket.id); + return ticket.id; + } + } + } +} + +export function clearErrorHandlingConfig () { + clearTimeout(timeoutId); + errorsQueue = []; +} \ No newline at end of file diff --git a/src/utils/hooks/useAuthRedirect.ts b/src/utils/hooks/useAuthRedirect.ts new file mode 100644 index 00000000..7f9ff030 --- /dev/null +++ b/src/utils/hooks/useAuthRedirect.ts @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { + clearAuthToken, + getMessageFromFetchError, + setAuthToken, + getAuthToken, +} from "@frontend/kitui"; +import { + clearUserData, + setUser, + setUserAccount, + setUserId, + useUserStore, +} from "@root/stores/user"; +import { logout } from "@root/api/auth"; +import { clearCustomTariffs } from "@root/stores/customTariffs"; +import { clearTickets } from "@root/stores/tickets"; +import { setNotEnoughMoneyAmount } from "@stores/cart"; + +export const useAuthRedirect = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [isProcessing, setIsProcessing] = useState(false); + const user = useUserStore((state) => state.user); + + const action = searchParams.get("action"); + const dif = searchParams.get("dif"); + const token = searchParams.get("data"); + const userId = searchParams.get("userid"); + const wayback = searchParams.get("wayback"); + + useEffect(() => { + if (isProcessing) return; + + // Если пользователь уже авторизован и это тот же пользователь + if (user?._id === userId) { + let returnUrl = `/payment?action=${action}&dif=${dif}&user=${userId}`; + if (wayback) returnUrl += `&wayback=${wayback}`; + navigate(returnUrl, { replace: true }); + return; + } + + // Если есть все необходимые параметры для авторизации + if (action && dif && token && userId) { + setIsProcessing(true); + + (async () => { + try { + // Очищаем старые данные если есть токен + if (getAuthToken()) { + clearAuthToken(); + clearUserData(); + clearCustomTariffs(); + clearTickets(); + setNotEnoughMoneyAmount(0); + await logout(); + } + + // Устанавливаем новый токен и ID пользователя + setAuthToken(token); + setUserId(userId); + + // Перенаправляем на страницу оплаты + let returnUrl = `/payment?action=${action}&dif=${dif}&user=${userId}`; + if (wayback) returnUrl += `&wayback=${wayback}`; + navigate(returnUrl, { replace: true }); + } catch (error) { + console.error("Auth redirect error:", error); + // В случае ошибки перенаправляем на главную страницу тарифов + navigate("/tariffs", { replace: true }); + } finally { + setIsProcessing(false); + } + })(); + } + }, [user, action, dif, token, userId, wayback, navigate, isProcessing]); + + return { isProcessing }; +}; \ No newline at end of file diff --git a/src/utils/hooks/useAutoPay.ts b/src/utils/hooks/useAutoPay.ts index cc46fde5..a78d88d8 100644 --- a/src/utils/hooks/useAutoPay.ts +++ b/src/utils/hooks/useAutoPay.ts @@ -1,44 +1,42 @@ import { cartApi } from "@api/cart"; import { useUserStore } from "@/stores/user"; -import moment from "moment"; import { enqueueSnackbar } from "notistack"; import { useEffect } from "react"; -import { redirect, useNavigate, useSearchParams } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount"; import { startCC } from "@/stores/cc"; import { setEditQuizId, setCurrentStep } from "@root/quizes/actions"; +/* +Есть три пути по которому мы ходили из квиза в хаб. Нам нехватило денег при: +1)Покупке обычного тарифа +2)Покупке тарифа-заказ-квиза +3)Покупке тарифа в настройке квиза в вкладке ИИ +*/ + export const useAfterPay = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); + const userId = useUserStore(store => store.userId) const userAccount = useUserStore(state => state.userAccount); + const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart); - const purpose = searchParams.get("purpose"); - const paymentUserId = searchParams.get("userid"); - const currentCC = searchParams.get("cc"); - const wayback = searchParams.get("wayback"); - - // Обработка wayback параметра - useEffect(() => { - if (wayback) { - const quizId = wayback.split("_")[1]; - if (quizId) { - setEditQuizId(Number(quizId)); - setCurrentStep(17); // Шаг для персонализации AI - navigate("/personalization-ai"); - } - } - }, [wayback, navigate]); + let URLaction = searchParams.get("action");//что мы, собсна, хотим: оплатить, пополнить, заказать квиз + let URLuserId = searchParams.get("userid");//тот кто начал всё это действо + let URLadditionalinformation = searchParams.get("additionalinformation");//его токен useEffect(() => { - //Звёзды сошлись, будем оплачивать корзину - if (paymentUserId && paymentUserId === userId) { + + setSearchParams({}, { replace: true }); - if (purpose === "paycart") { - setSearchParams({}, { replace: true }); - if (currentCC) startCC() + if (userId && URLuserId && userId === URLuserId) { + + if (URLaction === "buy") startPayCartProcess(URLuserId); + + if (URLaction === "createquizcc") { + startCC(); (async () => { //Проверяем можем ли мы оплатить корзину здесь и сейчас @@ -46,19 +44,46 @@ export const useAfterPay = () => { if (payCartError) { //Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты - startPayCartProcess(paymentUserId) + startPayCartProcess(URLuserId); } else { - if (currentCC) navigate("/tariffs") - cancelPayCartProcess() + navigate("/tariffs"); + cancelPayCartProcess(); } })() } - } - }, [purpose, paymentUserId]) + + //Покупка ИИ тарифа из настройки квиза ИИ вкладки + if (URLaction === "buy" && URLadditionalinformation) { + const quizId = Number(URLadditionalinformation); + + if (quizId) { + setEditQuizId(Number(quizId)); //Выбираем квиз + setCurrentStep(17); // Шаг для персонализации AI + + // Проверяем wayback параметр для определения куда переходить + const wayback = searchParams.get("wayback"); + if (wayback === "edit") { + // Сначала переходим на /edit, затем на /personalization-ai + navigate("/edit"); + // Используем setTimeout чтобы дать время для загрузки /edit + setTimeout(() => { + navigate("/personalization-ai"); + }, 100); + } else { + // Прямой переход на /personalization-ai + navigate("/personalization-ai"); + } + } + } + + } + }, []); + + //Обработка необходимости купить после пополнения useEffect(() => { if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) { - const deadline = siteReadyPayCart[userId] + const deadline = siteReadyPayCart[userId]; if (calcTimeOfReadyPayCart(deadline)) { //Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло. @@ -66,11 +91,13 @@ export const useAfterPay = () => { const [, payCartError] = await cartApi.pay(); if (!payCartError) { - enqueueSnackbar("Товары успешно приобретены") - cancelPayCartProcess() + enqueueSnackbar("Товары успешно приобретены"); + cancelPayCartProcess(); } })() } } - }, [userAccount, userId, siteReadyPayCart]) + }, [userAccount, userId, siteReadyPayCart, userWithWallet]) + + } \ No newline at end of file diff --git a/src/utils/hooks/usePipeSubscriber.ts b/src/utils/hooks/usePipeSubscriber.ts index 3c450e6b..b6b58d63 100644 --- a/src/utils/hooks/usePipeSubscriber.ts +++ b/src/utils/hooks/usePipeSubscriber.ts @@ -4,7 +4,7 @@ import { useSSETab } from "./useSSETab"; import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount"; import { setCash } from "@/stores/cash"; import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter"; -import { inCart } from "@/pages/Tariffs/Tariffs"; +import { inCart } from "@/pages/Tariffs/utils"; type Ping = [{ event: "ping" }] @@ -43,8 +43,6 @@ export const usePipeSubscriber = () => { `/customer/v1.0.1/account/pipe?Authorization=${token}`, onNewData: (data) => { let message = data[0] as PipeMessage - console.log("truba") - console.log(message) updateSSEValue(message) //Пропускаем пингование diff --git a/src/utils/hooks/useUserAccountFetcher.ts b/src/utils/hooks/useUserAccountFetcher.ts index f057114f..948c5aa4 100644 --- a/src/utils/hooks/useUserAccountFetcher.ts +++ b/src/utils/hooks/useUserAccountFetcher.ts @@ -1,7 +1,7 @@ import { useEffect, useLayoutEffect, useRef } from "react"; import { createUserAccount, devlog } from "@frontend/kitui"; import { isAxiosError } from "axios"; -import { makeRequest } from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import type { UserAccount } from "@frontend/kitui"; import { setUserAccount } from "@/stores/user"; @@ -37,27 +37,52 @@ export const useUserAccountFetcher = ({ }) .then((result) => { devlog("User account", result); - console.log(result) if (result) onNewUserAccountRef.current(result); }) .catch((error) => { devlog("Error fetching user account", error); if (error.response?.status === 409) return; if (isAxiosError(error) && error.response?.status === 404) { - createUserAccount(controller.signal, url.replace("get", "create")) + + // Формируем правильный URL для создания аккаунта + let createUrl = url; + if (url.includes("/customer/v1.0.1/account")) { + // Для customerAccount используем тот же URL (POST запрос) + createUrl = url; + } else if (url.includes("/squiz/account/get")) { + // Для userAccount заменяем get на create + createUrl = url.replace("get", "create"); + } + + + createUserAccount(controller.signal, createUrl) .then((result) => { devlog("Created user account", result); - console.log("это пойдёт в стор: ") - console.log(result) - if (result) onNewUserAccountRef.current(result.created_account as T); + + // Проверяем структуру ответа и записываем в стор + if (result) { + // Если результат содержит created_account, используем его + if (result.created_account) { + onNewUserAccountRef.current(result.created_account as T); + } + // Если результат сам является аккаунтом (для customerAccount) + else if (result.userId && result.wallet) { + onNewUserAccountRef.current(result as T); + } + // Если ничего не подходит, логируем для диагностики + else { + onNewUserAccountRef.current(result as T); + } + } }) .catch((error) => { if (error.response?.status === 409) return; devlog("Error creating user account", error); + console.error("useUserAccountFetcher: Error creating account:", error); onErrorRef.current?.(error); }); } else { - console.log(error) + console.error(error) onErrorRef.current?.(error); } }); diff --git a/src/utils/parse-error.ts b/src/utils/parse-error.ts index 9d689731..fd2bb26a 100644 --- a/src/utils/parse-error.ts +++ b/src/utils/parse-error.ts @@ -29,7 +29,7 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => { if (error.message === "Failed to fetch") return ["Ошибка сети"]; //ДЛЯ ОПЛАТЫ ТАРИФА - if(error.response.status === 402) { + if(error.response?.status === 402) { console.error(error.response?.data.message) return error.response?.data.message } @@ -40,8 +40,8 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => { const status = error.response.status; if(status === 409 || status === 401 || status === 404) { - const serverErrorMessage = error.response.data.message - console.log(serverErrorMessage) + const responseData = error.response.data as any; + const serverErrorMessage = responseData?.message || responseData?.error; const translatedMessage = translateMessage[serverErrorMessage?.toLowerCase() || ""] return [translatedMessage || "", serverError.statusCode]; }