z p модалки с корректными запросами

This commit is contained in:
Nastya 2025-09-03 21:30:38 +03:00
parent 862ed4f395
commit 7da86c5b2e
6 changed files with 169 additions and 62 deletions

@ -4,14 +4,14 @@ 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;
id: number;
accountID: string;
type: LeadTargetType;
quizID: number;
target: string; // содержит подстроку "zapier" или "postback"
inviteLink?: string;
deleted?: boolean;
createdAt?: string;
}
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;

@ -3,10 +3,11 @@ import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button }
import CloseIcon from "@mui/icons-material/Close";
import { Quiz } from "@/model/quiz/quiz";
import CustomTextField from "@/ui_kit/CustomTextField";
import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget } from "@/api/leadtarget";
import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget, updateLeadTarget } from "@/api/leadtarget";
import { useFormik } from "formik";
import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink";
import { useSnackbar } from "notistack";
import { useLeadTargets } from "@/pages/IntegrationsPage/hooks/useLeadTargets";
type PostbackModalProps = {
isModalOpen: boolean;
@ -27,28 +28,26 @@ export const PostbackModal: FC<PostbackModalProps> = ({
const [isSaving, setIsSaving] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar();
const deleteLeadTargetsByQuizType = async (quizId: number, type: "webhook") => {
const [targets] = await getLeadTargetsByQuiz(quizId);
if (!targets || targets.length === 0) {
console.log("No targets found for deletion");
return;
}
const toDelete = targets.filter(t => t.Type === type);
console.log("Targets to delete:", toDelete);
for (const t of toDelete) {
console.log("Deleting target with ID:", t.ID);
await deleteLeadTarget(t.ID);
}
};
const handleSubmit = async (values: { token: string; domain: string }) => {
const tokenValue = (values.token || "").trim();
const target = (values.domain || "").trim();
try {
setIsSaving(true);
// 1) Асинхронно получаем текущие цели
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
const existing = (items ?? []).filter((t) => t.type === "webhook");
console.log("Saving flow -> existing webhook targets:", existing);
if (!tokenValue && !target) {
await deleteLeadTargetsByQuizType(quiz.backendId, "webhook");
const deletePromises = existing.map((t) => deleteLeadTarget(t.id));
await Promise.all(deletePromises);
enqueueSnackbar("Postback удален", { variant: "success" });
} else if (existing.length > 0) {
const [first, ...extra] = existing;
await Promise.all([
updateLeadTarget({ id: first.id, target }),
...extra.map((t) => deleteLeadTarget(t.id)),
]);
enqueueSnackbar("Postback обновлен", { variant: "success" });
} else {
await createLeadTarget({
type: "webhook",
@ -58,6 +57,8 @@ export const PostbackModal: FC<PostbackModalProps> = ({
});
enqueueSnackbar("Postback сохранен", { variant: "success" });
}
await refresh();
} catch (error) {
enqueueSnackbar("Ошибка при сохранении", { variant: "error" });
} finally {
@ -65,11 +66,18 @@ export const PostbackModal: FC<PostbackModalProps> = ({
}
};
const { isLoading, postbackTarget, refresh } = useLeadTargets(quiz?.backendId, isModalOpen);
const formik = useFormik<{ token: string; domain: string }>({
initialValues: { token: "", domain: "" },
initialValues: { token: "", domain: postbackTarget?.target ?? "" },
onSubmit: handleSubmit,
});
useEffect(() => {
formik.setFieldValue("domain", postbackTarget?.target ?? "");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [postbackTarget?.target]);
useEffect(() => {
if (isModalOpen) {
setTimeout(() => {
@ -153,17 +161,21 @@ export const PostbackModal: FC<PostbackModalProps> = ({
>
Домен
</Typography>
<CustomTextField
id="postback-domain"
placeholder="токен в формате ХХХХХХ"
value={formik.values.domain}
onChange={(e) => formik.setFieldValue("domain", e.target.value)}
maxLength={150}
/>
{isLoading ? (
<Box sx={{ width: "100%", height: 44, borderRadius: "8px", bgcolor: "action.hover" }} />
) : (
<CustomTextField
id="postback-domain"
placeholder="токен в формате ХХХХХХ"
value={formik.values.domain}
onChange={(e) => formik.setFieldValue("domain", e.target.value)}
maxLength={150}
/>
)}
</Box>
<Button
disabled={isSaving}
disabled={isSaving || isLoading}
type="submit"
variant="contained"
sx={{

@ -3,10 +3,11 @@ import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button }
import CloseIcon from "@mui/icons-material/Close";
import { Quiz } from "@/model/quiz/quiz";
import CustomTextField from "@/ui_kit/CustomTextField";
import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget } from "@/api/leadtarget";
import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget, updateLeadTarget } from "@/api/leadtarget";
import { useFormik } from "formik";
import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink";
import { useSnackbar } from "notistack";
import { useLeadTargets } from "@/pages/IntegrationsPage/hooks/useLeadTargets";
type ZapierModalProps = {
isModalOpen: boolean;
@ -27,28 +28,30 @@ export const ZapierModal: FC<ZapierModalProps> = ({
const [isSaving, setIsSaving] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar();
const deleteLeadTargetsByQuizType = async (quizId: number, type: "webhook") => {
const [targets] = await getLeadTargetsByQuiz(quizId);
if (!targets || targets.length === 0) {
console.log("No targets found for deletion");
return;
}
const toDelete = targets.filter(t => t.Type === type);
console.log("Targets to delete:", toDelete);
for (const t of toDelete) {
console.log("Deleting target with ID:", t.ID);
await deleteLeadTarget(t.ID);
}
};
const handleSubmit = async (values: { webhookUrl: string }) => {
const target = (values.webhookUrl || "").trim();
try {
setIsSaving(true);
// 1) Асинхронно получаем текущие цели
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
const existing = (items ?? []).filter((t) => t.type === "webhook");
console.log("Saving flow -> existing webhook targets:", existing);
if (!target) {
await deleteLeadTargetsByQuizType(quiz.backendId, "webhook");
// Пустое значение — удаляем все
const deletePromises = existing.map((t) => deleteLeadTarget(t.id));
await Promise.all(deletePromises);
enqueueSnackbar("Webhook удален", { variant: "success" });
} else if (existing.length > 0) {
// Уже существует — обновляем первый и удаляем все лишние
const [first, ...extra] = existing;
await Promise.all([
updateLeadTarget({ id: first.id, target }),
...extra.map((t) => deleteLeadTarget(t.id)),
]);
enqueueSnackbar("Webhook обновлен", { variant: "success" });
} else {
// Не существует — создаем
await createLeadTarget({
type: "webhook",
quizID: quiz.backendId,
@ -56,6 +59,8 @@ export const ZapierModal: FC<ZapierModalProps> = ({
});
enqueueSnackbar("Webhook сохранен", { variant: "success" });
}
await refresh();
} catch (error) {
enqueueSnackbar("Ошибка при сохранении", { variant: "error" });
} finally {
@ -63,11 +68,18 @@ export const ZapierModal: FC<ZapierModalProps> = ({
}
};
const { isLoading, zapierTarget, refresh } = useLeadTargets(quiz?.backendId, isModalOpen);
const formik = useFormik<{ webhookUrl: string }>({
initialValues: { webhookUrl: "" },
initialValues: { webhookUrl: zapierTarget?.target ?? "" },
onSubmit: handleSubmit,
});
useEffect(() => {
formik.setFieldValue("webhookUrl", zapierTarget?.target ?? "");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [zapierTarget?.target]);
useEffect(() => {
if (isModalOpen) {
setTimeout(() => {
@ -160,15 +172,19 @@ export const ZapierModal: FC<ZapierModalProps> = ({
gap: isMobile ? "10px" : "38px"
}}
>
<CustomTextField
id="zapier-webhook-url"
placeholder="введите url здесь"
value={formik.values.webhookUrl}
onChange={(e) => formik.setFieldValue("webhookUrl", e.target.value)}
maxLength={150}
/>
{isLoading ? (
<Box sx={{ width: "100%", height: 44, borderRadius: "8px", bgcolor: "action.hover" }} />
) : (
<CustomTextField
id="zapier-webhook-url"
placeholder="введите url здесь"
value={formik.values.webhookUrl}
onChange={(e) => formik.setFieldValue("webhookUrl", e.target.value)}
maxLength={150}
/>
)}
<Button
disabled={isSaving}
disabled={isSaving || isLoading}
type="submit"
variant="contained"
sx={{

@ -5,7 +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 { getLeadTargetsByQuiz, LeadTargetModel } from "@/api/leadtarget";
import { QuizMetricType } from "@model/quizSettings";
interface IntegrationsPageProps {
@ -31,7 +31,9 @@ export const IntegrationsPage = ({
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);
const [leadTargets, setLeadTargets] = useState<LeadTargetModel[] | null>(null);
const [zapierTarget, setZapierTarget] = useState<LeadTargetModel | null>(null);
const [postbackTarget, setPostbackTarget] = useState<LeadTargetModel | null>(null);
useEffect(() => {
@ -39,16 +41,34 @@ export const IntegrationsPage = ({
}, [navigate, editQuizId]);
useEffect(() => {
// Загрузка связанных с квизом данных интеграций при входе на страницу
const load = async () => {
if (!leadTargetsLoaded && quiz?.id) {
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
setLeadTargets(items ?? []);
const list = items ?? [];
console.log("LeadTargets fetched:", list);
setLeadTargets(list);
const webhookOnly = list.filter((t) => t.type === "webhook");
console.log("Webhook-only targets:", webhookOnly);
const zapier = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("zapier")) ?? null;
const postback = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("postback")) ?? null;
console.log("Distributed targets:", { zapier, postback });
setZapierTarget(zapier);
setPostbackTarget(postback);
setLeadTargetsLoaded(true);
}
};
load();
}, [leadTargetsLoaded, quiz?.id]);
const refreshLeadTargets = async () => {
if (!quiz?.id) return;
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
const list = items ?? [];
setLeadTargets(list);
const webhookOnly = list.filter((t) => t.type === "webhook");
setZapierTarget(webhookOnly.find((t) => (t.target || "").toLowerCase().includes("zapier")) ?? null);
setPostbackTarget(webhookOnly.find((t) => (t.target || "").toLowerCase().includes("postback")) ?? null);
};
const heightBar = heightSidebar + 51 + 88 + 36 + 25;
if (quiz === undefined)
@ -103,6 +123,8 @@ export const IntegrationsPage = ({
setIsPostbackModalOpen={setIsPostbackModalOpen}
isPostbackModalOpen={isPostbackModalOpen}
handleClosePostbackModal={handleClosePostbackModal}
zapierTarget={zapierTarget}
postbackTarget={postbackTarget}
/>
</Box>
</>

@ -7,6 +7,7 @@ import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
import { VKPixelLogo } from "../mocks/VKPixelLogo";
import { QuizMetricType } from "@model/quizSettings";
import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
import type { LeadTargetModel } from "@/api/leadtarget";
import { useCurrentQuiz } from "@/stores/quizes/hooks";
const AnalyticsModal = lazy(() =>
@ -48,6 +49,8 @@ type PartnersBoardProps = {
setIsPostbackModalOpen: (value: boolean) => void;
isPostbackModalOpen: boolean;
handleClosePostbackModal: () => void;
zapierTarget?: LeadTargetModel | null;
postbackTarget?: LeadTargetModel | null;
};
export const PartnersBoard: FC<PartnersBoardProps> = ({
@ -65,6 +68,8 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
setIsPostbackModalOpen,
isPostbackModalOpen,
handleClosePostbackModal,
zapierTarget,
postbackTarget,
}) => {
const theme = useTheme();
const quiz = useCurrentQuiz();
@ -173,6 +178,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
handleCloseModal={handleCloseZapierModal}
companyName={companyName}
quiz={quiz!}
currentTarget={zapierTarget ?? null}
/>
</Suspense>
)}
@ -183,6 +189,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
handleCloseModal={handleClosePostbackModal}
companyName={companyName}
quiz={quiz!}
currentTarget={postbackTarget ?? null}
/>
</Suspense>
)}

@ -0,0 +1,50 @@
import { useCallback, useEffect, useState } from "react";
import { getLeadTargetsByQuiz, LeadTargetModel } from "@/api/leadtarget";
type UseLeadTargetsResult = {
isLoading: boolean;
zapierTarget: LeadTargetModel | null;
postbackTarget: LeadTargetModel | null;
refresh: () => Promise<void>;
};
export function useLeadTargets(quizBackendId: number | undefined, isOpen: boolean): UseLeadTargetsResult {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [zapierTarget, setZapierTarget] = useState<LeadTargetModel | null>(null);
const [postbackTarget, setPostbackTarget] = useState<LeadTargetModel | null>(null);
const load = useCallback(async () => {
if (!quizBackendId) return;
setIsLoading(true);
try {
const [items] = await getLeadTargetsByQuiz(quizBackendId);
const list = items ?? [];
console.log("LeadTargets fetched:", list);
const webhookOnly = list.filter((t) => t.type === "webhook");
console.log("Webhook-only targets:", webhookOnly);
const zapier = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("zapier")) ?? null;
const postback = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("postback")) ?? null;
console.log("Distributed targets:", { zapier, postback });
setZapierTarget(zapier);
setPostbackTarget(postback);
} finally {
setIsLoading(false);
}
}, [quizBackendId]);
useEffect(() => {
if (isOpen) {
load();
}
}, [isOpen, load]);
return {
isLoading,
zapierTarget,
postbackTarget,
refresh: load,
};
}