особые условия для вывода картинок
This commit is contained in:
parent
b0ff595f5d
commit
f4e8898400
@ -1,3 +1,4 @@
|
|||||||
|
1.0.4 _ 2025-09-14 _ особые условия для вывода картинок
|
||||||
1.0.3 _ 2025-09-12 _ среднее время не учитывает нули
|
1.0.3 _ 2025-09-12 _ среднее время не учитывает нули
|
||||||
1.0.2 _ 2025-09-07 _ добавлена автозапись в стейджинг
|
1.0.2 _ 2025-09-07 _ добавлена автозапись в стейджинг
|
||||||
1.0.1 Страница заявок корректно отображает мультиответ
|
1.0.1 Страница заявок корректно отображает мультиответ
|
||||||
|
|||||||
@ -33,11 +33,17 @@ import { ReactComponent as ResetIcon } from "@icons/Analytics/reset.svg";
|
|||||||
import type { Moment } from "moment";
|
import type { Moment } from "moment";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { Quiz } from "@model/quiz/quiz";
|
import type { Quiz } from "@model/quiz/quiz";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
import { useQuestions } from "@/stores/questions/hooks";
|
||||||
|
|
||||||
export default function Analytics() {
|
export default function Analytics() {
|
||||||
const { quizes, editQuizId } = useQuizStore();
|
|
||||||
|
|
||||||
const [quiz, setQuiz] = useState<Quiz>({} as Quiz);
|
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
const globalQuestions = useQuestions({quizId: quiz?.backendId}).questions;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [isOpen, setOpen] = useState<boolean>(false);
|
const [isOpen, setOpen] = useState<boolean>(false);
|
||||||
const [isOpenEnd, setOpenEnd] = useState<boolean>(false);
|
const [isOpenEnd, setOpenEnd] = useState<boolean>(false);
|
||||||
const [from, setFrom] = useState<Moment | null>(null);
|
const [from, setFrom] = useState<Moment | null>(null);
|
||||||
@ -47,29 +53,24 @@ export default function Analytics() {
|
|||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
|
|
||||||
const { devices, general, questions, isLoading } = useAnalytics({
|
const { devices, general, questions, isLoading } = useAnalytics({
|
||||||
ready: Boolean(Object.keys(quiz).length),
|
ready: quiz ? Boolean(Object.keys(quiz).length) : false,
|
||||||
quizId: editQuizId?.toString() || "",
|
quizId: quiz?.backendId?.toString() || "",
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetTime = () => {
|
const resetTime = () => {
|
||||||
setFrom(moment(new Date(quiz.created_at)));
|
setFrom(moment(new Date(quiz?.created_at)));
|
||||||
setTo(moment().add(1, "days"));
|
setTo(moment().add(1, "days"));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (quizes.length > 0) {
|
if (quiz) setFrom(moment(new Date(quiz?.created_at)));
|
||||||
const quiz = quizes.find((q) => q.backendId === editQuizId);
|
}, [quiz]);
|
||||||
if (quiz === undefined) throw new Error("Не удалось получить квиз");
|
|
||||||
setQuiz(quiz);
|
|
||||||
setFrom(moment(new Date(quiz.created_at)));
|
|
||||||
}
|
|
||||||
}, [quizes]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getData = async (): Promise<void> => {
|
const getData = async (): Promise<void> => {
|
||||||
if (editQuizId !== null) {
|
if (quiz?.backendId !== null) {
|
||||||
const [gottenQuizes, gottenQuizesError] = await quizApi.getList();
|
const [gottenQuizes, gottenQuizesError] = await quizApi.getList();
|
||||||
|
|
||||||
if (gottenQuizesError) {
|
if (gottenQuizesError) {
|
||||||
@ -86,8 +87,8 @@ export default function Analytics() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (editQuizId === undefined) redirect("/list");
|
if (quiz?.backendId === undefined) redirect("/list");
|
||||||
}, [editQuizId]);
|
}, [quiz?.backendId]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@ -264,7 +265,7 @@ export default function Analytics() {
|
|||||||
data={general}
|
data={general}
|
||||||
day={86400 - moment(to).unix() - moment(from).unix() > 0}
|
day={86400 - moment(to).unix() - moment(from).unix() > 0}
|
||||||
/>
|
/>
|
||||||
<AnswersStatistics data={questions} />
|
<AnswersStatistics data={questions} globalQuestions={globalQuestions}/>
|
||||||
<Devices data={devices} />
|
<Devices data={devices} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { PaginationRenderItemParams } from "@mui/material";
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ButtonBase,
|
ButtonBase,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Pagination as MuiPagination,
|
Pagination as MuiPagination,
|
||||||
@ -19,6 +20,10 @@ import { ReactComponent as LeftArrowIcon } from "@icons/Analytics/leftArrow.svg"
|
|||||||
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
|
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
|
||||||
import { extractOrder } from "@utils/extractOrder";
|
import { extractOrder } from "@utils/extractOrder";
|
||||||
import { parseTitle } from "../utils/parseTitle";
|
import { parseTitle } from "../utils/parseTitle";
|
||||||
|
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
|
||||||
|
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||||
|
import { timewebContent, timewebContentFile } from "@/pages/QuizAnswersPage/cardAnswers/helper";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
|
||||||
type AnswerProps = {
|
type AnswerProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -28,6 +33,7 @@ type AnswerProps = {
|
|||||||
|
|
||||||
type AnswersProps = {
|
type AnswersProps = {
|
||||||
data: Record<string, Record<string, number>> | null;
|
data: Record<string, Record<string, number>> | null;
|
||||||
|
globalQuestions: AnyTypedQuizQuestion[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaginationProps = {
|
type PaginationProps = {
|
||||||
@ -40,8 +46,13 @@ const Answer = ({ title, percent, highlight }: AnswerProps) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const parsedTitle = parseTitle(title);
|
const parsedTitle = parseTitle(title);
|
||||||
|
|
||||||
|
console.log("Привет, я Answer. И вот что я о себе знаю:")
|
||||||
|
console.log("-------------------------------------------------------------------")
|
||||||
|
console.log("{ title, percent, highlight }")
|
||||||
|
console.log({ title, percent, highlight })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ padding: "15px 25px" }}>
|
<Box sx={{ padding: "15px 25px", width: "100%" }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@ -199,7 +210,8 @@ const Pagination = ({ page, setPage, pagesAmount }: PaginationProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Answers: FC<AnswersProps> = ({ data }) => {
|
export const Answers: FC<AnswersProps> = ({ data, globalQuestions }) => {
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const answers = useMemo(() => {
|
const answers = useMemo(() => {
|
||||||
@ -209,17 +221,32 @@ export const Answers: FC<AnswersProps> = ({ data }) => {
|
|||||||
);
|
);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
const currentAnswer = answers[page - 1];
|
const currentAnswer = answers[page - 1];
|
||||||
|
const globalQuestion = globalQuestions.find(e => {
|
||||||
|
console.log("---")
|
||||||
|
console.log("Привет, я ищу глоб вопро ", currentAnswer[0].slice(0, -4))
|
||||||
|
console.log(" c ", e.title)
|
||||||
|
console.log("---")
|
||||||
|
console.log(e.title === currentAnswer[0].slice(0, -4))
|
||||||
|
return e.title === currentAnswer[0].slice(0, -4)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("globalQuestionglobalQuestionglobalQuestionglobalQuestionglobalQuestionglobalQuestion")
|
||||||
|
console.log(globalQuestion)
|
||||||
|
|
||||||
const percentsSum = Object.values(currentAnswer?.[1] ?? {}).reduce(
|
const percentsSum = Object.values(currentAnswer?.[1] ?? {}).reduce(
|
||||||
(total, item) => (total += item),
|
(total, item) => (total += item),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("currentAnswer")
|
||||||
|
console.log(currentAnswer)
|
||||||
const currentAnswerExtended =
|
const currentAnswerExtended =
|
||||||
percentsSum >= 100
|
percentsSum >= 100
|
||||||
? Object.entries(currentAnswer?.[1] ?? {})
|
? Object.entries(currentAnswer?.[1] ?? {})
|
||||||
: [
|
: [
|
||||||
...Object.entries(currentAnswer?.[1] ?? {}),
|
...Object.entries(currentAnswer?.[1] ?? {}),
|
||||||
["Другое", 100 - percentsSum] as [string, number],
|
["Другое", 100 - percentsSum] as [string, number],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
@ -286,16 +313,92 @@ export const Answers: FC<AnswersProps> = ({ data }) => {
|
|||||||
<NextIcon />
|
<NextIcon />
|
||||||
</ButtonBase> */}
|
</ButtonBase> */}
|
||||||
</Box>
|
</Box>
|
||||||
{currentAnswerExtended.map(([title, percent], index) => (
|
{currentAnswerExtended.map(([title, percent], index) => {
|
||||||
<Answer
|
console.log("kdgjhskjdfhkhsdgksfdhgjsdhfgkjsfdhgkj")
|
||||||
key={index}
|
console.log("ewrqwrwr")
|
||||||
title={title}
|
console.log("checkFileExtension")
|
||||||
percent={percent}
|
console.log(checkFileExtension(title))
|
||||||
highlight={!index}
|
console.log("quiz?.backendId")
|
||||||
/>
|
console.log(quiz?.backendId)
|
||||||
))}
|
console.log("globalQuestion?.backendId")
|
||||||
</Paper>
|
console.log(globalQuestion?.backendId)
|
||||||
|
console.log("data")
|
||||||
|
console.log(data)
|
||||||
|
if (checkFileExtension(title) && quiz?.backendId && globalQuestion?.backendId) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline - flex",
|
||||||
|
width: "100 %",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0 48px",
|
||||||
|
paddingLeft: "26px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
// <IconButton target="_blank" href={`/image/${title}`}
|
||||||
|
sx={{
|
||||||
|
width: "50px",
|
||||||
|
height: "40px",
|
||||||
|
bgcolor: "#e9eaf0",
|
||||||
|
borderRadius: "5px"
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log(timewebContentFile(quiz?.qid.toString(), title, globalQuestion?.backendId.toString()))
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = timewebContentFile(quiz?.qid.toString(), title, globalQuestion?.backendId.toString());
|
||||||
|
link.download = title
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InsertDriveFileIcon />
|
||||||
|
</IconButton>
|
||||||
|
< Answer
|
||||||
|
key={index}
|
||||||
|
title={title}
|
||||||
|
percent={percent}
|
||||||
|
highlight={!index}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
< Answer
|
||||||
|
key={index}
|
||||||
|
title={title}
|
||||||
|
percent={percent}
|
||||||
|
highlight={!index}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Paper >
|
||||||
<Pagination page={page} setPage={setPage} pagesAmount={answers.length} />
|
<Pagination page={page} setPage={setPage} pagesAmount={answers.length} />
|
||||||
</Box>
|
</Box >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
function checkFileExtension(filename: string, maxLength = 6) {
|
||||||
|
if (typeof filename !== 'string') return false;
|
||||||
|
|
||||||
|
// Ищем последнюю точку в строке
|
||||||
|
const lastDotIndex = filename.lastIndexOf('.');
|
||||||
|
|
||||||
|
// Если точки нет или она в конце строки - возвращаем false
|
||||||
|
if (lastDotIndex === -1 || lastDotIndex === filename.length - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем расширение (часть после последней точки)
|
||||||
|
const extension = filename.slice(lastDotIndex + 1);
|
||||||
|
|
||||||
|
// Проверяем что расширение состоит только из букв и не превышает максимальную длину
|
||||||
|
return /^[a-zA-Zа-яА-ЯёЁ]+$/.test(extension) && extension.length <= maxLength;
|
||||||
|
}
|
||||||
@ -5,12 +5,14 @@ import { QuestionsResponse } from "@api/statistic";
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Funnel } from "./FunnelAnswers/Funnel";
|
import { Funnel } from "./FunnelAnswers/Funnel";
|
||||||
import { Results } from "./Results";
|
import { Results } from "./Results";
|
||||||
|
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
|
||||||
|
|
||||||
type AnswersStatisticsProps = {
|
type AnswersStatisticsProps = {
|
||||||
data: QuestionsResponse | null;
|
data: QuestionsResponse | null;
|
||||||
|
globalQuestions: AnyTypedQuizQuestion[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnswersStatistics: FC<AnswersStatisticsProps> = ({ data }) => {
|
export const AnswersStatistics: FC<AnswersStatisticsProps> = ({ data, globalQuestions }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
|
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(850));
|
const isMobile = useMediaQuery(theme.breakpoints.down(850));
|
||||||
@ -33,7 +35,7 @@ export const AnswersStatistics: FC<AnswersStatisticsProps> = ({ data }) => {
|
|||||||
gap: "40px",
|
gap: "40px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Answers data={data?.Questions || null} />
|
<Answers data={data?.Questions || null} globalQuestions={globalQuestions} />
|
||||||
<Funnel data={data?.Funnel || []} funnelData={data?.FunnelData || []} />
|
<Funnel data={data?.Funnel || []} funnelData={data?.FunnelData || []} />
|
||||||
</Box>
|
</Box>
|
||||||
<Results data={data?.Results || null} />
|
<Results data={data?.Results || null} />
|
||||||
|
|||||||
@ -8,13 +8,20 @@ import { useQuestionsStore } from "./store";
|
|||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function useQuestions() {
|
export function useQuestions({ quizId }: { quizId?: number } = {}) {
|
||||||
const quiz = useCurrentQuiz();
|
const currentQuiz = useCurrentQuiz();
|
||||||
const { isLoading, error, isValidating } = useSWR(
|
const currentQuizId = quizId ?? currentQuiz?.backendId;
|
||||||
["questions", quiz?.backendId],
|
|
||||||
|
const { data, isLoading, error, isValidating } = useSWR(
|
||||||
|
currentQuizId ? ["questions", currentQuizId] : null,
|
||||||
([, id]) => questionApi.getList({ quiz_id: id }),
|
([, id]) => questionApi.getList({ quiz_id: id }),
|
||||||
{
|
{
|
||||||
onSuccess: ([questions]) => setQuestions(questions),
|
onSuccess: (data) => {
|
||||||
|
// Добавляем проверку на существование данных
|
||||||
|
if (data && Array.isArray(data[0])) {
|
||||||
|
setQuestions(data[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const message = isAxiosError<string>(error)
|
const message = isAxiosError<string>(error)
|
||||||
? error.response?.data ?? ""
|
? error.response?.data ?? ""
|
||||||
@ -25,7 +32,13 @@ export function useQuestions() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const questions = useQuestionsStore((state) => state.questions);
|
const questions = useQuestionsStore((state) => state.questions);
|
||||||
|
|
||||||
return { questions, isLoading, error, isValidating };
|
return {
|
||||||
|
questions: questions || [], // Гарантируем возврат массива
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isValidating
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user