Merge branch 'staging'
This commit is contained in:
commit
8a1067bfd5
@ -2,9 +2,8 @@ name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
registry_package:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
CreateImage:
|
||||
|
@ -2,25 +2,30 @@ name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'staging'
|
||||
registry_package:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
CreateImage:
|
||||
runs-on: [skeris]
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
with:
|
||||
runner: skeris
|
||||
secrets:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
# CreateImage:
|
||||
# runs-on: [skeris]
|
||||
# uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
# with:
|
||||
# runner: skeris
|
||||
# secrets:
|
||||
# REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
# REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DeployService:
|
||||
if: contains(github.event.package.name, 'staging')
|
||||
runs-on: [frontstaging]
|
||||
needs: CreateImage
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||
with:
|
||||
runner: frontstaging
|
||||
actionid: ${{ gitea.run_id }}
|
||||
container:
|
||||
image: gitea.pena:3000/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/docker/docker.sock:/run/user/1000/docker/docker.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
|
||||
- run: compose -f deployments/staging/docker-compose.yaml up -d
|
||||
|
||||
|
||||
|
@ -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
|
13
Dockerfile
13
Dockerfile
@ -0,0 +1,13 @@
|
||||
FROM gitea.pena/penadevops/container-images/node:main as build
|
||||
|
||||
WORKDIR /usr/app
|
||||
COPY . .
|
||||
|
||||
|
||||
RUN npm install --force && yarn cache clean
|
||||
RUN psstat.sh "npm run build"
|
||||
|
||||
FROM gitea.pena/penadevops/container-images/nginx:main as result
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=build /usr/app/build/ /usr/share/nginx/html
|
||||
COPY hub.conf /etc/nginx/conf.d/default.conf
|
@ -2,6 +2,7 @@ services:
|
||||
squiz:
|
||||
container_name: squiz
|
||||
restart: unless-stopped
|
||||
image: gitea.pena/squiz/frontpanel/staging:$GITHUB_RUN_NUMBER
|
||||
image: gitea.pena/squiz/frontpanel/staging:latest
|
||||
hostname: squiz
|
||||
tty: true
|
||||
pull_policy: always
|
||||
|
@ -6,7 +6,7 @@
|
||||
"@craco/craco": "^7.0.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@frontend/kitui": "^1.0.108",
|
||||
"@frontend/kitui": "1.0.110",
|
||||
"@frontend/squzanswerer": "^1.0.57",
|
||||
"@mui/icons-material": "^5.10.14",
|
||||
"@mui/material": "^5.10.14",
|
||||
@ -69,6 +69,7 @@
|
||||
"test": "craco test",
|
||||
"eject": "craco eject",
|
||||
"code:format": "prettier --write --ignore-unknown",
|
||||
"deploy": "docker login gitea.pena && docker build -t gitea.pena/squiz/frontpanel/$(git branch --show-current):latest . && docker push gitea.pena/squiz/frontpanel/$(git branch --show-current):latest",
|
||||
"prepare": "husky install",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run"
|
||||
|
@ -165,7 +165,7 @@
|
||||
console.log(params.get("debug"))
|
||||
if (params.get("debug")) {
|
||||
console.log(
|
||||
"mhgfhdhfjhffhfhjfghjgf"
|
||||
"params.get(debug) is true"
|
||||
)
|
||||
let scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute('src', "https://markknol.github.io/console-log-viewer/console-log-viewer.js");
|
||||
|
68
src/App.tsx
68
src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { clearAuthToken, getMessageFromFetchError, UserAccount, useUserFetcher } from "@frontend/kitui";
|
||||
import { clearAuthToken, createMakeRequestConfig, getMessageFromFetchError, handleComponentError, UserAccount, useTicketsFetcher, useUserFetcher } from "@frontend/kitui";
|
||||
import type { OriginalUserAccount } from "@root/user";
|
||||
import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user";
|
||||
import ContactFormModal from "@ui_kit/ContactForm";
|
||||
@ -8,7 +8,7 @@ import { useAfterPay } from "@utils/hooks/useAutoPay";
|
||||
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import type { SuspenseProps } from "react";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { lazy, Suspense, useEffect } from "react";
|
||||
import { lazily } from "react-lazily";
|
||||
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useAmoAccount } from "./api/integration";
|
||||
@ -23,7 +23,11 @@ import { InfoPrivilege } from "./pages/InfoPrivilege";
|
||||
import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
|
||||
import Landing from "./pages/Landing/Landing";
|
||||
import Main from "./pages/main";
|
||||
import Debug from "./pages/Debug";
|
||||
import { setTicketData, setTickets, useTicketStore } from "./stores/ticket";
|
||||
import { parseAxiosError } from "./utils/parse-error";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { handleLogoutClick } from "./utils/HandleLogoutClick";
|
||||
|
||||
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
|
||||
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));
|
||||
@ -39,6 +43,12 @@ const PersonalizationAI = lazy(() => import("./pages/PersonalizationAI/Personali
|
||||
let params = new URLSearchParams(document.location.search);
|
||||
const isTest = Boolean(params.get("test"))
|
||||
|
||||
createMakeRequestConfig(
|
||||
handleLogoutClick,
|
||||
(error, info, getTickets) => handleComponentError(error, info, getTickets()),
|
||||
() => useTicketStore.getState().tickets
|
||||
);
|
||||
|
||||
const routeslink = [
|
||||
{
|
||||
path: "/edit",
|
||||
@ -73,12 +83,16 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => (
|
||||
<Suspense fallback={fallback ?? <></>}>{children}</Suspense>
|
||||
);
|
||||
|
||||
const ApologyPage = () => <div><p>Что-то пошло не так</p></div>
|
||||
|
||||
export default function App() {
|
||||
window.LoadingObserver = false;
|
||||
const userId = useUserStore((state) => state.userId);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { data: amoAccount } = useAmoAccount();
|
||||
const tickets = useTicketStore(store => store.tickets);
|
||||
|
||||
|
||||
useUserFetcher({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
|
||||
@ -97,8 +111,11 @@ export default function App() {
|
||||
useUserAccountFetcher<UserAccount>({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1/account`,
|
||||
userId,
|
||||
onNewUserAccount: setCustomerAccount,
|
||||
onNewUserAccount: (account) => {
|
||||
setCustomerAccount(account);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("App: Error in customerAccount fetcher:", error);
|
||||
const errorMessage = getMessageFromFetchError(error);
|
||||
if (errorMessage) {
|
||||
enqueueSnackbar(errorMessage);
|
||||
@ -124,6 +141,37 @@ export default function App() {
|
||||
},
|
||||
});
|
||||
|
||||
useTicketsFetcher({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
|
||||
ticketsPerPage: 10,
|
||||
ticketApiPage: 0,
|
||||
onSuccess: (result) => {
|
||||
if (result.data?.length) {
|
||||
// Записываем все тикеты в стор
|
||||
setTickets(result.data);
|
||||
|
||||
const currentTicket = result.data.find(
|
||||
({ origin }) => !origin.includes("/support"),
|
||||
);
|
||||
|
||||
if (!currentTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTicketData({
|
||||
ticketId: currentTicket.id,
|
||||
sessionId: currentTicket.sess,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const message = parseAxiosError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
},
|
||||
onFetchStateChange: () => { },
|
||||
enabled: Boolean(userId),
|
||||
});
|
||||
|
||||
useAfterPay();
|
||||
|
||||
if (location.state?.redirectTo)
|
||||
@ -136,7 +184,10 @@ export default function App() {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ApologyPage}
|
||||
onError={(error, info) => handleComponentError(error, info, () => useTicketStore.getState().tickets)}
|
||||
>
|
||||
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}
|
||||
|
||||
<ContactFormModal />
|
||||
@ -259,6 +310,10 @@ export default function App() {
|
||||
path={"/image/:srcImage"}
|
||||
element={<ChatImageNewWindow />}
|
||||
/>
|
||||
<Route
|
||||
path={"/debug"}
|
||||
element={<div></div>}
|
||||
/>
|
||||
<Route element={<PrivateRoute />}>
|
||||
{routeslink.map((e, i) => (
|
||||
<Route
|
||||
@ -280,6 +335,9 @@ export default function App() {
|
||||
))}
|
||||
</Route>
|
||||
</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";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
|
||||
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";
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { useToken } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
import useSWR from "swr";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
|
||||
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { defaultQuizConfig } from "@model/quizSettings";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
|
||||
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";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
import type { GetTariffsResponse } from "@frontend/kitui";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createTicket as createTicketRequest } from "@frontend/kitui";
|
||||
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
@ -15,47 +15,7 @@ type SendFileResponse = {
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`;
|
||||
|
||||
export const sendTicketMessage = async (
|
||||
ticketId: string,
|
||||
message: string,
|
||||
systemError: boolean
|
||||
): Promise<[null, string?]> => {
|
||||
try {
|
||||
const sendTicketMessageResponse = await makeRequest<
|
||||
SendTicketMessageRequest,
|
||||
null
|
||||
>({
|
||||
url: `${API_URL}/send`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { ticket: ticketId, message: message, lang: "ru", files: [], system: systemError },
|
||||
|
||||
});
|
||||
|
||||
return [sendTicketMessageResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось отправить сообщение. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const shownMessage = async (id: string): Promise<[null, string?]> => {
|
||||
try {
|
||||
const shownMessageResponse = await makeRequest<{ id: string }, null>({
|
||||
url: `${API_URL}/shown`,
|
||||
method: "POST",
|
||||
useToken: true,
|
||||
body: { id },
|
||||
});
|
||||
|
||||
return [shownMessageResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось прочесть сообщение. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const sendFile = async (
|
||||
ticketId: string,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
|
15
src/assets/icons/logo/Postback.tsx
Normal file
15
src/assets/icons/logo/Postback.tsx
Normal file
File diff suppressed because one or more lines are too long
19
src/assets/icons/logo/PostbackPC.tsx
Normal file
19
src/assets/icons/logo/PostbackPC.tsx
Normal file
@ -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>
|
||||
);
|
BIN
src/assets/icons/logo/zapier.png
Normal file
BIN
src/assets/icons/logo/zapier.png
Normal file
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 { CheckFastlink } from "@ui_kit/CheckFastlink";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { handleComponentError } from "./utils/handleComponentError";
|
||||
import { handleComponentError } from "@frontend/kitui";
|
||||
|
||||
moment.locale("ru");
|
||||
polyfillCountryFlagEmojis();
|
||||
@ -38,7 +38,6 @@ const snackbarAction = (snackbarId: SnackbarKey) => (
|
||||
</Button>
|
||||
);
|
||||
|
||||
const ApologyPage = () => <div><p>Что-то пошло не так</p></div>
|
||||
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
|
||||
@ -65,12 +64,7 @@ root.render(
|
||||
>
|
||||
<CssBaseline />
|
||||
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ApologyPage}
|
||||
onError={handleComponentError}
|
||||
>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
<CheckFastlink />
|
||||
</SnackbarProvider>
|
||||
</BrowserRouter>
|
||||
|
@ -69,6 +69,8 @@ export type QuizTheme =
|
||||
export enum QuizMetricType {
|
||||
yandex = "yandexMetricsNumber",
|
||||
vk = "vkMetricsNumber",
|
||||
zapier = "zapierIntegration",
|
||||
postback = "postbackIntegration",
|
||||
}
|
||||
|
||||
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";
|
||||
|
@ -42,16 +42,9 @@ const GeneralItem = ({
|
||||
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
|
||||
);
|
||||
const days = data.map(([value]) => value);
|
||||
const { time } = data.reduce(
|
||||
(total, [_, value]) => ({
|
||||
defaultValue: value > 0 ? value : total.defaultValue,
|
||||
time: [...total.time, value > 0 ? value : total.defaultValue],
|
||||
}),
|
||||
{ defaultValue: 0, time: [] as number[] },
|
||||
);
|
||||
|
||||
const numberValue = calculateTime
|
||||
? time.reduce((total, value) => total + value, 0) / days.length
|
||||
? Object.values(general).reduce((total, value) => total + value, 0) / Object.values(general).length
|
||||
: conversionValue
|
||||
? conversionValue
|
||||
: Object.values(general).reduce((total, item) => total + item, 0);
|
||||
@ -82,17 +75,17 @@ const GeneralItem = ({
|
||||
<LineChart
|
||||
xAxis={[
|
||||
{
|
||||
data: statiscticsResult ? days : Object.keys(general),
|
||||
data: Object.keys(general),
|
||||
valueFormatter: (value) => {
|
||||
const timestamp = Number(value);
|
||||
if (isNaN(timestamp)) return 'Invalid Date';
|
||||
return moment.unix(timestamp).format(statiscticsResult ? "DD/MM/YYYY" : "DD/MM/YYYY HH") + (statiscticsResult ? "" : "ч");
|
||||
return moment.unix(timestamp).format("DD/MM/YYYY");
|
||||
},
|
||||
},
|
||||
]}
|
||||
series={[
|
||||
{
|
||||
data: Object.values(statiscticsResult ? time : general),
|
||||
data: Object.values(general),
|
||||
valueFormatter: (value) =>
|
||||
calculateTime
|
||||
? getCalculatedTime(value)
|
||||
@ -131,11 +124,21 @@ export const General: FC<GeneralProps> = ({ data, day }) => {
|
||||
const generalResponse = Object.entries(data).reduce(
|
||||
(total, [fatherKey, values]) => {
|
||||
const value = Object.keys(values).reduce((totalValue, key) => {
|
||||
if (Number(key) - currentDate < 0) {
|
||||
const keyTimestamp = Number(key);
|
||||
const todayStart = moment().startOf('day').unix();
|
||||
const todayEnd = moment().endOf('day').unix();
|
||||
|
||||
// Включаем данные за сегодня и прошлые дни, исключаем будущие дни
|
||||
if (keyTimestamp >= todayStart && keyTimestamp <= todayEnd) {
|
||||
// Сегодняшний день - включаем
|
||||
return { ...totalValue, [key]: values[key] };
|
||||
} else if (keyTimestamp < todayStart) {
|
||||
// Прошлые дни - включаем
|
||||
return { ...totalValue, [key]: values[key] };
|
||||
} else {
|
||||
// Будущие дни - исключаем
|
||||
return totalValue;
|
||||
}
|
||||
|
||||
return totalValue;
|
||||
}, {});
|
||||
|
||||
return { ...total, [fatherKey]: value };
|
||||
|
149
src/pages/Debug.tsx
Normal file
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])
|
||||
const [blockButton, setBlockButton] = useState(false)
|
||||
|
||||
console.log("selectedQuestions")
|
||||
console.log(selectedQuestions)
|
||||
console.log("SCFworld")
|
||||
console.log(SCFworld)
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
|
@ -95,8 +95,6 @@ export const SwitchPages = ({
|
||||
const [specialPage, setSpecialPage] = useState<"deleteCell" | "removeAccount" | "settingsBlock" | "accountInfo" | "amoLogin" | "">(accountInfo ? "accountInfo" : "amoLogin")
|
||||
const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null);
|
||||
|
||||
console.log("--")
|
||||
console.log(selectedQuestions)
|
||||
|
||||
const startDeleteTagQuestion = (itemForDelete) => {
|
||||
setOpenDelete(itemForDelete)
|
||||
@ -135,9 +133,6 @@ export const SwitchPages = ({
|
||||
|
||||
if (type === "tag") {
|
||||
setSelectedTags((prevState) => {
|
||||
console.log(prevState)
|
||||
console.log(scope)
|
||||
console.log(id)
|
||||
return({
|
||||
...prevState,
|
||||
[scope]: [...prevState[scope as TagKeys], id],
|
||||
@ -147,9 +142,6 @@ export const SwitchPages = ({
|
||||
if (type === "question") {
|
||||
const q = questions.find(e => e.backendId === Number(id))
|
||||
setSelectedQuestions((prevState) => {
|
||||
console.log(prevState)
|
||||
console.log(scope)
|
||||
console.log(id)
|
||||
return ({
|
||||
...prevState,
|
||||
[scope]: [...prevState[scope as QuestionKeys], {
|
||||
|
@ -353,7 +353,7 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que
|
||||
};
|
||||
};
|
||||
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`;
|
||||
|
||||
export const resetAmoTagsFields = async () => {
|
||||
|
@ -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);
|
||||
|
||||
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
|
||||
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
|
||||
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editQuizId === null) navigate("/list");
|
||||
@ -44,6 +46,12 @@ export const IntegrationsPage = ({
|
||||
const handleCloseAmoSRMModal = () => {
|
||||
setIsAmoCrmModalOpen(false);
|
||||
};
|
||||
const handleCloseZapierModal = () => {
|
||||
setIsZapierModalOpen(false);
|
||||
};
|
||||
const handleClosePostbackModal = () => {
|
||||
setIsPostbackModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -60,7 +68,7 @@ export const IntegrationsPage = ({
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ marginBottom: "40px", color: "#333647" }}
|
||||
sx={{ marginBottom: "40px", color: theme.palette.grey3.main }}
|
||||
>
|
||||
Интеграции
|
||||
</Typography>
|
||||
@ -73,6 +81,12 @@ export const IntegrationsPage = ({
|
||||
setIsAmoCrmModalOpen={setIsAmoCrmModalOpen}
|
||||
isAmoCrmModalOpen={isAmoCrmModalOpen}
|
||||
handleCloseAmoSRMModal={handleCloseAmoSRMModal}
|
||||
setIsZapierModalOpen={setIsZapierModalOpen}
|
||||
isZapierModalOpen={isZapierModalOpen}
|
||||
handleCloseZapierModal={handleCloseZapierModal}
|
||||
setIsPostbackModalOpen={setIsPostbackModalOpen}
|
||||
isPostbackModalOpen={isPostbackModalOpen}
|
||||
handleClosePostbackModal={handleClosePostbackModal}
|
||||
/>
|
||||
</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 { ServiceButton } from "./ServiceButton/ServiceButton";
|
||||
import { ServiceButton } from "./buttons/ServiceButton";
|
||||
import { ZapierButton } from "./buttons/ZapierButton";
|
||||
import { PostbackButton } from "./buttons/PostbackButton";
|
||||
import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
|
||||
// import AnalyticsModal from "./AnalyticsModal/AnalyticsModal";
|
||||
import { VKPixelLogo } from "../mocks/VKPixelLogo";
|
||||
import { QuizMetricType } from "@model/quizSettings";
|
||||
import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
|
||||
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
|
||||
const AnalyticsModal = lazy(() =>
|
||||
import("./AnalyticsModal/AnalyticsModal").then((module) => ({
|
||||
@ -21,6 +21,18 @@ const AmoCRMModal = lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const ZapierModal = lazy(() =>
|
||||
import("../IntegrationsModal/Zapier").then((module) => ({
|
||||
default: module.ZapierModal,
|
||||
}))
|
||||
);
|
||||
|
||||
const PostbackModal = lazy(() =>
|
||||
import("../IntegrationsModal/Postback").then((module) => ({
|
||||
default: module.PostbackModal,
|
||||
}))
|
||||
);
|
||||
|
||||
type PartnersBoardProps = {
|
||||
setIsModalOpen: (value: boolean) => void;
|
||||
companyName: keyof typeof QuizMetricType | null;
|
||||
@ -30,6 +42,12 @@ type PartnersBoardProps = {
|
||||
setIsAmoCrmModalOpen: (value: boolean) => void;
|
||||
isAmoCrmModalOpen: boolean;
|
||||
handleCloseAmoSRMModal: () => void;
|
||||
setIsZapierModalOpen: (value: boolean) => void;
|
||||
isZapierModalOpen: boolean;
|
||||
handleCloseZapierModal: () => void;
|
||||
setIsPostbackModalOpen: (value: boolean) => void;
|
||||
isPostbackModalOpen: boolean;
|
||||
handleClosePostbackModal: () => void;
|
||||
};
|
||||
|
||||
export const PartnersBoard: FC<PartnersBoardProps> = ({
|
||||
@ -41,13 +59,31 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
||||
setIsAmoCrmModalOpen,
|
||||
isAmoCrmModalOpen,
|
||||
handleCloseAmoSRMModal,
|
||||
setIsZapierModalOpen,
|
||||
isZapierModalOpen,
|
||||
handleCloseZapierModal,
|
||||
setIsPostbackModalOpen,
|
||||
isPostbackModalOpen,
|
||||
handleClosePostbackModal,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
|
||||
const quiz = useCurrentQuiz();
|
||||
|
||||
const user = useUserStore();
|
||||
const sectionTitleStyles = {
|
||||
textAlign: { xs: "start", sm: "start", md: "start" } as const,
|
||||
lineHeight: "1",
|
||||
marginBottom: "12px",
|
||||
marginTop: "20px",
|
||||
color: theme.palette.grey3.main,
|
||||
fontSize: "18px",
|
||||
};
|
||||
|
||||
const containerStyles = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: { xs: "start", sm: "start", md: "start" },
|
||||
gap: { xs: "15px", md: "20px" },
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -59,49 +95,28 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
textAlign: { xs: "start", sm: "start", md: "start" },
|
||||
lineHeight: "1",
|
||||
marginBottom: "12px",
|
||||
}}
|
||||
>
|
||||
CRM
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: { xs: "start", sm: "start", md: "start" },
|
||||
}}
|
||||
>
|
||||
<ServiceButton
|
||||
logo={<AmoCRMLogo />}
|
||||
setIsModalOpen={setIsAmoCrmModalOpen}
|
||||
setCompanyName={setCompanyName}
|
||||
name={"amoCRM"}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
textAlign: { xs: "start", sm: "start", md: "start" },
|
||||
lineHeight: "1",
|
||||
marginBottom: "12px",
|
||||
...sectionTitleStyles,
|
||||
marginTop: 0,
|
||||
}}
|
||||
>
|
||||
CRM
|
||||
</Typography>
|
||||
<Box sx={containerStyles}>
|
||||
<ServiceButton
|
||||
logo={<AmoCRMLogo />}
|
||||
setIsModalOpen={setIsAmoCrmModalOpen}
|
||||
setCompanyName={setCompanyName}
|
||||
name={"amoCRM"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" sx={sectionTitleStyles}>
|
||||
Аналитика
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: { xs: "start", sm: "start", md: "start" },
|
||||
}}
|
||||
>
|
||||
<Box sx={containerStyles}>
|
||||
<ServiceButton
|
||||
logo={<YandexMetricaLogo />}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
@ -114,9 +129,24 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
||||
name={"vk"}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
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>
|
||||
|
||||
{companyName && (
|
||||
<Suspense>
|
||||
<AnalyticsModal
|
||||
@ -132,7 +162,27 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
||||
isModalOpen={isAmoCrmModalOpen}
|
||||
handleCloseModal={handleCloseAmoSRMModal}
|
||||
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>
|
||||
)}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
12
src/pages/Payment/Payment.tsx
Normal file
12
src/pages/Payment/Payment.tsx
Normal file
@ -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 [linksOpen, setLinksOpen] = useState(true);
|
||||
|
||||
console.log("auditory-___---_auditory__---__-__auditory_------__---__-__---_------__---__-__---_------__---__-____--__")
|
||||
console.log(auditory)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
112
src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx
Normal file
112
src/pages/PersonalizationAI/CreateButtonWithTooltip.tsx
Normal file
@ -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 AgeInputWithSelect from "./AgeInputWithSelect";
|
||||
import { useState, useEffect } from "react";
|
||||
import CreateButtonWithTooltip from "./CreateButtonWithTooltip";
|
||||
|
||||
interface GenderAndAgeSelectorProps {
|
||||
gender: string;
|
||||
@ -190,23 +191,12 @@ export default function GenderAndAgeSelector({
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
<CreateButtonWithTooltip
|
||||
gender={gender}
|
||||
age={age}
|
||||
ageError={ageError}
|
||||
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>
|
||||
);
|
||||
}
|
@ -11,16 +11,17 @@ import { useSnackbar } from "notistack";
|
||||
import { PayModal } from "./PayModal";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { cartApi } from "@/api/cart";
|
||||
import { outCart } from "../Tariffs/Tariffs";
|
||||
import { inCart } from "../Tariffs/Tariffs";
|
||||
import { outCart } from "../Tariffs/utils";
|
||||
import { inCart } from "../Tariffs/utils";
|
||||
import { isTestServer } from "@/utils/hooks/useDomainDefine";
|
||||
import { useToken } from "@frontend/kitui";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { setUserAccount, setCustomerAccount } from "@/stores/user";
|
||||
import { quizApi } from "@api/quiz";
|
||||
import { setQuizes } from "@root/quizes/actions";
|
||||
import TooltipClickInfo from "@/ui_kit/Toolbars/TooltipClickInfo";
|
||||
import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest";
|
||||
|
||||
const tariff = isTestServer ? "6844b8858258f5cc35791ef7" : "6851db40acfb4d3e5fcd9b19";
|
||||
export default function PersonalizationAI() {
|
||||
@ -92,7 +93,7 @@ export default function PersonalizationAI() {
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
enqueueSnackbar("Ошибка при обновлении данных пользователя", { variant: "error" });
|
||||
return null;
|
||||
}),
|
||||
@ -102,7 +103,7 @@ export default function PersonalizationAI() {
|
||||
useToken: true,
|
||||
withCredentials: false,
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
enqueueSnackbar("Ошибка при обновлении данных клиента", { variant: "error" });
|
||||
return null;
|
||||
})
|
||||
@ -115,7 +116,7 @@ export default function PersonalizationAI() {
|
||||
setCustomerAccount(customerAccountResult);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
enqueueSnackbar("Ошибка при обновлении данных", { variant: "error" });
|
||||
}
|
||||
}
|
||||
@ -128,8 +129,6 @@ export default function PersonalizationAI() {
|
||||
(async () => {
|
||||
if (quiz?.backendId) {
|
||||
const [result, error] = await auditoryGet({ quizId: quiz.backendId });
|
||||
console.log("result-___---_------__---__-__---_------__---__-__---_------__---__-__---_------__---__-____--__")
|
||||
console.log(result)
|
||||
if (result) {
|
||||
setAuditory(result);
|
||||
}
|
||||
@ -209,8 +208,6 @@ export default function PersonalizationAI() {
|
||||
setUtmParams(paramString ? `&${paramString}` : "");
|
||||
};
|
||||
|
||||
console.log("______----giga_chat-----__--_---_--_----__--__-__--_--__--__--_---_______-quiz")
|
||||
console.log(quiz?.giga_chat)
|
||||
const startCreate = async () => {
|
||||
if (quiz?.giga_chat) {
|
||||
createNewLink();
|
||||
@ -240,7 +237,15 @@ export default function PersonalizationAI() {
|
||||
//если денег не хватило
|
||||
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
|
||||
var link = document.createElement("a");
|
||||
link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=50000&data=${token}&userid=${userId}&from=AI&wayback=ai_${quiz?.backendId}`;
|
||||
link.href = generateHubWalletRequestURL({
|
||||
wayback: "personalization-ai",
|
||||
action: "buy",
|
||||
dif: "50000",
|
||||
userid: userId,
|
||||
additionalinformation: quiz?.backendId.toString(),
|
||||
token
|
||||
});
|
||||
//link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=50000&data=${token}&userid=${userId}&from=AI&wayback=ai_${quiz?.backendId}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
return;
|
||||
@ -263,12 +268,9 @@ export default function PersonalizationAI() {
|
||||
|
||||
|
||||
// Обновляем данные квиза после успешной оплаты
|
||||
console.log("Обновляем данные квиза после оплаты");
|
||||
const [quizes, quizesError] = await quizApi.getList();
|
||||
console.log("Получены данные квизов:", quizes);
|
||||
if (!quizesError) {
|
||||
setQuizes(quizes);
|
||||
console.log("Данные квизов обновлены в сторе");
|
||||
} else {
|
||||
console.error("Ошибка при получении данных квизов:", quizesError);
|
||||
}
|
||||
@ -289,7 +291,7 @@ export default function PersonalizationAI() {
|
||||
lineHeight: "21.4px"
|
||||
}}>
|
||||
Данный раздел позволяет вам создавать персонализированный опрос под каждую целевую аудиторию отдельно, наш AI перефразирует ваши вопросы согласно настройкам.
|
||||
<br/>Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
|
||||
<br />Для этого нужно выбрать пол и возраст вашей аудитории и получите персональную ссылку с нужными настройками в списке ниже.
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
color: theme.palette.grey3.main, fontSize: "18px", maxWidth: 796, m: 0,
|
||||
@ -298,7 +300,7 @@ export default function PersonalizationAI() {
|
||||
wordSpacing: "0.1px",
|
||||
lineHeight: "21.4px"
|
||||
}}>
|
||||
Так же вы можете обогатить свою ссылку UTM метками в поле "вставьте свою ссылку" и эти метки применятся ко всем вашим ссылкам.
|
||||
Так же вы можете обогатить свою ссылку UTM метками в поле "вставьте свою ссылку" и эти метки применятся ко всем вашим ссылкам.
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
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 { updateQuestion } from "@root/questions/actions";
|
||||
let params = (new URL(document.location)).searchParams;
|
||||
console.log(params.get("data"));
|
||||
const BranchingMap = lazy(() =>
|
||||
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" }}>
|
||||
{openBranchingPage ? (
|
||||
|
@ -126,7 +126,6 @@ const QuestionPageCardTitle = memo<Props>(function ({
|
||||
value={title}
|
||||
placeholder={"Заголовок вопроса"}
|
||||
onChange={({ target }) => {
|
||||
console.log(target.value.length)
|
||||
if (target.value.length > maxLengthTextField) {
|
||||
enqueueSnackbar("Превышена длина вводимого текста")
|
||||
} else {
|
||||
|
@ -31,9 +31,6 @@ export const DraggableList = ({
|
||||
createUntypedQuestion(Number(quiz.backendId));
|
||||
}
|
||||
}, [quiz, filteredQuestions]);
|
||||
console.log(quiz)
|
||||
console.log(questions)
|
||||
// if () {}uploadQuestionImage
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
|
@ -15,7 +15,6 @@ export default function VariantAdornment({
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
console.log("VariantAdornment extendedText", extendedText)
|
||||
return (
|
||||
<Box sx={{ cursor: "pointer" }}>
|
||||
<Box data-cy="choose-emoji-button" onClick={onClick}>
|
||||
|
@ -44,11 +44,6 @@ export default function SliderOptions({ question, openBranchingPage, setOpenBran
|
||||
});
|
||||
}, 5000);
|
||||
const updateStepsDebounced = useDebouncedCallback((value: string) => {
|
||||
console.log("value")
|
||||
console.log(value)
|
||||
console.log(value.toString())
|
||||
console.log("ReplaceToNotStartZero(Number(value)) _____________________________________")
|
||||
console.log(ReplaceToNotStartZero(Number(value)))
|
||||
updateQuestion<QuizQuestionNumber>(question.id, (question) => {
|
||||
question.content.step = ReplaceToNotStartZero(Number(value));
|
||||
});
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Tabs as MuiTabs } from "@mui/material";
|
||||
import { CustomTab } from "./CustomTab";
|
||||
import { TypePages } from "./types";
|
||||
|
||||
type TabsProps = {
|
||||
names: string[];
|
||||
items: string[];
|
||||
selectedItem: "day" | "count" | "dop" | "hide" | "create";
|
||||
setSelectedItem: (num: "day" | "count" | "dop") => void;
|
||||
selectedItem: TypePages;
|
||||
setSelectedItem: (num: TypePages) => void;
|
||||
toDop: () => void;
|
||||
};
|
||||
|
||||
export const Tabs = ({
|
||||
@ -18,7 +20,7 @@ export const Tabs = ({
|
||||
sx={{ m: "25px" }}
|
||||
TabIndicatorProps={{ sx: { display: "none" } }}
|
||||
value={selectedItem}
|
||||
onChange={(event, newValue: "day" | "count" | "dop") => {
|
||||
onChange={(event, newValue: TypePages) => {
|
||||
setSelectedItem(newValue);
|
||||
}}
|
||||
variant="scrollable"
|
||||
|
147
src/pages/Tariffs/TariffCardDisplaySelector.tsx
Normal file
147
src/pages/Tariffs/TariffCardDisplaySelector.tsx
Normal file
@ -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 { useToken } from "@frontend/kitui";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
IconButton,
|
||||
Modal,
|
||||
Paper,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useUserStore } from "@root/user";
|
||||
import { LogoutButton } from "@ui_kit/LogoutButton";
|
||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useEffect, useState } from "react";
|
||||
import { withErrorBoundary } from "react-error-boundary";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Logotip from "../../pages/Landing/images/icons/QuizLogo";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import CollapsiblePromocodeField from "./CollapsiblePromocodeField";
|
||||
import { Tabs } from "./Tabs";
|
||||
import { createTariffElements } from "./tariffsUtils/createTariffElements";
|
||||
import { currencyFormatter } from "./tariffsUtils/currencyFormatter";
|
||||
import { useWallet, setCash } from "@root/cash";
|
||||
import { handleLogoutClick } from "@utils/HandleLogoutClick";
|
||||
import { cartApi } from "@api/cart";
|
||||
|
||||
import { Other } from "./pages/Other";
|
||||
import { TariffCardDisplaySelector } from "./TariffCardDisplaySelector";
|
||||
import { ModalRequestCreate } from "./ModalRequestCreate";
|
||||
import { cancelCC, useCC } from "@/stores/cc";
|
||||
import { NavSelect } from "./NavSelect";
|
||||
import { useTariffs } from '@utils/hooks/useTariffs';
|
||||
import { useDiscounts } from '@utils/hooks/useDiscounts';
|
||||
import { PaymentConfirmationModal } from "./components/PaymentConfirmationModal";
|
||||
import { TariffsHeader } from "./components/TariffsHeader";
|
||||
import { inCart, outCart } from "./utils";
|
||||
import { generateHubWalletRequestURL } from "@/utils/generateHubWalletRequest";
|
||||
|
||||
const StepperText: Record<string, string> = {
|
||||
day: "Тарифы на время",
|
||||
@ -50,12 +45,17 @@ function TariffPage() {
|
||||
const userId = useUserStore((state) => state.userId);
|
||||
const navigate = useNavigate();
|
||||
const user = useUserStore((state) => state.customerAccount);
|
||||
const a = useUserStore((state) => state.customerAccount); //c wallet
|
||||
console.log("________________34563875693785692576_____________USERRRRRRR")
|
||||
console.log(a)
|
||||
const { data: discounts } = useDiscounts(userId);
|
||||
const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
|
||||
const userAccount = useUserStore((state) => state.userAccount);
|
||||
// console.info("________________userWithWallet_____________USERRRRRRR")
|
||||
// console.info(userWithWallet)
|
||||
// console.info("________________userAccount_____________")
|
||||
// console.info(userAccount)
|
||||
// console.info("________________customerAccount_____________")
|
||||
// console.info(user)
|
||||
const { data: discounts, error: discountsError, isLoading: discountsLoading } = useDiscounts(userId);
|
||||
const [isRequestCreate, setIsRequestCreate] = useState(false);
|
||||
const [openModal, setOpenModal] = useState({});
|
||||
const [openModal, setOpenModal] = useState<{ id?: string; price?: number }>({});
|
||||
const { cashString, cashCop, cashRub } = useWallet();
|
||||
const [selectedItem, setSelectedItem] = useState<TypePages>("day");
|
||||
const { isTestServer } = useDomainDefine();
|
||||
@ -65,17 +65,14 @@ function TariffPage() {
|
||||
|
||||
const { data: tariffs, error: tariffsError, isLoading: tariffsLoading } = useTariffs();
|
||||
|
||||
console.log("________34563875693785692576_____ TARIFFS")
|
||||
console.log(tariffs)
|
||||
|
||||
useEffect(() => {
|
||||
if (a) {
|
||||
if (userWithWallet && user) {
|
||||
let cs = currencyFormatter.format(Number(user.wallet.cash) / 100);
|
||||
let cc = Number(user.wallet.cash);
|
||||
let cr = Number(user.wallet.cash) / 100;
|
||||
setCash(cs, cc, cr);
|
||||
}
|
||||
}, [a]);
|
||||
}, [userWithWallet, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cc) {
|
||||
@ -83,7 +80,26 @@ console.log(tariffs)
|
||||
cancelCC()
|
||||
}
|
||||
}, [])
|
||||
if (!user || !tariffs || !discounts) return <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 tryBuy = async ({ id, price }: { id: string; price: number }) => {
|
||||
@ -109,11 +125,20 @@ console.log(tariffs)
|
||||
//если денег не хватило
|
||||
if (payError?.includes("insufficient funds") || payError?.includes("Payment Required")) {
|
||||
let cashDif = Number(payError.split(":")[1]);
|
||||
var link = document.createElement("a");
|
||||
link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`;
|
||||
if (cc) link.href = link.href + "&cc=true"//после покупки тарифа и возвращения будем знать что надо открыть модалку
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
if (!userId) {
|
||||
enqueueSnackbar("Ошибка: ID пользователя не найден");
|
||||
return;
|
||||
}
|
||||
|
||||
const l = generateHubWalletRequestURL({
|
||||
action: cc ? "createquizcc" : "buy",
|
||||
dif: cashDif.toString(),
|
||||
userid: userId,
|
||||
wayback: "list",
|
||||
token
|
||||
});
|
||||
window.location.href = l;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -169,63 +194,10 @@ console.log(tariffs)
|
||||
setIsRequestCreate(true)
|
||||
}
|
||||
|
||||
if (!a) return null;
|
||||
if (!userWithWallet) return null;
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<TariffsHeader cashString={cashString} />
|
||||
<Box
|
||||
sx={{
|
||||
p: "25px",
|
||||
@ -281,9 +253,9 @@ console.log(tariffs)
|
||||
discounts,
|
||||
openModalHC,
|
||||
)}
|
||||
{(selectedItem === "dop" || selectedItem === "hide" || selectedItem === "create")
|
||||
{(selectedItem === "hide" || selectedItem === "create" || selectedItem === "premium" || selectedItem === "analytics" || selectedItem === "custom")
|
||||
&& (
|
||||
<Other
|
||||
<TariffCardDisplaySelector
|
||||
selectedItem={selectedItem}
|
||||
content={[
|
||||
{
|
||||
@ -294,48 +266,86 @@ console.log(tariffs)
|
||||
title: "Создать квиз на заказ",
|
||||
onClick: () => setSelectedItem("create")
|
||||
},
|
||||
{
|
||||
title: "Премиум функции",
|
||||
onClick: () => setSelectedItem("premium")
|
||||
},
|
||||
{
|
||||
title: "Расширенная аналитика",
|
||||
onClick: () => setSelectedItem("analytics")
|
||||
},
|
||||
{
|
||||
title: "Кастомные тарифы",
|
||||
onClick: () => setSelectedItem("custom")
|
||||
},
|
||||
]}
|
||||
|
||||
tariffs={tariffs}
|
||||
user={user}
|
||||
discounts={discounts}
|
||||
discounts={discounts || []}
|
||||
openModalHC={openModalHC}
|
||||
userPrivilegies={userPrivilegies}
|
||||
startRequestCreate={startRequestCreate}
|
||||
/>
|
||||
)}
|
||||
{selectedItem === "dop" && (
|
||||
<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}
|
||||
userPrivilegies={userPrivilegies}
|
||||
startRequestCreate={startRequestCreate}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Modal
|
||||
<PaymentConfirmationModal
|
||||
open={Object.values(openModal).length > 0}
|
||||
onClose={() => setOpenModal({})}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
Вы подтверждаете платёж в сумму{" "}
|
||||
{openModal.price ? openModal.price.toFixed(2) : 0} ₽
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={() => tryBuy(openModal)}>
|
||||
купить
|
||||
</Button>
|
||||
</Paper>
|
||||
</Modal>
|
||||
onConfirm={() => {
|
||||
if (openModal.id && openModal.price !== undefined) {
|
||||
tryBuy({ id: openModal.id, price: openModal.price });
|
||||
}
|
||||
}}
|
||||
price={openModal.price || 0}
|
||||
/>
|
||||
<ModalRequestCreate open={isRequestCreate} onClose={() => setIsRequestCreate(false)} />
|
||||
</>
|
||||
);
|
||||
@ -364,47 +374,3 @@ const LoadingPage = () => (
|
||||
</Typography>
|
||||
</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));
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
51
src/pages/Tariffs/components/PaymentConfirmationModal.tsx
Normal file
51
src/pages/Tariffs/components/PaymentConfirmationModal.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
83
src/pages/Tariffs/components/TariffsHeader.tsx
Normal file
83
src/pages/Tariffs/components/TariffsHeader.tsx
Normal file
@ -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,
|
||||
icon?: ReactNode
|
||||
) => {
|
||||
console.log("start work createTariffElements")
|
||||
console.log("filteredTariffs ", filteredTariffs)
|
||||
console.log("user ", user)
|
||||
console.log("user.isUserNko, ", user.isUserNko)
|
||||
const tariffElements = filteredTariffs
|
||||
.filter((tariff) => tariff.privileges.length > 0)
|
||||
.map((tariff, index) => {
|
||||
|
@ -1 +1 @@
|
||||
type TypePages = "count" | "day" | "dop" | "hide" | "create"
|
||||
type TypePages = "count" | "day" | "dop" | "hide" | "create" | "premium" | "analytics" | "custom"
|
46
src/pages/Tariffs/utils.ts
Normal file
46
src/pages/Tariffs/utils.ts
Normal file
@ -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 { useUserStore } from "@root/user";
|
||||
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { setAuthToken } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
import { recoverUser } from "@api/user";
|
||||
|
@ -242,7 +242,7 @@ export default function SignupDialog() {
|
||||
</Link>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/restore"
|
||||
to="/recover"
|
||||
state={{ backgroundLocation: location.state.backgroundLocation }}
|
||||
sx={{ color: "#7E2AEA" }}
|
||||
>
|
||||
|
@ -53,8 +53,6 @@ export default function AvailablePrivilege() {
|
||||
}
|
||||
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "");
|
||||
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "");
|
||||
console.log(userPrivileges)
|
||||
console.log(quizUnlimTime)
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@ -6,8 +6,8 @@ import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import { Box, Button, IconButton, Popover, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { inCart } from "../../pages/Tariffs/Tariffs";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { inCart } from "../../pages/Tariffs/utils";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
|
||||
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 { createJSONStorage, devtools, persist } from "zustand/middleware";
|
||||
import { useUserStore } from "./user";
|
||||
@ -21,11 +21,12 @@ interface AuthData {
|
||||
interface TicketStore {
|
||||
unauthData: AuthData;
|
||||
authData: AuthData;
|
||||
tickets: Ticket[];
|
||||
}
|
||||
|
||||
let params = new URLSearchParams(document.location.search);
|
||||
|
||||
const debug = params.get("debug");
|
||||
|
||||
const initAuthData = {
|
||||
sessionData: null,
|
||||
isMessageSending: false,
|
||||
@ -35,10 +36,12 @@ const initAuthData = {
|
||||
lastMessageId: undefined,
|
||||
isPreventAutoscroll: false,
|
||||
unauthTicketMessageFetchState: "idle" as FetchState,
|
||||
tickets: []
|
||||
};
|
||||
const initState = {
|
||||
unauthData: initAuthData,
|
||||
authData: initAuthData,
|
||||
tickets: []
|
||||
};
|
||||
|
||||
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?.[sortedMessages.length - 1]?.id || "";
|
||||
console.log("ticketStudy, ", sortedMessages)
|
||||
});
|
||||
|
||||
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,
|
||||
action?: A,
|
||||
) {
|
||||
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) =>
|
||||
useUserStore.setState({ userAccount });
|
||||
|
||||
export const setCustomerAccount = (customerAccount: UserAccount) =>
|
||||
export const setCustomerAccount = (customerAccount: UserAccount) => {
|
||||
useUserStore.setState({ customerAccount });
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { Box, Button, Modal, Typography } from "@mui/material";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { mutate } from "swr";
|
||||
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import { getDiscounts } from "@api/discounts";
|
||||
|
||||
import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";
|
||||
|
@ -1,9 +1,6 @@
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputBase,
|
||||
SxProps,
|
||||
Theme,
|
||||
Typography,
|
||||
@ -17,46 +14,45 @@ import {
|
||||
useTicketStore,
|
||||
} from "@root/ticket";
|
||||
import type { TouchEvent, WheelEvent } from "react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ChatMessage from "./ChatMessage";
|
||||
import ChatVideo from "./ChatVideo";
|
||||
import SendIcon from "@icons/SendIcon";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import ChatMessageRenderer from "./ChatMessageRenderer";
|
||||
import ChatInput from "./ChatInput";
|
||||
import UserCircleIcon from "./UserCircleIcon";
|
||||
import { throttle, TicketMessage } from "@frontend/kitui";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import { useUserStore } from "@root/user";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import ChatImage from "./ChatImage";
|
||||
import ChatDocument from "@ui_kit/FloatingSupportChat/ChatDocument";
|
||||
import {
|
||||
ACCEPT_SEND_MEDIA_TYPES_MAP,
|
||||
checkAcceptableMediaType,
|
||||
} from "@utils/checkAcceptableMediaType";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
sx?: SxProps<Theme>;
|
||||
onclickArrow?: () => void;
|
||||
sendMessage: (a: string) => Promise<boolean>;
|
||||
sendFile: (a: File | undefined) => Promise<true>;
|
||||
greetingMessage: TicketMessage;
|
||||
sendFile: (a: File | undefined) => Promise<void>;
|
||||
}
|
||||
|
||||
const greetingMessage: TicketMessage = {
|
||||
id: "greeting",
|
||||
ticket_id: "",
|
||||
user_id: "system",
|
||||
session_id: "",
|
||||
message: "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут",
|
||||
files: [],
|
||||
shown: {},
|
||||
request_screenshot: "",
|
||||
created_at: new Date().toISOString(),
|
||||
system: false
|
||||
};
|
||||
|
||||
export default function Chat({
|
||||
open = false,
|
||||
sx,
|
||||
onclickArrow,
|
||||
sendMessage,
|
||||
sendFile,
|
||||
greetingMessage,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(800));
|
||||
const [messageField, setMessageField] = useState<string>("");
|
||||
const [disableFileButton, setDisableFileButton] = useState(false);
|
||||
|
||||
const user = useUserStore((state) => state.user?._id);
|
||||
const ticket = useTicketStore(
|
||||
@ -72,31 +68,11 @@ export default function Chat({
|
||||
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
addOrUpdateUnauthMessages([greetingMessage]);
|
||||
if (open) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const sendMessageHC = async () => {
|
||||
const successful = await sendMessage(messageField);
|
||||
if (successful) {
|
||||
setMessageField("");
|
||||
}
|
||||
};
|
||||
const sendFileHC = async (file: File) => {
|
||||
const check = checkAcceptableMediaType(file);
|
||||
if (check.length > 0) {
|
||||
enqueueSnackbar(check);
|
||||
return;
|
||||
}
|
||||
setDisableFileButton(true);
|
||||
await sendFile(file);
|
||||
setDisableFileButton(false);
|
||||
};
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const throttledScrollHandler = useMemo(
|
||||
() =>
|
||||
throttle(() => {
|
||||
@ -152,14 +128,6 @@ export default function Chat({
|
||||
behavior,
|
||||
});
|
||||
}
|
||||
const handleTextfieldKeyPress: React.KeyboardEventHandler<
|
||||
HTMLInputElement | HTMLTextAreaElement
|
||||
> = (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessageHC();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -240,164 +208,28 @@ export default function Chat({
|
||||
>
|
||||
{ticket.sessionData?.ticketId &&
|
||||
messages.map((message) => {
|
||||
const isFileVideo = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType),
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileImage = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType),
|
||||
);
|
||||
}
|
||||
};
|
||||
const isFileDocument = () => {
|
||||
if (message.files) {
|
||||
return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some(
|
||||
(fileType) =>
|
||||
message.files[0].toLowerCase().endsWith(fileType),
|
||||
);
|
||||
}
|
||||
};
|
||||
if (message.files.length > 0 && isFileImage()) {
|
||||
return (
|
||||
<ChatImage
|
||||
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 && 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
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const isSelf = (ticket.sessionData?.sessionId || user) === message.user_id;
|
||||
|
||||
return (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
<ChatMessageRenderer
|
||||
key={message.id}
|
||||
text={message.message}
|
||||
createdAt={message.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
message.user_id
|
||||
}
|
||||
message={message}
|
||||
isSelf={isSelf}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!ticket.sessionData?.ticketId && (
|
||||
<ChatMessage
|
||||
unAuthenticated
|
||||
text={greetingMessage.message}
|
||||
createdAt={greetingMessage.created_at}
|
||||
isSelf={
|
||||
(ticket.sessionData?.sessionId || user) ===
|
||||
greetingMessage.user_id
|
||||
}
|
||||
<ChatMessageRenderer
|
||||
message={greetingMessage}
|
||||
isSelf={false}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<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={(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>
|
||||
<ChatInput
|
||||
sendMessage={sendMessage}
|
||||
sendFile={sendFile}
|
||||
isMessageSending={isMessageSending}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
137
src/ui_kit/FloatingSupportChat/ChatInput.tsx
Normal file
137
src/ui_kit/FloatingSupportChat/ChatInput.tsx
Normal file
@ -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;
|
70
src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
Normal file
70
src/ui_kit/FloatingSupportChat/ChatMessageRenderer.tsx
Normal file
@ -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 { forwardRef, useEffect, useState } from "react";
|
||||
import { forwardRef, useEffect, useState, useMemo } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
@ -44,10 +44,9 @@ interface Props {
|
||||
handleChatClickClose: () => void;
|
||||
handleChatClickSwitch: () => void;
|
||||
sendMessage: (a: string) => Promise<boolean>;
|
||||
sendFile: (a: File | undefined) => Promise<true>;
|
||||
sendFile: (a: File | undefined) => Promise<void>;
|
||||
modalWarningType: string | null;
|
||||
setModalWarningType: any;
|
||||
greetingMessage: TicketMessage;
|
||||
}
|
||||
|
||||
export default function FloatingSupportChat({
|
||||
@ -59,7 +58,6 @@ export default function FloatingSupportChat({
|
||||
sendFile,
|
||||
modalWarningType,
|
||||
setModalWarningType,
|
||||
greetingMessage,
|
||||
}: Props) {
|
||||
const [monitorType, setMonitorType] = useState<"desktop" | "mobile" | "">("");
|
||||
const theme = useTheme();
|
||||
@ -72,6 +70,48 @@ export default function FloatingSupportChat({
|
||||
(state) => state[user ? "authData" : "unauthData"],
|
||||
);
|
||||
|
||||
const ticket = useTicketStore(
|
||||
(state) => state[user ? "authData" : "unauthData"],
|
||||
);
|
||||
|
||||
// Функция для подсчёта непрочитанных сообщений согласно новой логике
|
||||
const unreadCount = useMemo(() => {
|
||||
if (messages.length === 0) return 0;
|
||||
|
||||
const currentUserId = user || ticket?.sessionData?.sessionId;
|
||||
if (!currentUserId) return 0;
|
||||
|
||||
// Если последнее сообщение моё - не показываем количество
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage.user_id === currentUserId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Если последнее сообщение не моё и оно не прочитано - считаем его как +1
|
||||
if (lastMessage.shown?.me !== 1) {
|
||||
let count = 1;
|
||||
|
||||
// Считаем все последующие сообщения до тех пор пока не воткнёмся в моё сообщение
|
||||
for (let i = messages.length - 2; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
|
||||
// Если встретили моё сообщение - останавливаемся
|
||||
if (message.user_id === currentUserId) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Если сообщение не прочитано - добавляем к счётчику
|
||||
if (message.shown?.me !== 1) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}, [messages, user, ticket?.sessionData?.sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
if (document.fullscreenElement) {
|
||||
@ -108,7 +148,6 @@ export default function FloatingSupportChat({
|
||||
sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
|
||||
sendMessage={sendMessage}
|
||||
sendFile={sendFile}
|
||||
greetingMessage={greetingMessage}
|
||||
/>
|
||||
<Dialog
|
||||
fullScreen
|
||||
@ -121,7 +160,6 @@ export default function FloatingSupportChat({
|
||||
onclickArrow={handleChatClickClose}
|
||||
sendMessage={sendMessage}
|
||||
sendFile={sendFile}
|
||||
greetingMessage={greetingMessage}
|
||||
/>
|
||||
</Dialog>
|
||||
<Fab
|
||||
@ -162,9 +200,7 @@ export default function FloatingSupportChat({
|
||||
/>
|
||||
)}
|
||||
<Badge
|
||||
badgeContent={
|
||||
messages.filter(({ shown }) => shown?.me !== 1).length || 0
|
||||
}
|
||||
badgeContent={unreadCount}
|
||||
sx={{
|
||||
"& .MuiBadge-badge": {
|
||||
display: isChatOpened ? "none" : "flex",
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
TicketMessage,
|
||||
createTicket,
|
||||
shownMessage,
|
||||
useSSESubscription,
|
||||
useTicketMessages,
|
||||
useTicketsFetcher,
|
||||
sendFile as sf
|
||||
} from "@frontend/kitui";
|
||||
import FloatingSupportChat from "./FloatingSupportChat";
|
||||
import { useUserStore } from "@root/user";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { sendTicketMessage, shownMessage } from "../../api/ticket";
|
||||
import { useSSETab } from "../../utils/hooks/useSSETab";
|
||||
import {
|
||||
addOrUpdateUnauthMessages,
|
||||
@ -21,7 +23,6 @@ import {
|
||||
} from "@root/ticket";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
|
||||
import { selectSendingMethod } from "./utils";
|
||||
|
||||
type ModalWarningType =
|
||||
@ -71,60 +72,6 @@ export default () => {
|
||||
setIsChatOpened((state) => !state);
|
||||
};
|
||||
|
||||
const getGreetingMessage: TicketMessage = useMemo(() => {
|
||||
const workingHoursMessage =
|
||||
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
|
||||
const offHoursMessage =
|
||||
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
|
||||
const date = new Date();
|
||||
const currentHourUTC = date.getUTCHours();
|
||||
const MscTime = 3; // Москва UTC+3;
|
||||
const moscowHour = (currentHourUTC + MscTime) % 24;
|
||||
const greetingMessage =
|
||||
moscowHour >= 3 && moscowHour < 10
|
||||
? offHoursMessage
|
||||
: workingHoursMessage;
|
||||
|
||||
return {
|
||||
created_at: new Date().toISOString(),
|
||||
files: [],
|
||||
id: "111",
|
||||
message: greetingMessage,
|
||||
request_screenshot: "",
|
||||
session_id: "greetingMessage",
|
||||
shown: { me: 1 },
|
||||
ticket_id: "111",
|
||||
user_id: "greetingMessage",
|
||||
};
|
||||
}, [isChatOpened]);
|
||||
|
||||
useTicketsFetcher({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getTickets`,
|
||||
ticketsPerPage: 10,
|
||||
ticketApiPage: 0,
|
||||
onSuccess: (result) => {
|
||||
if (result.data?.length) {
|
||||
const currentTicket = result.data.find(
|
||||
({ origin }) => !origin.includes("/support"),
|
||||
);
|
||||
|
||||
if (!currentTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTicketData({
|
||||
ticketId: currentTicket.id,
|
||||
sessionId: currentTicket.sess,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const message = parseAxiosError(error);
|
||||
if (message) enqueueSnackbar(message);
|
||||
},
|
||||
onFetchStateChange: () => {},
|
||||
enabled: Boolean(user),
|
||||
});
|
||||
|
||||
useTicketMessages({
|
||||
url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/getMessages`,
|
||||
@ -156,7 +103,6 @@ export default () => {
|
||||
);
|
||||
if (isTicketClosed) {
|
||||
cleanAuthTicketData();
|
||||
addOrUpdateUnauthMessages([getGreetingMessage]);
|
||||
if (!user) {
|
||||
cleanUnauthTicketData();
|
||||
localStorage.removeItem("unauth-ticket");
|
||||
@ -184,33 +130,44 @@ export default () => {
|
||||
({ shown }) => shown?.me !== 1,
|
||||
);
|
||||
|
||||
newMessages.map(async ({ id }) => {
|
||||
await shownMessage(id);
|
||||
});
|
||||
// Находим последнее сообщение, которое не от пользователя
|
||||
const lastNonUserMessage = newMessages
|
||||
.filter(({ user_id }) => (ticket.sessionData?.sessionId || user) !== user_id)
|
||||
.pop();
|
||||
|
||||
// Отправляем shown только на последнее сообщение, которое не от пользователя
|
||||
if (lastNonUserMessage) {
|
||||
shownMessage(lastNonUserMessage.id);
|
||||
}
|
||||
}
|
||||
}, [isChatOpened, ticket.messages]);
|
||||
|
||||
const sendMessage = async (messageField: string) => {
|
||||
if (!messageField || ticket.isMessageSending) return false;
|
||||
|
||||
|
||||
setSseEnabled(true);
|
||||
setIsMessageSending(true);
|
||||
|
||||
let successful = await selectSendingMethod({messageField});
|
||||
let successful = await selectSendingMethod({ messageField });
|
||||
|
||||
setIsMessageSending(false);
|
||||
return successful;
|
||||
};
|
||||
const sendFile = async (file: File) => {
|
||||
if (file === undefined) return true;
|
||||
const sendFile = async (file: File | undefined): Promise<void> => {
|
||||
if (file === undefined) return;
|
||||
|
||||
let ticketId = ticket.sessionData?.ticketId;
|
||||
if (!ticket.sessionData?.ticketId) {
|
||||
const [data, createError] = await createTicket("", Boolean(user));
|
||||
const [data, createError] = await createTicket({
|
||||
message: "",
|
||||
useToken: Boolean(user),
|
||||
systemError: false
|
||||
});
|
||||
ticketId = data?.Ticket;
|
||||
|
||||
if (createError || !data) {
|
||||
enqueueSnackbar(createError);
|
||||
enqueueSnackbar(`Не удалось создать диалог ${parseAxiosError(createError)}`);
|
||||
return;
|
||||
} else {
|
||||
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
||||
}
|
||||
@ -219,15 +176,16 @@ export default () => {
|
||||
}
|
||||
|
||||
if (ticketId !== undefined) {
|
||||
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
|
||||
|
||||
const [_, sendFileError] = await sendFileRequest(ticketId, file);
|
||||
|
||||
if (sendFileError) {
|
||||
enqueueSnackbar(sendFileError);
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
setModalWarningType("errorSize");
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
const [_, sendFileError] = await sf({ticketId, file});
|
||||
|
||||
if (sendFileError) {
|
||||
enqueueSnackbar(`Не удалось отправить файл ${parseAxiosError(sendFileError)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -241,7 +199,6 @@ export default () => {
|
||||
sendFile={sendFile}
|
||||
modalWarningType={modalWarningType}
|
||||
setModalWarningType={setModalWarningType}
|
||||
greetingMessage={getGreetingMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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,55 +1,51 @@
|
||||
import { sendTicketMessage } from "@/api/ticket";
|
||||
import { setTicketData, useTicketStore } from "@/stores/ticket";
|
||||
import { useUserStore } from "@root/user";
|
||||
import { createTicket, sendFile as sendFileRequest } from "@api/ticket";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { createTicket, sendTicketMessage } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "@/utils/parse-error";
|
||||
|
||||
interface SelectSendingMethod {
|
||||
messageField: string;
|
||||
isSnackbar?: boolean;
|
||||
systemError?: boolean;
|
||||
|
||||
}
|
||||
export const selectSendingMethod = async ({messageField, isSnackbar = true, systemError = false}: SelectSendingMethod) => {
|
||||
console.log("click")
|
||||
messageField: string;
|
||||
isSnackbar?: boolean;
|
||||
systemError?: boolean;
|
||||
|
||||
}
|
||||
export const selectSendingMethod = async ({ messageField, isSnackbar = true, systemError = false }: SelectSendingMethod) => {
|
||||
const user = useUserStore.getState().user?._id;
|
||||
const ticket = useTicketStore.getState()[user ? "authData" : "unauthData"];
|
||||
console.log(ticket)
|
||||
|
||||
console.log("click 2")
|
||||
let successful = false;
|
||||
if (!(window.location.hostname == 'localhost' && systemError )) { //предупреждать о системных ошибках вне локалхост
|
||||
if (!ticket.sessionData?.ticketId) {
|
||||
console.log("autorisated 2")
|
||||
const [data, createError] = await createTicket(
|
||||
messageField,
|
||||
Boolean(user),
|
||||
false,
|
||||
);
|
||||
if (!(window.location.hostname == 'localhost' && systemError)) { //предупреждать о системных ошибках вне локалхост
|
||||
if (!ticket.sessionData?.ticketId) {
|
||||
const [data, createError] = await createTicket({
|
||||
message: messageField,
|
||||
useToken: Boolean(user),
|
||||
systemError: false,
|
||||
});
|
||||
|
||||
if (createError || !data) {
|
||||
successful = false;
|
||||
|
||||
if (isSnackbar) enqueueSnackbar(createError);
|
||||
} else {
|
||||
successful = true;
|
||||
|
||||
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
||||
}
|
||||
if (createError || !data) {
|
||||
successful = false;
|
||||
|
||||
if (isSnackbar) enqueueSnackbar(`Не удалось открыть диалог ${parseAxiosError(createError)}`);
|
||||
} else {
|
||||
const [_, sendTicketMessageError] = await sendTicketMessage(
|
||||
ticket.sessionData?.ticketId,
|
||||
messageField,
|
||||
false,
|
||||
);
|
||||
successful = true;
|
||||
|
||||
if (sendTicketMessageError) {
|
||||
successful = false;
|
||||
if (isSnackbar) enqueueSnackbar(sendTicketMessageError);
|
||||
}
|
||||
setTicketData({ ticketId: data.Ticket, sessionId: data.sess });
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
const [_, sendTicketMessageError] = await sendTicketMessage({
|
||||
ticketId: ticket.sessionData?.ticketId,
|
||||
message: messageField,
|
||||
systemError: false,
|
||||
});
|
||||
successful = true;
|
||||
|
||||
if (sendTicketMessageError) {
|
||||
successful = false;
|
||||
if (isSnackbar) enqueueSnackbar(sendTicketMessageError);
|
||||
}
|
||||
}
|
||||
}
|
||||
return successful;
|
||||
}
|
@ -29,9 +29,6 @@ export const NavigationPanel: FC<Props> = ({
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
||||
const lastStep = currentStep + 1 === totalSteps;
|
||||
|
||||
console.log("nextStepName")
|
||||
console.log(nextStepName)
|
||||
|
||||
const handlePrevStep = () => {
|
||||
if (currentStep === 0) return;
|
||||
setCurrentStep(currentStep - 1);
|
||||
|
@ -46,8 +46,6 @@ export default function WorkSpace({
|
||||
modalModels[currentStepName]
|
||||
), [currentStepName]);
|
||||
|
||||
// console.log(" промежуточный рендер которому должно быть похуй")
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
|
@ -13,7 +13,6 @@ export const SwitchAI = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const quiz = useCurrentQuiz();
|
||||
const account = useUserStore()
|
||||
console.log(account.userId)
|
||||
if (account.userId === "6755b1ddd5802e9f13663f56") {
|
||||
return (
|
||||
<>
|
||||
|
42
src/utils/generateHubWalletRequest.ts
Normal file
42
src/utils/generateHubWalletRequest.ts
Normal file
@ -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 { Ticket, createTicket, getAuthToken, sendTicketMessage } from "..";
|
||||
|
||||
let errorsQueue: ComponentError[] = [];
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
interface ComponentError {
|
||||
timestamp: number;
|
||||
@ -8,40 +11,99 @@ interface ComponentError {
|
||||
componentStack: string | null | undefined;
|
||||
}
|
||||
|
||||
export function handleComponentError(error: Error, info: ErrorInfo) {
|
||||
const componentError: ComponentError = {
|
||||
function isErrorReportingAllowed(error?: Error): boolean {
|
||||
// Если ошибка помечена как debug-override — всегда отправлять
|
||||
if (error && (error as any).__forceSend) return true;
|
||||
// Проверяем домен
|
||||
const currentDomain = window.location.hostname;
|
||||
return currentDomain !== 'localhost';
|
||||
}
|
||||
|
||||
// Новый API: getTickets — callback, возвращающий актуальные тикеты
|
||||
export function handleComponentError(error: Error, info: ErrorInfo, getTickets: () => Ticket[]) {
|
||||
//репортим только о авторизонышах
|
||||
if (!getAuthToken()) return;
|
||||
// Проверяем разрешение на отправку ошибок (по домену)
|
||||
if (!isErrorReportingAllowed(error)) {
|
||||
console.log('❌ Отправка ошибки заблокирована:', error.message);
|
||||
return;
|
||||
}
|
||||
console.log(`✅ Обработка ошибки: ${error.message}`);
|
||||
// Копируем __forceSend если есть
|
||||
const componentError: ComponentError & { __forceSend?: boolean } = {
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
message: error.message,
|
||||
callStack: error.stack,
|
||||
componentStack: info.componentStack,
|
||||
...(error && (error as any).__forceSend ? { __forceSend: true } : {})
|
||||
};
|
||||
|
||||
queueErrorRequest(componentError);
|
||||
queueErrorRequest(componentError, getTickets);
|
||||
}
|
||||
|
||||
let errorsQueue: ComponentError[] = [];
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
function queueErrorRequest(error: ComponentError) {
|
||||
// Ставит ошибку в очередь для отправки, через 1 секунду вызывает sendErrorsToServer
|
||||
export function queueErrorRequest(error: ComponentError, getTickets: () => Ticket[]) {
|
||||
errorsQueue.push(error);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
sendErrorsToServer();
|
||||
sendErrorsToServer(getTickets);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function sendErrorsToServer() {
|
||||
// makeRequest({
|
||||
// url: "",
|
||||
// method: "POST",
|
||||
// body: errorsQueue,
|
||||
// useToken: true,
|
||||
// });
|
||||
// selectSendingMethod({
|
||||
// messageField: `Fake-sending ${errorsQueue.length} errors to server ${JSON.stringify(errorsQueue)}`,
|
||||
// isSnackbar: false,
|
||||
// systemError: true
|
||||
// });
|
||||
// errorsQueue = [];
|
||||
// Отправляет накопленные ошибки в тикеты, ищет существующий тикет с system: true или создает новый
|
||||
export async function sendErrorsToServer(getTickets: () => Ticket[]) {
|
||||
if (errorsQueue.length === 0) return;
|
||||
// Проверяем разрешение на отправку ошибок (по домену и debug-override)
|
||||
// Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё
|
||||
const forceSend = errorsQueue.some(e => (e as any).__forceSend);
|
||||
if (!forceSend && !isErrorReportingAllowed()) {
|
||||
console.log('❌ Отправка ошибок заблокирована, очищаем очередь');
|
||||
errorsQueue = [];
|
||||
return;
|
||||
}
|
||||
const tickets = getTickets();
|
||||
try {
|
||||
// Формируем сообщение об ошибке
|
||||
const errorMessage = errorsQueue.map(error => {
|
||||
return `[${new Date(error.timestamp * 1000).toISOString()}] ${error.message}\n\nCall Stack:\n${error.callStack || 'N/A'}\n\nComponent Stack:\n${error.componentStack || 'N/A'}`;
|
||||
}).join('\n\n---\n\n');
|
||||
// ВСЕГДА ищем тикет через API
|
||||
const existingSystemTicket = await findSystemTicket(tickets);
|
||||
if (existingSystemTicket) {
|
||||
sendTicketMessage({
|
||||
ticketId: existingSystemTicket,
|
||||
message: errorMessage,
|
||||
systemError: true,
|
||||
});
|
||||
} else {
|
||||
// Создаем новый тикет для ошибки
|
||||
createTicket({
|
||||
message: errorMessage,
|
||||
useToken: true,
|
||||
systemError: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in sendErrorsToServer:', error);
|
||||
} finally {
|
||||
// Очищаем очередь ошибок
|
||||
errorsQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Ищет существующий тикет с system: true
|
||||
export async function findSystemTicket(tickets: Ticket[]) {
|
||||
for (const ticket of tickets) {
|
||||
console.log("[findSystemTicket] Проверяем тикет:", ticket);
|
||||
if (!('messages' in ticket)) {
|
||||
if (ticket.top_message && ticket.top_message.system === true) {
|
||||
console.log("[findSystemTicket] Найден тикет по top_message.system:true:", ticket.id);
|
||||
return ticket.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearErrorHandlingConfig () {
|
||||
clearTimeout(timeoutId);
|
||||
errorsQueue = [];
|
||||
}
|
80
src/utils/hooks/useAuthRedirect.ts
Normal file
80
src/utils/hooks/useAuthRedirect.ts
Normal file
@ -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 { useUserStore } from "@/stores/user";
|
||||
import moment from "moment";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useEffect } from "react";
|
||||
import { redirect, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount";
|
||||
import { startCC } from "@/stores/cc";
|
||||
import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
|
||||
|
||||
/*
|
||||
Есть три пути по которому мы ходили из квиза в хаб. Нам нехватило денег при:
|
||||
1)Покупке обычного тарифа
|
||||
2)Покупке тарифа-заказ-квиза
|
||||
3)Покупке тарифа в настройке квиза в вкладке ИИ
|
||||
*/
|
||||
|
||||
export const useAfterPay = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const userId = useUserStore(store => store.userId)
|
||||
const userAccount = useUserStore(state => state.userAccount);
|
||||
const userWithWallet = useUserStore((state) => state.customerAccount); //c wallet
|
||||
const siteReadyPayCart = useNotEnoughMoneyAmount(state => state.siteReadyPayCart);
|
||||
|
||||
const purpose = searchParams.get("purpose");
|
||||
const paymentUserId = searchParams.get("userid");
|
||||
const currentCC = searchParams.get("cc");
|
||||
const wayback = searchParams.get("wayback");
|
||||
|
||||
// Обработка wayback параметра
|
||||
useEffect(() => {
|
||||
if (wayback) {
|
||||
const quizId = wayback.split("_")[1];
|
||||
if (quizId) {
|
||||
setEditQuizId(Number(quizId));
|
||||
setCurrentStep(17); // Шаг для персонализации AI
|
||||
navigate("/personalization-ai");
|
||||
}
|
||||
}
|
||||
}, [wayback, navigate]);
|
||||
let URLaction = searchParams.get("action");//что мы, собсна, хотим: оплатить, пополнить, заказать квиз
|
||||
let URLuserId = searchParams.get("userid");//тот кто начал всё это действо
|
||||
let URLadditionalinformation = searchParams.get("additionalinformation");//его токен
|
||||
|
||||
useEffect(() => {
|
||||
//Звёзды сошлись, будем оплачивать корзину
|
||||
if (paymentUserId && paymentUserId === userId) {
|
||||
|
||||
setSearchParams({}, { replace: true });
|
||||
|
||||
if (purpose === "paycart") {
|
||||
setSearchParams({}, { replace: true });
|
||||
if (currentCC) startCC()
|
||||
if (userId && URLuserId && userId === URLuserId) {
|
||||
|
||||
if (URLaction === "buy") startPayCartProcess(URLuserId);
|
||||
|
||||
if (URLaction === "createquizcc") {
|
||||
startCC();
|
||||
(async () => {
|
||||
|
||||
//Проверяем можем ли мы оплатить корзину здесь и сейчас
|
||||
@ -46,19 +44,46 @@ export const useAfterPay = () => {
|
||||
|
||||
if (payCartError) {
|
||||
//Не получилось купить корзину. Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты
|
||||
startPayCartProcess(paymentUserId)
|
||||
startPayCartProcess(URLuserId);
|
||||
} else {
|
||||
if (currentCC) navigate("/tariffs")
|
||||
cancelPayCartProcess()
|
||||
navigate("/tariffs");
|
||||
cancelPayCartProcess();
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
}, [purpose, paymentUserId])
|
||||
|
||||
|
||||
//Покупка ИИ тарифа из настройки квиза ИИ вкладки
|
||||
if (URLaction === "buy" && URLadditionalinformation) {
|
||||
const quizId = Number(URLadditionalinformation);
|
||||
|
||||
if (quizId) {
|
||||
setEditQuizId(Number(quizId)); //Выбираем квиз
|
||||
setCurrentStep(17); // Шаг для персонализации AI
|
||||
|
||||
// Проверяем wayback параметр для определения куда переходить
|
||||
const wayback = searchParams.get("wayback");
|
||||
if (wayback === "edit") {
|
||||
// Сначала переходим на /edit, затем на /personalization-ai
|
||||
navigate("/edit");
|
||||
// Используем setTimeout чтобы дать время для загрузки /edit
|
||||
setTimeout(() => {
|
||||
navigate("/personalization-ai");
|
||||
}, 100);
|
||||
} else {
|
||||
// Прямой переход на /personalization-ai
|
||||
navigate("/personalization-ai");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}, []);
|
||||
|
||||
//Обработка необходимости купить после пополнения
|
||||
useEffect(() => {
|
||||
if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) {
|
||||
const deadline = siteReadyPayCart[userId]
|
||||
const deadline = siteReadyPayCart[userId];
|
||||
if (calcTimeOfReadyPayCart(deadline)) {
|
||||
|
||||
//Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло.
|
||||
@ -66,11 +91,13 @@ export const useAfterPay = () => {
|
||||
const [, payCartError] = await cartApi.pay();
|
||||
|
||||
if (!payCartError) {
|
||||
enqueueSnackbar("Товары успешно приобретены")
|
||||
cancelPayCartProcess()
|
||||
enqueueSnackbar("Товары успешно приобретены");
|
||||
cancelPayCartProcess();
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
}, [userAccount, userId, siteReadyPayCart])
|
||||
}, [userAccount, userId, siteReadyPayCart, userWithWallet])
|
||||
|
||||
|
||||
}
|
@ -4,7 +4,7 @@ import { useSSETab } from "./useSSETab";
|
||||
import { cancelPayCartProcess } from "@/stores/notEnoughMoneyAmount";
|
||||
import { setCash } from "@/stores/cash";
|
||||
import { currencyFormatter } from "@/pages/Tariffs/tariffsUtils/currencyFormatter";
|
||||
import { inCart } from "@/pages/Tariffs/Tariffs";
|
||||
import { inCart } from "@/pages/Tariffs/utils";
|
||||
|
||||
type Ping = [{ event: "ping" }]
|
||||
|
||||
@ -43,8 +43,6 @@ export const usePipeSubscriber = () => {
|
||||
`/customer/v1.0.1/account/pipe?Authorization=${token}`,
|
||||
onNewData: (data) => {
|
||||
let message = data[0] as PipeMessage
|
||||
console.log("truba")
|
||||
console.log(message)
|
||||
updateSSEValue(message)
|
||||
|
||||
//Пропускаем пингование
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { createUserAccount, devlog } from "@frontend/kitui";
|
||||
import { isAxiosError } from "axios";
|
||||
import { makeRequest } from "@api/makeRequest";
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
import type { UserAccount } from "@frontend/kitui";
|
||||
import { setUserAccount } from "@/stores/user";
|
||||
|
||||
@ -37,27 +37,52 @@ export const useUserAccountFetcher = <T = UserAccount>({
|
||||
})
|
||||
.then((result) => {
|
||||
devlog("User account", result);
|
||||
console.log(result)
|
||||
if (result) onNewUserAccountRef.current(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
devlog("Error fetching user account", error);
|
||||
if (error.response?.status === 409) return;
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
createUserAccount(controller.signal, url.replace("get", "create"))
|
||||
|
||||
// Формируем правильный URL для создания аккаунта
|
||||
let createUrl = url;
|
||||
if (url.includes("/customer/v1.0.1/account")) {
|
||||
// Для customerAccount используем тот же URL (POST запрос)
|
||||
createUrl = url;
|
||||
} else if (url.includes("/squiz/account/get")) {
|
||||
// Для userAccount заменяем get на create
|
||||
createUrl = url.replace("get", "create");
|
||||
}
|
||||
|
||||
|
||||
createUserAccount(controller.signal, createUrl)
|
||||
.then((result) => {
|
||||
devlog("Created user account", result);
|
||||
console.log("это пойдёт в стор: ")
|
||||
console.log(result)
|
||||
if (result) onNewUserAccountRef.current(result.created_account as T);
|
||||
|
||||
// Проверяем структуру ответа и записываем в стор
|
||||
if (result) {
|
||||
// Если результат содержит created_account, используем его
|
||||
if (result.created_account) {
|
||||
onNewUserAccountRef.current(result.created_account as T);
|
||||
}
|
||||
// Если результат сам является аккаунтом (для customerAccount)
|
||||
else if (result.userId && result.wallet) {
|
||||
onNewUserAccountRef.current(result as T);
|
||||
}
|
||||
// Если ничего не подходит, логируем для диагностики
|
||||
else {
|
||||
onNewUserAccountRef.current(result as T);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response?.status === 409) return;
|
||||
devlog("Error creating user account", error);
|
||||
console.error("useUserAccountFetcher: Error creating account:", error);
|
||||
onErrorRef.current?.(error);
|
||||
});
|
||||
} else {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
onErrorRef.current?.(error);
|
||||
}
|
||||
});
|
||||
|
@ -29,7 +29,7 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
|
||||
if (error.message === "Failed to fetch") return ["Ошибка сети"];
|
||||
|
||||
//ДЛЯ ОПЛАТЫ ТАРИФА
|
||||
if(error.response.status === 402) {
|
||||
if(error.response?.status === 402) {
|
||||
console.error(error.response?.data.message)
|
||||
return error.response?.data.message
|
||||
}
|
||||
@ -40,8 +40,8 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
|
||||
|
||||
const status = error.response.status;
|
||||
if(status === 409 || status === 401 || status === 404) {
|
||||
const serverErrorMessage = error.response.data.message
|
||||
console.log(serverErrorMessage)
|
||||
const responseData = error.response.data as any;
|
||||
const serverErrorMessage = responseData?.message || responseData?.error;
|
||||
const translatedMessage = translateMessage[serverErrorMessage?.toLowerCase() || ""]
|
||||
return [translatedMessage || "", serverError.statusCode];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user