Merge branch 'video-upload-modal' into dev

This commit is contained in:
nflnkr 2024-06-21 14:06:30 +03:00
commit 67a5d3c63f
33 changed files with 808 additions and 1034 deletions

@ -23,9 +23,7 @@ export type AccountResponse = {
driveURL: string;
};
export const getAccount = async (): Promise<
[AccountResponse | null, string?]
> => {
export const getAccount = async (): Promise<[AccountResponse | null, string?]> => {
try {
const response = await makeRequest<void, AccountResponse>({
method: "GET",
@ -95,10 +93,7 @@ export type TagsResponse = {
items: Tag[];
};
export const getTags = async ({
page,
size,
}: PaginationRequest): Promise<[TagsResponse | null, string?]> => {
export const getTags = async ({ page, size }: PaginationRequest): Promise<[TagsResponse | null, string?]> => {
try {
const tagsResponse = await makeRequest<PaginationRequest, TagsResponse>({
method: "GET",
@ -133,10 +128,7 @@ export type UsersResponse = {
items: User[];
};
export const getUsers = async ({
page,
size,
}: PaginationRequest): Promise<[UsersResponse | null, string?]> => {
export const getUsers = async ({ page, size }: PaginationRequest): Promise<[UsersResponse | null, string?]> => {
try {
const usersResponse = await makeRequest<PaginationRequest, UsersResponse>({
method: "GET",
@ -171,14 +163,9 @@ export const getSteps = async ({
page,
size,
pipelineId,
}: PaginationRequest & { pipelineId: number }): Promise<
[StepsResponse | null, string?]
> => {
}: PaginationRequest & { pipelineId: number }): Promise<[StepsResponse | null, string?]> => {
try {
const stepsResponse = await makeRequest<
PaginationRequest & { pipelineId: number },
StepsResponse
>({
const stepsResponse = await makeRequest<PaginationRequest & { pipelineId: number }, StepsResponse>({
method: "GET",
url: `${API_URL}/steps?page=${page}&size=${size}&pipelineID=${pipelineId}`,
});
@ -206,15 +193,9 @@ export type PipelinesResponse = {
items: Pipeline[];
};
export const getPipelines = async ({
page,
size,
}: PaginationRequest): Promise<[PipelinesResponse | null, string?]> => {
export const getPipelines = async ({ page, size }: PaginationRequest): Promise<[PipelinesResponse | null, string?]> => {
try {
const pipelinesResponse = await makeRequest<
PaginationRequest,
PipelinesResponse
>({
const pipelinesResponse = await makeRequest<PaginationRequest, PipelinesResponse>({
method: "GET",
url: `${API_URL}/pipelines?page=${page}&size=${size}`,
});
@ -226,7 +207,7 @@ export const getPipelines = async ({
};
//получение настроек интеграции
export type QuestionID = Record<string, number>
export type QuestionID = Record<string, number>;
export type IntegrationRules = {
PipelineID: number;
@ -238,13 +219,11 @@ export type IntegrationRules = {
Contact: number[] | null;
Company: number[] | null;
Customer: number[] | null;
}
};
};
export type FieldsRule = Record<Partial<QuestionKeys>, null | [{QuestionID: QuestionID;}]>
export type FieldsRule = Record<Partial<QuestionKeys>, null | [{ QuestionID: QuestionID }]>;
export const getIntegrationRules = async (
quizID: string,
): Promise<[IntegrationRules | null, string?]> => {
export const getIntegrationRules = async (quizID: string): Promise<[IntegrationRules | null, string?]> => {
try {
const settingsResponse = await makeRequest<void, IntegrationRules>({
method: "GET",
@ -275,7 +254,7 @@ export type IntegrationRulesUpdate = {
export const setIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
@ -291,7 +270,7 @@ export const setIntegrationRules = async (
};
export const updateIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
@ -326,13 +305,10 @@ export type CustomFieldsResponse = {
};
export const getCustomFields = async (
pagination: PaginationRequest,
pagination: PaginationRequest
): Promise<[CustomFieldsResponse | null, string?]> => {
try {
const fieldsResponse = await makeRequest<
PaginationRequest,
CustomFieldsResponse
>({
const fieldsResponse = await makeRequest<PaginationRequest, CustomFieldsResponse>({
method: "GET",
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
});
@ -356,4 +332,4 @@ export const removeAmoAccount = async (): Promise<[void | null, string?]> => {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отвязать аккаунт. ${error}`];
}
};
};

@ -1,18 +1,9 @@
import * as React from "react";
import { FC, useMemo } from "react";
import CheckboxIcon from "@icons/Checkbox";
import {
SelectChangeEvent,
Typography,
useTheme,
Box,
FormControlLabel,
RadioGroup,
Radio,
} from "@mui/material";
import { SelectChangeEvent, Typography, useTheme, Box, FormControlLabel, RadioGroup, Radio } from "@mui/material";
import { MinifiedData, TagKeys } from "@/pages/IntegrationsPage/IntegrationsModal/types";
type CustomRadioGroupProps = {
items: MinifiedData[] | [];
selectedItemId?: string | null;
@ -32,18 +23,19 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
const currentItem = useMemo(() => {
if (selectedItemId !== null && selectedItemId.length > 0) {
return items.find(item => item.id === selectedItemId) || null
return items.find((item) => item.id === selectedItemId) || null;
}
return null;
}, [selectedItemId, items])
}, [selectedItemId, items]);
const filteredItems = useMemo(() => {
let newArray = items
if (activeScope !== undefined) newArray =newArray.filter(item => {
return item.entity === activeScope
})
return newArray
}, items)
let newArray = items;
if (activeScope !== undefined)
newArray = newArray.filter((item) => {
return item.entity === activeScope;
});
return newArray;
}, items);
const onScroll = React.useCallback((e: React.UIEvent<HTMLDivElement>) => {
const scrollHeight = e.currentTarget.scrollHeight;
@ -56,10 +48,9 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
}
}, []);
const formControlLabels = useMemo(() => {
if (filteredItems.length !== 0) {
return filteredItems.map(item =>
return filteredItems.map((item) => (
<FormControlLabel
key={item.id}
sx={{
@ -71,15 +62,12 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
borderRadius: "12px",
margin: 0,
backgroundColor:
currentItem?.id === item.id
? theme.palette.background.default
: theme.palette.common.white,
currentItem?.id === item.id ? theme.palette.background.default : theme.palette.common.white,
"&.MuiFormControlLabel-root > .MuiTypography-root": {
width: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}
textOverflow: "ellipsis",
},
}}
value={item.id}
control={
@ -97,7 +85,7 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
label={item.title}
labelPlacement={"start"}
/>
)
));
}
return (
<Box

@ -1,16 +1,6 @@
import * as React from "react";
import { FC, useCallback, useMemo, useRef, useState } from "react";
import {
Avatar,
MenuItem,
Select,
SelectChangeEvent,
Typography,
useMediaQuery,
useTheme,
Box,
} from "@mui/material";
import { Avatar, MenuItem, Select, SelectChangeEvent, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
import arrow_down from "../../assets/icons/arrow_down.svg";
import { MinifiedData } from "@/pages/IntegrationsPage/IntegrationsModal/types";
@ -21,12 +11,7 @@ type CustomSelectProps = {
handleScroll: () => void;
};
export const CustomSelect: FC<CustomSelectProps> = ({
items,
selectedItemId,
setSelectedItem,
handleScroll,
}) => {
export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, setSelectedItem, handleScroll }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -48,9 +33,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({
}
}, []);
const currentItem = useMemo(() => (
items.find(item => item.id === selectedItemId) || null
), [selectedItemId, items])
const currentItem = useMemo(() => items.find((item) => item.id === selectedItemId) || null, [selectedItemId, items]);
const menuItems = useMemo(() => {
if (items.length !== 0) {
@ -78,9 +61,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({
<Typography
sx={{
width: "33%",
borderRight: isMobile
? "none"
: "1px solid rgba(154, 154, 175, 0.1)",
borderRight: isMobile ? "none" : "1px solid rgba(154, 154, 175, 0.1)",
padding: isMobile ? "5px 0 5px 20px" : "10px 0 10px 20px",
}}
>
@ -89,9 +70,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({
<Typography
sx={{
width: "33%",
borderRight: isMobile
? "none"
: "1px solid rgba(154, 154, 175, 0.1)",
borderRight: isMobile ? "none" : "1px solid rgba(154, 154, 175, 0.1)",
padding: isMobile ? "5px 0 5px 20px" : "10px 0 10px 20px",
color: isMobile ? "#9A9AAF" : "#4D4D4D",
}}
@ -103,7 +82,11 @@ export const CustomSelect: FC<CustomSelectProps> = ({
));
}
return (
<MenuItem key={"-1"} disabled sx={{ padding: "12px", zIndex: 2 }}>
<MenuItem
key={"-1"}
disabled
sx={{ padding: "12px", zIndex: 2 }}
>
нет данных
</MenuItem>
);
@ -118,10 +101,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({
width: "100%",
height: "56px",
padding: "5px",
color:
currentItem === null
? theme.palette.grey2.main
: theme.palette.brightPurple.main,
color: currentItem === null ? theme.palette.grey2.main : theme.palette.brightPurple.main,
border: `2px solid ${theme.palette.common.white}`,
borderRadius: "30px",
background: "#EFF0F5",
@ -129,7 +109,9 @@ export const CustomSelect: FC<CustomSelectProps> = ({
alignItems: "center",
cursor: "pointer",
}}
onClick={() => {if (ref.current !== null) ref.current?.click()}}
onClick={() => {
if (ref.current !== null) ref.current?.click();
}}
>
<Avatar sx={{ width: 46, height: 46, marginRight: 1 }} />
<Typography
@ -176,25 +158,23 @@ export const CustomSelect: FC<CustomSelectProps> = ({
paddingTop: "50px",
marginTop: "-50px",
borderRadius: "28px",
},
},
}}
sx={{
display: "block",
"& .MuiSelect-select.MuiSelect-outlined.MuiInputBase-input": {
display: "none"
},
"& .MuiSelect-icon":{
display: "none"
},
"& .MuiOutlinedInput-notchedOutline": {
border: 0
},
"& .MuiMenu-root.MuiModal-root": {
zIndex: 0
}
display: "block",
"& .MuiSelect-select.MuiSelect-outlined.MuiInputBase-input": {
display: "none",
},
"& .MuiSelect-icon": {
display: "none",
},
"& .MuiOutlinedInput-notchedOutline": {
border: 0,
},
"& .MuiMenu-root.MuiModal-root": {
zIndex: 0,
},
}}
onChange={({ target }: SelectChangeEvent<string>) => setSelectedItem(target.value)}
onClick={toggleOpened}

@ -8,10 +8,7 @@ type AmoAccountInfoProps = {
accountInfo: AccountResponse;
};
export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({
handleNextStep,
accountInfo,
}) => {
export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({ handleNextStep, accountInfo }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -24,9 +21,7 @@ export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({
}}
>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography sx={{ color: theme.palette.grey2.main }}>
{title}:
</Typography>
<Typography sx={{ color: theme.palette.grey2.main }}>{title}:</Typography>
</Box>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography>{value || "нет данных"}</Typography>
@ -43,12 +38,15 @@ export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({
}}
>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography sx={{ color: theme.palette.grey2.main }}>
{title}:
</Typography>
<Typography sx={{ color: theme.palette.grey2.main }}>{title}:</Typography>
</Box>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<a target="_blank" href={link}>{link}</a>
<a
target="_blank"
href={link}
>
{link}
</a>
</Box>
</Box>
);

@ -1,12 +1,5 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
import {
Dialog,
IconButton,
Typography,
useMediaQuery,
useTheme,
Box,
} from "@mui/material";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
import { useQuestions } from "@/stores/questions/hooks";
import { redirect } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
@ -24,13 +17,26 @@ import { AmoQuestions } from "./AmoQuestions/AmoQuestions";
import { AmoModalTitle } from "./AmoModalTitle/AmoModalTitle";
import { AmoSettingsBlock } from "./SettingsBlock/AmoSettingsBlock";
import { AmoAccountInfo } from "./AmoAccountInfo/AmoAccountInfo";
import { AccountResponse, FieldsRule, IntegrationRules, Pipeline, Step, User, getAccount, getIntegrationRules, getPipelines, getSteps, getTags, getUsers, setIntegrationRules, updateIntegrationRules } from "@api/integration";
import {
AccountResponse,
FieldsRule,
IntegrationRules,
Pipeline,
Step,
User,
getAccount,
getIntegrationRules,
getPipelines,
getSteps,
getTags,
getUsers,
setIntegrationRules,
updateIntegrationRules,
} from "@api/integration";
import type { QuestionID } from "@api/integration";
import { useAmoIntegration } from "./useAmoIntegration";
import { QuestionKeys, TagKeys, TagQuestionHC } from "./types";
type IntegrationsModalProps = {
isModalOpen: boolean;
handleCloseModal: () => void;
@ -38,14 +44,7 @@ type IntegrationsModalProps = {
quizID: number | undefined;
};
export const AmoCRMModal: FC<IntegrationsModalProps> = ({
isModalOpen,
handleCloseModal,
companyName,
quizID,
}) => {
export const AmoCRMModal: FC<IntegrationsModalProps> = ({ isModalOpen, handleCloseModal, companyName, quizID }) => {
//Если нет контекста квиза, то и делать на этой страничке нечего
if (quizID === undefined) {
redirect("/list");
@ -57,16 +56,16 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const { questions } = useQuestions();
const minifiedQuestions = useMemo(() => questions
.filter((q) =>
q.type !== "result"
&& q.type !== null
)
.map(({ backendId, title }) => ({
id: backendId.toString() as string,
title
})), [questions])
const minifiedQuestions = useMemo(
() =>
questions
.filter((q) => q.type !== "result" && q.type !== null)
.map(({ backendId, title }) => ({
id: backendId.toString() as string,
title,
})),
[questions]
);
const [step, setStep] = useState<number>(0);
const [isSettingsBlock, setIsSettingsBlock] = useState<boolean>(false);
@ -100,54 +99,54 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
quizID,
isModalOpen,
isTryRemoveAccount,
})
});
const handleAddTagQuestion = useCallback((scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => {
if (!scope || !id) return;
const handleAddTagQuestion = useCallback(
(scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => {
if (!scope || !id) return;
if (type === "tag") {
setSelectedTags((prevState) => ({
...prevState,
[scope]: [...prevState[scope as TagKeys], id],
}));
}
if (type === "tag") {
setSelectedTags((prevState) => ({
...prevState,
[scope]: [...prevState[scope as TagKeys], id],
}));
}
if (type === "question") {
setSelectedQuestions((prevState) => ({
...prevState,
[scope]: [...prevState[scope as QuestionKeys], id],
}));
}
}, [setSelectedQuestions, setSelectedTags]);
if (type === "question") {
setSelectedQuestions((prevState) => ({
...prevState,
[scope]: [...prevState[scope as QuestionKeys], id],
}));
}
},
[setSelectedQuestions, setSelectedTags]
);
const handleDeleteTagQuestion = useCallback(() => {
if (openDelete === null || !openDelete.scope || !openDelete.id || !openDelete.type) return;
if (openDelete.type === "tag") {
let newArray = selectedTags[openDelete.scope];
const index = newArray.indexOf(openDelete.id)
if (index !== -1)
newArray.splice(index, 1);
const index = newArray.indexOf(openDelete.id);
if (index !== -1) newArray.splice(index, 1);
setSelectedTags((prevState) => ({
...prevState,
[openDelete.scope]: newArray
[openDelete.scope]: newArray,
}));
}
if (openDelete.type === "question") {
let newArray = selectedQuestions[openDelete.scope as QuestionKeys];
const index = newArray.indexOf(openDelete.id)
if (index !== -1)
newArray.splice(index, 1);
const index = newArray.indexOf(openDelete.id);
if (index !== -1) newArray.splice(index, 1);
setSelectedQuestions((prevState) => ({
...prevState,
[openDelete.scope]: newArray
[openDelete.scope]: newArray,
}));
}
setOpenDelete(null)
setOpenDelete(null);
}, [openDelete]);
const handleNextStep = () => {
setStep((prevState) => prevState + 1);
};
@ -155,45 +154,44 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
setStep((prevState) => prevState - 1);
};
const handleSave = () => {
if (quizID === undefined) return
if (selectedPipeline === null) return enqueueSnackbar("Выберите воронку")
if (selectedPipeline === null) return enqueueSnackbar("Выберите этап воронки")
if (quizID === undefined) return;
if (selectedPipeline === null) return enqueueSnackbar("Выберите воронку");
if (selectedPipeline === null) return enqueueSnackbar("Выберите этап воронки");
const body = {
PipelineID: Number(selectedPipeline),
StepID: Number(selectedPipelineStep),
PerformerID: Number(selectedDealUser),
// FieldsRule: questionsBackend,
TagsToAdd: selectedTags
}
const FieldsRule = {
Company: [{ "QuestionID": {} }],
Lead: [{ "QuestionID": {} }],
Customer: [{ "QuestionID": {} }],
TagsToAdd: selectedTags,
};
console.log("selectedQuestions")
console.log(selectedQuestions)
const FieldsRule = {
Company: [{ QuestionID: {} }],
Lead: [{ QuestionID: {} }],
Customer: [{ QuestionID: {} }],
};
console.log("selectedQuestions");
console.log(selectedQuestions);
for (let key in FieldsRule) {
console.log("current key ", key)
console.log("current key ", key);
selectedQuestions[key as QuestionKeys].forEach((id) => {
FieldsRule[key as QuestionKeys][0].QuestionID[id] = 0
})
FieldsRule[key as QuestionKeys][0].QuestionID[id] = 0;
});
}
for (let key in body.TagsToAdd) {
body.TagsToAdd[key as TagKeys] = body.TagsToAdd[key as TagKeys].map(id => Number(id))
body.TagsToAdd[key as TagKeys] = body.TagsToAdd[key as TagKeys].map((id) => Number(id));
}
body.FieldsRule = FieldsRule
body.FieldsRule = FieldsRule;
console.log("На отправку")
console.log(body)
console.log("На отправку");
console.log(body);
if (firstRules) {
setIntegrationRules(quizID.toString(), body)
setIntegrationRules(quizID.toString(), body);
} else {
updateIntegrationRules(quizID.toString(), body)
updateIntegrationRules(quizID.toString(), body);
}
handleCloseModal();
@ -202,9 +200,7 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
const steps = useMemo(
() => [
{
title: accountInfo
? "Информация об аккаунте"
: "Авторизация в аккаунте",
title: accountInfo ? "Информация об аккаунте" : "Авторизация в аккаунте",
isSettingsAvailable: false,
component: accountInfo ? (
<AmoAccountInfo
@ -239,11 +235,9 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
users={arrayOfUsers}
selectedDealUser={selectedDealUser}
selectedStep={selectedPipelineStep}
steps={arrayOfPipelinesSteps}
setSelectedDealPerformer={setSelectedDealPerformer}
setSelectedStep={setSelectedPipelineStep}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
/>
@ -270,7 +264,7 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
tagsItems={arrayOfTags}
selectedTags={selectedTags}
openDelete={setOpenDelete}
handleScroll={() => { }}
handleScroll={() => {}}
handleAddTag={handleAddTagQuestion}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
@ -307,7 +301,7 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
arrayOfUsers,
minifiedQuestions,
arrayOfTags,
],
]
);
const stepTitles = steps.map((step) => step.title);
@ -354,9 +348,7 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
top: "15px",
}}
>
<CloseIcon
sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }}
/>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
<Box
sx={{
@ -376,29 +368,25 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
setStep={setStep}
startRemoveAccount={() => setIsTryRemoveAccount(true)}
/>
{openDelete !== null ?
(
<AmoDeleteTagQuestion
close={() => setOpenDelete(null)}
deleteItem={handleDeleteTagQuestion}
/>
)
:
(<>
{isTryRemoveAccount && (
<AmoRemoveAccount
stopThisPage={() => setIsTryRemoveAccount(false)}
/>
)}
{openDelete !== null ? (
<AmoDeleteTagQuestion
close={() => setOpenDelete(null)}
deleteItem={handleDeleteTagQuestion}
/>
) : (
<>
{isTryRemoveAccount && <AmoRemoveAccount stopThisPage={() => setIsTryRemoveAccount(false)} />}
{isSettingsBlock && (
<Box sx={{ flexGrow: 1, width: "100%" }}>
<AmoSettingsBlock
stepTitles={stepTitles}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedDealUser={arrayOfUsers.find(u => u.id === selectedDealUser)?.title || "не указан"}
selectedFunnel={arrayOfPipelines.find(p => p.id === selectedPipeline)?.title || "нет данных"}
selectedStage={arrayOfPipelinesSteps.find(s => s.id === selectedPipelineStep)?.title || "нет данных"}
selectedDealUser={arrayOfUsers.find((u) => u.id === selectedDealUser)?.title || "не указан"}
selectedFunnel={arrayOfPipelines.find((p) => p.id === selectedPipeline)?.title || "нет данных"}
selectedStage={
arrayOfPipelinesSteps.find((s) => s.id === selectedPipelineStep)?.title || "нет данных"
}
selectedQuestions={selectedQuestions}
selectedTags={selectedTags}
/>
@ -407,8 +395,8 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
{!isSettingsBlock && !isTryRemoveAccount && (
<Box sx={{ flexGrow: 1, width: "100%" }}>{steps[step].component}</Box>
)}
</>)
}
</>
)}
</Box>
</Dialog>
);

@ -130,9 +130,8 @@ export const AmoLogin: FC<IntegrationStep1Props> = ({ handleNextStep }) => {
lineHeight: "1",
}}
>
После нажатия на кнопку - "Подключить", вас переадресует на страницу
подключения интеграции в ваш аккаунт AmoCRM. Пожалуйста, согласитесь
на всё, что мы предлагаем, иначе чуда не случится.
После нажатия на кнопку - "Подключить", вас переадресует на страницу подключения интеграции в ваш аккаунт
AmoCRM. Пожалуйста, согласитесь на всё, что мы предлагаем, иначе чуда не случится.
</Typography>
</Box>
<Box sx={{ marginTop: "50px" }}>

@ -19,7 +19,7 @@ export const AmoModalTitle: FC<AmoModalTitleProps> = ({
setIsSettingsBlock,
isSettingsBlock,
setStep,
startRemoveAccount
startRemoveAccount,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -27,7 +27,7 @@ export const AmoModalTitle: FC<AmoModalTitleProps> = ({
const handleClick = useCallback(async () => {
if (isSettingsBlock) {
startRemoveAccount();
setIsSettingsBlock(false)
setIsSettingsBlock(false);
setStep(0);
return;
}

@ -1,7 +1,4 @@
import {
FC,
useState,
} from "react";
import { FC, useState } from "react";
import { ItemsSelectionView } from "./ItemsSelectionView/ItemsSelectionView";
import { ItemDetailsView } from "./ItemDetailsView/ItemDetailsView";
import { Box } from "@mui/material";
@ -27,24 +24,24 @@ export const AmoQuestions: FC<Props> = ({
handleAddQuestion,
handlePrevStep,
handleNextStep,
openDelete
openDelete,
}) => {
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeScope, setActiveScope] = useState<QuestionKeys | null>(null);
const [selectedQuestion, setSelectedQuestion] = useState<string | null>(null);
const handleAdd = () => {
if (activeScope === null || selectedQuestion === null) return
setActiveScope(null)
handleAddQuestion(activeScope, selectedQuestion, "question")
}
const handleDelete = (id: string, scope:QuestionKeys) => {
if (activeScope === null || selectedQuestion === null) return;
setActiveScope(null);
handleAddQuestion(activeScope, selectedQuestion, "question");
};
const handleDelete = (id: string, scope: QuestionKeys) => {
openDelete({
id,
scope,
type: "question"
})
}
type: "question",
});
};
return (
<Box
@ -74,7 +71,7 @@ export const AmoQuestions: FC<Props> = ({
) : (
// Табличка
<ItemDetailsView
items={questionsItems}
items={questionsItems}
setActiveScope={setActiveScope}
selectedQuestions={selectedQuestions}
setIsSelection={setIsSelection}

@ -19,14 +19,14 @@ export const AnswerItem: FC<AnswerItemProps> = ({ fieldName, fieldValue, deleteH
display: "flex",
alignItems: "center",
flexDirection: "column",
justifyContent: "space-between"
justifyContent: "space-between",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%"
}}
sx={{
overflow: "hidden",
width: "100%",
}}
>
<Typography
sx={{
@ -58,7 +58,7 @@ export const AnswerItem: FC<AnswerItemProps> = ({ fieldName, fieldValue, deleteH
</Box>
<IconButton
sx={{
m: "auto"
m: "auto",
}}
onClick={deleteHC}
>

@ -11,13 +11,7 @@ type ItemProps = {
data: SelectedTags | SelectedQuestions;
deleteHC: (id: string, scope: QuestionKeys | TagKeys) => void;
};
export const Item: FC<ItemProps> = ({
items,
title,
onAddBtnClick,
data,
deleteHC
}) => {
export const Item: FC<ItemProps> = ({ items, title, onAddBtnClick, data, deleteHC }) => {
const theme = useTheme();
const titleDictionary = {
@ -28,7 +22,7 @@ export const Item: FC<ItemProps> = ({
};
const translatedTitle = titleDictionary[title];
const selectedOptions = data[title]
const selectedOptions = data[title];
return (
<Box
sx={{
@ -50,16 +44,14 @@ export const Item: FC<ItemProps> = ({
height: "40px",
}}
>
<Typography sx={{ fontSize: "16px", fontWeight: 500 }}>
{translatedTitle}
</Typography>
<Typography sx={{ fontSize: "16px", fontWeight: 500 }}>{translatedTitle}</Typography>
</Box>
{selectedOptions &&
selectedOptions.map((id, index) => (
<AnswerItem
key={id + index}
fieldValue={"Значение поля"}
fieldName={items.find(e => e.id === id)?.title || id}
fieldName={items.find((e) => e.id === id)?.title || id}
deleteHC={() => deleteHC(selectedOptions[index], title)}
/>
))}

@ -13,7 +13,7 @@ type ItemDetailsViewProps = {
handleLargeBtn: () => void;
selectedQuestions: SelectedQuestions;
setActiveScope: (value: QuestionKeys | null) => void;
deleteHC: (id: string, scope:QuestionKeys) => void;
deleteHC: (id: string, scope: QuestionKeys) => void;
};
export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
@ -51,7 +51,7 @@ export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
flexWrap: "wrap",
justifyContent: "start",
}}
>
>
{selectedQuestions &&
Object.keys(selectedQuestions).map((item) => (
<Item

@ -1,49 +1,48 @@
import { FC } from "react"
import { Button, Typography, useTheme, Box } from "@mui/material"
import { FC } from "react";
import { Button, Typography, useTheme, Box } from "@mui/material";
interface Props {
deleteItem: () => void;
close: () => void;
deleteItem: () => void;
close: () => void;
}
export const AmoDeleteTagQuestion: FC<Props> = ({
close,
deleteItem,
}) => {
const theme = useTheme();
export const AmoDeleteTagQuestion: FC<Props> = ({ close, deleteItem }) => {
const theme = useTheme();
return (
<Box
return (
<Box
sx={{
mt: "30px",
}}
>
<Typography textAlign="center">Вы хотите удалить элемент?</Typography>
<Box
sx={{
mt: "30px"
display: "flex",
justifyContent: "space-evenly",
flexWrap: "wrap",
margin: "30px auto",
}}
>
<Button
variant="contained"
sx={{
width: "150px",
}}
onClick={close}
>
<Typography textAlign="center">
Вы хотите удалить элемент?
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-evenly",
flexWrap: "wrap",
margin: "30px auto",
}}
>
<Button
variant="contained"
sx={{
width: "150px",
}}
onClick={close}
>отмена</Button>
<Button
variant="contained"
sx={{
width: "150px",
}}
onClick={deleteItem}
>удалить</Button>
</Box>
</Box >
)
}
отмена
</Button>
<Button
variant="contained"
sx={{
width: "150px",
}}
onClick={deleteItem}
>
удалить
</Button>
</Box>
</Box>
);
};

@ -1,13 +1,9 @@
import {
FC,
useState,
} from "react";
import { FC, useState } from "react";
import { Box } from "@mui/material";
import { ItemsSelectionView } from "../AmoQuestions/ItemsSelectionView/ItemsSelectionView";
import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView";
import { MinifiedData, QuestionKeys, SelectedTags, TagKeys, TagQuestionHC } from "../types";
type Props = {
tagsItems: MinifiedData[] | [];
selectedTags: SelectedTags;
@ -32,17 +28,17 @@ export const AmoTags: FC<Props> = ({
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const handleAdd = () => {
if (activeScope === null || selectedTag === null) return
setActiveScope(null)
handleAddTag(activeScope, selectedTag, "tag")
}
if (activeScope === null || selectedTag === null) return;
setActiveScope(null);
handleAddTag(activeScope, selectedTag, "tag");
};
const handleDelete = (id: string, scope: TagKeys) => {
openDelete({
id,
scope,
type: "tag"
})
}
type: "tag",
});
};
return (
<Box

@ -11,7 +11,7 @@ type TagsDetailsViewProps = {
handleNextStep: () => void;
setActiveScope: (value: TagKeys | null) => void;
selectedTags: SelectedTags;
deleteHC: (id: string, scope:TagKeys) => void;
deleteHC: (id: string, scope: TagKeys) => void;
};
export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
@ -56,11 +56,7 @@ export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
alignItems: "center",
}}
>
<Typography
sx={{ fontSize: "14px", color: theme.palette.grey2.main }}
>
Результат
</Typography>
<Typography sx={{ fontSize: "14px", color: theme.palette.grey2.main }}>Результат</Typography>
</Box>
<Box
sx={{

@ -14,7 +14,6 @@ type Props = {
setSelectedDealPerformer: (value: string | null) => void;
selectedPipeline: string | null;
setSelectedPipeline: (value: string | null) => void;
};
export const Pipelines: FC<Props> = ({

@ -64,9 +64,7 @@ export const SettingItem: FC<SettingItemProps> = ({
);
}
if (step === 4) {
const isFilled = Object.values(selectedTags).some(
(array) => array.length > 0,
);
const isFilled = Object.values(selectedTags).some((array) => array.length > 0);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (
@ -96,9 +94,7 @@ export const SettingItem: FC<SettingItemProps> = ({
);
}
if (step === 5) {
const isFilled = Object.values(selectedQuestions).some(
(array) => array.length > 0,
);
const isFilled = Object.values(selectedQuestions).some((array) => array.length > 0);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (

@ -12,7 +12,7 @@ export type MinifiedData = {
};
export type TagQuestionHC = {
scope: QuestionKeys | TagKeys,
id: string,
type: "question" | "tag"
};
scope: QuestionKeys | TagKeys;
id: string;
type: "question" | "tag";
};

@ -1,256 +1,250 @@
import { useEffect, useState } from "react";
import { enqueueSnackbar } from "notistack";
import type {
TagKeys,
SelectedTags,
QuestionKeys,
SelectedQuestions,
MinifiedData,
} from "./types";
import type { TagKeys, SelectedTags, QuestionKeys, SelectedQuestions, MinifiedData } from "./types";
import {
AccountResponse,
getIntegrationRules,
getPipelines,
getSteps,
getTags,
getUsers,
getAccount,
FieldsRule,
AccountResponse,
getIntegrationRules,
getPipelines,
getSteps,
getTags,
getUsers,
getAccount,
FieldsRule,
} from "@/api/integration";
const SIZE = 75;
interface Props {
isModalOpen: boolean;
isTryRemoveAccount: boolean;
quizID: number;
isModalOpen: boolean;
isTryRemoveAccount: boolean;
quizID: number;
}
export const useAmoIntegration = ({
isModalOpen,
isTryRemoveAccount,
quizID,
}: Props) => {
const [isloadingPage, setIsLoadingPage] = useState<boolean>(true);
const [firstRules, setFirstRules] = useState<boolean>(false);
const [accountInfo, setAccountInfo] = useState<AccountResponse | null>(null);
export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID }: Props) => {
const [isloadingPage, setIsLoadingPage] = useState<boolean>(true);
const [firstRules, setFirstRules] = useState<boolean>(false);
const [accountInfo, setAccountInfo] = useState<AccountResponse | null>(null);
const [arrayOfPipelines, setArrayOfPipelines] = useState<MinifiedData[]>([]);
const [arrayOfPipelinesSteps, setArrayOfPipelinesSteps] = useState<MinifiedData[]>([]);
const [arrayOfUsers, setArrayOfUsers] = useState<MinifiedData[]>([]);
const [arrayOfTags, setArrayOfTags] = useState<MinifiedData[]>([]);
const [arrayOfPipelines, setArrayOfPipelines] = useState<MinifiedData[]>([]);
const [arrayOfPipelinesSteps, setArrayOfPipelinesSteps] = useState<MinifiedData[]>([]);
const [arrayOfUsers, setArrayOfUsers] = useState<MinifiedData[]>([]);
const [arrayOfTags, setArrayOfTags] = useState<MinifiedData[]>([]);
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
const [selectedPipelineStep, setSelectedPipelineStep] = useState<string | null>(null);
const [selectedDealUser, setSelectedDealPerformer] = useState<string | null>(null);
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
const [selectedPipelineStep, setSelectedPipelineStep] = useState<string | null>(null);
const [selectedDealUser, setSelectedDealPerformer] = useState<string | null>(null);
const [questionsBackend, setQuestionsBackend] = useState<FieldsRule>({} as FieldsRule);
const [selectedTags, setSelectedTags] = useState<SelectedTags>({
const [questionsBackend, setQuestionsBackend] = useState<FieldsRule>({} as FieldsRule);
const [selectedTags, setSelectedTags] = useState<SelectedTags>({
Lead: [],
Contact: [],
Company: [],
Customer: [],
});
const [selectedQuestions, setSelectedQuestions] = useState<SelectedQuestions>({
Lead: [],
Company: [],
Customer: [],
});
const [pageOfPipelines, setPageOfPipelines] = useState(1);
const [pageOfPipelinesSteps, setPageOfPipelinesSteps] = useState(1);
const [pageOfUsers, setPageOfUsers] = useState(1);
const [pageOfTags, setPageOfTags] = useState(1);
useEffect(() => {
if (isModalOpen && !isTryRemoveAccount) {
const fetchAccountRules = async () => {
setIsLoadingPage(true);
const [account, accountError] = await getAccount();
if (accountError) {
if (!accountError.includes("Not Found")) enqueueSnackbar(accountError);
setAccountInfo(null);
}
if (account) {
setAccountInfo(account);
}
const [settingsResponse, rulesError] = await getIntegrationRules(quizID.toString());
if (rulesError) {
if (rulesError === "first") setFirstRules(true);
if (!rulesError.includes("Not Found") && !rulesError.includes("first")) enqueueSnackbar(rulesError);
}
if (settingsResponse) {
if (settingsResponse.PipelineID) setSelectedPipeline(settingsResponse.PipelineID.toString());
if (settingsResponse.StepID) setSelectedPipelineStep(settingsResponse.StepID.toString());
if (settingsResponse.PerformerID) setSelectedDealPerformer(settingsResponse.PerformerID.toString());
if (Boolean(settingsResponse.FieldsRule) && Object.keys(settingsResponse?.FieldsRule).length > 0) {
const gottenQuestions = { ...selectedQuestions };
setQuestionsBackend(settingsResponse.FieldsRule);
for (let key in settingsResponse.FieldsRule) {
if (
settingsResponse.FieldsRule[key as QuestionKeys] !== null &&
Array.isArray(settingsResponse.FieldsRule[key as QuestionKeys])
) {
const gottenList = settingsResponse.FieldsRule[key as QuestionKeys];
if (gottenList !== null) gottenQuestions[key as QuestionKeys] = Object.keys(gottenList[0].QuestionID);
}
}
setSelectedQuestions(gottenQuestions);
}
if (Boolean(settingsResponse.TagsToAdd) && Object.keys(settingsResponse.TagsToAdd).length > 0) {
const gottenTags = { ...selectedTags };
for (let key in settingsResponse.TagsToAdd) {
const gottenList = settingsResponse.TagsToAdd[key as TagKeys];
if (gottenList !== null && Array.isArray(gottenList)) {
gottenTags[key as TagKeys] = gottenList.map((e) => e.toString());
}
}
setSelectedTags(gottenTags);
}
setFirstRules(false);
}
setIsLoadingPage(false);
};
fetchAccountRules();
} else {
//Вот по-хорошему компонент должен размонтироваться и стереть всё. Но это будет сделано позже
setArrayOfPipelines([]);
setArrayOfPipelinesSteps([]);
setArrayOfUsers([]);
setArrayOfTags([]);
setSelectedPipeline(null);
setSelectedPipelineStep(null);
setSelectedDealPerformer(null);
setQuestionsBackend({} as FieldsRule);
setSelectedTags({
Lead: [],
Contact: [],
Company: [],
Customer: [],
});
const [selectedQuestions, setSelectedQuestions] = useState<SelectedQuestions>({
});
setSelectedQuestions({
Lead: [],
Company: [],
Customer: [],
});
setPageOfPipelines(1);
setPageOfPipelinesSteps(1);
setPageOfUsers(1);
setPageOfTags(1);
}
}, [isModalOpen, isTryRemoveAccount]);
useEffect(() => {
getPipelines({
page: pageOfPipelines,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedPipelines: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedPipelines.push({
id: step.AmoID.toString(),
title: step.Name,
});
});
setArrayOfPipelines((prevItems) => [...prevItems, ...minifiedPipelines]);
setPageOfPipelinesSteps(1);
}
});
}, [pageOfPipelines]);
useEffect(() => {
const oldData = pageOfPipelinesSteps === 1 ? [] : arrayOfPipelinesSteps;
if (selectedPipeline !== null)
getSteps({
page: pageOfPipelinesSteps,
size: SIZE,
pipelineId: Number(selectedPipeline),
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedSteps: MinifiedData[] = [];
const [pageOfPipelines, setPageOfPipelines] = useState(1);
const [pageOfPipelinesSteps, setPageOfPipelinesSteps] = useState(1);
const [pageOfUsers, setPageOfUsers] = useState(1);
const [pageOfTags, setPageOfTags] = useState(1);
useEffect(() => {
if (isModalOpen && !isTryRemoveAccount) {
const fetchAccountRules = async () => {
setIsLoadingPage(true)
const [account, accountError] = await getAccount();
if (accountError) {
if (!accountError.includes("Not Found")) enqueueSnackbar(accountError)
setAccountInfo(null);
}
if (account) {
setAccountInfo(account);
}
const [settingsResponse, rulesError] = await getIntegrationRules(quizID.toString());
if (rulesError) {
if (rulesError === "first") setFirstRules(true);
if (!rulesError.includes("Not Found") && !rulesError.includes("first")) enqueueSnackbar(rulesError);
}
if (settingsResponse) {
if (settingsResponse.PipelineID) setSelectedPipeline(settingsResponse.PipelineID.toString())
if (settingsResponse.StepID) setSelectedPipelineStep(settingsResponse.StepID.toString())
if (settingsResponse.PerformerID) setSelectedDealPerformer(settingsResponse.PerformerID.toString())
if (Boolean(settingsResponse.FieldsRule) &&
Object.keys(settingsResponse?.FieldsRule).length > 0) {
const gottenQuestions = { ...selectedQuestions }
setQuestionsBackend(settingsResponse.FieldsRule)
for (let key in settingsResponse.FieldsRule) {
if (settingsResponse.FieldsRule[key as QuestionKeys] !== null && Array.isArray(settingsResponse.FieldsRule[key as QuestionKeys])) {
const gottenList = settingsResponse.FieldsRule[key as QuestionKeys]
if (gottenList !== null)
gottenQuestions[key as QuestionKeys] = Object.keys(gottenList[0].QuestionID)
}
}
setSelectedQuestions(gottenQuestions)
}
if (Boolean(settingsResponse.TagsToAdd) &&
Object.keys(settingsResponse.TagsToAdd).length > 0) {
const gottenTags = { ...selectedTags }
for (let key in settingsResponse.TagsToAdd) {
const gottenList = settingsResponse.TagsToAdd[key as TagKeys]
if (gottenList !== null && Array.isArray(gottenList)) {
gottenTags[key as TagKeys] = gottenList.map(e => e.toString())
}
}
setSelectedTags(gottenTags)
}
setFirstRules(false);
}
setIsLoadingPage(false)
};
fetchAccountRules();
} else {
//Вот по-хорошему компонент должен размонтироваться и стереть всё. Но это будет сделано позже
setArrayOfPipelines([]);
setArrayOfPipelinesSteps([]);
setArrayOfUsers([]);
setArrayOfTags([]);
setSelectedPipeline(null);
setSelectedPipelineStep(null);
setSelectedDealPerformer(null);
setQuestionsBackend({} as FieldsRule);
setSelectedTags({
Lead: [],
Contact: [],
Company: [],
Customer: [],
response.items.forEach((step) => {
minifiedSteps.push({
id: step.AmoID.toString(),
title: step.Name,
});
setSelectedQuestions({
Lead: [],
Company: [],
Customer: [],
});
setPageOfPipelines(1);
setPageOfPipelinesSteps(1);
setPageOfUsers(1);
setPageOfTags(1);
});
setArrayOfPipelinesSteps([...oldData, ...minifiedSteps]);
}
}, [isModalOpen, isTryRemoveAccount]);
});
}, [selectedPipeline, pageOfPipelinesSteps]);
useEffect(() => {
getUsers({
page: pageOfUsers,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedUsers: MinifiedData[] = [];
useEffect(() => {
getPipelines({
page: pageOfPipelines,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedPipelines: MinifiedData[] = []
response.items.forEach((step) => {
minifiedPipelines.push({
id: step.AmoID.toString(),
title: step.Name
})
})
setArrayOfPipelines((prevItems) => [...prevItems, ...minifiedPipelines]);
setPageOfPipelinesSteps(1)
}
})
}, [pageOfPipelines])
useEffect(() => {
const oldData = pageOfPipelinesSteps === 1 ? [] : arrayOfPipelinesSteps
if (selectedPipeline !== null)
getSteps({
page: pageOfPipelinesSteps,
size: SIZE,
pipelineId: Number(selectedPipeline),
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedSteps: MinifiedData[] = []
response.items.forEach((step) => {
minifiedSteps.push({
id: step.AmoID.toString(),
title: step.Name
})
})
setArrayOfPipelinesSteps([...oldData, ...minifiedSteps]);
}
});
}, [selectedPipeline, pageOfPipelinesSteps])
useEffect(() => {
getUsers({
page: pageOfUsers,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedUsers: MinifiedData[] = []
response.items.forEach((step) => {
minifiedUsers.push({
id: step.amoUserID.toString(),
title: step.name
})
})
setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]);
}
response.items.forEach((step) => {
minifiedUsers.push({
id: step.amoUserID.toString(),
title: step.name,
});
});
}, [pageOfUsers])
useEffect(() => {
getTags({
page: pageOfTags,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = []
setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]);
}
});
}, [pageOfUsers]);
useEffect(() => {
getTags({
page: pageOfTags,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedTags.push({
id: step.AmoID.toString(),
title: step.Name,
entity: step.Entity === "leads" ? "Lead" :
step.Entity === "contacts" ? "Contact" :
step.Entity === "companies" ? "Company" : "Customer"
})
})
setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]);
}
response.items.forEach((step) => {
minifiedTags.push({
id: step.AmoID.toString(),
title: step.Name,
entity:
step.Entity === "leads"
? "Lead"
: step.Entity === "contacts"
? "Contact"
: step.Entity === "companies"
? "Company"
: "Customer",
});
});
}, [pageOfTags])
setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]);
}
});
}, [pageOfTags]);
return ({
isloadingPage,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
selectedPipeline,
setSelectedPipeline,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
questionsBackend,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
})
}
return {
isloadingPage,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
selectedPipeline,
setSelectedPipeline,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
questionsBackend,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
};
};

@ -11,13 +11,13 @@ import { useCurrentQuiz } from "@/stores/quizes/hooks";
const AnalyticsModal = lazy(() =>
import("./AnalyticsModal/AnalyticsModal").then((module) => ({
default: module.AnalyticsModal,
})),
}))
);
const AmoCRMModal = lazy(() =>
import("../IntegrationsModal/AmoCRMModal").then((module) => ({
default: module.AmoCRMModal,
})),
}))
);
type PartnersBoardProps = {

@ -58,7 +58,7 @@ export default function PageOptions({ disableInput, question }: Props) {
</Box>
<MediaSelectionAndDisplay
resultData={question}
question={question}
cropAspectRatio={{ width: 1388.8, height: 793.2 }}
/>
</Box>

@ -1,16 +1,8 @@
import {
Box,
Button,
ButtonBase,
Modal,
Typography,
useTheme,
} from "@mui/material";
import SelectableButton from "@ui_kit/SelectableButton";
import { Box, Button, ButtonBase, Dialog, Typography, useTheme } from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton";
import { useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon";
import type { DragEvent } from "react";
type BackgroundTypeModal = "linkVideo" | "ownVideo";
@ -18,18 +10,12 @@ type BackgroundTypeModal = "linkVideo" | "ownVideo";
type HelpQuestionsProps = {
open: boolean;
onClose: () => void;
video: string;
video: string | null;
onUpload: (number: string) => void;
};
export const UploadVideoModal = ({
open,
onClose,
video,
onUpload,
}: HelpQuestionsProps) => {
const [backgroundTypeModal, setBackgroundTypeModal] =
useState<BackgroundTypeModal>("linkVideo");
export default function UploadVideoModal({ open, onClose, video, onUpload }: HelpQuestionsProps) {
const [backgroundTypeModal, setBackgroundTypeModal] = useState<BackgroundTypeModal>("linkVideo");
const theme = useTheme();
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
@ -42,118 +28,102 @@ export const UploadVideoModal = ({
};
return (
<Modal
<Dialog
open={open}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "690px",
bgcolor: "background.paper",
PaperProps={{
sx: {
maxWidth: "640px",
borderRadius: "12px",
boxShadow: 24,
p: 0,
overflow: "hidden",
},
}}
>
<Box
sx={{
display: "flex",
padding: "20px",
background: theme.palette.background.default,
}}
>
<Box
sx={{
display: "flex",
padding: "20px",
background: theme.palette.background.default,
}}
<Typography sx={{ color: "#9A9AAF" }}>
Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить собственное
</Typography>
<Button
onClick={onClose}
variant="contained"
>
<Typography sx={{ color: "#9A9AAF" }}>
Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить
собственное
</Typography>
<Button onClick={onClose} variant="contained">
Готово
</Button>
</Box>
<Box sx={{ padding: "20px", gap: "10px", display: "flex" }}>
<SelectableButton
isSelected={backgroundTypeModal === "linkVideo"}
onClick={() => setBackgroundTypeModal("linkVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Ссылка на видео
</SelectableButton>
<SelectableButton
isSelected={backgroundTypeModal === "ownVideo"}
onClick={() => setBackgroundTypeModal("ownVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Загрузить свое
</SelectableButton>
</Box>
{backgroundTypeModal === "linkVideo" ? (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
Ссылка на видео
</Typography>
<CustomTextField
placeholder={"http://example.com"}
text={video}
onChange={({ target }) => onUpload(target.value || " ")}
/>
</Box>
) : (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
Загрузите видео
</Typography>
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input
onChange={({ target }) => {
if (target.files?.length) {
onUpload(URL.createObjectURL(target.files[0] || " "));
}
}}
hidden
accept="video/*"
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
onDrop={handleDrop}
sx={{
width: "580px",
padding: "33px 33px 33px 50px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "50px",
}}
>
<UploadIcon />
<Box sx={{ color: "#9A9AAF" }}>
<Typography sx={{ fontWeight: "500" }}>
Добавить видео
</Typography>
<Typography sx={{ fontSize: "16px" }}>
Принимает .mp4 и .mov формат максимум 100мб
</Typography>
</Box>
</Box>
</ButtonBase>
</Box>
)}
Готово
</Button>
</Box>
</Modal>
<Box sx={{ padding: "20px", gap: "10px", display: "flex" }}>
<SelectableButton
isSelected={backgroundTypeModal === "linkVideo"}
onClick={() => setBackgroundTypeModal("linkVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Ссылка на видео
</SelectableButton>
<SelectableButton
isSelected={backgroundTypeModal === "ownVideo"}
onClick={() => setBackgroundTypeModal("ownVideo")}
sx={{ maxWidth: "170px", padding: "10px" }}
>
Загрузить свое
</SelectableButton>
</Box>
{backgroundTypeModal === "linkVideo" ? (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Ссылка на видео</Typography>
<CustomTextField
placeholder={"http://example.com"}
value={video || ""}
onChange={({ target }) => onUpload(target.value || " ")}
/>
</Box>
) : (
<Box sx={{ padding: "20px" }}>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Загрузите видео</Typography>
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input
onChange={({ target }) => {
if (target.files?.length) {
onUpload(URL.createObjectURL(target.files[0] || " "));
}
}}
hidden
accept="video/*"
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) => event.preventDefault()}
onDrop={handleDrop}
sx={{
width: "580px",
padding: "33px 33px 33px 50px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "50px",
}}
>
<UploadIcon />
<Box sx={{ color: "#9A9AAF" }}>
<Typography sx={{ fontWeight: "500" }}>Добавить видео</Typography>
<Typography sx={{ fontSize: "16px" }}>Принимает .mp4 и .mov формат максимум 100мб</Typography>
</Box>
</Box>
</ButtonBase>
</Box>
)}
</Dialog>
);
};
}

@ -5,7 +5,7 @@ import SelectableButton from "@ui_kit/SelectableButton";
import UploadBox from "@ui_kit/UploadBox";
import { memo, useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon";
import { UploadVideoModal } from "./UploadVideoModal";
import UploadVideoModal from "./UploadVideoModal";
type BackgroundType = "text" | "video";
@ -15,11 +15,7 @@ type HelpQuestionsProps = {
hintText: string;
};
const HelpQuestions = memo<HelpQuestionsProps>(function ({
questionId,
hintVideo,
hintText,
}) {
const HelpQuestions = memo<HelpQuestionsProps>(function ({ questionId, hintVideo, hintText }) {
const [open, setOpen] = useState(false);
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
@ -71,15 +67,17 @@ const HelpQuestions = memo<HelpQuestionsProps>(function ({
</>
) : (
<Box>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
Загрузите видео
</Typography>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Загрузите видео</Typography>
<ButtonBase
onClick={() => setOpen(true)}
sx={{ justifyContent: "flex-start" }}
>
{hintVideo ? (
<video src={hintVideo} width="400" controls />
<video
src={hintVideo}
width="400"
controls
/>
) : (
<>
<UploadBox

@ -342,7 +342,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
/>
<MediaSelectionAndDisplay
resultData={resultData}
question={resultData}
cropAspectRatio={{ width: 305.9, height: 305.9 }}
/>

@ -6,16 +6,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { SwitchSetting } from "../SwichResult";
import Info from "@icons/Info";
import {
Box,
IconButton,
Paper,
Button,
Typography,
useMediaQuery,
useTheme,
Popover,
} from "@mui/material";
import { Box, IconButton, Paper, Button, Typography, useMediaQuery, useTheme, Popover } from "@mui/material";
import ExpandLessIconBG from "@icons/ExpandLessIconBG";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
@ -88,10 +79,7 @@ const InfoView = () => {
flexDirection: "column",
}}
>
<Typography>
Oтправка письма с результатом респонденту после отображения на
экране
</Typography>
<Typography>Oтправка письма с результатом респонденту после отображения на экране</Typography>
</Paper>
</Popover>
</>
@ -190,7 +178,10 @@ export const WhenCard = ({ quizExpand }: Props) => {
}}
>
{whenValues.map(({ title, value, id }, index) => (
<Box display="flex">
<Box
display="flex"
key={id}
>
<Button
id={id}
onClick={() => {
@ -201,32 +192,16 @@ export const WhenCard = ({ quizExpand }: Props) => {
}}
key={title}
sx={{
bgcolor:
quiz?.config.resultInfo.showResultForm === value
? " #7E2AEA"
: "#F2F3F7",
color:
quiz?.config.resultInfo.showResultForm === value
? " white"
: "#9A9AAF",
minWidth: isSmallMonitor
? isMobile
? undefined
: "310px"
: "auto",
bgcolor: quiz?.config.resultInfo.showResultForm === value ? " #7E2AEA" : "#F2F3F7",
color: quiz?.config.resultInfo.showResultForm === value ? " white" : "#9A9AAF",
minWidth: isSmallMonitor ? (isMobile ? undefined : "310px") : "auto",
borderRadius: "8px",
width: isMobile ? "100%" : "220px",
height: "44px",
fontSize: "17px",
border:
quiz?.config.resultInfo.showResultForm === value
? "none"
: "1px solid #9A9AAF",
border: quiz?.config.resultInfo.showResultForm === value ? "none" : "1px solid #9A9AAF",
"&:hover": {
backgroundColor:
quiz?.config.resultInfo.showResultForm === value
? "#581CA7"
: "#7E2AEA",
backgroundColor: quiz?.config.resultInfo.showResultForm === value ? "#581CA7" : "#7E2AEA",
color: "white",
},
}}
@ -252,32 +227,16 @@ export const WhenCard = ({ quizExpand }: Props) => {
});
}}
sx={{
bgcolor:
quiz?.config.resultInfo.when === "email"
? " #7E2AEA"
: "#F2F3F7",
color:
quiz?.config.resultInfo.when === "email"
? " white"
: "#9A9AAF",
minWidth: isSmallMonitor
? isMobile
? undefined
: "310px"
: "auto",
bgcolor: quiz?.config.resultInfo.when === "email" ? " #7E2AEA" : "#F2F3F7",
color: quiz?.config.resultInfo.when === "email" ? " white" : "#9A9AAF",
minWidth: isSmallMonitor ? (isMobile ? undefined : "310px") : "auto",
borderRadius: "8px",
width: isMobile ? "100%" : "220px",
height: "44px",
fontSize: "17px",
border:
quiz?.config.resultInfo.when === "email"
? "none"
: "1px solid #9A9AAF",
border: quiz?.config.resultInfo.when === "email" ? "none" : "1px solid #9A9AAF",
"&:hover": {
backgroundColor:
quiz?.config.resultInfo.when === "email"
? "#581CA7"
: "#7E2AEA",
backgroundColor: quiz?.config.resultInfo.when === "email" ? "#581CA7" : "#7E2AEA",
color: "white",
},
}}

@ -1,10 +1,5 @@
import { Box, Typography } from "@mui/material";
import {
clearUserData,
OriginalUserAccount,
setUserAccount,
useUserStore,
} from "@root/user";
import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";
import { clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
@ -54,22 +49,10 @@ export default function AvailablePrivilege() {
if (created_at.length === 0) return 0;
const currentDate = moment();
return Number(
(
moment(
moment(created_at).add(amount, "days").diff(currentDate),
).unix() / 86400
).toFixed(1),
);
return Number((moment(moment(created_at).add(amount, "days").diff(currentDate)).unix() / 86400).toFixed(1));
}
const quizUnlimDays = getCramps(
quizUnlimTime,
userPrivileges?.quizUnlimTime?.created_at || "",
);
const squizBadgeDays = getCramps(
squizHideBadge,
userPrivileges?.squizHideBadge?.created_at || "",
);
const quizUnlimDays = getCramps(quizUnlimTime, userPrivileges?.quizUnlimTime?.created_at || "");
const squizBadgeDays = getCramps(squizHideBadge, userPrivileges?.squizHideBadge?.created_at || "");
const currentDate = moment();
@ -85,10 +68,16 @@ export default function AvailablePrivilege() {
flexWrap: "wrap",
}}
>
<Typography variant={"body1"} sx={{ color: "#9A9AAF" }}>
<Typography
variant={"body1"}
sx={{ color: "#9A9AAF" }}
>
Вам доступно:
</Typography>
<Typography variant={"body1"} sx={{ color: "#4D4D4D" }}>
<Typography
variant={"body1"}
sx={{ color: "#4D4D4D" }}
>
Безлимитные заявки:{" "}
<strong>
{quizUnlimDays > 0 && quizUnlimDays < 1
@ -97,12 +86,18 @@ export default function AvailablePrivilege() {
</strong>
</Typography>
{quizCnt !== 0 && (
<Typography variant={"body1"} sx={{ color: "#4D4D4D" }}>
<Typography
variant={"body1"}
sx={{ color: "#4D4D4D" }}
>
Заявки: <strong>{quizCnt} шт.</strong>
</Typography>
)}
{squizHideBadge !== 0 && (
<Typography variant={"body1"} sx={{ color: "#4D4D4D" }}>
<Typography
variant={"body1"}
sx={{ color: "#4D4D4D" }}
>
Скрытие логотипа PenaQuiz:{" "}
<strong>
{squizBadgeDays > 0 && squizBadgeDays < 1

@ -27,11 +27,7 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import {
incrementCurrentStep,
updateQuiz,
uploadQuizImage,
} from "@root/quizes/actions";
import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField";
@ -47,24 +43,12 @@ import { DropZone } from "./dropZone";
import Extra from "./extra";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { VideoElement } from "./VideoElement";
import * as React from "react";
import UploadVideoModal from "../Questions/UploadVideoModal";
const designTypes = [
[
"standard",
(color: string) => <LayoutStandartIcon color={color} />,
"Standard",
],
[
"expanded",
(color: string) => <LayoutExpandedIcon color={color} />,
"Expanded",
],
[
"centered",
(color: string) => <LayoutCenteredIcon color={color} />,
"Centered",
],
["standard", (color: string) => <LayoutStandartIcon color={color} />, "Standard"],
["expanded", (color: string) => <LayoutExpandedIcon color={color} />, "Expanded"],
["centered", (color: string) => <LayoutCenteredIcon color={color} />, "Centered"],
] as const;
export default function StartPageSettings() {
@ -78,12 +62,28 @@ export default function StartPageSettings() {
const [faviconUploding, setFaviconUploading] = useState<boolean>(false);
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
const [logoUploding, setLogoUploading] = useState<boolean>(false);
const [isVideoUploadDialogOpen, setIsVideoUploadDialogOpen] = useState<boolean>(false);
if (!quiz) return null;
const MobileVersionHC = (bool: boolean) => {
setMobileVersion(bool);
};
async function handleVideoUpload(videoUrl: string) {
if (!quiz) return;
setBackgroundUploading(true);
if (videoUrl.startsWith("blob:")) {
const videoBlob = await (await fetch(videoUrl)).blob();
uploadQuizImage(quiz.id, videoBlob, (quiz, url) => {
quiz.config.startpage.background.video = url;
});
} else {
updateQuiz(quiz.id, (quiz) => {
quiz.config.startpage.background.video = videoUrl;
});
}
setBackgroundUploading(false);
}
const designType = quiz?.config?.startpageType;
let cropAspectRatio:
@ -129,6 +129,12 @@ export default function StartPageSettings() {
return (
<>
<UploadVideoModal
open={isVideoUploadDialogOpen}
onClose={() => setIsVideoUploadDialogOpen(false)}
onUpload={handleVideoUpload}
video={quiz.config.startpage.background.video}
/>
<Typography
variant="h5"
sx={{ marginTop: "60px", marginBottom: isSmallMonitor ? "0" : "40px" }}
@ -143,25 +149,22 @@ export default function StartPageSettings() {
fontWeight: 500,
fontSize: "16px",
color: formState === "design" ? "#7E2AEA" : "#7D7E86",
borderBottom:
formState === "design"
? "2px solid #7E2AEA"
: "1px solid transparent",
borderBottom: formState === "design" ? "2px solid #7E2AEA" : "1px solid transparent",
}}
>
Дизайн
</Typography>
</Button>
<Button id="contentButton" onClick={() => setFormState("content")}>
<Button
id="contentButton"
onClick={() => setFormState("content")}
>
<Typography
sx={{
fontWeight: 500,
fontSize: "16px",
color: formState === "content" ? "#7E2AEA" : "#7D7E86",
borderBottom:
formState === "content"
? "2px solid #7E2AEA"
: "1px solid transparent",
borderBottom: formState === "content" ? "2px solid #7E2AEA" : "1px solid transparent",
}}
>
Контент
@ -222,8 +225,7 @@ export default function StartPageSettings() {
displayEmpty
onChange={(e) =>
updateQuiz(quiz.id, (quiz) => {
quiz.config.startpageType = e.target
.value as QuizStartpageType;
quiz.config.startpageType = e.target.value as QuizStartpageType;
})
}
sx={{
@ -280,11 +282,7 @@ export default function StartPageSettings() {
color: theme.palette.grey2.main,
}}
>
{type[1](
type[0] === designType
? theme.palette.orange.main
: theme.palette.grey2.main,
)}
{type[1](type[0] === designType ? theme.palette.orange.main : theme.palette.grey2.main)}
{type[2]}
</MenuItem>
))}
@ -331,10 +329,7 @@ export default function StartPageSettings() {
{quiz.config.startpage.background.type === "image" && (
<Box
sx={{
display:
quiz.config.startpage.background.type === "image"
? "flex"
: "none",
display: quiz.config.startpage.background.type === "image" ? "flex" : "none",
flexDirection: "column",
}}
>
@ -368,15 +363,12 @@ export default function StartPageSettings() {
sx={{ maxWidth: "300px" }}
cropAspectRatio={cropAspectRatio}
imageUrl={quiz.config.startpage.background.desktop}
originalImageUrl={
quiz.config.startpage.background.originalDesktop
}
originalImageUrl={quiz.config.startpage.background.originalDesktop}
onImageUploadClick={async (file) => {
setBackgroundUploading(true);
await uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.desktop = url;
quiz.config.startpage.background.originalDesktop =
url;
quiz.config.startpage.background.originalDesktop = url;
});
setBackgroundUploading(false);
@ -426,7 +418,10 @@ export default function StartPageSettings() {
{isMobile ? (
<TooltipClickInfo title={"Можно загрузить видео."} />
) : (
<Tooltip title="Можно загрузить видео." placement="top">
<Tooltip
title="Можно загрузить видео."
placement="top"
>
<Box>
<InfoIcon />
</Box>
@ -445,7 +440,7 @@ export default function StartPageSettings() {
) : (
<>
<ButtonBase
component="label"
onClick={() => setIsVideoUploadDialogOpen(true)}
sx={{
justifyContent: "center",
height: "48px",
@ -455,29 +450,6 @@ export default function StartPageSettings() {
my: "20px",
}}
>
<input
onChange={async (event) => {
setBackgroundUploading(true);
const file = event.target.files?.[0];
if (file) {
await uploadQuizImage(
quiz.id,
file,
(quiz, url) => {
quiz.config.startpage.background.video =
url;
},
);
}
setBackgroundUploading(false);
}}
hidden
accept=".mp4"
multiple
type="file"
/>
<UploadBox
icon={<UploadIcon />}
sx={{
@ -559,10 +531,7 @@ export default function StartPageSettings() {
<>
<Box
sx={{
display:
quiz.config.startpage.background.type === "image"
? "flex"
: "none",
display: quiz.config.startpage.background.type === "image" ? "flex" : "none",
flexDirection: "column",
}}
>
@ -641,10 +610,7 @@ export default function StartPageSettings() {
<>
<Box
sx={{
display:
quiz.config.startpage.background.type === "image"
? "flex"
: "none",
display: quiz.config.startpage.background.type === "image" ? "flex" : "none",
flexDirection: "column",
}}
>
@ -870,22 +836,24 @@ export default function StartPageSettings() {
maxLength={1000}
/>
<Extra />
<Box sx={{display: "flex", gap: "20px", alignItems: "center"}}>
<CustomizedSwitch
checked={quiz.config.antifraud}
onChange={(e) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.antifraud = e.target.checked;
})
}}
/>
<Typography sx={{fontWeight: 500,
color: theme.palette.grey3.main,}}
>
Включить антифрод</Typography>
</Box>
<Box sx={{ display: "flex", gap: "20px", alignItems: "center" }}>
<CustomizedSwitch
checked={quiz.config.antifraud}
onChange={(e) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.antifraud = e.target.checked;
});
}}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
Включить антифрод
</Typography>
</Box>
</>
)}
</Box>

@ -2,6 +2,7 @@ import Box from "@mui/material/Box";
import { FC } from "react";
import DeleteIcon from "@mui/icons-material/Delete";
import { IconButton, SxProps, Theme } from "@mui/material";
import { QuizVideo } from "@frontend/squzanswerer";
type VideoElementProps = {
videoSrc: string;
@ -20,12 +21,7 @@ export const VideoElement: FC<VideoElementProps> = ({
}) => {
return (
<Box sx={{ position: "relative", width: `${width}px` }}>
<video
style={{ borderRadius: "8px" }}
src={videoSrc}
width={width}
controls
/>
<QuizVideo videoUrl={videoSrc} />
<IconButton
onClick={onDeleteClick}
sx={{

@ -292,6 +292,6 @@ export const uploadQuizImage = async (quizId: string, blob: Blob, updateFn: (qui
});
};
function setProducedState<A extends string | { type: unknown }>(recipe: (state: QuizStore) => void, action?: A) {
function setProducedState<A extends string | { type: string }>(recipe: (state: QuizStore) => void, action?: A) {
useQuizStore.setState((state) => produce(state, recipe), false, action);
}

@ -21,7 +21,7 @@ export const setResults = (results: RawResult | []) =>
{
type: "setResults",
results,
},
}
);
const removeResult = (resultId: number) =>
@ -34,9 +34,7 @@ const removeResult = (resultId: number) =>
export const deleteResult = async (resultId: number) =>
requestQueue.enqueue(`deleteResult-${resultId}`, async () => {
const result = useResultStore
.getState()
.results.find((r) => r.id === resultId);
const result = useResultStore.getState().results.find((r) => r.id === resultId);
if (!result) return;
const [_, deleteError] = await resultApi.delete(Number(result.id));
@ -51,57 +49,46 @@ export const deleteResult = async (resultId: number) =>
removeResult(resultId);
});
export const obsolescenceResult = async (
resultId: number,
editQuizId: number,
) => {
requestQueue.enqueue(
`obsolescenceResult-${resultId}-${editQuizId}`,
async () => {
const result = useResultStore
.getState()
.results.find((r) => r.id === resultId);
if (!result) return;
if (result.new === false) return;
let lossDebouncer: null | ReturnType<typeof setTimeout> = null;
let lossId: number[] = [];
if (!lossId.includes(resultId)) lossId.push(resultId);
if (typeof lossDebouncer === "number") clearTimeout(lossDebouncer);
lossDebouncer = setTimeout(async () => {
//стреляем на лишение новизны
const [_, obsolescenceError] = await resultApi.obsolescence(lossId);
export const obsolescenceResult = async (resultId: number, editQuizId: number) => {
requestQueue.enqueue(`obsolescenceResult-${resultId}-${editQuizId}`, async () => {
const result = useResultStore.getState().results.find((r) => r.id === resultId);
if (!result) return;
if (result.new === false) return;
let lossDebouncer: null | ReturnType<typeof setTimeout> = null;
let lossId: number[] = [];
if (!lossId.includes(resultId)) lossId.push(resultId);
if (typeof lossDebouncer === "number") clearTimeout(lossDebouncer);
lossDebouncer = setTimeout(async () => {
//стреляем на лишение новизны
const [_, obsolescenceError] = await resultApi.obsolescence(lossId);
if (obsolescenceError) {
devlog("Error", obsolescenceError);
if (obsolescenceError) {
devlog("Error", obsolescenceError);
enqueueSnackbar(obsolescenceError);
enqueueSnackbar(obsolescenceError);
return;
}
lossId = [];
}, 3000);
const [resultList, resultError] = await resultApi.getList(editQuizId);
if (resultError || !resultList) {
return;
}
setResults(resultList);
},
);
lossId = [];
}, 3000);
const [resultList, resultError] = await resultApi.getList(editQuizId);
if (resultError || !resultList) {
return;
}
setResults(resultList);
});
};
export const ExportResults = async (
filterNew: string,
filterDate: string,
openPrePaymentModal: () => void,
editQuizId: number,
editQuizId: number
) => {
const [data, resultError] = await resultApi.export(
editQuizId,
parseFilters(filterNew, filterDate),
);
const [data, resultError] = await resultApi.export(editQuizId, parseFilters(filterNew, filterDate));
if (resultError) {
if (resultError?.includes("Payment Required")) {
@ -110,7 +97,6 @@ export const ExportResults = async (
return;
}
const blob = new Blob([data as BlobPart], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8",
});
@ -121,9 +107,6 @@ export const ExportResults = async (
link.click();
};
function setProducedState<A extends string | { type: string }>(
recipe: (state: ResultStore) => void,
action?: A,
) {
function setProducedState<A extends string | { type: string }>(recipe: (state: ResultStore) => void, action?: A) {
useResultStore.setState((state) => produce(state, recipe), false, action);
}

@ -6,19 +6,13 @@ import type { SxProps, Theme } from "@mui/material";
interface Props {
sx?: SxProps<Theme>;
imageSrc?: string;
imageSrc?: string | null;
onImageClick?: () => void;
onPlusClick?: () => void;
uploading: boolean;
}
export default function AddOrEditImageButton({
onImageClick,
onPlusClick,
sx,
imageSrc,
uploading = false,
}: Props) {
export default function AddOrEditImageButton({ onImageClick, onPlusClick, sx, imageSrc, uploading = false }: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));

@ -6,12 +6,7 @@ import { mutate } from "swr";
import { makeRequest } from "@api/makeRequest";
import { getDiscounts } from "@api/discounts";
import {
clearUserData,
OriginalUserAccount,
setUserAccount,
useUserStore,
} from "@root/user";
import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";
import { parseAxiosError } from "@utils/parse-error";
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
import type { Discount } from "@model/discounts";
@ -55,9 +50,7 @@ export function CheckFastlink() {
return;
}
enqueueSnackbar(
greetings !== "" ? greetings : "Промокод успешно активирован",
);
enqueueSnackbar(greetings !== "" ? greetings : "Промокод успешно активирован");
localStorage.setItem("fl", "");
const [responseAccount, accountError] = await getAccount();
@ -100,12 +93,7 @@ export function CheckFastlink() {
}
}
}
}, [
user.userId,
discounts,
user.customerAccount?.createdAt,
user.userAccount?.created_at,
]);
}, [user.userId, discounts, user.customerAccount?.createdAt, user.userAccount?.created_at]);
return (
<Modal
@ -125,7 +113,11 @@ export function CheckFastlink() {
borderRadius: 2,
}}
>
<Typography textAlign="center" variant="h6" component="h2">
<Typography
textAlign="center"
variant="h6"
component="h2"
>
Заменить текущий промокод?
</Typography>
<Box

@ -1,9 +1,13 @@
import { FC, useState } from "react";
import { QuizQuestionPage } from "@/model/questionTypes/page";
import { QuizQuestionResult } from "@/model/questionTypes/result";
import InfoIcon from "@icons/InfoIcon";
import UploadIcon from "@icons/UploadIcon";
import { Box, Button, ButtonBase, Skeleton, Tooltip, Typography, useTheme } from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import UploadBox from "@ui_kit/UploadBox";
import { FC, useState } from "react";
import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../utils/useDisclosure";
import { useCurrentQuiz } from "../stores/quizes/hooks";
@ -12,16 +16,19 @@ import UploadBox from "@ui_kit/UploadBox";
import UploadIcon from "@icons/UploadIcon";
import InfoIcon from "@icons/InfoIcon";
import { VideoElement } from "../pages/startPage/VideoElement";
import { useCurrentQuiz } from "../stores/quizes/hooks";
import { useDisclosure } from "../utils/useDisclosure";
import UploadVideoModal from "@/pages/Questions/UploadVideoModal";
interface Iprops {
resultData: QuizQuestionPage | QuizQuestionResult;
interface Props {
question: QuizQuestionPage | QuizQuestionResult;
cropAspectRatio: {
width: number;
height: number;
};
}
export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRatio }) => {
export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio }) => {
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
const quizQid = useCurrentQuiz()?.qid;
@ -29,10 +36,12 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const [isVideoUploadDialogOpen, setIsVideoUploadDialogOpen] = useState<boolean>(false);
async function handleImageUpload(file: File) {
setPictureUploading(true);
const url = await uploadQuestionImage(resultData.id, quizQid, file, (question, url) => {
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
@ -43,11 +52,32 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
}
function handleCropModalSaveClick(imageBlob: Blob) {
uploadQuestionImage(resultData.id, quizQid, imageBlob, (question, url) => {
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
question.content.back = url;
});
}
async function handleVideoUpload(videoUrl: string) {
setBackgroundUploading(true);
if (videoUrl.startsWith("blob:")) {
const videoBlob = await (await fetch(videoUrl)).blob();
uploadQuestionImage(question.id, quizQid, videoBlob, (question, url) => {
if (!("video" in question.content)) return;
question.content.video = url;
});
} else {
updateQuestion(question.id, (question) => {
if (!("video" in question.content)) return;
question.content.video = videoUrl;
});
}
setBackgroundUploading(false);
}
return (
<Box
sx={{
@ -64,61 +94,70 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
>
<Button
sx={{
color: resultData.content.useImage ? "#7E2AEA" : "#9A9AAF",
color: question.content.useImage ? "#7E2AEA" : "#9A9AAF",
fontSize: "16px",
"&:hover": {
background: "none",
},
}}
variant="text"
onClick={() => updateQuestion(resultData.id, (question) => (question.content.useImage = true))}
onClick={() =>
updateQuestion(question.id, (question) => {
if (!("useImage" in question.content)) return;
question.content.useImage = true;
})
}
>
Изображение
</Button>
<Button
sx={{
color: resultData.content.useImage ? "#9A9AAF" : "#7E2AEA",
color: question.content.useImage ? "#9A9AAF" : "#7E2AEA",
fontSize: "16px",
"&:hover": {
background: "none",
},
}}
variant="text"
onClick={() => updateQuestion(resultData.id, (question) => (question.content.useImage = false))}
onClick={() =>
updateQuestion(question.id, (question) => {
if (!("useImage" in question.content)) return;
question.content.useImage = false;
})
}
>
Видео
</Button>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
updateQuestion(question.id, (question) => {
question.content.back = null;
question.content.originalBack = null;
});
}}
>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
updateQuestion(resultData.id, (question) => {
question.content.back = null;
question.content.originalBack = null;
});
}}
cropAspectRatio={cropAspectRatio}
/>
</Box>
{resultData.content.useImage && (
cropAspectRatio={cropAspectRatio}
/>
<UploadVideoModal
open={isVideoUploadDialogOpen}
onClose={() => setIsVideoUploadDialogOpen(false)}
onUpload={handleVideoUpload}
video={question.content.video}
/>
{question.content.useImage && (
<Box
sx={{
cursor: "pointer",
@ -129,11 +168,11 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
}}
>
<AddOrEditImageButton
imageSrc={resultData.content.back}
imageSrc={question.content.back}
uploading={pictureUploding}
onImageClick={() => {
if (resultData.content.back) {
return openCropModal(resultData.content.back, resultData.content.originalBack);
if (question.content.back) {
return openCropModal(question.content.back, question.content.originalBack);
}
openImageUploadModal();
@ -144,9 +183,9 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
/>
</Box>
)}
{!resultData.content.useImage && (
{!question.content.useImage && (
<>
{!resultData.content.video ? (
{!question.content.video ? (
<>
<Box
sx={{
@ -179,7 +218,7 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
) : (
<>
<ButtonBase
component="label"
onClick={() => setIsVideoUploadDialogOpen(true)}
sx={{
justifyContent: "center",
height: "48px",
@ -189,22 +228,6 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
my: "20px",
}}
>
<input
onChange={async (event) => {
setBackgroundUploading(true);
const file = event.target.files?.[0];
if (file) {
await uploadQuestionImage(resultData.id, quizQid, file, (question, url) => {
question.content.video = url;
});
}
setBackgroundUploading(false);
}}
hidden
accept=".mp4"
multiple
type="file"
/>
<UploadBox
icon={<UploadIcon />}
sx={{
@ -218,10 +241,12 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
</>
) : (
<VideoElement
videoSrc={resultData.content.video}
videoSrc={question.content.video}
theme={theme}
onDeleteClick={() => {
updateQuestion(resultData.id, (question) => {
updateQuestion(question.id, (question) => {
if (!("video" in question.content)) return;
question.content.video = null;
});
}}

@ -15,8 +15,7 @@ const translateMessage: Record<string, string> = {
"field <email> is empty": 'Поле "E-mail" не заполнено',
"field <phoneNumber> is empty": 'Поле "Номер телефона" не заполнено',
"user with this email or login is exist": "Пользователь уже существует",
"user with this login is exist":
"Пользователь с таким логином уже существует",
"user with this login is exist": "Пользователь с таким логином уже существует",
"promocode already activated": "Промокод уже активирован",
"promocode not found": "Промокод не найден",
"promo code is expired": "Промокод истек",
@ -25,7 +24,7 @@ const translateMessage: Record<string, string> = {
export const parseAxiosError = (nativeError: unknown): [string, number?] => {
const error = nativeError as AxiosError;
console.error(error)
console.error(error);
if (process.env.NODE_ENV !== "production") console.error(error);
if (error.message === "Failed to fetch") return ["Ошибка сети"];
@ -40,10 +39,7 @@ console.error(error)
if ("statusCode" in serverError) {
SEMessage = serverError?.message.toLowerCase() || "";
}
if (
"error" in serverError &&
!("statusCode" in (error.response.data as ServerError))
) {
if ("error" in serverError && !("statusCode" in (error.response.data as ServerError))) {
SEMessage = serverError.error.toLowerCase() || "";
}
const translatedMessage = translateMessage[SEMessage || ""]?.toLowerCase();