feat: Devices and General

This commit is contained in:
IlyaDoronin 2024-03-21 17:24:56 +03:00
parent 35a84f301f
commit 31a8b613bb
5 changed files with 375 additions and 86 deletions

@ -1,9 +1,8 @@
import { makeRequest } from "@frontend/kitui"; 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 = { export type DevicesResponse = {
device: Record<string, number>; device: Record<string, number>;
@ -11,13 +10,26 @@ export type DevicesResponse = {
browser: Record<string, number>; 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, quizId: string,
): Promise<[any | null, string?]> => { ): Promise<[DevicesResponse | null, string?]> => {
try { try {
const devicesResponse = await makeRequest<unknown, DevicesResponse>({ const devicesResponse = await makeRequest<unknown, DevicesResponse>({
method: "POST", method: "POST",
url: `${apiUrl}/devices?quizID=${quizId}`, url: `${apiUrl}/${quizId}/devices`,
useToken: false, useToken: false,
withCredentials: true, withCredentials: true,
}); });
@ -26,6 +38,44 @@ export const getDevicesList = async (
} catch (nativeError) { } catch (nativeError) {
const [error] = parseAxiosError(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 { import {
Box, Box,
Button, Button,
@ -11,39 +11,16 @@ import {
import { DatePicker } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers";
import { LineChart } from "@mui/x-charts"; import { LineChart } from "@mui/x-charts";
import moment from "moment"; import moment from "moment";
import { enqueueSnackbar } from "notistack";
import HeaderFull from "@ui_kit/Header/HeaderFull"; import HeaderFull from "@ui_kit/Header/HeaderFull";
import SectionWrapper from "@ui_kit/SectionWrapper"; import SectionWrapper from "@ui_kit/SectionWrapper";
import { General } from "./General";
import { Devices } from "./Devices"; import { Devices } from "./Devices";
import { getDevicesList } from "@api/statistic";
import CalendarIcon from "@icons/CalendarIcon"; 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() { export default function Analytics() {
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK);
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const [isOpenEnd, setOpenEnd] = useState(false); const [isOpenEnd, setOpenEnd] = useState(false);
@ -80,8 +57,8 @@ export default function Analytics() {
const now = moment(); const now = moment();
return ( return (
<> <>
<HeaderFull isRequest={true} /> <HeaderFull isRequest />
<SectionWrapper component={"section"} sx={{ paddingTop: "60px" }}> <SectionWrapper component={"section"} sx={{ padding: "60px 20px" }}>
<Typography variant={"h4"}>Аналитика</Typography> <Typography variant={"h4"}>Аналитика</Typography>
<Box <Box
sx={{ sx={{
@ -194,22 +171,8 @@ export default function Analytics() {
Сбросить Сбросить
</Button> </Button>
</Box> </Box>
<Box> <General />
<Paper> <Devices />
<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} />
</SectionWrapper> </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 { PieChart } from "@mui/x-charts";
import { enqueueSnackbar } from "notistack";
import { getDevices } from "@api/statistic";
import type { DevicesResponse } from "@api/statistic"; import type { DevicesResponse } from "@api/statistic";
type DevicesProps = {
devices: DevicesResponse;
};
type DeviceProps = { type DeviceProps = {
title: string; title: string;
devices: Record<string, number>; 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 Device = ({ title, devices }: DeviceProps) => {
const theme = useTheme();
const data = Object.entries(devices).map(([id, value], index) => ({
id,
value,
color: COLORS[index],
}));
return ( return (
<Paper> <Paper
<Box sx={{ height: "235px" }}> sx={{
<PieChart overflow: "hidden",
series={[ minHeight: "500px",
{ display: "flex",
data: Object.entries(devices).map(([id, value]) => ({ flexDirection: "column",
id, gap: "30px",
value, borderRadius: "12px",
})), boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
innerRadius: 50, }}
}, >
]} <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> </Box>
</Paper> </Paper>
); );
}; };
export const Devices = ({ devices }: DevicesProps) => { export const Devices = () => {
const [devices, setDevices] = useState<DevicesResponse>(DEVICES_MOCK);
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); 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 ( return (
<Box <Box sx={{ marginTop: "120px" }}>
sx={{ <Typography
display: "grid", component="h3"
gridTemplateColumns: isTablet sx={{
? isMobile fontSize: "24px",
? "1fr" fontWeight: "bold",
: "1fr 1fr" color: theme.palette.text.primary,
: "1fr 1fr 1fr", }}
gap: "20px", >
marginTop: "30px", Статистика пользователей
}} </Typography>
> <Box
<Device title="Устройства" devices={devices.device} /> sx={{
<Device title="Операционные системы" devices={devices.os} /> display: "grid",
<Device title="Браузеры" devices={devices.browser} /> 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> </Box>
); );
}; };

@ -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 ArrowLeft from "@icons/questionsPage/arrowLeft";
import { cleanAuthTicketData } from "@root/ticket"; 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 theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));