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 type LeadTargetType = "mail" | "telegram" | "whatsapp" | "webhook";
export interface LeadTargetModel { export interface LeadTargetModel {
ID: number; id: number;
AccountID: string; accountID: string;
Type: LeadTargetType; type: LeadTargetType;
QuizID: number; quizID: number;
Target: string; target: string; // содержит подстроку "zapier" или "postback"
InviteLink?: string; inviteLink?: string;
Deleted?: boolean; deleted?: boolean;
CreatedAt?: string; createdAt?: string;
} }
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`; 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 CloseIcon from "@mui/icons-material/Close";
import { Quiz } from "@/model/quiz/quiz"; import { Quiz } from "@/model/quiz/quiz";
import CustomTextField from "@/ui_kit/CustomTextField"; 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 { useFormik } from "formik";
import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink"; import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useLeadTargets } from "@/pages/IntegrationsPage/hooks/useLeadTargets";
type PostbackModalProps = { type PostbackModalProps = {
isModalOpen: boolean; isModalOpen: boolean;
@ -27,28 +28,26 @@ export const PostbackModal: FC<PostbackModalProps> = ({
const [isSaving, setIsSaving] = useState<boolean>(false); const [isSaving, setIsSaving] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar(); 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 handleSubmit = async (values: { token: string; domain: string }) => {
const tokenValue = (values.token || "").trim(); const tokenValue = (values.token || "").trim();
const target = (values.domain || "").trim(); const target = (values.domain || "").trim();
try { try {
setIsSaving(true); 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) { if (!tokenValue && !target) {
await deleteLeadTargetsByQuizType(quiz.backendId, "webhook"); const deletePromises = existing.map((t) => deleteLeadTarget(t.id));
await Promise.all(deletePromises);
enqueueSnackbar("Postback удален", { variant: "success" }); 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 { } else {
await createLeadTarget({ await createLeadTarget({
type: "webhook", type: "webhook",
@ -58,6 +57,8 @@ export const PostbackModal: FC<PostbackModalProps> = ({
}); });
enqueueSnackbar("Postback сохранен", { variant: "success" }); enqueueSnackbar("Postback сохранен", { variant: "success" });
} }
await refresh();
} catch (error) { } catch (error) {
enqueueSnackbar("Ошибка при сохранении", { variant: "error" }); enqueueSnackbar("Ошибка при сохранении", { variant: "error" });
} finally { } 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 }>({ const formik = useFormik<{ token: string; domain: string }>({
initialValues: { token: "", domain: "" }, initialValues: { token: "", domain: postbackTarget?.target ?? "" },
onSubmit: handleSubmit, onSubmit: handleSubmit,
}); });
useEffect(() => {
formik.setFieldValue("domain", postbackTarget?.target ?? "");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [postbackTarget?.target]);
useEffect(() => { useEffect(() => {
if (isModalOpen) { if (isModalOpen) {
setTimeout(() => { setTimeout(() => {
@ -153,6 +161,9 @@ export const PostbackModal: FC<PostbackModalProps> = ({
> >
Домен Домен
</Typography> </Typography>
{isLoading ? (
<Box sx={{ width: "100%", height: 44, borderRadius: "8px", bgcolor: "action.hover" }} />
) : (
<CustomTextField <CustomTextField
id="postback-domain" id="postback-domain"
placeholder="токен в формате ХХХХХХ" placeholder="токен в формате ХХХХХХ"
@ -160,10 +171,11 @@ export const PostbackModal: FC<PostbackModalProps> = ({
onChange={(e) => formik.setFieldValue("domain", e.target.value)} onChange={(e) => formik.setFieldValue("domain", e.target.value)}
maxLength={150} maxLength={150}
/> />
)}
</Box> </Box>
<Button <Button
disabled={isSaving} disabled={isSaving || isLoading}
type="submit" type="submit"
variant="contained" variant="contained"
sx={{ sx={{

@ -3,10 +3,11 @@ import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button }
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { Quiz } from "@/model/quiz/quiz"; import { Quiz } from "@/model/quiz/quiz";
import CustomTextField from "@/ui_kit/CustomTextField"; 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 { useFormik } from "formik";
import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink"; import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useLeadTargets } from "@/pages/IntegrationsPage/hooks/useLeadTargets";
type ZapierModalProps = { type ZapierModalProps = {
isModalOpen: boolean; isModalOpen: boolean;
@ -27,28 +28,30 @@ export const ZapierModal: FC<ZapierModalProps> = ({
const [isSaving, setIsSaving] = useState<boolean>(false); const [isSaving, setIsSaving] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar(); 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 handleSubmit = async (values: { webhookUrl: string }) => {
const target = (values.webhookUrl || "").trim(); const target = (values.webhookUrl || "").trim();
try { try {
setIsSaving(true); 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) { if (!target) {
await deleteLeadTargetsByQuizType(quiz.backendId, "webhook"); // Пустое значение — удаляем все
const deletePromises = existing.map((t) => deleteLeadTarget(t.id));
await Promise.all(deletePromises);
enqueueSnackbar("Webhook удален", { variant: "success" }); 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 { } else {
// Не существует — создаем
await createLeadTarget({ await createLeadTarget({
type: "webhook", type: "webhook",
quizID: quiz.backendId, quizID: quiz.backendId,
@ -56,6 +59,8 @@ export const ZapierModal: FC<ZapierModalProps> = ({
}); });
enqueueSnackbar("Webhook сохранен", { variant: "success" }); enqueueSnackbar("Webhook сохранен", { variant: "success" });
} }
await refresh();
} catch (error) { } catch (error) {
enqueueSnackbar("Ошибка при сохранении", { variant: "error" }); enqueueSnackbar("Ошибка при сохранении", { variant: "error" });
} finally { } finally {
@ -63,11 +68,18 @@ export const ZapierModal: FC<ZapierModalProps> = ({
} }
}; };
const { isLoading, zapierTarget, refresh } = useLeadTargets(quiz?.backendId, isModalOpen);
const formik = useFormik<{ webhookUrl: string }>({ const formik = useFormik<{ webhookUrl: string }>({
initialValues: { webhookUrl: "" }, initialValues: { webhookUrl: zapierTarget?.target ?? "" },
onSubmit: handleSubmit, onSubmit: handleSubmit,
}); });
useEffect(() => {
formik.setFieldValue("webhookUrl", zapierTarget?.target ?? "");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [zapierTarget?.target]);
useEffect(() => { useEffect(() => {
if (isModalOpen) { if (isModalOpen) {
setTimeout(() => { setTimeout(() => {
@ -160,6 +172,9 @@ export const ZapierModal: FC<ZapierModalProps> = ({
gap: isMobile ? "10px" : "38px" gap: isMobile ? "10px" : "38px"
}} }}
> >
{isLoading ? (
<Box sx={{ width: "100%", height: 44, borderRadius: "8px", bgcolor: "action.hover" }} />
) : (
<CustomTextField <CustomTextField
id="zapier-webhook-url" id="zapier-webhook-url"
placeholder="введите url здесь" placeholder="введите url здесь"
@ -167,8 +182,9 @@ export const ZapierModal: FC<ZapierModalProps> = ({
onChange={(e) => formik.setFieldValue("webhookUrl", e.target.value)} onChange={(e) => formik.setFieldValue("webhookUrl", e.target.value)}
maxLength={150} maxLength={150}
/> />
)}
<Button <Button
disabled={isSaving} disabled={isSaving || isLoading}
type="submit" type="submit"
variant="contained" variant="contained"
sx={{ sx={{

@ -5,7 +5,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuizStore } from "@root/quizes/store"; import { useQuizStore } from "@root/quizes/store";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { PartnersBoard } from "./PartnersBoard/PartnersBoard"; import { PartnersBoard } from "./PartnersBoard/PartnersBoard";
import { getLeadTargetsByQuiz } from "@/api/leadtarget"; import { getLeadTargetsByQuiz, LeadTargetModel } from "@/api/leadtarget";
import { QuizMetricType } from "@model/quizSettings"; import { QuizMetricType } from "@model/quizSettings";
interface IntegrationsPageProps { interface IntegrationsPageProps {
@ -31,7 +31,9 @@ export const IntegrationsPage = ({
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false); const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false); const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
const [leadTargetsLoaded, setLeadTargetsLoaded] = 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(() => { useEffect(() => {
@ -39,16 +41,34 @@ export const IntegrationsPage = ({
}, [navigate, editQuizId]); }, [navigate, editQuizId]);
useEffect(() => { useEffect(() => {
// Загрузка связанных с квизом данных интеграций при входе на страницу
const load = async () => { const load = async () => {
if (!leadTargetsLoaded && quiz?.id) { if (!leadTargetsLoaded && quiz?.id) {
const [items] = await getLeadTargetsByQuiz(quiz.backendId); 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); setLeadTargetsLoaded(true);
} }
}; };
load(); load();
}, [leadTargetsLoaded, quiz?.id]); }, [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; const heightBar = heightSidebar + 51 + 88 + 36 + 25;
if (quiz === undefined) if (quiz === undefined)
@ -103,6 +123,8 @@ export const IntegrationsPage = ({
setIsPostbackModalOpen={setIsPostbackModalOpen} setIsPostbackModalOpen={setIsPostbackModalOpen}
isPostbackModalOpen={isPostbackModalOpen} isPostbackModalOpen={isPostbackModalOpen}
handleClosePostbackModal={handleClosePostbackModal} handleClosePostbackModal={handleClosePostbackModal}
zapierTarget={zapierTarget}
postbackTarget={postbackTarget}
/> />
</Box> </Box>
</> </>

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