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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,256 +1,250 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import type { import type { TagKeys, SelectedTags, QuestionKeys, SelectedQuestions, MinifiedData } from "./types";
TagKeys,
SelectedTags,
QuestionKeys,
SelectedQuestions,
MinifiedData,
} from "./types";
import { import {
AccountResponse, AccountResponse,
getIntegrationRules, getIntegrationRules,
getPipelines, getPipelines,
getSteps, getSteps,
getTags, getTags,
getUsers, getUsers,
getAccount, getAccount,
FieldsRule, FieldsRule,
} from "@/api/integration"; } from "@/api/integration";
const SIZE = 75; const SIZE = 75;
interface Props { interface Props {
isModalOpen: boolean; isModalOpen: boolean;
isTryRemoveAccount: boolean; isTryRemoveAccount: boolean;
quizID: number; quizID: number;
} }
export const useAmoIntegration = ({ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID }: Props) => {
isModalOpen, const [isloadingPage, setIsLoadingPage] = useState<boolean>(true);
isTryRemoveAccount, const [firstRules, setFirstRules] = useState<boolean>(false);
quizID, const [accountInfo, setAccountInfo] = useState<AccountResponse | null>(null);
}: 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 [arrayOfPipelines, setArrayOfPipelines] = useState<MinifiedData[]>([]);
const [arrayOfPipelinesSteps, setArrayOfPipelinesSteps] = useState<MinifiedData[]>([]); const [arrayOfPipelinesSteps, setArrayOfPipelinesSteps] = useState<MinifiedData[]>([]);
const [arrayOfUsers, setArrayOfUsers] = useState<MinifiedData[]>([]); const [arrayOfUsers, setArrayOfUsers] = useState<MinifiedData[]>([]);
const [arrayOfTags, setArrayOfTags] = useState<MinifiedData[]>([]); const [arrayOfTags, setArrayOfTags] = useState<MinifiedData[]>([]);
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null); const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
const [selectedPipelineStep, setSelectedPipelineStep] = useState<string | null>(null); const [selectedPipelineStep, setSelectedPipelineStep] = useState<string | null>(null);
const [selectedDealUser, setSelectedDealPerformer] = useState<string | null>(null); const [selectedDealUser, setSelectedDealPerformer] = useState<string | null>(null);
const [questionsBackend, setQuestionsBackend] = useState<FieldsRule>({} as FieldsRule); const [questionsBackend, setQuestionsBackend] = useState<FieldsRule>({} as FieldsRule);
const [selectedTags, setSelectedTags] = useState<SelectedTags>({ 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: [], Lead: [],
Contact: [], Contact: [],
Company: [], Company: [],
Customer: [], Customer: [],
}); });
const [selectedQuestions, setSelectedQuestions] = useState<SelectedQuestions>({ setSelectedQuestions({
Lead: [], Lead: [],
Company: [], Company: [],
Customer: [], 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); response.items.forEach((step) => {
const [pageOfPipelinesSteps, setPageOfPipelinesSteps] = useState(1); minifiedSteps.push({
const [pageOfUsers, setPageOfUsers] = useState(1); id: step.AmoID.toString(),
const [pageOfTags, setPageOfTags] = useState(1); title: step.Name,
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: [],
}); });
setSelectedQuestions({ });
Lead: [], setArrayOfPipelinesSteps([...oldData, ...minifiedSteps]);
Company: [],
Customer: [],
});
setPageOfPipelines(1);
setPageOfPipelinesSteps(1);
setPageOfUsers(1);
setPageOfTags(1);
} }
}, [isModalOpen, isTryRemoveAccount]); });
}, [selectedPipeline, pageOfPipelinesSteps]);
useEffect(() => {
getUsers({
page: pageOfUsers,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedUsers: MinifiedData[] = [];
useEffect(() => { response.items.forEach((step) => {
getPipelines({ minifiedUsers.push({
page: pageOfPipelines, id: step.amoUserID.toString(),
size: SIZE, title: step.name,
}).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]);
}
}); });
}, [pageOfUsers]) setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]);
useEffect(() => { }
getTags({ });
page: pageOfTags, }, [pageOfUsers]);
size: SIZE, useEffect(() => {
}).then(([response]) => { getTags({
if (response && response.items !== null) { page: pageOfTags,
const minifiedTags: MinifiedData[] = [] size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = [];
response.items.forEach((step) => { response.items.forEach((step) => {
minifiedTags.push({ minifiedTags.push({
id: step.AmoID.toString(), id: step.AmoID.toString(),
title: step.Name, title: step.Name,
entity: step.Entity === "leads" ? "Lead" : entity:
step.Entity === "contacts" ? "Contact" : step.Entity === "leads"
step.Entity === "companies" ? "Company" : "Customer" ? "Lead"
}) : step.Entity === "contacts"
}) ? "Contact"
setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]); : step.Entity === "companies"
} ? "Company"
: "Customer",
});
}); });
}, [pageOfTags]) setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]);
}
});
}, [pageOfTags]);
return ({ return {
isloadingPage, isloadingPage,
firstRules, firstRules,
accountInfo, accountInfo,
arrayOfPipelines, arrayOfPipelines,
arrayOfPipelinesSteps, arrayOfPipelinesSteps,
arrayOfUsers, arrayOfUsers,
arrayOfTags, arrayOfTags,
selectedPipeline, selectedPipeline,
setSelectedPipeline, setSelectedPipeline,
selectedPipelineStep, selectedPipelineStep,
setSelectedPipelineStep, setSelectedPipelineStep,
selectedDealUser, selectedDealUser,
setSelectedDealPerformer, setSelectedDealPerformer,
questionsBackend, questionsBackend,
selectedTags, selectedTags,
setSelectedTags, setSelectedTags,
selectedQuestions, selectedQuestions,
setSelectedQuestions, setSelectedQuestions,
setPageOfPipelines, setPageOfPipelines,
setPageOfPipelinesSteps, setPageOfPipelinesSteps,
setPageOfUsers, setPageOfUsers,
setPageOfTags, setPageOfTags,
}) };
} };

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

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

@ -1,16 +1,8 @@
import { import { Box, Button, ButtonBase, Dialog, Typography, useTheme } from "@mui/material";
Box,
Button,
ButtonBase,
Modal,
Typography,
useTheme,
} from "@mui/material";
import SelectableButton from "@ui_kit/SelectableButton";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton";
import { useState } from "react"; import { useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon"; import UploadIcon from "../../assets/icons/UploadIcon";
import type { DragEvent } from "react"; import type { DragEvent } from "react";
type BackgroundTypeModal = "linkVideo" | "ownVideo"; type BackgroundTypeModal = "linkVideo" | "ownVideo";
@ -18,18 +10,12 @@ type BackgroundTypeModal = "linkVideo" | "ownVideo";
type HelpQuestionsProps = { type HelpQuestionsProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
video: string; video: string | null;
onUpload: (number: string) => void; onUpload: (number: string) => void;
}; };
export const UploadVideoModal = ({ export default function UploadVideoModal({ open, onClose, video, onUpload }: HelpQuestionsProps) {
open, const [backgroundTypeModal, setBackgroundTypeModal] = useState<BackgroundTypeModal>("linkVideo");
onClose,
video,
onUpload,
}: HelpQuestionsProps) => {
const [backgroundTypeModal, setBackgroundTypeModal] =
useState<BackgroundTypeModal>("linkVideo");
const theme = useTheme(); const theme = useTheme();
const handleDrop = (event: DragEvent<HTMLDivElement>) => { const handleDrop = (event: DragEvent<HTMLDivElement>) => {
@ -42,118 +28,102 @@ export const UploadVideoModal = ({
}; };
return ( return (
<Modal <Dialog
open={open} open={open}
onClose={onClose} onClose={onClose}
aria-labelledby="modal-modal-title" PaperProps={{
aria-describedby="modal-modal-description" sx: {
> maxWidth: "640px",
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "690px",
bgcolor: "background.paper",
borderRadius: "12px", borderRadius: "12px",
boxShadow: 24, boxShadow: 24,
p: 0, p: 0,
overflow: "hidden", overflow: "hidden",
},
}}
>
<Box
sx={{
display: "flex",
padding: "20px",
background: theme.palette.background.default,
}} }}
> >
<Box <Typography sx={{ color: "#9A9AAF" }}>
sx={{ Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить собственное
display: "flex", </Typography>
padding: "20px", <Button
background: theme.palette.background.default, onClick={onClose}
}} variant="contained"
> >
<Typography sx={{ color: "#9A9AAF" }}> Готово
Видео можно вставить с любого хостинга: YouTube, Vimeo или загрузить </Button>
собственное
</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>
)}
</Box> </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 UploadBox from "@ui_kit/UploadBox";
import { memo, useState } from "react"; import { memo, useState } from "react";
import UploadIcon from "../../assets/icons/UploadIcon"; import UploadIcon from "../../assets/icons/UploadIcon";
import { UploadVideoModal } from "./UploadVideoModal"; import UploadVideoModal from "./UploadVideoModal";
type BackgroundType = "text" | "video"; type BackgroundType = "text" | "video";
@ -15,11 +15,7 @@ type HelpQuestionsProps = {
hintText: string; hintText: string;
}; };
const HelpQuestions = memo<HelpQuestionsProps>(function ({ const HelpQuestions = memo<HelpQuestionsProps>(function ({ questionId, hintVideo, hintText }) {
questionId,
hintVideo,
hintText,
}) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text"); const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
@ -71,15 +67,17 @@ const HelpQuestions = memo<HelpQuestionsProps>(function ({
</> </>
) : ( ) : (
<Box> <Box>
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}> <Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>Загрузите видео</Typography>
Загрузите видео
</Typography>
<ButtonBase <ButtonBase
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
sx={{ justifyContent: "flex-start" }} sx={{ justifyContent: "flex-start" }}
> >
{hintVideo ? ( {hintVideo ? (
<video src={hintVideo} width="400" controls /> <video
src={hintVideo}
width="400"
controls
/>
) : ( ) : (
<> <>
<UploadBox <UploadBox

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

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

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

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

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

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

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

@ -6,12 +6,7 @@ import { mutate } from "swr";
import { makeRequest } from "@api/makeRequest"; import { makeRequest } from "@api/makeRequest";
import { getDiscounts } from "@api/discounts"; import { getDiscounts } from "@api/discounts";
import { import { clearUserData, OriginalUserAccount, setUserAccount, useUserStore } from "@root/user";
clearUserData,
OriginalUserAccount,
setUserAccount,
useUserStore,
} from "@root/user";
import { parseAxiosError } from "@utils/parse-error"; import { parseAxiosError } from "@utils/parse-error";
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher"; import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
import type { Discount } from "@model/discounts"; import type { Discount } from "@model/discounts";
@ -55,9 +50,7 @@ export function CheckFastlink() {
return; return;
} }
enqueueSnackbar( enqueueSnackbar(greetings !== "" ? greetings : "Промокод успешно активирован");
greetings !== "" ? greetings : "Промокод успешно активирован",
);
localStorage.setItem("fl", ""); localStorage.setItem("fl", "");
const [responseAccount, accountError] = await getAccount(); 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 ( return (
<Modal <Modal
@ -125,7 +113,11 @@ export function CheckFastlink() {
borderRadius: 2, borderRadius: 2,
}} }}
> >
<Typography textAlign="center" variant="h6" component="h2"> <Typography
textAlign="center"
variant="h6"
component="h2"
>
Заменить текущий промокод? Заменить текущий промокод?
</Typography> </Typography>
<Box <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 { Box, Button, ButtonBase, Skeleton, Tooltip, Typography, useTheme } from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions"; import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; 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 { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../utils/useDisclosure"; import { useDisclosure } from "../utils/useDisclosure";
import { useCurrentQuiz } from "../stores/quizes/hooks"; import { useCurrentQuiz } from "../stores/quizes/hooks";
@ -12,16 +16,19 @@ import UploadBox from "@ui_kit/UploadBox";
import UploadIcon from "@icons/UploadIcon"; import UploadIcon from "@icons/UploadIcon";
import InfoIcon from "@icons/InfoIcon"; import InfoIcon from "@icons/InfoIcon";
import { VideoElement } from "../pages/startPage/VideoElement"; 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 { interface Props {
resultData: QuizQuestionPage | QuizQuestionResult; question: QuizQuestionPage | QuizQuestionResult;
cropAspectRatio: { cropAspectRatio: {
width: number; width: number;
height: number; height: number;
}; };
} }
export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRatio }) => { export const MediaSelectionAndDisplay: FC<Props> = ({ question, cropAspectRatio }) => {
const [pictureUploding, setPictureUploading] = useState<boolean>(false); const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false); const [backgroundUploding, setBackgroundUploading] = useState<boolean>(false);
const quizQid = useCurrentQuiz()?.qid; const quizQid = useCurrentQuiz()?.qid;
@ -29,10 +36,12 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } = const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState(); useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const [isVideoUploadDialogOpen, setIsVideoUploadDialogOpen] = useState<boolean>(false);
async function handleImageUpload(file: File) { async function handleImageUpload(file: File) {
setPictureUploading(true); 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.back = url;
question.content.originalBack = url; question.content.originalBack = url;
}); });
@ -43,11 +52,32 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
} }
function handleCropModalSaveClick(imageBlob: Blob) { function handleCropModalSaveClick(imageBlob: Blob) {
uploadQuestionImage(resultData.id, quizQid, imageBlob, (question, url) => { uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
question.content.back = 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 ( return (
<Box <Box
sx={{ sx={{
@ -64,61 +94,70 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
> >
<Button <Button
sx={{ sx={{
color: resultData.content.useImage ? "#7E2AEA" : "#9A9AAF", color: question.content.useImage ? "#7E2AEA" : "#9A9AAF",
fontSize: "16px", fontSize: "16px",
"&:hover": { "&:hover": {
background: "none", background: "none",
}, },
}} }}
variant="text" 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>
<Button <Button
sx={{ sx={{
color: resultData.content.useImage ? "#9A9AAF" : "#7E2AEA", color: question.content.useImage ? "#9A9AAF" : "#7E2AEA",
fontSize: "16px", fontSize: "16px",
"&:hover": { "&:hover": {
background: "none", background: "none",
}, },
}} }}
variant="text" 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> </Button>
</Box> </Box>
<UploadImageModal
<Box isOpen={isImageUploadOpen}
sx={{ onClose={closeImageUploadModal}
display: "flex", handleImageChange={handleImageUpload}
flexDirection: "column", />
<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;
});
}} }}
> cropAspectRatio={cropAspectRatio}
<UploadImageModal />
isOpen={isImageUploadOpen} <UploadVideoModal
onClose={closeImageUploadModal} open={isVideoUploadDialogOpen}
handleImageChange={handleImageUpload} onClose={() => setIsVideoUploadDialogOpen(false)}
/> onUpload={handleVideoUpload}
<CropModal video={question.content.video}
isOpen={isCropModalOpen} />
imageBlob={imageBlob} {question.content.useImage && (
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 && (
<Box <Box
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
@ -129,11 +168,11 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
}} }}
> >
<AddOrEditImageButton <AddOrEditImageButton
imageSrc={resultData.content.back} imageSrc={question.content.back}
uploading={pictureUploding} uploading={pictureUploding}
onImageClick={() => { onImageClick={() => {
if (resultData.content.back) { if (question.content.back) {
return openCropModal(resultData.content.back, resultData.content.originalBack); return openCropModal(question.content.back, question.content.originalBack);
} }
openImageUploadModal(); openImageUploadModal();
@ -144,9 +183,9 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
/> />
</Box> </Box>
)} )}
{!resultData.content.useImage && ( {!question.content.useImage && (
<> <>
{!resultData.content.video ? ( {!question.content.video ? (
<> <>
<Box <Box
sx={{ sx={{
@ -179,7 +218,7 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
) : ( ) : (
<> <>
<ButtonBase <ButtonBase
component="label" onClick={() => setIsVideoUploadDialogOpen(true)}
sx={{ sx={{
justifyContent: "center", justifyContent: "center",
height: "48px", height: "48px",
@ -189,22 +228,6 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
my: "20px", 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 <UploadBox
icon={<UploadIcon />} icon={<UploadIcon />}
sx={{ sx={{
@ -218,10 +241,12 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData, cropAspectRat
</> </>
) : ( ) : (
<VideoElement <VideoElement
videoSrc={resultData.content.video} videoSrc={question.content.video}
theme={theme} theme={theme}
onDeleteClick={() => { onDeleteClick={() => {
updateQuestion(resultData.id, (question) => { updateQuestion(question.id, (question) => {
if (!("video" in question.content)) return;
question.content.video = null; question.content.video = null;
}); });
}} }}

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