Merge branch 'analytics' into dev
@ -5,22 +5,27 @@ import { parseAxiosError } from "@utils/parse-error";
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic";
|
||||
|
||||
export type DevicesResponse = {
|
||||
device: Record<string, number>;
|
||||
os: Record<string, number>;
|
||||
browser: Record<string, number>;
|
||||
Device: Record<string, number>;
|
||||
OS: Record<string, number>;
|
||||
Browser: Record<string, number>;
|
||||
};
|
||||
|
||||
export type GeneralResponse = {
|
||||
open: Record<string, number>;
|
||||
result: Record<string, number>;
|
||||
avtime: Record<string, number>;
|
||||
conversation: Record<string, number>;
|
||||
Open: Record<string, number>;
|
||||
Result: Record<string, number>;
|
||||
AvTime: Record<string, number>;
|
||||
Conversion: Record<string, number>;
|
||||
};
|
||||
|
||||
export type QuestionsResponse = {
|
||||
funnel: number[];
|
||||
results: Record<string, number>;
|
||||
questions: Record<string, Record<string, number>>;
|
||||
Funnel: number[];
|
||||
Results: Record<string, number>;
|
||||
Questions: Record<string, Record<string, number>>;
|
||||
};
|
||||
|
||||
type TRequest = {
|
||||
to: number;
|
||||
from: number;
|
||||
};
|
||||
|
||||
export const getDevices = async (
|
||||
@ -29,7 +34,7 @@ export const getDevices = async (
|
||||
from: number,
|
||||
): Promise<[DevicesResponse | null, string?]> => {
|
||||
try {
|
||||
const devicesResponse = await makeRequest<unknown, DevicesResponse>({
|
||||
const devicesResponse = await makeRequest<TRequest, DevicesResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/devices`,
|
||||
withCredentials: true,
|
||||
@ -50,7 +55,7 @@ export const getGeneral = async (
|
||||
from: number,
|
||||
): Promise<[GeneralResponse | null, string?]> => {
|
||||
try {
|
||||
const generalResponse = await makeRequest<unknown, GeneralResponse>({
|
||||
const generalResponse = await makeRequest<TRequest, GeneralResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/general`,
|
||||
withCredentials: true,
|
||||
@ -71,7 +76,7 @@ export const getQuestions = async (
|
||||
from: number,
|
||||
): Promise<[QuestionsResponse | null, string?]> => {
|
||||
try {
|
||||
const questionsResponse = await makeRequest<unknown, QuestionsResponse>({
|
||||
const questionsResponse = await makeRequest<TRequest, QuestionsResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/questions`,
|
||||
withCredentials: true,
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
d="M18.7891 11.2006L14.526 15.4943M10.3154 15.2084L14.526 19.2993L22.7891 10.7006M7.21053 15.4144L11 19.0962"
|
||||
stroke="#FC712F"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 395 B After Width: | Height: | Size: 394 B |
@ -1,3 +1,3 @@
|
||||
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 1.5L1 6L6 10.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 1.5L1 6L6 10.5" stroke="white" stroke-width="1.5" strokeLinecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 211 B |
@ -10,14 +10,14 @@
|
||||
d="M11.5 10.5L15.5 15L11.5 19.5"
|
||||
stroke="#FC712F"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.5 10.5L19.5 15L15.5 19.5"
|
||||
stroke="#FC712F"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 466 B |
@ -1,3 +1,3 @@
|
||||
<svg width="14" height="7" viewBox="0 0 14 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1L7 6L13 1" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 1L7 6L13 1" stroke="#7E2AEA" stroke-width="1.5" strokeLinecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 210 B After Width: | Height: | Size: 209 B |
4
src/assets/icons/Analytics/reset.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.8955 10.1257H22.7705V5.25073" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.3174 19.3172C18.0677 20.5679 16.4752 21.4198 14.7413 21.7652C13.0074 22.1107 11.21 21.9341 9.57649 21.2579C7.94295 20.5816 6.54667 19.4361 5.56428 17.9662C4.5819 16.4962 4.05754 14.768 4.05754 13C4.05754 11.232 4.5819 9.50376 5.56428 8.03384C6.54667 6.56391 7.94295 5.41838 9.57649 4.74213C11.21 4.06589 13.0074 3.88932 14.7413 4.23477C16.4752 4.58022 18.0677 5.43216 19.3174 6.68282L22.7705 10.1258" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@ -1,3 +1,3 @@
|
||||
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L6 6L1 10.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 1.5L6 6L1 10.5" stroke="white" stroke-width="1.5" strokeLinecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 211 B |
@ -14,21 +14,21 @@ export default function ChartLineUp(sx: SxProps<Theme>) {
|
||||
d="M21 19.5H3V4.5"
|
||||
stroke="#7E2AEA"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M19.5 6L12 13.5L9 10.5L3 16.5"
|
||||
stroke="#7E2AEA"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M19.5 9.75V6H15.75"
|
||||
stroke="#7E2AEA"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;stroke:#7E2AEA;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#7E2AEA;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:none;stroke:#7E2AEA;stroke-width:2;strokeLinecap:round;stroke-linejoin:round;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M6.3,24.7C6.3,14.4,14.7,6,25.1,6s18.8,8.4,18.8,18.8s-8.4,18.8-18.8,18.8S6.3,35.1,6.3,24.7z"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 724 B |
@ -4,7 +4,7 @@
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;stroke:#7E2AEA;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#7E2AEA;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:none;stroke:#7E2AEA;stroke-width:2;strokeLinecap:round;stroke-linejoin:round;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M43.8,24.7c0,10.4-8.4,18.8-18.8,18.8S6.3,35.1,6.3,24.7S14.7,6,25.1,6S43.8,14.4,43.8,24.7z"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 724 B After Width: | Height: | Size: 723 B |
@ -1,63 +1,78 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { ReactNode, useEffect, useLayoutEffect, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Paper,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import { LineChart } from "@mui/x-charts";
|
||||
import { redirect } from "react-router-dom";
|
||||
import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
|
||||
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
||||
import moment from "moment";
|
||||
import { useQuizStore } from "@root/quizes/store";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { useAnalytics } from "@utils/hooks/useAnalytics";
|
||||
|
||||
import HeaderFull from "@ui_kit/Header/HeaderFull";
|
||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||
|
||||
import { General } from "./General";
|
||||
import { AnswersStatistics } from "./Answers";
|
||||
import { AnswersStatistics } from "./Answers/AnswersStatistics";
|
||||
import { Devices } from "./Devices";
|
||||
|
||||
import CalendarIcon from "@icons/CalendarIcon";
|
||||
import { redirect } from "react-router-dom";
|
||||
import { ReactComponent as ResetIcon } from "@icons/Analytics/reset.svg";
|
||||
|
||||
import type { Moment } from "moment";
|
||||
|
||||
export default function Analytics() {
|
||||
const quiz = useCurrentQuiz();
|
||||
const { editQuizId } = useQuizStore();
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [isOpenEnd, setOpenEnd] = useState(false);
|
||||
const [to, setTo] = useState(null);
|
||||
const [from, setFrom] = useState(null);
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
const [isOpenEnd, setOpenEnd] = useState<boolean>(false);
|
||||
const [from, setFrom] = useState<Moment | null>(null);
|
||||
const [to, setTo] = useState<Moment | null>(moment(Date.now()));
|
||||
|
||||
const { devices, general, questions } = useAnalytics({
|
||||
quizId: editQuizId?.toString(),
|
||||
to,
|
||||
from,
|
||||
});
|
||||
|
||||
const resetTime = () => {
|
||||
setTo(null);
|
||||
setFrom(null);
|
||||
};
|
||||
useLayoutEffect(() => {
|
||||
if (editQuizId === undefined) redirect("/list");
|
||||
}, [editQuizId]);
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
|
||||
const { devices, general, questions } = useAnalytics({
|
||||
quizId: editQuizId?.toString() || "",
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
const resetTime = () => {
|
||||
setFrom(moment(0));
|
||||
setTo(moment(Date.now()));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (quiz) {
|
||||
console.log(moment(new Date(quiz.created_at)));
|
||||
setFrom(moment(new Date(quiz.created_at)));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (editQuizId === undefined) redirect("/list");
|
||||
}, [editQuizId]);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const onAdornmentClick = () => {
|
||||
setOpen((old) => !old);
|
||||
if (isOpenEnd === true) {
|
||||
if (isOpenEnd) {
|
||||
handleCloseEnd();
|
||||
}
|
||||
};
|
||||
@ -65,22 +80,21 @@ export default function Analytics() {
|
||||
const handleCloseEnd = () => {
|
||||
setOpenEnd(false);
|
||||
};
|
||||
|
||||
const handleOpenEnd = () => {
|
||||
setOpenEnd(true);
|
||||
};
|
||||
|
||||
const onAdornmentClickEnd = () => {
|
||||
setOpenEnd((old) => !old);
|
||||
if (isOpen === true) {
|
||||
if (isOpen) {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
console.log("questions", questions);
|
||||
console.log("general", general);
|
||||
console.log("devices", devices);
|
||||
|
||||
const now = moment();
|
||||
return (
|
||||
<>
|
||||
<LocalizationProvider dateAdapter={AdapterMoment}>
|
||||
<HeaderFull isRequest />
|
||||
<SectionWrapper component={"section"} sx={{ padding: "60px 20px" }}>
|
||||
<Typography variant={"h4"}>Аналитика</Typography>
|
||||
@ -88,13 +102,19 @@ export default function Analytics() {
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: isMobile ? "15px" : "20px",
|
||||
alignItems: "end",
|
||||
alignItems: isMobile ? "center" : "end",
|
||||
justifyContent: "space-between",
|
||||
padding: "40px 0 20px",
|
||||
borderBottom: `1px solid ${theme.palette.grey2.main}`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", gap: isMobile ? "15px" : "20px" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: isMobile ? "15px" : "20px",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
@ -112,9 +132,9 @@ export default function Analytics() {
|
||||
onOpen={handleOpen}
|
||||
// defaultValue={now}
|
||||
sx={{
|
||||
width: isMobile ? "146px" : "169px",
|
||||
|
||||
width: isMobile ? "285px" : "170px",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
background: theme.palette.background.paper,
|
||||
borderRadius: "10px",
|
||||
fontSize: "16px",
|
||||
},
|
||||
@ -123,18 +143,20 @@ export default function Analytics() {
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
//@ts-ignore
|
||||
//TODO: fix types in @mui/x-date-pickers
|
||||
textField: {
|
||||
InputProps: {
|
||||
endAdornment: (
|
||||
<IconButton onClick={onAdornmentClick}>
|
||||
<CalendarIcon />
|
||||
</IconButton>
|
||||
),
|
||||
) as ReactNode,
|
||||
},
|
||||
},
|
||||
}}
|
||||
value={to}
|
||||
onChange={(newValue) => setTo(newValue)}
|
||||
value={from}
|
||||
onChange={setFrom}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
@ -146,8 +168,6 @@ export default function Analytics() {
|
||||
color: "4D4D4D",
|
||||
}}
|
||||
>
|
||||
value={from}
|
||||
onChange={(newValue) => setValue(setFrom)}
|
||||
Дата окончания
|
||||
</Typography>
|
||||
<DatePicker
|
||||
@ -156,8 +176,9 @@ export default function Analytics() {
|
||||
onOpen={handleOpenEnd}
|
||||
// defaultValue={now}
|
||||
sx={{
|
||||
width: isMobile ? "146px" : "169px",
|
||||
width: isMobile ? "285px" : "170px",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
background: theme.palette.background.paper,
|
||||
borderRadius: "10px",
|
||||
fontSize: "16px",
|
||||
},
|
||||
@ -176,6 +197,8 @@ export default function Analytics() {
|
||||
},
|
||||
},
|
||||
}}
|
||||
value={to}
|
||||
onChange={setTo}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -184,8 +207,9 @@ export default function Analytics() {
|
||||
onClick={resetTime}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
minWidth: isMobile ? "144px" : "180px",
|
||||
px: isMobile ? "31px" : "43px",
|
||||
padding: isMobile ? "8px" : "9px 48px",
|
||||
minWidth: "auto",
|
||||
marginTop: isMobile ? "25px" : null,
|
||||
color: theme.palette.brightPurple.main,
|
||||
"&:hover": {
|
||||
backgroundColor: "#581CA7",
|
||||
@ -197,13 +221,13 @@ export default function Analytics() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Сбросить
|
||||
{isMobile ? <ResetIcon /> : "Сбросить"}
|
||||
</Button>
|
||||
</Box>
|
||||
<General data={general} />
|
||||
<AnswersStatistics data={questions} />
|
||||
<Devices data={devices} />
|
||||
</SectionWrapper>
|
||||
</>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import type { PaginationRenderItemParams } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
ButtonBase,
|
||||
Input,
|
||||
LinearProgress,
|
||||
Pagination as MuiPagination,
|
||||
PaginationItem,
|
||||
Input,
|
||||
ButtonBase,
|
||||
useTheme,
|
||||
Paper,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import { ReactComponent as DoubleCheckIcon } from "@icons/Analytics/doubleCheck.svg";
|
||||
@ -17,14 +18,16 @@ import { ReactComponent as NextIcon } from "@icons/Analytics/next.svg";
|
||||
import { ReactComponent as LeftArrowIcon } from "@icons/Analytics/leftArrow.svg";
|
||||
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
|
||||
|
||||
import type { PaginationRenderItemParams } from "@mui/material";
|
||||
|
||||
type AnswerProps = {
|
||||
title: string;
|
||||
percent: number;
|
||||
highlight?: boolean;
|
||||
};
|
||||
|
||||
type AnswersProps = {
|
||||
data: Record<string, Record<string, number>> | null;
|
||||
};
|
||||
|
||||
const ANSWERS_MOCK: Record<string, number> = {
|
||||
"Добавьте ответ": 67,
|
||||
"Вопрос пропущен": 7,
|
||||
@ -186,16 +189,16 @@ const Pagination = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Answers = (props) => {
|
||||
export const Answers: FC<AnswersProps> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
console.log(props.data);
|
||||
|
||||
if (Object.keys(props.data).length === 0)
|
||||
if (!data) {
|
||||
return (
|
||||
<Typography textAlign="center" m="10px 0">
|
||||
нет данных об ответах
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Paper
|
||||
@ -250,14 +253,24 @@ export const Answers = (props) => {
|
||||
<NextIcon />
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
{Object.entries(props.data).map(([title, percent], index) => (
|
||||
<Answer
|
||||
key={title}
|
||||
title={title}
|
||||
percent={percent}
|
||||
highlight={!index}
|
||||
/>
|
||||
))}
|
||||
{/*{Object.entries(data).map(([title, percent], index) => (*/}
|
||||
{/* <Answer*/}
|
||||
{/* key={title}*/}
|
||||
{/* title={title}*/}
|
||||
{/* percent={percent}*/}
|
||||
{/* highlight={!index}*/}
|
||||
{/* />*/}
|
||||
{/*))}*/}
|
||||
{Object.entries(data).map(([title, values], index) =>
|
||||
Object.entries(values).map(([subTitle, percent]) => (
|
||||
<Answer
|
||||
key={subTitle}
|
||||
title={subTitle}
|
||||
percent={percent}
|
||||
highlight={!index}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Paper>
|
||||
<Pagination />
|
||||
</Box>
|
||||
|
||||
@ -1,24 +1,20 @@
|
||||
import {
|
||||
Box,
|
||||
ButtonBase,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
|
||||
import { Answers } from "./Answers";
|
||||
import { QuestionsResponse } from "@api/statistic";
|
||||
import { FC } from "react";
|
||||
import { Funnel } from "./Funnel";
|
||||
import { Results } from "./Results";
|
||||
|
||||
import { ReactComponent as OpenIcon } from "@icons/Analytics/open.svg";
|
||||
type AnswersStatisticsProps = {
|
||||
data: QuestionsResponse | null;
|
||||
};
|
||||
|
||||
export const AnswersStatistics = (props) => {
|
||||
export const AnswersStatistics: FC<AnswersStatisticsProps> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(850));
|
||||
|
||||
console.log(props);
|
||||
|
||||
return (
|
||||
<Box sx={{ marginTop: "120px" }}>
|
||||
<Typography
|
||||
@ -59,10 +55,10 @@ export const AnswersStatistics = (props) => {
|
||||
gap: "40px",
|
||||
}}
|
||||
>
|
||||
<Answers data={props.data?.Questions || {}} />
|
||||
<Funnel data={props.data?.Funnel || {}} />
|
||||
<Answers data={data?.Questions || null} />
|
||||
<Funnel data={data?.Funnel || null} />
|
||||
</Box>
|
||||
<Results data={props.data?.Results || {}} />
|
||||
<Results data={data?.Results || null} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@ -1,19 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FC, useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
type FunnelItemProps = {
|
||||
title: string;
|
||||
percent: number;
|
||||
};
|
||||
|
||||
type FunnelProps = {
|
||||
data: number[] | null;
|
||||
};
|
||||
|
||||
const FUNNEL_MOCK: Record<string, number> = {
|
||||
"Стартовая страница": 100,
|
||||
"Воронка квиза": 0,
|
||||
@ -100,11 +103,10 @@ const FunnelItem = ({ title, percent }: FunnelItemProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Funnel = (props) => {
|
||||
export const Funnel: FC<FunnelProps> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(850));
|
||||
console.log(props);
|
||||
useEffect(() => {
|
||||
// const requestFunnel = async () => {
|
||||
// const [funnelResponse, funnelError] = await getGeneral("14761");
|
||||
@ -121,7 +123,7 @@ export const Funnel = (props) => {
|
||||
// requestFunnel();
|
||||
}, []);
|
||||
|
||||
if (Object.keys(props.data).length === 0)
|
||||
if (!data)
|
||||
return (
|
||||
<Typography textAlign="center" m="10px 0">
|
||||
нет данных о разделах
|
||||
@ -141,7 +143,7 @@ export const Funnel = (props) => {
|
||||
<FunnelItem
|
||||
key={title}
|
||||
title={title}
|
||||
percent={index > 0 ? props.data[index - 1] : percent}
|
||||
percent={index > 0 ? data[index - 1] : percent}
|
||||
/>
|
||||
))}
|
||||
</Paper>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { FC } from "react";
|
||||
|
||||
type ResultProps = {
|
||||
title: string;
|
||||
@ -13,6 +13,10 @@ type ResultProps = {
|
||||
highlight?: boolean;
|
||||
};
|
||||
|
||||
type ResultsProps = {
|
||||
data: Record<string, number> | null;
|
||||
};
|
||||
|
||||
const RESULTS_MOCK: Record<string, number> = {
|
||||
"Заголовок результата": 100,
|
||||
"Результат пропущен": 7,
|
||||
@ -69,10 +73,10 @@ const Result = ({ title, percent, highlight }: ResultProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Results = (props) => {
|
||||
export const Results: FC<ResultsProps> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (Object.keys(props.data).length === 0)
|
||||
if (!data)
|
||||
return (
|
||||
<Typography margin="20px 0 0 0" textAlign="center" m="10px 0">
|
||||
нет данных о результатах
|
||||
@ -98,7 +102,7 @@ export const Results = (props) => {
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
{Object.entries(props.data).map(([title, percent], index) => (
|
||||
{Object.entries(data).map(([title, percent], index) => (
|
||||
<Result
|
||||
key={title}
|
||||
title={title}
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FC } from "react";
|
||||
import { Box, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { PieChart } from "@mui/x-charts";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
import { getDevices } from "@api/statistic";
|
||||
|
||||
import type { DevicesResponse } from "@api/statistic";
|
||||
|
||||
type DeviceProps = {
|
||||
title: string;
|
||||
devices: Record<string, number>;
|
||||
devices: Record<string, number> | null;
|
||||
};
|
||||
|
||||
type DevicesProps = {
|
||||
data: DevicesResponse | null;
|
||||
};
|
||||
|
||||
const COLORS: Record<number, string> = {
|
||||
@ -19,17 +20,12 @@ const COLORS: Record<number, string> = {
|
||||
3: "#0886FB",
|
||||
};
|
||||
|
||||
const DEVICES_MOCK: DevicesResponse = {
|
||||
device: { PC: 75, Mobile: 25 },
|
||||
os: { Windows: 44, AndroidOS: 25, "OS X": 19, Linux: 13 },
|
||||
browser: { Chrome: 75, Firefox: 25 },
|
||||
};
|
||||
|
||||
const Device = ({ title, devices }: DeviceProps) => {
|
||||
const theme = useTheme();
|
||||
console.log("devices ", devices);
|
||||
if (devices === undefined || Object.keys(devices).length === 0)
|
||||
if (!devices) {
|
||||
return <Typography>{title} - нет данных</Typography>;
|
||||
}
|
||||
|
||||
const data = Object.entries(devices).map(([id, value], index) => ({
|
||||
id,
|
||||
value,
|
||||
@ -98,8 +94,7 @@ const Device = ({ title, devices }: DeviceProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Devices = ({ data = {} }) => {
|
||||
const [devices, setDevices] = useState<DevicesResponse>(data);
|
||||
export const Devices: FC<DevicesProps> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||
@ -150,9 +145,9 @@ export const Devices = ({ data = {} }) => {
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
<Device title="Устройства" devices={devices.device} />
|
||||
<Device title="Операционные системы" devices={devices.os} />
|
||||
<Device title="Браузеры" devices={devices.browser} />
|
||||
<Device title="Устройства" devices={data?.Device || null} />
|
||||
<Device title="Операционные системы" devices={data?.OS || null} />
|
||||
<Device title="Браузеры" devices={data?.Browser || null} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { LineChart } from "@mui/x-charts";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
import { getGeneral } from "@api/statistic";
|
||||
|
||||
import type { GeneralResponse } from "@api/statistic";
|
||||
import { FC } from "react";
|
||||
|
||||
type GeneralProps = {
|
||||
type GeneralItemsProps = {
|
||||
title: string;
|
||||
general: Record<string, number>;
|
||||
color: string;
|
||||
numberType: "sum" | "percent";
|
||||
};
|
||||
|
||||
type GeneralProps = {
|
||||
data: GeneralResponse | null;
|
||||
};
|
||||
|
||||
const COLORS: Record<number, string> = {
|
||||
0: "#61BB1A",
|
||||
1: "#7E2AEA",
|
||||
@ -21,14 +22,12 @@ const COLORS: Record<number, string> = {
|
||||
3: "#0886FB",
|
||||
};
|
||||
|
||||
const GENERAL_MOCK: GeneralResponse = {
|
||||
open: { 100: 20, 50: 10, 60: 5 },
|
||||
result: { 100: 90, 10: 3, 50: 48 },
|
||||
avtime: { 100: 0, 2000: 550, 60: 0 },
|
||||
conversation: { 100: 50, 1000: 50, 10000: 50 },
|
||||
};
|
||||
|
||||
const GeneralItem = ({ title, general, color, numberType }: GeneralProps) => {
|
||||
const GeneralItem = ({
|
||||
title,
|
||||
general,
|
||||
color,
|
||||
numberType,
|
||||
}: GeneralItemsProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||
|
||||
@ -65,17 +64,18 @@ const GeneralItem = ({ title, general, color, numberType }: GeneralProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const General = (props: any) => {
|
||||
export const General: FC<GeneralProps> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||
|
||||
if (Object.keys(props.data).length === 0)
|
||||
if (!data) {
|
||||
return (
|
||||
<Typography textAlign="center" m="10px 0">
|
||||
нет данных о ключевых метриках
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box sx={{ marginTop: "45px" }}>
|
||||
<Typography
|
||||
@ -103,25 +103,25 @@ export const General = (props: any) => {
|
||||
<GeneralItem
|
||||
title="Открыли квиз"
|
||||
numberType="sum"
|
||||
general={props.data.open || { 0: 0 }}
|
||||
general={data.Open || { 0: 0 }}
|
||||
color={COLORS[0]}
|
||||
/>
|
||||
<GeneralItem
|
||||
title="Получено заявок"
|
||||
numberType="sum"
|
||||
general={props.data.result || { 0: 0 }}
|
||||
general={data.Result || { 0: 0 }}
|
||||
color={COLORS[1]}
|
||||
/>
|
||||
<GeneralItem
|
||||
title="Конверсия"
|
||||
numberType="percent"
|
||||
general={props.data.conversation || { 0: 0 }}
|
||||
general={data.Conversion || { 0: 0 }}
|
||||
color={COLORS[2]}
|
||||
/>
|
||||
<GeneralItem
|
||||
title="Среднее время прохождения квиза"
|
||||
numberType="percent"
|
||||
general={props.data.avtime || { 0: 0 }}
|
||||
general={data.AvTime || { 0: 0 }}
|
||||
color={COLORS[3]}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@ -1,32 +1,60 @@
|
||||
import { getGeneral, getDevices, getQuestions } from "@api/statistic";
|
||||
import { useEffect, useState } from "react";
|
||||
import moment from "moment";
|
||||
import {
|
||||
DevicesResponse,
|
||||
GeneralResponse,
|
||||
getDevices,
|
||||
getGeneral,
|
||||
getQuestions,
|
||||
QuestionsResponse,
|
||||
} from "@api/statistic";
|
||||
|
||||
interface Props {
|
||||
quizId: string;
|
||||
to: number;
|
||||
from: number;
|
||||
import type { Moment } from "moment";
|
||||
|
||||
interface useAnalyticsProps {
|
||||
quizId: string | undefined;
|
||||
to: Moment | null;
|
||||
from: Moment | null;
|
||||
}
|
||||
|
||||
export function useAnalytics({ quizId, to, from }: Props) {
|
||||
const formatTo = to === null ? 0 : moment(to).unix();
|
||||
const formatFrom = from === null ? 0 : moment(from).unix();
|
||||
console.log(to, from);
|
||||
if (quizId === undefined) return {};
|
||||
const [devices, setDevices] = useState({});
|
||||
const [general, setGeneral] = useState({});
|
||||
const [questions, setQuestions] = useState({});
|
||||
export function useAnalytics({ quizId, to, from }: useAnalyticsProps) {
|
||||
const formatTo = to?.unix();
|
||||
const formatFrom = from?.unix();
|
||||
|
||||
const [devices, setDevices] = useState<DevicesResponse | null>(null);
|
||||
const [general, setGeneral] = useState<GeneralResponse | null>(null);
|
||||
const [questions, setQuestions] = useState<QuestionsResponse | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const gottenGeneral = await getGeneral(quizId, formatTo, formatFrom);
|
||||
const gottenDevices = await getDevices(quizId, formatTo, formatFrom);
|
||||
const gottenQuestions = await getQuestions(quizId, formatTo, formatFrom);
|
||||
setDevices(gottenGeneral[0]);
|
||||
setGeneral(gottenDevices[0]);
|
||||
setQuestions(gottenQuestions[0]);
|
||||
})();
|
||||
}, [to, from]);
|
||||
if (!quizId) return;
|
||||
|
||||
const requestStatistics = async () => {
|
||||
if (!formatTo || !formatFrom) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (gottenDevices) {
|
||||
setDevices(gottenDevices);
|
||||
}
|
||||
|
||||
if (gottenQuestions) {
|
||||
setQuestions(gottenQuestions);
|
||||
}
|
||||
};
|
||||
|
||||
requestStatistics();
|
||||
}, [quizId, to, from]);
|
||||
|
||||
return { devices, general, questions };
|
||||
}
|
||||
|
||||