From 751d9eb4f31f5272f2eade689e8d7b43c9335b3c Mon Sep 17 00:00:00 2001 From: Nastya Date: Wed, 3 Sep 2025 06:03:45 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D0=B5=D1=80=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D1=82=D0=B1=D0=B5=D0=BA=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D1=8B=20=D0=BA=20api=20+?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B1=D0=B8=D0=BB=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Analytics/Analytics.tsx | 21 ++- src/pages/Analytics/AnalyticsSkeleton.tsx | 102 ++++++++++++ .../InstructionYoutubeLink.tsx | 36 +++++ .../IntegrationsModal/Postback/index.tsx | 146 ++++++++++-------- .../IntegrationsModal/Zapier/index.tsx | 145 +++++++++++------ src/utils/hooks/useAnalytics.ts | 39 +++-- 6 files changed, 352 insertions(+), 137 deletions(-) create mode 100644 src/pages/Analytics/AnalyticsSkeleton.tsx create mode 100644 src/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink.tsx diff --git a/src/pages/Analytics/Analytics.tsx b/src/pages/Analytics/Analytics.tsx index c64912f6..6a5c2751 100644 --- a/src/pages/Analytics/Analytics.tsx +++ b/src/pages/Analytics/Analytics.tsx @@ -18,6 +18,7 @@ import SectionWrapper from "@ui_kit/SectionWrapper"; import { General } from "./General"; import { AnswersStatistics } from "./Answers/AnswersStatistics"; import { Devices } from "./Devices"; +import AnalyticsSkeleton from "./AnalyticsSkeleton"; import { setQuizes } from "@root/quizes/actions"; import { useQuizStore } from "@root/quizes/store"; @@ -45,7 +46,7 @@ export default function Analytics() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); - const { devices, general, questions } = useAnalytics({ + const { devices, general, questions, isLoading } = useAnalytics({ ready: Boolean(Object.keys(quiz).length), quizId: editQuizId?.toString() || "", from, @@ -255,12 +256,18 @@ export default function Analytics() { {isMobile ? : "Сбросить"} - 0} - /> - - + {isLoading ? ( + + ) : ( + <> + 0} + /> + + + + )} ); diff --git a/src/pages/Analytics/AnalyticsSkeleton.tsx b/src/pages/Analytics/AnalyticsSkeleton.tsx new file mode 100644 index 00000000..71b4886b --- /dev/null +++ b/src/pages/Analytics/AnalyticsSkeleton.tsx @@ -0,0 +1,102 @@ +import { Box, Paper, Skeleton, Typography, useMediaQuery, useTheme } from "@mui/material"; + +export default function AnalyticsSkeleton() { + const theme = useTheme(); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + const isMobile = useMediaQuery(theme.breakpoints.down(700)); + + const card = ( + + + + + + ); + + return ( + + + + Ключевые метрики + + + {card} + {card} + {card} + {card} + + + + + + Статистика по ответам + + + + + + + + + + + + + + + {[...Array(4)].map((_, idx) => ( + + + + ))} + + + + + + + Статистика пользователей + + + {[...Array(3)].map((_, i) => ( + + + + + {[...Array(4)].map((_, idx) => ( + + + + + ))} + + + ))} + + + + ); +} + + diff --git a/src/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink.tsx b/src/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink.tsx new file mode 100644 index 00000000..4c085ad3 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink.tsx @@ -0,0 +1,36 @@ +import { FC } from "react"; +import { Box, Link, BoxProps } from "@mui/material"; +import OrangeYoutube from "@/assets/icons/OrangeYoutube"; + +interface InstructionYoutubeLinkProps extends BoxProps {} + +const InstructionYoutubeLink: FC = ({ sx, ...props }) => { + return ( + + + Смотреть инструкцию + + + ); +}; + +export default InstructionYoutubeLink; \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx index 4a701275..7e61decb 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/Postback/index.tsx @@ -1,10 +1,12 @@ -import { FC, useState } from "react"; -import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button, Link } from "@mui/material"; +import { FC, useState, useEffect } from "react"; +import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button } 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"; +import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget } from "@/api/leadtarget"; +import { useFormik } from "formik"; +import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink"; +import { useSnackbar } from "notistack"; type PostbackModalProps = { isModalOpen: boolean; @@ -22,8 +24,60 @@ export const PostbackModal: FC = ({ const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - const [token, setToken] = useState(""); - const [domain, setDomain] = useState(""); + const [isSaving, setIsSaving] = useState(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); + if (!tokenValue && !target) { + await deleteLeadTargetsByQuizType(quiz.backendId, "webhook"); + enqueueSnackbar("Postback удален", { variant: "success" }); + } else { + await createLeadTarget({ + type: "webhook", + quizID: quiz.backendId, + target, + name: tokenValue || undefined, + }); + enqueueSnackbar("Postback сохранен", { variant: "success" }); + } + } catch (error) { + enqueueSnackbar("Ошибка при сохранении", { variant: "error" }); + } finally { + setIsSaving(false); + } + }; + + const formik = useFormik<{ token: string; domain: string }>({ + initialValues: { token: "", domain: "" }, + onSubmit: handleSubmit, + }); + + useEffect(() => { + if (isModalOpen) { + setTimeout(() => { + const input = document.getElementById("postback-domain") as HTMLInputElement | null; + input?.focus(); + }, 0); + } + }, [isModalOpen]); return ( = ({ PaperProps={{ sx: { maxWidth: isTablet ? "100%" : "919px", - height: "314px", + height: isMobile ? "303px" : "214px", + // height: "314px", borderRadius: "12px", }, }} @@ -69,63 +124,30 @@ export const PostbackModal: FC = ({ overflow: "auto", }} > - - Смотреть инструкцию - + {!isMobile && } - Токен авторизации - - setToken(e.target.value)} - maxLength={150} - /> - @@ -134,30 +156,21 @@ export const PostbackModal: FC = ({ setDomain(e.target.value)} + value={formik.values.domain} + onChange={(e) => formik.setFieldValue("domain", e.target.value)} maxLength={150} /> + {isMobile && } ); diff --git a/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx b/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx index 627f202a..d96079dd 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/Zapier/index.tsx @@ -1,10 +1,12 @@ -import { FC, useState } from "react"; -import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Link, Button } from "@mui/material"; +import { FC, useState, useEffect } from "react"; +import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, 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"; +import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget } from "@/api/leadtarget"; +import { useFormik } from "formik"; +import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink"; +import { useSnackbar } from "notistack"; type ZapierModalProps = { isModalOpen: boolean; @@ -22,7 +24,58 @@ export const ZapierModal: FC = ({ const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - const [webhookUrl, setWebhookUrl] = useState(""); + const [isSaving, setIsSaving] = useState(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); + if (!target) { + await deleteLeadTargetsByQuizType(quiz.backendId, "webhook"); + enqueueSnackbar("Webhook удален", { variant: "success" }); + } else { + await createLeadTarget({ + type: "webhook", + quizID: quiz.backendId, + target, + }); + enqueueSnackbar("Webhook сохранен", { variant: "success" }); + } + } catch (error) { + enqueueSnackbar("Ошибка при сохранении", { variant: "error" }); + } finally { + setIsSaving(false); + } + }; + + const formik = useFormik<{ webhookUrl: string }>({ + initialValues: { webhookUrl: "" }, + onSubmit: handleSubmit, + }); + + useEffect(() => { + if (isModalOpen) { + setTimeout(() => { + const input = document.getElementById("zapier-webhook-url") as HTMLInputElement | null; + input?.focus(); + }, 0); + } + }, [isModalOpen]); return ( = ({ PaperProps={{ sx: { maxWidth: isTablet ? "100%" : "919px", - height: "195px", + height: isMobile ? "303px" : "195px", borderRadius: "12px", }, }} @@ -62,6 +115,7 @@ export const ZapierModal: FC = ({ + = ({ overflow: "auto", }} > + {!isMobile && } + - Смотреть инструкцию - - - - - URL webhook - + + URL webhook + + + setWebhookUrl(e.target.value)} + value={formik.values.webhookUrl} + onChange={(e) => formik.setFieldValue("webhookUrl", e.target.value)} maxLength={150} /> + {isMobile && } diff --git a/src/utils/hooks/useAnalytics.ts b/src/utils/hooks/useAnalytics.ts index 7cb80665..30c0ea5d 100644 --- a/src/utils/hooks/useAnalytics.ts +++ b/src/utils/hooks/useAnalytics.ts @@ -25,36 +25,47 @@ export function useAnalytics({ ready, quizId, to, from }: useAnalyticsProps) { const [devices, setDevices] = useState(null); const [general, setGeneral] = useState(null); const [questions, setQuestions] = useState(null); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - if (!quizId || !ready) return; + if (!quizId || !ready) { + setIsLoading(true); + return; + } const requestStatistics = async () => { if (!formatTo || !formatFrom) { + setIsLoading(true); return; } - const [gottenGeneral] = await getGeneral(quizId, formatTo, formatFrom); - const [gottenDevices] = await getDevices(quizId, formatTo, formatFrom); - const [gottenQuestions] = await getQuestions(quizId, formatTo, formatFrom); + setIsLoading(true); - getGraphics(quizId, formatTo, formatFrom) + try { + const [gottenGeneral] = await getGeneral(quizId, formatTo, formatFrom); + const [gottenDevices] = await getDevices(quizId, formatTo, formatFrom); + const [gottenQuestions] = await getQuestions(quizId, formatTo, formatFrom); - if (gottenGeneral) { - setGeneral(gottenGeneral); - } + getGraphics(quizId, formatTo, formatFrom); - if (gottenDevices) { - setDevices(gottenDevices); - } + if (gottenGeneral) { + setGeneral(gottenGeneral); + } - if (gottenQuestions) { - setQuestions(gottenQuestions); + if (gottenDevices) { + setDevices(gottenDevices); + } + + if (gottenQuestions) { + setQuestions(gottenQuestions); + } + } finally { + setIsLoading(false); } }; requestStatistics(); }, [ready, quizId, to, from]); - return { devices, general, questions }; + return { devices, general, questions, isLoading }; }