модалки интеграций запер и постбек

This commit is contained in:
Nastya 2025-09-01 14:27:20 +03:00
parent f2cd81f29b
commit 8a4e980ed4
7 changed files with 2556 additions and 51 deletions

101
src/api/leadtarget.ts Normal file

@ -0,0 +1,101 @@
import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
export type LeadTargetType = "mail" | "telegram" | "whatsapp" | "webhook";
export interface LeadTargetModel {
ID: number;
AccountID: string;
Type: LeadTargetType;
QuizID: number;
Target: string;
InviteLink?: string;
Deleted?: boolean;
CreatedAt?: string;
}
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
export const getLeadTargetsByQuiz = async (
quizId: number,
): Promise<[LeadTargetModel[] | null, string?]> => {
try {
const items = await makeRequest<unknown, LeadTargetModel[]>({
method: "GET",
url: `${API_URL}/account/leadtarget/${quizId}`,
});
return [items];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить цели лида. ${error}`];
}
};
export const createLeadTarget = async (
body: {
type: LeadTargetType;
quizID: number;
target: string;
name?: string;
},
): Promise<[LeadTargetModel | true | null, string?]> => {
try {
const response = await makeRequest<typeof body, LeadTargetModel | true>({
method: "POST",
url: `${API_URL}/account/leadtarget`,
body,
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось создать цель лида. ${error}`];
}
};
export const updateLeadTarget = async (
body: {
id: number;
target: string;
},
): Promise<[LeadTargetModel | null, string?]> => {
try {
const updated = await makeRequest<typeof body, LeadTargetModel>({
method: "PUT",
url: `${API_URL}/account/leadtarget`,
body,
});
return [updated];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось обновить цель лида. ${error}`];
}
};
export const deleteLeadTarget = async (
id: number,
): Promise<[true | null, string?]> => {
try {
await makeRequest<unknown, unknown>({
method: "DELETE",
url: `${API_URL}/account/leadtarget/${id}`,
});
return [true];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось удалить цель лида. ${error}`];
}
};
export const leadTargetApi = {
getByQuiz: getLeadTargetsByQuiz,
create: createLeadTarget,
update: updateLeadTarget,
delete: deleteLeadTarget,
};

@ -0,0 +1,13 @@
import { Box, SxProps } from "@mui/material";
export default function OrangeYoutube(sx: SxProps) {
return (
<Box
sx={sx}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.9756 4.36328C21.0799 4.36346 21.9754 5.25898 21.9756 6.36328V17.6367C21.9754 18.741 21.0799 19.6365 19.9756 19.6367H4.02734C2.92289 19.6367 2.02754 18.7411 2.02734 17.6367V6.36328C2.02754 5.25887 2.92289 4.36328 4.02734 4.36328H19.9756ZM10.2227 8.18262C10.1533 8.17891 10.0838 8.19583 10.0225 8.23145C9.9614 8.26705 9.9107 8.32013 9.875 8.38477C9.83921 8.44959 9.81946 8.52397 9.81934 8.59961V15.4004C9.81946 15.476 9.83921 15.5504 9.875 15.6152C9.9107 15.6799 9.9614 15.7329 10.0225 15.7686C10.0838 15.8042 10.1533 15.8211 10.2227 15.8174C10.292 15.8137 10.3592 15.79 10.417 15.748L15.1025 12.3477C15.1552 12.3095 15.1986 12.258 15.2285 12.1973C15.2584 12.1366 15.2744 12.0689 15.2744 12C15.2744 11.9311 15.2584 11.8634 15.2285 11.8027C15.1986 11.742 15.1552 11.6905 15.1025 11.6523L10.417 8.25195C10.3592 8.21003 10.292 8.18633 10.2227 8.18262Z" fill="#FA590B" />
</svg>
</Box>
);
}

2204
src/openapi (1).yaml Normal file

File diff suppressed because it is too large Load Diff

@ -1,7 +1,10 @@
import { FC } from "react";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
import { FC, useState } from "react";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button, Link } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { Quiz } from "@/model/quiz/quiz";
import CustomTextField from "@/ui_kit/CustomTextField";
import OrangeYoutube from "@/assets/icons/OrangeYoutube";
import { createLeadTarget } from "@/api/leadtarget";
type PostbackModalProps = {
isModalOpen: boolean;
@ -19,6 +22,8 @@ export const PostbackModal: FC<PostbackModalProps> = ({
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [token, setToken] = useState<string>("");
const [domain, setDomain] = useState<string>("");
return (
<Dialog
@ -28,46 +33,138 @@ export const PostbackModal: FC<PostbackModalProps> = ({
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "919px",
height: "658px",
height: "314px",
borderRadius: "12px",
},
}}
>
<Box>
<Box
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 20px",
}}
>
<Typography
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 20px",
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
color: theme.palette.grey2.main,
}}
>
<Typography
Автоматизация с {companyName ? companyName : "Postback"}
</Typography>
<IconButton onClick={handleCloseModal}>
<CloseIcon />
</IconButton>
</Box>
<Box
sx={{
padding: "20px",
height: "calc(100% - 68px)",
overflow: "auto",
}}
>
<Box
sx={{
display: "flex",
justifyContent: "right"
}}>
<Link
href="https://youtube.com"
underline="hover"
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
color: theme.palette.grey2.main,
color: "#FA590B",
display: "inline-flex",
gap: "10px",
fontSize: "16px"
}}
><OrangeYoutube sx={{
width: "24px",
height: "24px",
}} /> Смотреть инструкцию</Link>
</Box>
<Box
sx={{
marginTop: "-43px",
display: "inline-flex",
alignItems: "end",
gap: "38px",
width: "100%"
}}
>
<Box
sx={{
width: "100%"
}}
>
<Typography
sx={{
fontWeight: 500,
color: "black",
mt: "11px",
mb: "14px",
width: "100%"
}}
>
Токен авторизации
</Typography>
<CustomTextField
id="postback-auth-token"
placeholder="токен в формате ХХХХХХ"
value={token}
onChange={(e) => setToken(e.target.value)}
maxLength={150}
/>
<Typography
sx={{
fontWeight: 500,
color: "black",
mt: "11px",
mb: "14px",
}}
>
Домен
</Typography>
<CustomTextField
id="postback-domain"
placeholder="токен в формате ХХХХХХ"
value={domain}
onChange={(e) => setDomain(e.target.value)}
maxLength={150}
/>
</Box>
<Button
onClick={async () => {
const target = domain.trim();
if (!token.trim() || !target) return;
// Отправляем webhook postback как целевой endpoint
await createLeadTarget({
type: "webhook",
quizID: quiz.backendId,
target,
name: token.trim(),
});
}}
variant="contained"
sx={{
backgroundColor: "#7E2AEA",
fontSize: "18px",
lineHeight: "18px",
width: "216px",
height: "44px",
p: "10px 20px",
}}
>
Интеграция с {companyName ? companyName : "Postback"}
</Typography>
<IconButton onClick={handleCloseModal}>
<CloseIcon />
</IconButton>
</Box>
<Box
sx={{
padding: "20px",
height: "calc(100% - 68px)",
overflow: "auto",
}}
>
<Typography variant="body1">
Интеграция с Postback находится в разработке.
</Typography>
Сохранить
</Button>
</Box>
</Box>
</Dialog>

@ -1,7 +1,10 @@
import { FC } from "react";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
import { FC, useState } from "react";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Link, Button } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { Quiz } from "@/model/quiz/quiz";
import OrangeYoutube from "@/assets/icons/OrangeYoutube";
import CustomTextField from "@/ui_kit/CustomTextField";
import { createLeadTarget } from "@/api/leadtarget";
type ZapierModalProps = {
isModalOpen: boolean;
@ -19,6 +22,7 @@ export const ZapierModal: FC<ZapierModalProps> = ({
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [webhookUrl, setWebhookUrl] = useState<string>("");
return (
<Dialog
@ -28,7 +32,7 @@ export const ZapierModal: FC<ZapierModalProps> = ({
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "919px",
height: "658px",
height: "195px",
borderRadius: "12px",
},
}}
@ -52,7 +56,7 @@ export const ZapierModal: FC<ZapierModalProps> = ({
color: theme.palette.grey2.main,
}}
>
Интеграция с {companyName ? companyName : "Zapier"}
Автоматизация с {companyName ? companyName : "Zapier"}
</Typography>
<IconButton onClick={handleCloseModal}>
<CloseIcon />
@ -65,9 +69,79 @@ export const ZapierModal: FC<ZapierModalProps> = ({
overflow: "auto",
}}
>
<Typography variant="body1">
Интеграция с Zapier находится в разработке.
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "right"
}}>
<Link
href="https://youtube.com"
underline="hover"
sx={{
color: "#FA590B",
display: "inline-flex",
gap: "10px",
fontSize: "16px"
}}
><OrangeYoutube sx={{
width: "24px",
height: "24px",
}} /> Смотреть инструкцию</Link>
</Box>
<Box
sx={{ marginTop: "-33px" }}
>
<Typography
sx={{
fontWeight: 500,
color: "black",
mt: "11px",
mb: "14px",
}}
>
URL webhook
</Typography>
<Box
sx={{
display: "inline-flex",
width: "100%",
gap: "38px"
}}
>
<CustomTextField
id="zapier-webhook-url"
placeholder="введите url здесь"
value={webhookUrl}
onChange={(e) => setWebhookUrl(e.target.value)}
maxLength={150}
/>
<Button
onClick={async () => {
const target = webhookUrl.trim();
if (!target) return;
await createLeadTarget({
type: "webhook",
quizID: quiz.backendId,
target,
});
}}
variant="contained"
sx={{
backgroundColor: "#7E2AEA",
fontSize: "18px",
lineHeight: "18px",
width: "216px",
height: "44px",
p: "10px 20px",
}}
>
Сохранить
</Button>
</Box>
</Box>
</Box>
</Box>
</Dialog>

@ -5,6 +5,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuizStore } from "@root/quizes/store";
import { useNavigate } from "react-router-dom";
import { PartnersBoard } from "./PartnersBoard/PartnersBoard";
import { getLeadTargetsByQuiz } from "@/api/leadtarget";
import { QuizMetricType } from "@model/quizSettings";
interface IntegrationsPageProps {
@ -29,10 +30,25 @@ export const IntegrationsPage = ({
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
const [leadTargetsLoaded, setLeadTargetsLoaded] = useState<boolean>(false);
const [leadTargets, setLeadTargets] = useState<any[] | null>(null);
useEffect(() => {
if (editQuizId === null) navigate("/list");
}, [navigate, editQuizId]);
useEffect(() => {
// Загрузка связанных с квизом данных интеграций при входе на страницу
const load = async () => {
if (!leadTargetsLoaded && quiz?.id) {
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
setLeadTargets(items ?? []);
setLeadTargetsLoaded(true);
}
};
load();
}, [leadTargetsLoaded, quiz?.id]);
const heightBar = heightSidebar + 51 + 88 + 36 + 25;
if (quiz === undefined)

@ -132,7 +132,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
/>
</Box>
{/* <Typography variant="h6" sx={sectionTitleStyles}>
<Typography variant="h6" sx={sectionTitleStyles}>
Автоматизация
</Typography>
<Box sx={containerStyles}>
@ -144,7 +144,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
setIsModalOpen={setIsPostbackModalOpen}
setCompanyName={setCompanyName}
/>
</Box> */}
</Box>
</Box>
{companyName && (