feat: Devices and General
This commit is contained in:
parent
35a84f301f
commit
31a8b613bb
@ -1,9 +1,8 @@
|
||||
import { makeRequest } from "@frontend/kitui";
|
||||
|
||||
import type { LoginRequest, LoginResponse } from "@frontend/kitui";
|
||||
import { parseAxiosError } from "../utils/parse-error";
|
||||
import { parseAxiosError } from "@utils/parse-error";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/statistic";
|
||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic";
|
||||
|
||||
export type DevicesResponse = {
|
||||
device: Record<string, number>;
|
||||
@ -11,13 +10,26 @@ export type DevicesResponse = {
|
||||
browser: Record<string, number>;
|
||||
};
|
||||
|
||||
export const getDevicesList = async (
|
||||
export type GeneralResponse = {
|
||||
open: Record<string, number>;
|
||||
result: Record<string, number>;
|
||||
avtime: Record<string, number>;
|
||||
conversation: Record<string, number>;
|
||||
};
|
||||
|
||||
export type QuestionsResponse = {
|
||||
funnel: number[];
|
||||
results: Record<string, number>;
|
||||
questions: Record<string, Record<string, number>>;
|
||||
};
|
||||
|
||||
export const getDevices = async (
|
||||
quizId: string,
|
||||
): Promise<[any | null, string?]> => {
|
||||
): Promise<[DevicesResponse | null, string?]> => {
|
||||
try {
|
||||
const devicesResponse = await makeRequest<unknown, DevicesResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/devices?quizID=${quizId}`,
|
||||
url: `${apiUrl}/${quizId}/devices`,
|
||||
useToken: false,
|
||||
withCredentials: true,
|
||||
});
|
||||
@ -26,6 +38,44 @@ export const getDevicesList = async (
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не получить статистику о девайсах. ${error}`];
|
||||
return [null, `Не удалось получить статистику о девайсах. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const getGeneral = async (
|
||||
quizId: string,
|
||||
): Promise<[GeneralResponse | null, string?]> => {
|
||||
try {
|
||||
const generalResponse = await makeRequest<unknown, GeneralResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/general`,
|
||||
useToken: false,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
return [generalResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось получить ключевые метрики. ${error}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const getQuestions = async (
|
||||
quizId: string,
|
||||
): Promise<[QuestionsResponse | null, string?]> => {
|
||||
try {
|
||||
const questionsResponse = await makeRequest<unknown, QuestionsResponse>({
|
||||
method: "POST",
|
||||
url: `${apiUrl}/${quizId}/questions`,
|
||||
useToken: false,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
return [questionsResponse];
|
||||
} catch (nativeError) {
|
||||
const [error] = parseAxiosError(nativeError);
|
||||
|
||||
return [null, `Не удалось получить статистику по результатам. ${error}`];
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@ -11,39 +11,16 @@ import {
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import { LineChart } from "@mui/x-charts";
|
||||
import moment from "moment";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
import HeaderFull from "@ui_kit/Header/HeaderFull";
|
||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||
|
||||
import { General } from "./General";
|
||||
import { Devices } from "./Devices";
|
||||
|
||||
import { getDevicesList } from "@api/statistic";
|
||||
|
||||
import CalendarIcon from "@icons/CalendarIcon";
|
||||
|
||||
import type { DevicesResponse } from "@api/statistic";
|
||||
|
||||
const DEVICES_MOCK: DevicesResponse = {
|
||||
device: {
|
||||
additionalProp1: 10,
|
||||
additionalProp2: 20,
|
||||
additionalProp3: 30,
|
||||
},
|
||||
os: {
|
||||
additionalProp1: 20,
|
||||
additionalProp2: 78,
|
||||
additionalProp3: 2,
|
||||
},
|
||||
browser: {
|
||||
additionalProp1: 55,
|
||||
additionalProp2: 11,
|
||||
additionalProp3: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Analytics() {
|
||||
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [isOpenEnd, setOpenEnd] = useState(false);
|
||||
|
||||
@ -80,8 +57,8 @@ export default function Analytics() {
|
||||
const now = moment();
|
||||
return (
|
||||
<>
|
||||
<HeaderFull isRequest={true} />
|
||||
<SectionWrapper component={"section"} sx={{ paddingTop: "60px" }}>
|
||||
<HeaderFull isRequest />
|
||||
<SectionWrapper component={"section"} sx={{ padding: "60px 20px" }}>
|
||||
<Typography variant={"h4"}>Аналитика</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
@ -194,22 +171,8 @@ export default function Analytics() {
|
||||
Сбросить
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
<Paper>
|
||||
<Typography>Открыли quiz</Typography>
|
||||
<LineChart
|
||||
xAxis={[{ data: [1, 2, 3, 5, 8, 10] }]}
|
||||
series={[
|
||||
{
|
||||
data: [2, 5.5, 2, 8.5, 1.5, 5],
|
||||
},
|
||||
]}
|
||||
width={500}
|
||||
height={300}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Devices devices={devices} />
|
||||
<General />
|
||||
<Devices />
|
||||
</SectionWrapper>
|
||||
</>
|
||||
);
|
||||
|
@ -1,58 +1,166 @@
|
||||
import { Box, Paper, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useEffect, useState } 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 DevicesProps = {
|
||||
devices: DevicesResponse;
|
||||
};
|
||||
|
||||
type DeviceProps = {
|
||||
title: string;
|
||||
devices: Record<string, number>;
|
||||
};
|
||||
|
||||
const COLORS: Record<number, string> = {
|
||||
0: "#7E2AEA",
|
||||
1: "#FA7738",
|
||||
2: "#62BB1C",
|
||||
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();
|
||||
const data = Object.entries(devices).map(([id, value], index) => ({
|
||||
id,
|
||||
value,
|
||||
color: COLORS[index],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<Box sx={{ height: "235px" }}>
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: Object.entries(devices).map(([id, value]) => ({
|
||||
id,
|
||||
value,
|
||||
})),
|
||||
innerRadius: 50,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Paper
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
minHeight: "500px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "30px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ margin: "20px" }}>{title}</Typography>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<PieChart
|
||||
height={245}
|
||||
width={245}
|
||||
margin={{ right: 0 }}
|
||||
series={[{ data, innerRadius: 50 }]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ background: theme.palette.background.default, padding: "20px" }}
|
||||
>
|
||||
{data.map(({ id, value, color }) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
marginBottom: "10px",
|
||||
"&:last-child": { margin: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
position: "relative",
|
||||
paddingLeft: "30px",
|
||||
"&::before": {
|
||||
content: "''",
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
background: color,
|
||||
height: "20px",
|
||||
width: "20px",
|
||||
borderRadius: "6px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{id}
|
||||
</Typography>
|
||||
<Typography>{value} %</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Devices = ({ devices }: DevicesProps) => {
|
||||
export const Devices = () => {
|
||||
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK);
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||
|
||||
useEffect(() => {
|
||||
const requestDevices = async () => {
|
||||
const [devicesResponse, devicesError] = await getDevices("14761");
|
||||
|
||||
if (devicesError) {
|
||||
enqueueSnackbar(devicesError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!devicesResponse) {
|
||||
enqueueSnackbar("Список девайсов пуст.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setDevices(devicesResponse);
|
||||
};
|
||||
|
||||
// requestDevices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: isTablet
|
||||
? isMobile
|
||||
? "1fr"
|
||||
: "1fr 1fr"
|
||||
: "1fr 1fr 1fr",
|
||||
gap: "20px",
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
<Device title="Устройства" devices={devices.device} />
|
||||
<Device title="Операционные системы" devices={devices.os} />
|
||||
<Device title="Браузеры" devices={devices.browser} />
|
||||
<Box sx={{ marginTop: "120px" }}>
|
||||
<Typography
|
||||
component="h3"
|
||||
sx={{
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
Статистика пользователей
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: isTablet
|
||||
? isMobile
|
||||
? "1fr"
|
||||
: "1fr 1fr"
|
||||
: "1fr 1fr 1fr",
|
||||
gap: "20px",
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
<Device title="Устройства" devices={devices.device} />
|
||||
<Device title="Операционные системы" devices={devices.os} />
|
||||
<Device title="Браузеры" devices={devices.browser} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
164
src/pages/Analytics/General.tsx
Normal file
164
src/pages/Analytics/General.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
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";
|
||||
|
||||
type GeneralProps = {
|
||||
title: string;
|
||||
general: Record<string, number>;
|
||||
color: string;
|
||||
numberType: "sum" | "percent";
|
||||
};
|
||||
|
||||
const COLORS: Record<number, string> = {
|
||||
0: "#61BB1A",
|
||||
1: "#7E2AEA",
|
||||
2: "#FB5607",
|
||||
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 theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||
|
||||
const numberValue =
|
||||
numberType === "sum"
|
||||
? Object.values(general).reduce((total, item) => total + item, 0)
|
||||
: Object.entries(general).reduce(
|
||||
(total, [key, value]) => total + (value / Number(key)) * 100,
|
||||
0,
|
||||
) / Object.keys(general).length;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ margin: "20px 20px 0" }}>{title}</Typography>
|
||||
<Typography sx={{ margin: "10px 20px 0", fontWeight: "bold" }}>
|
||||
{numberType === "sum" ? numberValue : `${numberValue.toFixed()}%`}
|
||||
</Typography>
|
||||
<LineChart
|
||||
xAxis={[{ data: Object.keys(general) }]}
|
||||
series={[{ data: Object.values(general) }]}
|
||||
height={220}
|
||||
colors={[color]}
|
||||
sx={{
|
||||
transform: isMobile ? "scale(1.1)" : "scale(1.2)",
|
||||
"& .MuiChartsAxis-tickContainer": { display: "none" },
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const General = () => {
|
||||
const [general, setGeneral] = useState<GeneralResponse>(GENERAL_MOCK);
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||
|
||||
useEffect(() => {
|
||||
const requestGeneral = async () => {
|
||||
const [generalResponse, generalError] = await getGeneral("14761");
|
||||
|
||||
if (generalError) {
|
||||
enqueueSnackbar(generalError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!generalResponse) {
|
||||
enqueueSnackbar("Список девайсов пуст.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setGeneral(generalResponse);
|
||||
};
|
||||
|
||||
// requestGeneral();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ marginTop: "45px" }}>
|
||||
<Typography
|
||||
component="h3"
|
||||
sx={{
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
Ключевые метрики
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: isTablet
|
||||
? isMobile
|
||||
? "1fr"
|
||||
: "1fr 1fr"
|
||||
: "1fr 1fr 1fr",
|
||||
gap: "20px",
|
||||
marginTop: "40px",
|
||||
}}
|
||||
>
|
||||
<GeneralItem
|
||||
title="Открыли квиз"
|
||||
numberType="sum"
|
||||
general={general.open}
|
||||
color={COLORS[0]}
|
||||
/>
|
||||
<GeneralItem
|
||||
title="Получено заявок"
|
||||
numberType="sum"
|
||||
general={general.result}
|
||||
color={COLORS[1]}
|
||||
/>
|
||||
<GeneralItem
|
||||
title="Конверсия"
|
||||
numberType="percent"
|
||||
general={general.conversation}
|
||||
color={COLORS[2]}
|
||||
/>
|
||||
<GeneralItem
|
||||
title="Среднее время прохождения квиза"
|
||||
numberType="percent"
|
||||
general={general.avtime}
|
||||
color={COLORS[3]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -21,7 +21,11 @@ import { ToTariffsButton } from "@ui_kit/Toolbars/ToTariffsButton";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import { cleanAuthTicketData } from "@root/ticket";
|
||||
|
||||
export default function HeaderFull({ isRequest }: boolean) {
|
||||
type HeaderFullProps = {
|
||||
isRequest?: boolean;
|
||||
};
|
||||
|
||||
export default function HeaderFull({ isRequest = false }: HeaderFullProps) {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
|
Loading…
Reference in New Issue
Block a user