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 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>
);
};

@ -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));