Merge branch 'staging'

This commit is contained in:
Nastya 2025-08-13 01:44:08 +03:00
commit 8a1067bfd5
84 changed files with 2018 additions and 1291 deletions

@ -2,9 +2,8 @@ name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry run-name: ${{ gitea.actor }} build image and push to container registry
on: on:
push: registry_package:
branches: types: [published]
- 'main'
jobs: jobs:
CreateImage: CreateImage:

@ -2,25 +2,30 @@ name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry run-name: ${{ gitea.actor }} build image and push to container registry
on: on:
push: registry_package:
branches: types: [published]
- 'staging'
jobs: jobs:
CreateImage: # CreateImage:
runs-on: [skeris] # runs-on: [skeris]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p # uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with: # with:
runner: skeris # runner: skeris
secrets: # secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }} # REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} # REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService: DeployService:
if: contains(github.event.package.name, 'staging')
runs-on: [frontstaging] runs-on: [frontstaging]
needs: CreateImage container:
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7 image: gitea.pena:3000/penadevops/container-images/node-compose:main
with: env:
runner: frontstaging GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
actionid: ${{ gitea.run_id }} 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

@ -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

@ -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

@ -2,6 +2,7 @@ services:
squiz: squiz:
container_name: squiz container_name: squiz
restart: unless-stopped restart: unless-stopped
image: gitea.pena/squiz/frontpanel/staging:$GITHUB_RUN_NUMBER image: gitea.pena/squiz/frontpanel/staging:latest
hostname: squiz hostname: squiz
tty: true tty: true
pull_policy: always

@ -6,7 +6,7 @@
"@craco/craco": "^7.0.0", "@craco/craco": "^7.0.0",
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.108", "@frontend/kitui": "1.0.110",
"@frontend/squzanswerer": "^1.0.57", "@frontend/squzanswerer": "^1.0.57",
"@mui/icons-material": "^5.10.14", "@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14", "@mui/material": "^5.10.14",
@ -69,6 +69,7 @@
"test": "craco test", "test": "craco test",
"eject": "craco eject", "eject": "craco eject",
"code:format": "prettier --write --ignore-unknown", "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", "prepare": "husky install",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run" "cypress:run": "cypress run"

@ -165,7 +165,7 @@
console.log(params.get("debug")) console.log(params.get("debug"))
if (params.get("debug")) { if (params.get("debug")) {
console.log( console.log(
"mhgfhdhfjhffhfhjfghjgf" "params.get(debug) is true"
) )
let scriptTag = document.createElement('script'); let scriptTag = document.createElement('script');
scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js"); scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js");

@ -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 type { OriginalUserAccount } from "@root/user";
import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user"; import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user";
import ContactFormModal from "@ui_kit/ContactForm"; import ContactFormModal from "@ui_kit/ContactForm";
@ -8,7 +8,7 @@ import { useAfterPay } from "@utils/hooks/useAutoPay";
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher"; import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import type { SuspenseProps } from "react"; import type { SuspenseProps } from "react";
import { lazy, Suspense } from "react"; import { lazy, Suspense, useEffect } from "react";
import { lazily } from "react-lazily"; import { lazily } from "react-lazily";
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { useAmoAccount } from "./api/integration"; import { useAmoAccount } from "./api/integration";
@ -23,7 +23,11 @@ import { InfoPrivilege } from "./pages/InfoPrivilege";
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog"; import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
import Landing from "./pages/Landing/Landing"; import Landing from "./pages/Landing/Landing";
import Main from "./pages/main"; 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 { ErrorBoundary } from "react-error-boundary";
import { handleLogoutClick } from "./utils/HandleLogoutClick";
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull")); const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery")); 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); let params = new URLSearchParams(document.location.search);
const isTest = Boolean(params.get("test")) const isTest = Boolean(params.get("test"))
createMakeRequestConfig(
handleLogoutClick,
(error, info, getTickets) => handleComponentError(error, info, getTickets()),
() => useTicketStore.getState().tickets
);
const routeslink = [ const routeslink = [
{ {
path: "/edit", path: "/edit",
@ -73,12 +83,16 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => (
<Suspense fallback={fallback ?? <></>}>{children}</Suspense> <Suspense fallback={fallback ?? <></>}>{children}</Suspense>
); );
const ApologyPage = () => <div><p>Что-то пошло не так</p></div>
export default function App() { export default function App() {
window.LoadingObserver = false; window.LoadingObserver = false;
const userId = useUserStore((state) => state.userId); const userId = useUserStore((state) => state.userId);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { data: amoAccount } = useAmoAccount(); const { data: amoAccount } = useAmoAccount();
const tickets = useTicketStore(store => store.tickets);
useUserFetcher({ useUserFetcher({
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`, url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
@ -97,8 +111,11 @@ export default function App() {
useUserAccountFetcher<UserAccount>({ useUserAccountFetcher<UserAccount>({
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`, url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
userId, userId,
onNewUserAccount: setCustomerAccount, onNewUserAccount: (account) => {
setCustomerAccount(account);
},
onError: (error) => { onError: (error) => {
console.error("App: Error in customerAccount fetcher:", error);
const errorMessage = getMessageFromFetchError(error); const errorMessage = getMessageFromFetchError(error);
if (errorMessage) { if (errorMessage) {
enqueueSnackbar(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(); useAfterPay();
if (location.state?.redirectTo) if (location.state?.redirectTo)
@ -136,7 +184,10 @@ export default function App() {
); );
return ( return (
<> <ErrorBoundary
FallbackComponent={ApologyPage}
onError={(error, info) => handleComponentError(error, info, () => useTicketStore.getState().tickets)}
>
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />} {amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}
<ContactFormModal /> <ContactFormModal />
@ -259,6 +310,10 @@ export default function App() {
path={"/image/:srcImage"} path={"/image/:srcImage"}
element={<ChatImageNewWindow />} element={<ChatImageNewWindow />}
/> />
<Route
path={"/debug"}
element={<div></div>}
/>
<Route element={<PrivateRoute />}> <Route element={<PrivateRoute />}>
{routeslink.map((e, i) => ( {routeslink.map((e, i) => (
<Route <Route
@ -280,6 +335,9 @@ export default function App() {
))} ))}
</Route> </Route>
</Routes> </Routes>
</>
{/* Компонент отладки ошибок - доступен по Ctrl+Shift+D */}
<Debug />
</ErrorBoundary>
); );
} }

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,5 +1,5 @@
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types"; import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { useToken } from "@frontend/kitui"; import { useToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import useSWR from "swr"; import useSWR from "swr";

@ -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 <TRequest = unknown, TResponse = unknown>(
data: MakeRequest,
): Promise<TResponse> => {
try {
const response = await KIT.makeRequest<unknown, TResponse>(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;
}
};

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines"; import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { defaultQuizConfig } from "@model/quizSettings"; import { defaultQuizConfig } from "@model/quizSettings";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import type { GetTariffsResponse } from "@frontend/kitui"; import type { GetTariffsResponse } from "@frontend/kitui";

@ -1,6 +1,6 @@
import { createTicket as createTicketRequest } from "@frontend/kitui"; import { createTicket as createTicketRequest } from "@frontend/kitui";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
@ -15,47 +15,7 @@ type SendFileResponse = {
const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`; 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 ( export const sendFile = async (
ticketId: string, ticketId: string,

@ -1,4 +1,4 @@
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";

File diff suppressed because one or more lines are too long

@ -0,0 +1,19 @@
import { Box, SxProps } from "@mui/material";
export default (sx: SxProps) => (
<Box
component="svg"
width="40px"
height="34px"
viewBox="0 0 40 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
sx={{
mr: "15px"
}}
>
<path d="M37 0H3C1.34315 0 0 1.34314 0 3V20.967H40V3C40 1.34315 38.6569 0 37 0Z" fill="#9A9AAF" fillOpacity="0.2" />
<path d="M0 24.1244V21.967H40V24.1244C40 25.7813 38.6569 27.1244 37 27.1244H25.5472L26.8066 33.5412H13.2534L14.3928 27.1244H3C1.34315 27.1244 0 25.7813 0 24.1244Z" fill="#9A9AAF" fillOpacity="0.2" />
<path d="M12.9803 9.56785L16.8969 12.7545C17.2236 13.0203 17.7125 12.7878 17.7125 12.3666V10.6689C19.4084 10.3847 22.8834 10.4557 23.2157 13.0131C23.631 16.2098 20.9105 17.2115 20.1629 17.318C19.4153 17.4246 19.7476 18 19.9137 18C22.115 18 25.5 16.2098 25.5 12.8213C25.3505 7.77475 20.246 7.19508 17.7125 7.53607V6.04142C17.7125 5.62197 17.2271 5.38894 16.8998 5.65124L12.9832 8.78983C12.7346 8.98903 12.7332 9.3668 12.9803 9.56785Z" fill="#7E2AEA" fillOpacity="0.5" />
</Box>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

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

@ -69,6 +69,8 @@ export type QuizTheme =
export enum QuizMetricType { export enum QuizMetricType {
yandex = "yandexMetricsNumber", yandex = "yandexMetricsNumber",
vk = "vkMetricsNumber", vk = "vkMetricsNumber",
zapier = "zapierIntegration",
postback = "postbackIntegration",
} }
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address"; export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";

@ -42,16 +42,9 @@ const GeneralItem = ({
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue), ([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
); );
const days = data.map(([value]) => value); 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 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
? conversionValue ? conversionValue
: Object.values(general).reduce((total, item) => total + item, 0); : Object.values(general).reduce((total, item) => total + item, 0);
@ -82,17 +75,17 @@ const GeneralItem = ({
<LineChart <LineChart
xAxis={[ xAxis={[
{ {
data: statiscticsResult ? days : Object.keys(general), data: Object.keys(general),
valueFormatter: (value) => { valueFormatter: (value) => {
const timestamp = Number(value); const timestamp = Number(value);
if (isNaN(timestamp)) return 'Invalid Date'; 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={[ series={[
{ {
data: Object.values(statiscticsResult ? time : general), data: Object.values(general),
valueFormatter: (value) => valueFormatter: (value) =>
calculateTime calculateTime
? getCalculatedTime(value) ? getCalculatedTime(value)
@ -131,11 +124,21 @@ export const General: FC<GeneralProps> = ({ data, day }) => {
const generalResponse = Object.entries(data).reduce( const generalResponse = Object.entries(data).reduce(
(total, [fatherKey, values]) => { (total, [fatherKey, values]) => {
const value = Object.keys(values).reduce((totalValue, key) => { const value = Object.keys(values).reduce((totalValue, key) => {
if (Number(key) - currentDate < 0) { const keyTimestamp = Number(key);
return { ...totalValue, [key]: values[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 }; return { ...total, [fatherKey]: value };

149
src/pages/Debug.tsx Normal file

@ -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 (
<Box sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
bgcolor: 'rgba(0,0,0,0.9)',
zIndex: 9999,
p: 4,
overflow: 'auto'
}}>
<Box sx={{ maxWidth: 800, mx: 'auto', bgcolor: 'background.paper', p: 3, borderRadius: 2 }}>
<Typography variant="h4" gutterBottom>
🛠 Отладка ошибок
</Typography>
<Typography variant="body2" sx={{ mb: 3, color: 'text.secondary' }}>
Ctrl+Shift+F для закрытия
</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Описание ошибки</TableCell>
<TableCell align="right">Действие</TableCell>
</TableRow>
</TableHead>
<TableBody>
{errorTests.map((test, index) => (
<TableRow key={index}>
<TableCell>{test.description}</TableCell>
<TableCell align="right">
<Button
variant="contained"
color="error"
size="small"
onClick={test.action}
>
Вызвать
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
</Box>
);
};
export default Debug;

@ -150,10 +150,6 @@ export const AmoQuestions: FC<Props> = ({
}, [activeScope]) }, [activeScope])
const [blockButton, setBlockButton] = useState(false) const [blockButton, setBlockButton] = useState(false)
console.log("selectedQuestions")
console.log(selectedQuestions)
console.log("SCFworld")
console.log(SCFworld)
return ( return (
<> <>
<Box <Box

@ -95,8 +95,6 @@ export const SwitchPages = ({
const [specialPage, setSpecialPage] = useState<"deleteCell" | "removeAccount" | "settingsBlock" | "accountInfo" | "amoLogin" | "">(accountInfo ? "accountInfo" : "amoLogin") const [specialPage, setSpecialPage] = useState<"deleteCell" | "removeAccount" | "settingsBlock" | "accountInfo" | "amoLogin" | "">(accountInfo ? "accountInfo" : "amoLogin")
const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null); const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null);
console.log("--")
console.log(selectedQuestions)
const startDeleteTagQuestion = (itemForDelete) => { const startDeleteTagQuestion = (itemForDelete) => {
setOpenDelete(itemForDelete) setOpenDelete(itemForDelete)
@ -135,9 +133,6 @@ export const SwitchPages = ({
if (type === "tag") { if (type === "tag") {
setSelectedTags((prevState) => { setSelectedTags((prevState) => {
console.log(prevState)
console.log(scope)
console.log(id)
return({ return({
...prevState, ...prevState,
[scope]: [...prevState[scope as TagKeys], id], [scope]: [...prevState[scope as TagKeys], id],
@ -147,9 +142,6 @@ export const SwitchPages = ({
if (type === "question") { if (type === "question") {
const q = questions.find(e => e.backendId === Number(id)) const q = questions.find(e => e.backendId === Number(id))
setSelectedQuestions((prevState) => { setSelectedQuestions((prevState) => {
console.log(prevState)
console.log(scope)
console.log(id)
return ({ return ({
...prevState, ...prevState,
[scope]: [...prevState[scope as QuestionKeys], { [scope]: [...prevState[scope as QuestionKeys], {

@ -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`; const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`;
export const resetAmoTagsFields = async () => { export const resetAmoTagsFields = async () => {

@ -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<PostbackModalProps> = ({
isModalOpen,
handleCloseModal,
companyName,
quiz
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<Dialog
open={isModalOpen}
onClose={handleCloseModal}
fullWidth
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "919px",
height: "658px",
borderRadius: "12px",
},
}}
>
<Box>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 20px",
}}
>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
color: theme.palette.grey2.main,
}}
>
Интеграция с {companyName ? companyName : "Postback"}
</Typography>
<IconButton onClick={handleCloseModal}>
<CloseIcon />
</IconButton>
</Box>
<Box
sx={{
padding: "20px",
height: "calc(100% - 68px)",
overflow: "auto",
}}
>
<Typography variant="body1">
Интеграция с Postback находится в разработке.
</Typography>
</Box>
</Box>
</Dialog>
);
};

@ -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<ZapierModalProps> = ({
isModalOpen,
handleCloseModal,
companyName,
quiz
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<Dialog
open={isModalOpen}
onClose={handleCloseModal}
fullWidth
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "919px",
height: "658px",
borderRadius: "12px",
},
}}
>
<Box>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 20px",
}}
>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
color: theme.palette.grey2.main,
}}
>
Интеграция с {companyName ? companyName : "Zapier"}
</Typography>
<IconButton onClick={handleCloseModal}>
<CloseIcon />
</IconButton>
</Box>
<Box
sx={{
padding: "20px",
height: "calc(100% - 68px)",
overflow: "auto",
}}
>
<Typography variant="body1">
Интеграция с Zapier находится в разработке.
</Typography>
</Box>
</Box>
</Dialog>
);
};

@ -27,6 +27,8 @@ export const IntegrationsPage = ({
>(null); >(null);
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false); const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (editQuizId === null) navigate("/list"); if (editQuizId === null) navigate("/list");
@ -44,6 +46,12 @@ export const IntegrationsPage = ({
const handleCloseAmoSRMModal = () => { const handleCloseAmoSRMModal = () => {
setIsAmoCrmModalOpen(false); setIsAmoCrmModalOpen(false);
}; };
const handleCloseZapierModal = () => {
setIsZapierModalOpen(false);
};
const handleClosePostbackModal = () => {
setIsPostbackModalOpen(false);
};
return ( return (
<> <>
@ -60,7 +68,7 @@ export const IntegrationsPage = ({
> >
<Typography <Typography
variant="h5" variant="h5"
sx={{ marginBottom: "40px", color: "#333647" }} sx={{ marginBottom: "40px", color: theme.palette.grey3.main }}
> >
Интеграции Интеграции
</Typography> </Typography>
@ -73,6 +81,12 @@ export const IntegrationsPage = ({
setIsAmoCrmModalOpen={setIsAmoCrmModalOpen} setIsAmoCrmModalOpen={setIsAmoCrmModalOpen}
isAmoCrmModalOpen={isAmoCrmModalOpen} isAmoCrmModalOpen={isAmoCrmModalOpen}
handleCloseAmoSRMModal={handleCloseAmoSRMModal} handleCloseAmoSRMModal={handleCloseAmoSRMModal}
setIsZapierModalOpen={setIsZapierModalOpen}
isZapierModalOpen={isZapierModalOpen}
handleCloseZapierModal={handleCloseZapierModal}
setIsPostbackModalOpen={setIsPostbackModalOpen}
isPostbackModalOpen={isPostbackModalOpen}
handleClosePostbackModal={handleClosePostbackModal}
/> />
</Box> </Box>
</> </>

@ -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 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 { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
// import AnalyticsModal from "./AnalyticsModal/AnalyticsModal";
import { VKPixelLogo } from "../mocks/VKPixelLogo"; import { VKPixelLogo } from "../mocks/VKPixelLogo";
import { QuizMetricType } from "@model/quizSettings"; import { QuizMetricType } from "@model/quizSettings";
import { AmoCRMLogo } from "../mocks/AmoCRMLogo"; import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
import { useCurrentQuiz } from "@/stores/quizes/hooks"; import { useCurrentQuiz } from "@/stores/quizes/hooks";
import { useUserStore } from "@/stores/user";
const AnalyticsModal = lazy(() => const AnalyticsModal = lazy(() =>
import("./AnalyticsModal/AnalyticsModal").then((module) => ({ 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 = { type PartnersBoardProps = {
setIsModalOpen: (value: boolean) => void; setIsModalOpen: (value: boolean) => void;
companyName: keyof typeof QuizMetricType | null; companyName: keyof typeof QuizMetricType | null;
@ -30,6 +42,12 @@ type PartnersBoardProps = {
setIsAmoCrmModalOpen: (value: boolean) => void; setIsAmoCrmModalOpen: (value: boolean) => void;
isAmoCrmModalOpen: boolean; isAmoCrmModalOpen: boolean;
handleCloseAmoSRMModal: () => void; handleCloseAmoSRMModal: () => void;
setIsZapierModalOpen: (value: boolean) => void;
isZapierModalOpen: boolean;
handleCloseZapierModal: () => void;
setIsPostbackModalOpen: (value: boolean) => void;
isPostbackModalOpen: boolean;
handleClosePostbackModal: () => void;
}; };
export const PartnersBoard: FC<PartnersBoardProps> = ({ export const PartnersBoard: FC<PartnersBoardProps> = ({
@ -41,13 +59,31 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
setIsAmoCrmModalOpen, setIsAmoCrmModalOpen,
isAmoCrmModalOpen, isAmoCrmModalOpen,
handleCloseAmoSRMModal, handleCloseAmoSRMModal,
setIsZapierModalOpen,
isZapierModalOpen,
handleCloseZapierModal,
setIsPostbackModalOpen,
isPostbackModalOpen,
handleClosePostbackModal,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const quiz = useCurrentQuiz(); 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 ( return (
<Box <Box
@ -59,24 +95,16 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
}} }}
> >
<Box> <Box>
<>
<Typography <Typography
variant="h6" variant="h6"
sx={{ sx={{
textAlign: { xs: "start", sm: "start", md: "start" }, ...sectionTitleStyles,
lineHeight: "1", marginTop: 0,
marginBottom: "12px",
}} }}
> >
CRM CRM
</Typography> </Typography>
<Box <Box sx={containerStyles}>
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: { xs: "start", sm: "start", md: "start" },
}}
>
<ServiceButton <ServiceButton
logo={<AmoCRMLogo />} logo={<AmoCRMLogo />}
setIsModalOpen={setIsAmoCrmModalOpen} setIsModalOpen={setIsAmoCrmModalOpen}
@ -84,24 +112,11 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
name={"amoCRM"} name={"amoCRM"}
/> />
</Box> </Box>
</>
<Typography <Typography variant="h6" sx={sectionTitleStyles}>
variant="h6"
sx={{
textAlign: { xs: "start", sm: "start", md: "start" },
lineHeight: "1",
marginBottom: "12px",
}}
>
Аналитика Аналитика
</Typography> </Typography>
<Box <Box sx={containerStyles}>
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: { xs: "start", sm: "start", md: "start" },
}}
>
<ServiceButton <ServiceButton
logo={<YandexMetricaLogo />} logo={<YandexMetricaLogo />}
setIsModalOpen={setIsModalOpen} setIsModalOpen={setIsModalOpen}
@ -114,9 +129,24 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
name={"vk"} name={"vk"}
setIsModalOpen={setIsModalOpen} setIsModalOpen={setIsModalOpen}
setCompanyName={setCompanyName} setCompanyName={setCompanyName}
></ServiceButton> />
</Box>
<Typography variant="h6" sx={sectionTitleStyles}>
Автоматизация
</Typography>
<Box sx={containerStyles}>
<ZapierButton
setIsModalOpen={setIsZapierModalOpen}
setCompanyName={setCompanyName}
/>
<PostbackButton
setIsModalOpen={setIsPostbackModalOpen}
setCompanyName={setCompanyName}
/>
</Box> </Box>
</Box> </Box>
{companyName && ( {companyName && (
<Suspense> <Suspense>
<AnalyticsModal <AnalyticsModal
@ -132,7 +162,27 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
isModalOpen={isAmoCrmModalOpen} isModalOpen={isAmoCrmModalOpen}
handleCloseModal={handleCloseAmoSRMModal} handleCloseModal={handleCloseAmoSRMModal}
companyName={companyName} companyName={companyName}
quiz={quiz} quiz={quiz!}
/>
</Suspense>
)}
{companyName && isZapierModalOpen && (
<Suspense>
<ZapierModal
isModalOpen={isZapierModalOpen}
handleCloseModal={handleCloseZapierModal}
companyName={companyName}
quiz={quiz!}
/>
</Suspense>
)}
{companyName && isPostbackModalOpen && (
<Suspense>
<PostbackModal
isModalOpen={isPostbackModalOpen}
handleCloseModal={handleClosePostbackModal}
companyName={companyName}
quiz={quiz!}
/> />
</Suspense> </Suspense>
)} )}

@ -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<PartnerItemProps> = ({
setIsModalOpen,
logo,
title,
name,
setCompanyName,
}) => {
const theme = useTheme();
const handleClick = () => {
setCompanyName(name as keyof typeof QuizMetricType);
setIsModalOpen(true);
};
return (
<>
<Box
sx={{
width: 250,
height: 60,
backgroundColor: "white",
borderRadius: "8px",
padding: "0 20px",
display: "flex",
alignItems: "center",
marginBottom: "2%",
marginRight: "2%",
cursor: "pointer",
}}
onClick={handleClick}
>
{logo && logo}
<Typography
sx={{
fontSize: "18px",
fontWeight: "400",
marginLeft: "15px",
}}
>
{title && title}
</Typography>
</Box>
</>
);
};

@ -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<IntegrationButtonProps> = ({
children,
onClick,
padding = "0 20px",
}) => {
const [isPressed, setIsPressed] = useState(false);
const handleMouseDown = () => setIsPressed(true);
const handleMouseUp = () => setIsPressed(false);
const handleMouseLeave = () => setIsPressed(false);
return (
<Box
sx={{
width: 250,
height: 60,
backgroundColor: "white",
borderRadius: "8px",
padding,
display: "flex",
alignItems: "center",
cursor: "pointer",
transition: "all 0.2s ease",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
transform: isPressed ? "translateY(1px)" : "translateY(0)",
"&:hover": {
backgroundColor: "#f8f9fa",
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.15)",
transform: "translateY(-1px)",
},
"&:active": {
transform: "translateY(1px)",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)",
},
}}
onClick={onClick}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
>
{children}
</Box>
);
};

@ -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<PostbackButtonProps> = ({
setIsModalOpen,
setCompanyName,
}) => {
const handleClick = () => {
setCompanyName("postback" as keyof typeof QuizMetricType);
setIsModalOpen(true);
};
return (
<IntegrationButton onClick={handleClick} padding="0 0 0 20px">
<>
{/* Иконка монитора */}
<PostbackPC />
{/* Текст Postback */}
<PostbackDefault />
</>
</IntegrationButton>
);
};

@ -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<PartnerItemProps> = ({
setIsModalOpen,
logo,
title,
name,
setCompanyName,
}) => {
const handleClick = () => {
setCompanyName(name as keyof typeof QuizMetricType);
setIsModalOpen(true);
};
return (
<IntegrationButton onClick={handleClick}>
{logo && logo}
<Typography
sx={{
fontSize: "18px",
fontWeight: "400",
marginLeft: "15px",
}}
>
{title && title}
</Typography>
</IntegrationButton>
);
};

@ -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<ZapierButtonProps> = ({
setIsModalOpen,
setCompanyName,
}) => {
const handleClick = () => {
setCompanyName("zapier" as keyof typeof QuizMetricType);
setIsModalOpen(true);
};
return (
<IntegrationButton onClick={handleClick} padding="14px 128px 14px 20px">
<Box
component="img"
src={zapierLogo}
alt="Zapier"
sx={{
width: "103px",
height: "33px",
objectFit: "contain",
}}
/>
</IntegrationButton>
);
};

@ -0,0 +1,12 @@
import { useAuthRedirect } from "../../utils/hooks/useAuthRedirect";
export default function Payment() {
// Используем хук авторизации
const { isProcessing } = useAuthRedirect();
// Если идет обработка авторизации, показываем загрузку
if (isProcessing) {
return <div>Идёт загрузка...</div>;
}
// ... existing component code ...

@ -11,8 +11,6 @@ export const AuditoryList = ({utmParams, auditory, onDelete}:{utmParams:string,a
const { isTestServer } = useDomainDefine(); const { isTestServer } = useDomainDefine();
const [linksOpen, setLinksOpen] = useState(true); const [linksOpen, setLinksOpen] = useState(true);
console.log("auditory-___---_auditory__---__-__auditory_------__---__-__---_------__---__-__---_------__---__-____--__")
console.log(auditory)
return ( return (
<> <>

@ -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 (
<ClickAwayListener onClickAway={handleTooltipClose}>
<div style={{ position: 'relative' }}>
{isDisabled && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
cursor: 'help'
}}
onMouseEnter={handleTooltipOpen}
onMouseLeave={handleTooltipClose}
onClick={handleTooltipOpen}
/>
)}
<Tooltip
PopperProps={{
disablePortal: true,
sx: {
"& .MuiTooltip-tooltip": {
fontSize: "14px",
padding: "12px",
maxWidth: "400px",
whiteSpace: "pre-line"
}
}
}}
placement="top"
onClose={handleTooltipClose}
open={open}
title={tooltipText}
>
<Button
onClick={onClick}
variant="contained"
disabled={isDisabled}
sx={{
bgcolor: theme.palette.brightPurple.main,
borderRadius: "8px",
width: "130px",
height: "48px",
boxShadow: "none",
textTransform: "none",
fontSize: "18px",
'&:hover': { bgcolor: theme.palette.brightPurple.main },
}}
>
Ок
</Button>
</Tooltip>
</div>
</ClickAwayListener>
);
}

@ -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 CheckboxIcon from "@icons/Checkbox";
import AgeInputWithSelect from "./AgeInputWithSelect"; import AgeInputWithSelect from "./AgeInputWithSelect";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import CreateButtonWithTooltip from "./CreateButtonWithTooltip";
interface GenderAndAgeSelectorProps { interface GenderAndAgeSelectorProps {
gender: string; gender: string;
@ -190,23 +191,12 @@ export default function GenderAndAgeSelector({
</FormControl> </FormControl>
</Box> </Box>
<Button <CreateButtonWithTooltip
gender={gender}
age={age}
ageError={ageError}
onClick={onStartCreate} onClick={onStartCreate}
variant="contained" />
disabled={!gender || !age || ageError}
sx={{
bgcolor: theme.palette.brightPurple.main,
borderRadius: "8px",
width: "130px",
height: "48px",
boxShadow: "none",
textTransform: "none",
fontSize: "18px",
'&:hover': { bgcolor: theme.palette.brightPurple.main },
}}
>
Ок
</Button>
</Box> </Box>
); );
} }

@ -11,16 +11,17 @@ import { useSnackbar } from "notistack";
import { PayModal } from "./PayModal"; import { PayModal } from "./PayModal";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { cartApi } from "@/api/cart"; import { cartApi } from "@/api/cart";
import { outCart } from "../Tariffs/Tariffs"; import { outCart } from "../Tariffs/utils";
import { inCart } from "../Tariffs/Tariffs"; import { inCart } from "../Tariffs/utils";
import { isTestServer } from "@/utils/hooks/useDomainDefine"; import { isTestServer } from "@/utils/hooks/useDomainDefine";
import { useToken } from "@frontend/kitui"; import { useToken } from "@frontend/kitui";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { setUserAccount, setCustomerAccount } from "@/stores/user"; import { setUserAccount, setCustomerAccount } from "@/stores/user";
import { quizApi } from "@api/quiz"; import { quizApi } from "@api/quiz";
import { setQuizes } from "@root/quizes/actions"; import { setQuizes } from "@root/quizes/actions";
import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo"; import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo";
import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest";
const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19"; const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19";
export default function PersonalizationAI() { export default function PersonalizationAI() {
@ -92,7 +93,7 @@ export default function PersonalizationAI() {
useToken: true, useToken: true,
withCredentials: false, withCredentials: false,
}).catch(error => { }).catch(error => {
console.log(error) console.error(error)
enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" }); enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" });
return null; return null;
}), }),
@ -102,7 +103,7 @@ export default function PersonalizationAI() {
useToken: true, useToken: true,
withCredentials: false, withCredentials: false,
}).catch(error => { }).catch(error => {
console.log(error) console.error(error)
enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" }); enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" });
return null; return null;
}) })
@ -115,7 +116,7 @@ export default function PersonalizationAI() {
setCustomerAccount(customerAccountResult); setCustomerAccount(customerAccountResult);
} }
} catch (error) { } catch (error) {
console.log(error) console.error(error)
enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" }); enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" });
} }
} }
@ -128,8 +129,6 @@ export default function PersonalizationAI() {
(async () => { (async () => {
if (quiz?.backendId) { if (quiz?.backendId) {
const [result, error] = await auditoryGet({ quizId: quiz.backendId }); const [result, error] = await auditoryGet({ quizId: quiz.backendId });
console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__")
console.log(result)
if (result) { if (result) {
setAuditory(result); setAuditory(result);
} }
@ -209,8 +208,6 @@ export default function PersonalizationAI() {
setUtmParams(paramString ? `&${paramString}` : ""); setUtmParams(paramString ? `&${paramString}` : "");
}; };
console.log("______----giga_chat-----__--_---_--_----__--__-__--_--__--__--_---_______-quiz")
console.log(quiz?.giga_chat)
const startCreate = async () => { const startCreate = async () => {
if (quiz?.giga_chat) { if (quiz?.giga_chat) {
createNewLink(); createNewLink();
@ -240,7 +237,15 @@ export default function PersonalizationAI() {
//если денег не хватило //если денег не хватило
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) { if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
var link = document.createElement("a"); 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); document.body.appendChild(link);
link.click(); link.click();
return; return;
@ -263,12 +268,9 @@ export default function PersonalizationAI() {
// Обновляем данные квиза после успешной оплаты // Обновляем данные квиза после успешной оплаты
console.log("Обновляем данные квиза после оплаты");
const [quizes, quizesError] = await quizApi.getList(); const [quizes, quizesError] = await quizApi.getList();
console.log("Получены данные квизов:", quizes);
if (!quizesError) { if (!quizesError) {
setQuizes(quizes); setQuizes(quizes);
console.log("Данные квизов обновлены в сторе");
} else { } else {
console.error("Ошибка при получении данных квизов:", quizesError); console.error("Ошибка при получении данных квизов:", quizesError);
} }
@ -289,7 +291,7 @@ export default function PersonalizationAI() {
lineHeight: "21.4px" lineHeight: "21.4px"
}}> }}>
Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам. Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам.
<br/>Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже. <br />Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
</Typography> </Typography>
<Typography sx={{ <Typography sx={{
color: theme.palette.grey3.main, fontSize: "18px", maxWidth: 796, m: 0, color: theme.palette.grey3.main, fontSize: "18px", maxWidth: 796, m: 0,

@ -8,7 +8,6 @@ import { uploadQuestionImage } from "@/stores/questions/actions";
import { useCurrentQuiz } from "@/stores/quizes/hooks"; import { useCurrentQuiz } from "@/stores/quizes/hooks";
import { updateQuestion } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
let params = (new URL(document.location)).searchParams; let params = (new URL(document.location)).searchParams;
console.log(params.get("data"));
const BranchingMap = lazy(() => const BranchingMap = lazy(() =>
import("./BranchingMap").then((module) => ({ default: module.BranchingMap })), import("./BranchingMap").then((module) => ({ default: module.BranchingMap })),
); );
@ -55,31 +54,6 @@ export const QuestionSwitchWindowTool = ({
}} }}
/> />
// <input type="file" multiple
// onChange={(e) => {
// 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);
// });
// }}
// />
} }
<Box sx={{ width: isTablet ? "100%" : "796px" }}> <Box sx={{ width: isTablet ? "100%" : "796px" }}>
{openBranchingPage ? ( {openBranchingPage ? (

@ -126,7 +126,6 @@ const QuestionPageCardTitle = memo<Props>(function ({
value={title} value={title}
placeholder={"Заголовок вопроса"} placeholder={"Заголовок вопроса"}
onChange={({ target }) => { onChange={({ target }) => {
console.log(target.value.length)
if (target.value.length > maxLengthTextField) { if (target.value.length > maxLengthTextField) {
enqueueSnackbar("Превышена длина вводимого текста") enqueueSnackbar("Превышена длина вводимого текста")
} else { } else {

@ -31,9 +31,6 @@ export const DraggableList = ({
createUntypedQuestion(Number(quiz.backendId)); createUntypedQuestion(Number(quiz.backendId));
} }
}, [quiz, filteredQuestions]); }, [quiz, filteredQuestions]);
console.log(quiz)
console.log(questions)
// if () {}uploadQuestionImage
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>

@ -15,7 +15,6 @@ export default function VariantAdornment({
}) { }) {
const theme = useTheme(); const theme = useTheme();
console.log("VariantAdornment extendedText", extendedText)
return ( return (
<Box sx={{ cursor: "pointer" }}> <Box sx={{ cursor: "pointer" }}>
<Box data-cy="choose-emoji-button" onClick={onClick}> <Box data-cy="choose-emoji-button" onClick={onClick}>

@ -44,11 +44,6 @@ export default function SliderOptions({ question, openBranchingPage, setOpenBran
}); });
}, 5000); }, 5000);
const updateStepsDebounced = useDebouncedCallback((value: string) => { 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<QuizQuestionNumber>(question.id, (question) => { updateQuestion<QuizQuestionNumber>(question.id, (question) => {
question.content.step = ReplaceToNotStartZero(Number(value)); question.content.step = ReplaceToNotStartZero(Number(value));
}); });

@ -1,11 +1,13 @@
import { Tabs as MuiTabs } from "@mui/material"; import { Tabs as MuiTabs } from "@mui/material";
import { CustomTab } from "./CustomTab"; import { CustomTab } from "./CustomTab";
import { TypePages } from "./types";
type TabsProps = { type TabsProps = {
names: string[]; names: string[];
items: string[]; items: string[];
selectedItem: "day" | "count" | "dop" | "hide" | "create"; selectedItem: TypePages;
setSelectedItem: (num: "day" | "count" | "dop") => void; setSelectedItem: (num: TypePages) => void;
toDop: () => void;
}; };
export const Tabs = ({ export const Tabs = ({
@ -18,7 +20,7 @@ export const Tabs = ({
sx={{ m: "25px" }} sx={{ m: "25px" }}
TabIndicatorProps={{ sx: { display: "none" } }} TabIndicatorProps={{ sx: { display: "none" } }}
value={selectedItem} value={selectedItem}
onChange={(event, newValue: "day" | "count" | "dop") => { onChange={(event, newValue: TypePages) => {
setSelectedItem(newValue); setSelectedItem(newValue);
}} }}
variant="scrollable" variant="scrollable"

@ -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 <Box
sx={{
display: "flex",
flexWrap: "wrap",
width: "100%"
}}>
{content.map(data => <NavCard {...data} key={data.title} />)}
</Box>
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,
<SmallIconPena />
)
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 <Box
sx={{
display: "flex",
flexWrap: "wrap",
width: "100%"
}}>
{content.map(data => <NavCard {...data} key={data.title} />)}
</Box>
}
}

@ -1,39 +1,34 @@
import { activatePromocode } from "@api/promocode"; import { activatePromocode } from "@api/promocode";
import { useToken } from "@frontend/kitui"; import { useToken } from "@frontend/kitui";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { import {
Box, Box,
Button,
Container,
IconButton,
Modal,
Paper,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useUserStore } from "@root/user"; import { useUserStore } from "@root/user";
import { LogoutButton } from "@ui_kit/LogoutButton";
import { useDomainDefine } from "@utils/hooks/useDomainDefine"; import { useDomainDefine } from "@utils/hooks/useDomainDefine";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { withErrorBoundary } from "react-error-boundary"; import { withErrorBoundary } from "react-error-boundary";
import { Link, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Logotip from "../../pages/Landing/images/icons/QuizLogo";
import CollapsiblePromocodeField from "./CollapsiblePromocodeField"; import CollapsiblePromocodeField from "./CollapsiblePromocodeField";
import { Tabs } from "./Tabs"; import { Tabs } from "./Tabs";
import { createTariffElements } from "./tariffsUtils/createTariffElements"; import { createTariffElements } from "./tariffsUtils/createTariffElements";
import { currencyFormatter } from "./tariffsUtils/currencyFormatter"; import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
import { useWallet, setCash } from "@root/cash"; import { useWallet, setCash } from "@root/cash";
import { handleLogoutClick } from "@utils/HandleLogoutClick";
import { cartApi } from "@api/cart"; import { cartApi } from "@api/cart";
import { Other } from "./pages/Other"; import { TariffCardDisplaySelector } from "./TariffCardDisplaySelector";
import { ModalRequestCreate } from "./ModalRequestCreate"; import { ModalRequestCreate } from "./ModalRequestCreate";
import { cancelCC, useCC } from "@/stores/cc"; import { cancelCC, useCC } from "@/stores/cc";
import { NavSelect } from "./NavSelect"; import { NavSelect } from "./NavSelect";
import { useTariffs } from '@utils/hooks/useTariffs'; import { useTariffs } from '@utils/hooks/useTariffs';
import { useDiscounts } from '@utils/hooks/useDiscounts'; 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<string, string> = { const StepperText: Record<string, string> = {
day: "Тарифы на время", day: "Тарифы на время",
@ -50,12 +45,17 @@ function TariffPage() {
const userId = useUserStore((state) => state.userId); const userId = useUserStore((state) => state.userId);
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserStore((state) => state.customerAccount); const user = useUserStore((state) => state.customerAccount);
const a = useUserStore((state) => state.customerAccount); //c wallet const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
console.log("________________34563875693785692576_____________USERRRRRRR") const userAccount = useUserStore((state) => state.userAccount);
console.log(a) // console.info("________________userWithWallet_____________USERRRRRRR")
const { data: discounts } = useDiscounts(userId); // 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 [isRequestCreate, setIsRequestCreate] = useState(false);
const [openModal, setOpenModal] = useState({}); const [openModal, setOpenModal] = useState<{ id?: string; price?: number }>({});
const { cashString, cashCop, cashRub } = useWallet(); const { cashString, cashCop, cashRub } = useWallet();
const [selectedItem, setSelectedItem] = useState<TypePages>("day"); const [selectedItem, setSelectedItem] = useState<TypePages>("day");
const { isTestServer } = useDomainDefine(); const { isTestServer } = useDomainDefine();
@ -65,17 +65,14 @@ function TariffPage() {
const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs(); const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs();
console.log("________34563875693785692576_____ TARIFFS")
console.log(tariffs)
useEffect(() => { useEffect(() => {
if (a) { if (userWithWallet && user) {
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100); let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
let cc = Number(user.wallet.cash); let cc = Number(user.wallet.cash);
let cr = Number(user.wallet.cash) / 100; let cr = Number(user.wallet.cash) / 100;
setCash(cs, cc, cr); setCash(cs, cc, cr);
} }
}, [a]); }, [userWithWallet, user]);
useEffect(() => { useEffect(() => {
if (cc) { if (cc) {
@ -83,7 +80,26 @@ console.log(tariffs)
cancelCC() cancelCC()
} }
}, []) }, [])
if (!user || !tariffs || !discounts) return <LoadingPage />;
// Проверяем, что все данные загружены и нет ошибок
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 <LoadingPage />;
}
if (hasErrors) {
return <LoadingPage />;
}
if (!hasAllData) {
return <LoadingPage />;
}
const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo); const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo);
const tryBuy = async ({ id, price }: { id: string; price: number }) => { 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")) { if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
let cashDif = Number(payError.split(":")[1]); 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 (!userId) {
if (cc) link.href = link.href + "&cc=true"//после покупки тарифа и возвращения будем знать что надо открыть модалку enqueueSnackbar("Ошибка: ID пользователя не найден");
document.body.appendChild(link); return;
link.click(); }
const l = generateHubWalletRequestURL({
action: cc ? "createquizcc" : "buy",
dif: cashDif.toString(),
userid: userId,
wayback: "list",
token
});
window.location.href = l;
return; return;
} }
@ -169,63 +194,10 @@ console.log(tariffs)
setIsRequestCreate(true) setIsRequestCreate(true)
} }
if (!a) return null; if (!userWithWallet) return null;
return ( return (
<> <>
<Container <TariffsHeader cashString={cashString} />
component="nav"
disableGutters
maxWidth={false}
sx={{
px: "16px",
display: "flex",
height: "80px",
alignItems: "center",
gap: isMobile ? "7px" : isTablet ? "20px" : "60px",
flexDirection: "row",
justifyContent: "space-between",
bgcolor: "white",
borderBottom: "1px solid #E3E3E3",
}}
>
<Link to="/">
<Logotip width={124} />
</Link>
<IconButton onClick={() => navigate("/list")}>
<ArrowLeft color="black" />
</IconButton>
<Box sx={{ display: "flex", ml: "auto" }}>
<Box sx={{ whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: "gray",
}}
>
Мой баланс
</Typography>
<Typography
variant="body2"
color={"#7e2aea"}
fontSize={
isMobile ? (cashString.length > 9 ? "13px" : "16px") : "16px"
}
>
{cashString}
</Typography>
</Box>
<LogoutButton
onClick={() => {
navigate("/");
handleLogoutClick();
}}
sx={{
ml: "20px",
}}
/>
</Box>
</Container>
<Box <Box
sx={{ sx={{
p: "25px", p: "25px",
@ -281,9 +253,9 @@ console.log(tariffs)
discounts, discounts,
openModalHC, openModalHC,
)} )}
{(selectedItem === "dop" || selectedItem === "hide" || selectedItem === "create") {(selectedItem === "hide" || selectedItem === "create" || selectedItem === "premium" || selectedItem === "analytics" || selectedItem === "custom")
&& ( && (
<Other <TariffCardDisplaySelector
selectedItem={selectedItem} selectedItem={selectedItem}
content={[ content={[
{ {
@ -294,48 +266,86 @@ console.log(tariffs)
title: "Создать квиз на заказ", title: "Создать квиз на заказ",
onClick: () => setSelectedItem("create") onClick: () => setSelectedItem("create")
}, },
{
title: "Премиум функции",
onClick: () => setSelectedItem("premium")
},
{
title: "Расширенная аналитика",
onClick: () => setSelectedItem("analytics")
},
{
title: "Кастомные тарифы",
onClick: () => setSelectedItem("custom")
},
]} ]}
tariffs={tariffs} tariffs={tariffs}
user={user} user={user}
discounts={discounts} discounts={discounts || []}
openModalHC={openModalHC}
userPrivilegies={userPrivilegies}
startRequestCreate={startRequestCreate}
/>
)}
{selectedItem === "dop" && (
<TariffCardDisplaySelector
selectedItem={selectedItem}
content={
selectedItem === "dop"
? [
{
title: `Убрать логотип "PenaQuiz"`,
onClick: () => 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} openModalHC={openModalHC}
userPrivilegies={userPrivilegies} userPrivilegies={userPrivilegies}
startRequestCreate={startRequestCreate} startRequestCreate={startRequestCreate}
/> />
)} )}
</Box> </Box>
<Modal <PaymentConfirmationModal
open={Object.values(openModal).length > 0} open={Object.values(openModal).length > 0}
onClose={() => setOpenModal({})} onClose={() => setOpenModal({})}
> onConfirm={() => {
<Paper if (openModal.id && openModal.price !== undefined) {
sx={{ tryBuy({ id: openModal.id, price: openModal.price });
position: "absolute" as "absolute", }
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
boxShadow: 24,
p: 4,
display: "flex",
justifyContent: "center",
flexDirection: "column",
}} }}
> price={openModal.price || 0}
<Typography />
id="modal-modal-title"
variant="h6"
component="h2"
mb="20px"
>
Вы подтверждаете платёж в сумму{" "}
{openModal.price ? openModal.price.toFixed(2) : 0}
</Typography>
<Button variant="contained" onClick={() => tryBuy(openModal)}>
купить
</Button>
</Paper>
</Modal>
<ModalRequestCreate open={isRequestCreate} onClose={() => setIsRequestCreate(false)} /> <ModalRequestCreate open={isRequestCreate} onClose={() => setIsRequestCreate(false)} />
</> </>
); );
@ -364,47 +374,3 @@ const LoadingPage = () => (
</Typography> </Typography>
</Box> </Box>
); );
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));
});
}
};

@ -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 (
<Modal open={open} onClose={onClose}>
<Paper
sx={{
position: "absolute" as "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
boxShadow: 24,
p: 4,
display: "flex",
justifyContent: "center",
flexDirection: "column",
}}
>
<Typography
id="modal-modal-title"
variant="h6"
component="h2"
mb="20px"
>
Вы подтверждаете платёж в сумму{" "}
{price ? price.toFixed(2) : 0}
</Typography>
<Button variant="contained" onClick={onConfirm}>
купить
</Button>
</Paper>
</Modal>
);
};

@ -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 (
<Container
component="nav"
disableGutters
maxWidth={false}
sx={{
px: "16px",
display: "flex",
height: "80px",
alignItems: "center",
gap: isMobile ? "7px" : isTablet ? "20px" : "60px",
flexDirection: "row",
justifyContent: "space-between",
bgcolor: "white",
borderBottom: "1px solid #E3E3E3",
}}
>
<Link to="/">
<Logotip width={124} />
</Link>
<IconButton onClick={() => navigate("/list")}>
<ArrowLeft color="black" />
</IconButton>
<Box sx={{ display: "flex", ml: "auto" }}>
<Box sx={{ whiteSpace: "nowrap" }}>
<Typography
sx={{
fontSize: "12px",
lineHeight: "14px",
color: "gray",
}}
>
Мой баланс
</Typography>
<Typography
variant="body2"
color={"#7e2aea"}
fontSize={
isMobile ? (cashString.length > 9 ? "13px" : "16px") : "16px"
}
>
{cashString}
</Typography>
</Box>
<LogoutButton
onClick={() => {
navigate("/");
handleLogoutClick();
}}
sx={{
ml: "20px",
}}
/>
</Box>
</Container>
);
};

@ -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 <Box
sx={{
justifyContent: "left",
display: "grid",
gap: "40px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
}))`,
}}
>
{createTariffElements(
filteredBadgeTariffs,
false,
user,
discounts,
openModalHC,
)}
</Box>
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 <Box
sx={{
justifyContent: "left",
display: "grid",
gap: "40px",
gridTemplateColumns: `repeat(auto-fit, minmax(300px, ${isTablet ? "436px" : "360px"
}))`,
}}
>
{createTariffElements(
filteredCreateTariffs,
false,
user,
discounts,
openModalHC,
sendRequest,
true,
<SmallIconPena/>
)}
</Box>
default:
return <Box
sx={{
display: "flex",
flexWrap: "wrap",
width: "100%"
}}>
{content.map(data => <NavCard {...data} key={data.title} />)}
</Box>
}
}

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

@ -1 +1 @@
type TypePages = "count" | "day" | "dop" | "hide" | "create" type TypePages = "count" | "day" | "dop" | "hide" | "create" | "premium" | "analytics" | "custom"

@ -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));
});
}
};

@ -18,7 +18,7 @@ import { object, string } from "yup";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useUserStore } from "@root/user"; import { useUserStore } from "@root/user";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { setAuthToken } from "@frontend/kitui"; import { setAuthToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import { recoverUser } from "@api/user"; import { recoverUser } from "@api/user";

@ -242,7 +242,7 @@ export default function SignupDialog() {
</Link> </Link>
<Link <Link
component={RouterLink} component={RouterLink}
to="/restore" to="/recover"
state={{ backgroundLocation: location.state.backgroundLocation }} state={{ backgroundLocation: location.state.backgroundLocation }}
sx={{ color: "#7E2AEA" }} sx={{ color: "#7E2AEA" }}
> >

@ -53,8 +53,6 @@ export default function AvailablePrivilege() {
} }
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || ""); const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "");
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || ""); const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "");
console.log(userPrivileges)
console.log(quizUnlimTime)
return ( return (
<Box <Box

@ -6,8 +6,8 @@ import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import { Box, Button, IconButton, Popover, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, IconButton, Popover, Typography, useMediaQuery, useTheme } from "@mui/material";
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions"; import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { inCart } from "../../pages/Tariffs/Tariffs"; import { inCart } from "../../pages/Tariffs/utils";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useDomainDefine } from "@utils/hooks/useDomainDefine"; import { useDomainDefine } from "@utils/hooks/useDomainDefine";
import CopyIcon from "@icons/CopyIcon"; import CopyIcon from "@icons/CopyIcon";

@ -1,4 +1,4 @@
import { FetchState, TicketMessage } from "@frontend/kitui"; import { FetchState, Ticket, TicketMessage } from "@frontend/kitui";
import { create } from "zustand"; import { create } from "zustand";
import { createJSONStorage, devtools, persist } from "zustand/middleware"; import { createJSONStorage, devtools, persist } from "zustand/middleware";
import { useUserStore } from "./user"; import { useUserStore } from "./user";
@ -21,11 +21,12 @@ interface AuthData {
interface TicketStore { interface TicketStore {
unauthData: AuthData; unauthData: AuthData;
authData: AuthData; authData: AuthData;
tickets: Ticket[];
} }
let params = new URLSearchParams(document.location.search); let params = new URLSearchParams(document.location.search);
const debug = params.get("debug");
const initAuthData = { const initAuthData = {
sessionData: null, sessionData: null,
isMessageSending: false, isMessageSending: false,
@ -35,10 +36,12 @@ const initAuthData = {
lastMessageId: undefined, lastMessageId: undefined,
isPreventAutoscroll: false, isPreventAutoscroll: false,
unauthTicketMessageFetchState: "idle" as FetchState, unauthTicketMessageFetchState: "idle" as FetchState,
tickets: []
}; };
const initState = { const initState = {
unauthData: initAuthData, unauthData: initAuthData,
authData: initAuthData, authData: initAuthData,
tickets: []
}; };
export const useTicketStore = create<TicketStore>()( export const useTicketStore = create<TicketStore>()(
@ -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?.at(-1)?.id || "";
if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.[sortedMessages.length - 1]?.id || ""; if (Array.isArray(sortedMessages)) ticket.lastMessageId = sortedMessages?.[sortedMessages.length - 1]?.id || "";
console.log("ticketStudy, ", sortedMessages)
}); });
export const incrementUnauthMessage = () => export const incrementUnauthMessage = () =>
@ -156,9 +158,17 @@ export const updateTicket = <T extends AuthData>(
}, },
); );
function setProducedState<A extends string | { type: unknown }>( function setProducedState<A extends string | { type: string }>(
recipe: (state: TicketStore) => void, recipe: (state: TicketStore) => void,
action?: A, action?: A,
) { ) {
useTicketStore.setState((state) => produce(state, recipe), false, action); useTicketStore.setState((state) => produce(state, recipe), false, action);
} }
// Функция для записи тикетов в стор
export const setTickets = (tickets: Ticket[] | null) => {
useTicketStore.setState((state) => ({
...state,
tickets: tickets || []
}), false);
};

@ -71,5 +71,6 @@ export const clearUserData = () => useUserStore.setState({ ...initialState });
export const setUserAccount = (userAccount: OriginalUserAccount) => export const setUserAccount = (userAccount: OriginalUserAccount) =>
useUserStore.setState({ userAccount }); useUserStore.setState({ userAccount });
export const setCustomerAccount = (customerAccount: UserAccount) => export const setCustomerAccount = (customerAccount: UserAccount) => {
useUserStore.setState({ customerAccount }); useUserStore.setState({ customerAccount });
};

@ -3,7 +3,7 @@ import { Box, Button, Modal, Typography } from "@mui/material";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { mutate } from "swr"; import { mutate } from "swr";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import { getDiscounts } from "@api/discounts"; import { getDiscounts } from "@api/discounts";
import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user"; import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";

@ -1,9 +1,6 @@
import { import {
Box, Box,
FormControl,
IconButton, IconButton,
InputAdornment,
InputBase,
SxProps, SxProps,
Theme, Theme,
Typography, Typography,
@ -17,46 +14,45 @@ import {
useTicketStore, useTicketStore,
} from "@root/ticket"; } from "@root/ticket";
import type { TouchEvent, WheelEvent } from "react"; import type { TouchEvent, WheelEvent } from "react";
import * as React from "react"; import { useEffect, useMemo, useRef } from "react";
import { useEffect, useMemo, useRef, useState } from "react"; import ChatMessageRenderer from "./ChatMessageRenderer";
import ChatMessage from "./ChatMessage"; import ChatInput from "./ChatInput";
import ChatVideo from "./ChatVideo";
import SendIcon from "@icons/SendIcon";
import UserCircleIcon from "./UserCircleIcon"; import UserCircleIcon from "./UserCircleIcon";
import { throttle, TicketMessage } from "@frontend/kitui"; import { throttle, TicketMessage } from "@frontend/kitui";
import ArrowLeft from "@icons/questionsPage/arrowLeft"; import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { useUserStore } from "@root/user"; 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 { interface Props {
open: boolean; open: boolean;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
onclickArrow?: () => void; onclickArrow?: () => void;
sendMessage: (a: string) => Promise<boolean>; sendMessage: (a: string) => Promise<boolean>;
sendFile: (a: File | undefined) => Promise<true>; sendFile: (a: File | undefined) => Promise<void>;
greetingMessage: TicketMessage;
} }
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({ export default function Chat({
open = false, open = false,
sx, sx,
onclickArrow, onclickArrow,
sendMessage, sendMessage,
sendFile, sendFile,
greetingMessage,
}: Props) { }: Props) {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(800)); const isMobile = useMediaQuery(theme.breakpoints.down(800));
const [messageField, setMessageField] = useState<string>("");
const [disableFileButton, setDisableFileButton] = useState(false);
const user = useUserStore((state) => state.user?._id); const user = useUserStore((state) => state.user?._id);
const ticket = useTicketStore( const ticket = useTicketStore(
@ -72,31 +68,11 @@ export default function Chat({
const chatBoxRef = useRef<HTMLDivElement>(null); const chatBoxRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
addOrUpdateUnauthMessages([greetingMessage]);
if (open) { if (open) {
scrollToBottom(); scrollToBottom();
} }
}, [open]); }, [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<HTMLInputElement>(null);
const throttledScrollHandler = useMemo( const throttledScrollHandler = useMemo(
() => () =>
throttle(() => { throttle(() => {
@ -152,14 +128,6 @@ export default function Chat({
behavior, behavior,
}); });
} }
const handleTextfieldKeyPress: React.KeyboardEventHandler<
HTMLInputElement | HTMLTextAreaElement
> = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessageHC();
}
};
return ( return (
<> <>
@ -240,164 +208,28 @@ export default function Chat({
> >
{ticket.sessionData?.ticketId && {ticket.sessionData?.ticketId &&
messages.map((message) => { messages.map((message) => {
const isFileVideo = () => { const isSelf = (ticket.sessionData?.sessionId || user) === message.user_id;
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 ( return (
<ChatImage <ChatMessageRenderer
unAuthenticated
key={message.id} key={message.id}
file={message.files[0]} message={message}
createdAt={message.created_at} isSelf={isSelf}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/>
);
}
if (message.files.length > 0 && isFileVideo()) {
return (
<ChatVideo
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/>
);
}
if (message.files.length > 0 && isFileDocument()) {
return (
<ChatDocument
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/>
);
}
return (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/> />
); );
})} })}
{!ticket.sessionData?.ticketId && ( {!ticket.sessionData?.ticketId && (
<ChatMessage <ChatMessageRenderer
unAuthenticated message={greetingMessage}
text={greetingMessage.message} isSelf={false}
createdAt={greetingMessage.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
greetingMessage.user_id
}
/> />
)} )}
</Box> </Box>
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}> <ChatInput
<InputBase sendMessage={sendMessage}
value={messageField} sendFile={sendFile}
fullWidth isMessageSending={isMessageSending}
placeholder="Введите сообщение..."
id="message"
multiline
onKeyDown={handleTextfieldKeyPress}
sx={{
width: "100%",
p: 0,
}}
inputProps={{
sx: {
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: upMd ? "30px" : "28px",
pb: upMd ? "30px" : "24px",
px: "19px",
maxHeight: "calc(19px * 5)",
color: "black",
},
}}
onChange={(e) => setMessageField(e.target.value)}
endAdornment={
<InputAdornment position="end">
<IconButton
disabled={disableFileButton}
onClick={() => {
if (!disableFileButton) fileInputRef.current?.click();
}}
>
<AttachFileIcon />
</IconButton>
<input
ref={fileInputRef}
id="fileinput"
onChange={(e) => {
if (e.target.files?.[0])
sendFileHC(e.target.files?.[0]);
}}
style={{ display: "none" }}
type="file"
/> />
<IconButton
disabled={isMessageSending}
onClick={sendMessageHC}
sx={{
height: "53px",
width: "53px",
mr: "13px",
p: 0,
opacity: isMessageSending ? 0.3 : 1,
}}
>
<SendIcon
style={{
width: "100%",
height: "100%",
}}
/>
</IconButton>
</InputAdornment>
}
/>
</FormControl>
</Box> </Box>
</Box> </Box>
)} )}

@ -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<boolean>;
sendFile: (file: File | undefined) => Promise<void>;
isMessageSending: boolean;
}
const ChatInput = ({ sendMessage, sendFile, isMessageSending }: ChatInputProps) => {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageField, setMessageField] = useState<string>("");
const [disableFileButton, setDisableFileButton] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
setMessageField(e.target.value);
}, []);
return (
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
<InputBase
value={messageField}
fullWidth
placeholder="Введите сообщение..."
id="message"
multiline
onKeyDown={handleTextfieldKeyPress}
sx={{
width: "100%",
p: 0,
}}
inputProps={{
sx: {
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: upMd ? "30px" : "28px",
pb: upMd ? "30px" : "24px",
px: "19px",
maxHeight: "calc(19px * 5)",
color: "black",
},
}}
onChange={handleInputChange}
endAdornment={
<InputAdornment position="end">
<IconButton
disabled={disableFileButton}
onClick={handleFileButtonClick}
>
<AttachFileIcon />
</IconButton>
<input
ref={fileInputRef}
id="fileinput"
onChange={handleFileInputChange}
style={{ display: "none" }}
type="file"
/>
<IconButton
disabled={isMessageSending}
onClick={handleSendMessage}
sx={{
height: "53px",
width: "53px",
mr: "13px",
p: 0,
opacity: isMessageSending ? 0.3 : 1,
}}
>
<SendIcon
style={{
width: "100%",
height: "100%",
}}
/>
</IconButton>
</InputAdornment>
}
/>
</FormControl>
);
};
export default ChatInput;

@ -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 <ChatImage {...commonProps} />;
case 'video':
return <ChatVideo {...commonProps} />;
case 'document':
return <ChatDocument {...commonProps} />;
default:
break;
}
}
// Текстовое сообщение
return (
<ChatMessage
unAuthenticated
text={message.message}
createdAt={message.created_at}
isSelf={isSelf}
/>
);
});
ChatMessageRenderer.displayName = 'ChatMessageRenderer';
export default ChatMessageRenderer;

@ -1,5 +1,5 @@
import type { ReactNode, Ref } from "react"; import type { ReactNode, Ref } from "react";
import { forwardRef, useEffect, useState } from "react"; import { forwardRef, useEffect, useState, useMemo } from "react";
import { import {
Badge, Badge,
Box, Box,
@ -44,10 +44,9 @@ interface Props {
handleChatClickClose: () => void; handleChatClickClose: () => void;
handleChatClickSwitch: () => void; handleChatClickSwitch: () => void;
sendMessage: (a: string) => Promise<boolean>; sendMessage: (a: string) => Promise<boolean>;
sendFile: (a: File | undefined) => Promise<true>; sendFile: (a: File | undefined) => Promise<void>;
modalWarningType: string | null; modalWarningType: string | null;
setModalWarningType: any; setModalWarningType: any;
greetingMessage: TicketMessage;
} }
export default function FloatingSupportChat({ export default function FloatingSupportChat({
@ -59,7 +58,6 @@ export default function FloatingSupportChat({
sendFile, sendFile,
modalWarningType, modalWarningType,
setModalWarningType, setModalWarningType,
greetingMessage,
}: Props) { }: Props) {
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">(""); const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
const theme = useTheme(); const theme = useTheme();
@ -72,6 +70,48 @@ export default function FloatingSupportChat({
(state) => state[user ? "authData" : "unauthData"], (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(() => { useEffect(() => {
const onResize = () => { const onResize = () => {
if (document.fullscreenElement) { if (document.fullscreenElement) {
@ -108,7 +148,6 @@ export default function FloatingSupportChat({
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }} sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
sendMessage={sendMessage} sendMessage={sendMessage}
sendFile={sendFile} sendFile={sendFile}
greetingMessage={greetingMessage}
/> />
<Dialog <Dialog
fullScreen fullScreen
@ -121,7 +160,6 @@ export default function FloatingSupportChat({
onclickArrow={handleChatClickClose} onclickArrow={handleChatClickClose}
sendMessage={sendMessage} sendMessage={sendMessage}
sendFile={sendFile} sendFile={sendFile}
greetingMessage={greetingMessage}
/> />
</Dialog> </Dialog>
<Fab <Fab
@ -162,9 +200,7 @@ export default function FloatingSupportChat({
/> />
)} )}
<Badge <Badge
badgeContent={ badgeContent={unreadCount}
messages.filter(({ shown }) => shown?.me !== 1).length || 0
}
sx={{ sx={{
"& .MuiBadge-badge": { "& .MuiBadge-badge": {
display: isChatOpened ? "none" : "flex", display: isChatOpened ? "none" : "flex",

@ -1,13 +1,15 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { import {
TicketMessage, TicketMessage,
createTicket,
shownMessage,
useSSESubscription, useSSESubscription,
useTicketMessages, useTicketMessages,
useTicketsFetcher, useTicketsFetcher,
sendFile as sf
} from "@frontend/kitui"; } from "@frontend/kitui";
import FloatingSupportChat from "./FloatingSupportChat"; import FloatingSupportChat from "./FloatingSupportChat";
import { useUserStore } from "@root/user"; 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 { useSSETab } from "../../utils/hooks/useSSETab";
import { import {
addOrUpdateUnauthMessages, addOrUpdateUnauthMessages,
@ -21,7 +23,6 @@ import {
} from "@root/ticket"; } from "@root/ticket";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
import { selectSendingMethod } from "./utils"; import { selectSendingMethod } from "./utils";
type ModalWarningType = type ModalWarningType =
@ -71,60 +72,6 @@ export default () => {
setIsChatOpened((state) => !state); 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({ useTicketMessages({
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`, url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
@ -156,7 +103,6 @@ export default () => {
); );
if (isTicketClosed) { if (isTicketClosed) {
cleanAuthTicketData(); cleanAuthTicketData();
addOrUpdateUnauthMessages([getGreetingMessage]);
if (!user) { if (!user) {
cleanUnauthTicketData(); cleanUnauthTicketData();
localStorage.removeItem("unauth-ticket"); localStorage.removeItem("unauth-ticket");
@ -184,9 +130,15 @@ export default () => {
({ shown }) => shown?.me !== 1, ({ 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]); }, [isChatOpened, ticket.messages]);
@ -196,21 +148,26 @@ export default () => {
setSseEnabled(true); setSseEnabled(true);
setIsMessageSending(true); setIsMessageSending(true);
let successful = await selectSendingMethod({messageField}); let successful = await selectSendingMethod({ messageField });
setIsMessageSending(false); setIsMessageSending(false);
return successful; return successful;
}; };
const sendFile = async (file: File) => { const sendFile = async (file: File | undefined): Promise<void> => {
if (file === undefined) return true; if (file === undefined) return;
let ticketId = ticket.sessionData?.ticketId; let ticketId = ticket.sessionData?.ticketId;
if (!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; ticketId = data?.Ticket;
if (createError || !data) { if (createError || !data) {
enqueueSnackbar(createError); enqueueSnackbar(`Не удалось создать диалог ${parseAxiosError(createError)}`);
return;
} else { } else {
setTicketData({ ticketId: data.Ticket, sessionId: data.sess }); setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
} }
@ -219,15 +176,16 @@ export default () => {
} }
if (ticketId !== undefined) { if (ticketId !== undefined) {
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize"); if (file.size > MAX_FILE_SIZE) {
setModalWarningType("errorSize");
const [_, sendFileError] = await sendFileRequest(ticketId, file); return;
if (sendFileError) {
enqueueSnackbar(sendFileError);
} }
return true; const [_, sendFileError] = await sf({ticketId, file});
if (sendFileError) {
enqueueSnackbar(`Не удалось отправить файл ${parseAxiosError(sendFileError)}`);
}
} }
}; };
@ -241,7 +199,6 @@ export default () => {
sendFile={sendFile} sendFile={sendFile}
modalWarningType={modalWarningType} modalWarningType={modalWarningType}
setModalWarningType={setModalWarningType} setModalWarningType={setModalWarningType}
greetingMessage={getGreetingMessage}
/> />
); );
}; };

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

@ -1,36 +1,32 @@
import { sendTicketMessage } from "@/api/ticket";
import { setTicketData, useTicketStore } from "@/stores/ticket"; import { setTicketData, useTicketStore } from "@/stores/ticket";
import { useUserStore } from "@root/user"; import { useUserStore } from "@root/user";
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { createTicket, sendTicketMessage } from "@frontend/kitui";
import { parseAxiosError } from "@/utils/parse-error";
interface SelectSendingMethod { interface SelectSendingMethod {
messageField: string; messageField: string;
isSnackbar?: boolean; isSnackbar?: boolean;
systemError?: boolean; systemError?: boolean;
} }
export const selectSendingMethod = async ({messageField, isSnackbar = true, systemError = false}: SelectSendingMethod) => { export const selectSendingMethod = async ({ messageField, isSnackbar = true, systemError = false }: SelectSendingMethod) => {
console.log("click")
const user = useUserStore.getState().user?._id; const user = useUserStore.getState().user?._id;
const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"]; const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"];
console.log(ticket)
console.log("click 2")
let successful = false; let successful = false;
if (!(window.location.hostname == 'localhost' && systemError )) { //предупреждать о системных ошибках вне локалхост if (!(window.location.hostname == 'localhost' && systemError)) { //предупреждать о системных ошибках вне локалхост
if (!ticket.sessionData?.ticketId) { if (!ticket.sessionData?.ticketId) {
console.log("autorisated 2") const [data, createError] = await createTicket({
const [data, createError] = await createTicket( message: messageField,
messageField, useToken: Boolean(user),
Boolean(user), systemError: false,
false, });
);
if (createError || !data) { if (createError || !data) {
successful = false; successful = false;
if (isSnackbar) enqueueSnackbar(createError); if (isSnackbar) enqueueSnackbar(`Не удалось открыть диалог ${parseAxiosError(createError)}`);
} else { } else {
successful = true; successful = true;
@ -38,11 +34,11 @@ export const selectSendingMethod = async ({messageField, isSnackbar = true, syst
} }
} else { } else {
const [_, sendTicketMessageError] = await sendTicketMessage( const [_, sendTicketMessageError] = await sendTicketMessage({
ticket.sessionData?.ticketId, ticketId: ticket.sessionData?.ticketId,
messageField, message: messageField,
false, systemError: false,
); });
successful = true; successful = true;
if (sendTicketMessageError) { if (sendTicketMessageError) {

@ -29,9 +29,6 @@ export const NavigationPanel: FC<Props> = ({
const isMobile = useMediaQuery(theme.breakpoints.down(786)); const isMobile = useMediaQuery(theme.breakpoints.down(786));
const lastStep = currentStep + 1 === totalSteps; const lastStep = currentStep + 1 === totalSteps;
console.log("nextStepName")
console.log(nextStepName)
const handlePrevStep = () => { const handlePrevStep = () => {
if (currentStep === 0) return; if (currentStep === 0) return;
setCurrentStep(currentStep - 1); setCurrentStep(currentStep - 1);

@ -46,8 +46,6 @@ export default function WorkSpace({
modalModels[currentStepName] modalModels[currentStepName]
), [currentStepName]); ), [currentStepName]);
// console.log(" промежуточный рендер которому должно быть похуй")
return ( return (
<> <>
<Box <Box

@ -13,7 +13,6 @@ export const SwitchAI = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const account = useUserStore() const account = useUserStore()
console.log(account.userId)
if (account.userId === "6755b1ddd5802e9f13663f56") { if (account.userId === "6755b1ddd5802e9f13663f56") {
return ( return (
<> <>

@ -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;
}

@ -1,5 +1,8 @@
import { selectSendingMethod } from "@/ui_kit/FloatingSupportChat/utils";
import { ErrorInfo } from "react"; import { ErrorInfo } from "react";
import { Ticket, createTicket, getAuthToken, sendTicketMessage } from "..";
let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout>;
interface ComponentError { interface ComponentError {
timestamp: number; timestamp: number;
@ -8,40 +11,99 @@ interface ComponentError {
componentStack: string | null | undefined; componentStack: string | null | undefined;
} }
export function handleComponentError(error: Error, info: ErrorInfo) { function isErrorReportingAllowed(error?: Error): boolean {
const componentError: ComponentError = { // Если ошибка помечена как 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), timestamp: Math.floor(Date.now() / 1000),
message: error.message, message: error.message,
callStack: error.stack, callStack: error.stack,
componentStack: info.componentStack, componentStack: info.componentStack,
...(error && (error as any).__forceSend ? { __forceSend: true } : {})
}; };
queueErrorRequest(componentError, getTickets);
queueErrorRequest(componentError);
} }
let errorsQueue: ComponentError[] = []; // Ставит ошибку в очередь для отправки, через 1 секунду вызывает sendErrorsToServer
let timeoutId: ReturnType<typeof setTimeout>; export function queueErrorRequest(error: ComponentError, getTickets: () => Ticket[]) {
function queueErrorRequest(error: ComponentError) {
errorsQueue.push(error); errorsQueue.push(error);
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
sendErrorsToServer(); sendErrorsToServer(getTickets);
}, 1000); }, 1000);
} }
async function sendErrorsToServer() { // Отправляет накопленные ошибки в тикеты, ищет существующий тикет с system: true или создает новый
// makeRequest({ export async function sendErrorsToServer(getTickets: () => Ticket[]) {
// url: "", if (errorsQueue.length === 0) return;
// method: "POST", // Проверяем разрешение на отправку ошибок (по домену и debug-override)
// body: errorsQueue, // Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё
// useToken: true, const forceSend = errorsQueue.some(e => (e as any).__forceSend);
// }); if (!forceSend && !isErrorReportingAllowed()) {
// selectSendingMethod({ console.log('❌ Отправка ошибок заблокирована, очищаем очередь');
// messageField: `Fake-sending ${errorsQueue.length} errors to server ${JSON.stringify(errorsQueue)}`, errorsQueue = [];
// isSnackbar: false, return;
// systemError: true }
// }); const tickets = getTickets();
// errorsQueue = []; 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 = [];
} }

@ -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 };
};

@ -1,44 +1,42 @@
import { cartApi } from "@api/cart"; import { cartApi } from "@api/cart";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import moment from "moment";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useEffect } from "react"; 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 { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount";
import { startCC } from "@/stores/cc"; import { startCC } from "@/stores/cc";
import { setEditQuizId, setCurrentStep } from "@root/quizes/actions"; import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
/*
Есть три пути по которому мы ходили из квиза в хаб. Нам нехватило денег при:
1)Покупке обычного тарифа
2)Покупке тарифа-заказ-квиза
3)Покупке тарифа в настройке квиза в вкладке ИИ
*/
export const useAfterPay = () => { export const useAfterPay = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const userId = useUserStore(store => store.userId) const userId = useUserStore(store => store.userId)
const userAccount = useUserStore(state => state.userAccount); const userAccount = useUserStore(state => state.userAccount);
const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart); const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart);
const purpose = searchParams.get("purpose"); let URLaction = searchParams.get("action");//что мы, собсна, хотим: оплатить, пополнить, заказать квиз
const paymentUserId = searchParams.get("userid"); let URLuserId = searchParams.get("userid");//тот кто начал всё это действо
const currentCC = searchParams.get("cc"); let URLadditionalinformation = searchParams.get("additionalinformation");//его токен
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]);
useEffect(() => { useEffect(() => {
//Звёзды сошлись, будем оплачивать корзину
if (paymentUserId && paymentUserId === userId) {
if (purpose === "paycart") {
setSearchParams({}, { replace: true }); setSearchParams({}, { replace: true });
if (currentCC) startCC()
if (userId && URLuserId && userId === URLuserId) {
if (URLaction === "buy") startPayCartProcess(URLuserId);
if (URLaction === "createquizcc") {
startCC();
(async () => { (async () => {
//Проверяем можем ли мы оплатить корзину здесь и сейчас //Проверяем можем ли мы оплатить корзину здесь и сейчас
@ -46,19 +44,46 @@ export const useAfterPay = () => {
if (payCartError) { if (payCartError) {
//Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты //Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты
startPayCartProcess(paymentUserId) startPayCartProcess(URLuserId);
} else { } else {
if (currentCC) navigate("/tariffs") navigate("/tariffs");
cancelPayCartProcess() 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(() => { useEffect(() => {
if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) { if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) {
const deadline = siteReadyPayCart[userId] const deadline = siteReadyPayCart[userId];
if (calcTimeOfReadyPayCart(deadline)) { if (calcTimeOfReadyPayCart(deadline)) {
//Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло. //Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло.
@ -66,11 +91,13 @@ export const useAfterPay = () => {
const [, payCartError] = await cartApi.pay(); const [, payCartError] = await cartApi.pay();
if (!payCartError) { if (!payCartError) {
enqueueSnackbar("Товары успешно приобретены") enqueueSnackbar("Товары успешно приобретены");
cancelPayCartProcess() cancelPayCartProcess();
} }
})() })()
} }
} }
}, [userAccount, userId, siteReadyPayCart]) }, [userAccount, userId, siteReadyPayCart, userWithWallet])
} }

@ -4,7 +4,7 @@ import { useSSETab } from "./useSSETab";
import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount"; import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount";
import { setCash } from "@/stores/cash"; import { setCash } from "@/stores/cash";
import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter"; import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter";
import { inCart } from "@/pages/Tariffs/Tariffs"; import { inCart } from "@/pages/Tariffs/utils";
type Ping = [{ event: "ping" }] type Ping = [{ event: "ping" }]
@ -43,8 +43,6 @@ export const usePipeSubscriber = () => {
`/customer/v1.0.1/account/pipe?Authorization=${token}`, `/customer/v1.0.1/account/pipe?Authorization=${token}`,
onNewData: (data) => { onNewData: (data) => {
let message = data[0] as PipeMessage let message = data[0] as PipeMessage
console.log("truba")
console.log(message)
updateSSEValue(message) updateSSEValue(message)
//Пропускаем пингование //Пропускаем пингование

@ -1,7 +1,7 @@
import { useEffect, useLayoutEffect, useRef } from "react"; import { useEffect, useLayoutEffect, useRef } from "react";
import { createUserAccount, devlog } from "@frontend/kitui"; import { createUserAccount, devlog } from "@frontend/kitui";
import { isAxiosError } from "axios"; import { isAxiosError } from "axios";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@frontend/kitui";
import type { UserAccount } from "@frontend/kitui"; import type { UserAccount } from "@frontend/kitui";
import { setUserAccount } from "@/stores/user"; import { setUserAccount } from "@/stores/user";
@ -37,27 +37,52 @@ export const useUserAccountFetcher = <T = UserAccount>({
}) })
.then((result) => { .then((result) => {
devlog("User account", result); devlog("User account", result);
console.log(result)
if (result) onNewUserAccountRef.current(result); if (result) onNewUserAccountRef.current(result);
}) })
.catch((error) => { .catch((error) => {
devlog("Error fetching user account", error); devlog("Error fetching user account", error);
if (error.response?.status === 409) return; if (error.response?.status === 409) return;
if (isAxiosError(error) && error.response?.status === 404) { 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) => { .then((result) => {
devlog("Created user account", 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) => { .catch((error) => {
if (error.response?.status === 409) return; if (error.response?.status === 409) return;
devlog("Error creating user account", error); devlog("Error creating user account", error);
console.error("useUserAccountFetcher: Error creating account:", error);
onErrorRef.current?.(error); onErrorRef.current?.(error);
}); });
} else { } else {
console.log(error) console.error(error)
onErrorRef.current?.(error); onErrorRef.current?.(error);
} }
}); });

@ -29,7 +29,7 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
if (error.message === "Failed to fetch") return ["Ошибка сети"]; if (error.message === "Failed to fetch") return ["Ошибка сети"];
//ДЛЯ ОПЛАТЫ ТАРИФА //ДЛЯ ОПЛАТЫ ТАРИФА
if(error.response.status === 402) { if(error.response?.status === 402) {
console.error(error.response?.data.message) console.error(error.response?.data.message)
return 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; const status = error.response.status;
if(status === 409 || status === 401 || status === 404) { if(status === 409 || status === 401 || status === 404) {
const serverErrorMessage = error.response.data.message const responseData = error.response.data as any;
console.log(serverErrorMessage) const serverErrorMessage = responseData?.message || responseData?.error;
const translatedMessage = translateMessage[serverErrorMessage?.toLowerCase() || ""] const translatedMessage = translateMessage[serverErrorMessage?.toLowerCase() || ""]
return [translatedMessage || "", serverError.statusCode]; return [translatedMessage || "", serverError.statusCode];
} }