Merge remote-tracking branch 'origin/staging'

This commit is contained in:
skeris 2024-05-07 01:20:13 +03:00
commit d49208b56f
24 changed files with 1120 additions and 555 deletions

@ -21,7 +21,7 @@ server {
return 200;
}
if ($host = sadmin.pena) {
proxy_pass http://10.6.0.11:59301;
proxy_pass http://10.8.0.6:59301;
}
if ($host != sadmin.pena) {
proxy_pass http://10.8.0.8:59301;
@ -63,7 +63,7 @@ server {
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Headers content-type,authorization always;
add_header Access-Control-Allow-Methods OPTIONS,GET,POST,PATCH,PUT,DELETE;
proxy_pass http://10.6.0.11:59301/;
proxy_pass http://10.8.0.6:59301/;
}
location /swagger/ {

@ -136,7 +136,7 @@ export function usePromocodes(
}
export function useAllPromocodes() {
const swrResponse = useSwr("allPromocodes", promocodeApi.getAllPromocodes, {
const { data } = useSwr("allPromocodes", promocodeApi.getAllPromocodes, {
keepPreviousData: true,
suspense: true,
onError(err) {
@ -145,5 +145,5 @@ export function useAllPromocodes() {
},
});
return swrResponse.data;
return data;
}

@ -1,28 +0,0 @@
import makeRequest from "@root/api/makeRequest";
export type QuizStatisticResponse = {
Registrations: number;
Quizes: number;
Results: number
};
type TRequest = {
to: number;
from: number;
};
export const getStatistic = async (
to: number,
from: number,
): Promise<QuizStatisticResponse> => {
try {
const generalResponse = await makeRequest<TRequest, QuizStatisticResponse>({
url: `${process.env.REACT_APP_DOMAIN}/squiz/statistic`,
body: { to, from }
})
return generalResponse;
} catch (nativeError) {
return { Registrations: 0, Quizes: 0, Results: 0 };
}
};

@ -0,0 +1,89 @@
import makeRequest from "@root/api/makeRequest";
import type {
GetStatisticSchildBody,
QuizStatisticsItem,
GetPromocodeStatisticsBody,
AllPromocodeStatistics,
} from "./types";
export type QuizStatisticResponse = {
Registrations: number;
Quizes: number;
Results: number;
};
type TRequest = {
to: number;
from: number;
};
export const getStatistic = async (
to: number,
from: number
): Promise<QuizStatisticResponse> => {
try {
const generalResponse = await makeRequest<TRequest, QuizStatisticResponse>({
url: `${process.env.REACT_APP_DOMAIN}/squiz/statistic`,
body: { to, from },
});
return generalResponse;
} catch (nativeError) {
return { Registrations: 0, Quizes: 0, Results: 0 };
}
};
export const getStatisticSchild = async (
from: number,
to: number
): Promise<QuizStatisticsItem[]> => {
try {
const StatisticResponse = await makeRequest<
GetStatisticSchildBody,
QuizStatisticsItem[]
>({
url: process.env.REACT_APP_DOMAIN + "/customer/quizlogo/stat",
method: "post",
useToken: true,
body: { to, from, page: 0, limit: 100 },
});
if (!StatisticResponse) {
throw new Error("Статистика не найдена");
}
return StatisticResponse;
} catch (nativeError) {
return [
{
ID: "0",
Regs: 0,
Money: 0,
Quizes: [{ QuizID: "0", Regs: 0, Money: 0 }],
},
];
}
};
export const getStatisticPromocode = async (
from: number,
to: number
): Promise<Record<string, AllPromocodeStatistics>> => {
try {
const StatisticPromo = await makeRequest<
GetPromocodeStatisticsBody,
Record<string, AllPromocodeStatistics>
>({
url: process.env.REACT_APP_DOMAIN + "/customer/promocode/ltv",
method: "post",
useToken: true,
body: { to, from },
});
return StatisticPromo;
} catch (nativeError) {
console.log(nativeError);
return {};
}
};

@ -0,0 +1,31 @@
import { Moment } from "moment";
export type GetStatisticSchildBody = {
to: Moment | null;
from: Moment | null;
page: number;
limit: number;
};
type StatisticsQuizes = {
QuizID: string;
Money: number;
Regs: number;
};
export type QuizStatisticsItem = {
ID: string;
Money: number;
Quizes: StatisticsQuizes[];
Regs: number;
};
export type GetPromocodeStatisticsBody = {
from: number;
to: number;
};
export type AllPromocodeStatistics = {
Money: number;
Regs: number;
};

@ -24,7 +24,7 @@ import { PromocodeManagement } from "@root/pages/dashboard/Content/PromocodeMana
import { SettingRoles } from "@pages/Setting/SettingRoles";
import Support from "@pages/dashboard/Content/Support/Support";
import ChatImageNewWindow from "@pages/dashboard/Content/Support/ChatImageNewWindow";
import QuizStatistic from "@pages/dashboard/Content/QuizStatistic";
import { QuizStatistics } from "@root/pages/dashboard/Content/QuizStatistics";
import theme from "./theme";
import "./index.css";
@ -106,10 +106,10 @@ root.render(
}
/>
<Route
path="/quizStatistic"
path="/quizStatistics"
element={
<PrivateRoute>
<QuizStatistic />
<QuizStatistics />
</PrivateRoute>
}
/>
@ -121,7 +121,7 @@ root.render(
/>
))}
</Route>
<Route path={"/image/:srcImage"} element={<ChatImageNewWindow />} />
<Route path={"/image/:srcImage"} element={<ChatImageNewWindow />} />
<Route path="*" element={<Error404 />} />
</Routes>

@ -21,6 +21,7 @@ import theme from "@root/theme";
import type { TextFieldProps } from "@mui/material";
import { CreatePromocodeBody } from "@root/model/promocodes";
import type { ChangeEvent } from "react";
import { enqueueSnackbar } from "notistack";
type BonusType = "discount" | "privilege";
@ -74,24 +75,34 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}, []);
const submitForm = (values: FormValues) => {
const currentPrivilege = privileges.find(
(item) => item.privilegeId === values.privilegeId
);
const body = {
...values,
};
const body = { ...values };
if ((body.layer === 1 && bonusType === "discount") || bonusType === "privilege") {
if (currentPrivilege === undefined) return;
body.serviceKey = currentPrivilege?.serviceKey
body.target = body.privilegeId
}
if ((body.layer === 2 && bonusType === "discount")) {
if (body.serviceKey === undefined) return;
body.target = body.serviceKey
}
if (
(body.layer === 1 && bonusType === "discount") ||
bonusType === "privilege"
) {
if (currentPrivilege === undefined) {
enqueueSnackbar("Привилегия не выбрана");
return;
}
body.serviceKey = currentPrivilege?.serviceKey;
body.target = body.privilegeId;
}
if (body.layer === 2 && bonusType === "discount") {
if (!body.serviceKey) {
enqueueSnackbar("Сервис не выбран");
return;
}
body.target = body.serviceKey;
}
const factorFromDiscountValue = 1 - body.factor / 100;
@ -178,6 +189,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
<CustomTextField
name="activationCount"
label="Количество активаций промокода"
required
onChange={({ target }) =>
setFieldValue(
"activationCount",
@ -253,64 +265,57 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
>
{values.layer === 1 ? "Выбор привилегии" : "Выбор сервиса"}
</Typography>
{
values.layer === 1 ?
<Field
name="privilegeId"
as={Select}
label={"Привилегия"}
sx={{
width: "100%",
border: "2px solid",
color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
}}
onChange={({ target }: SelectChangeProps) => {
setFieldValue("target", target.value)
setFieldValue("privilegeId", target.value)
}}
children={
privileges.map(({ name, privilegeId }) => (
<MenuItem key={privilegeId} value={privilegeId}>
{name}
</MenuItem>
))
}
/>
:
<Field
name="serviceKey"
as={Select}
label={"Сервис"}
sx={{
width: "100%",
border: "2px solid",
color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
}}
onChange={({ target }: SelectChangeProps) => {
setFieldValue("target", target.value)
setFieldValue("serviceKey", target.value)
}}
children={
SERVICE_LIST.map(({ displayName, serviceKey }) => (
<MenuItem key={serviceKey} value={serviceKey}>
{displayName}
</MenuItem>
))
}
/>
}
{values.layer === 1 ? (
<Field
name="privilegeId"
as={Select}
label={"Привилегия"}
sx={{
width: "100%",
border: "2px solid",
color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
}}
onChange={({ target }: SelectChangeProps) => {
setFieldValue("target", target.value);
setFieldValue("privilegeId", target.value);
}}
children={privileges.map(({ name, privilegeId }) => (
<MenuItem key={privilegeId} value={privilegeId}>
{name}
</MenuItem>
))}
/>
) : (
<Field
name="serviceKey"
as={Select}
label={"Сервис"}
sx={{
width: "100%",
border: "2px solid",
color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "none",
},
".MuiSvgIcon-root ": { fill: theme.palette.secondary.main },
}}
onChange={({ target }: SelectChangeProps) => {
setFieldValue("target", target.value);
setFieldValue("serviceKey", target.value);
}}
children={SERVICE_LIST.map(({ displayName, serviceKey }) => (
<MenuItem key={serviceKey} value={serviceKey}>
{displayName}
</MenuItem>
))}
/>
)}
<CustomTextField
name="threshold"
label="При каком значении применяется скидка"
@ -383,7 +388,7 @@ export const CreatePromocodeForm = ({ createPromocode }: Props) => {
}}
type="submit"
>
Cоздать
Создать
</Button>
</Form>
)}

@ -4,14 +4,20 @@ import { Promocode } from "@root/model/promocodes";
import { useMemo, useState } from "react";
import { BarChart, Delete } from "@mui/icons-material";
import {promocodeApi} from "@root/api/promocode/requests";
import { promocodeApi } from "@root/api/promocode/requests";
export function usePromocodeGridColDef(
setStatistics: (id: string) => void,
deletePromocode: (id: string) => void
) {
const validity = (value:string|number) => {if(value===0){return "неоганичен"}else {return new Date(value).toLocaleString()}}
return useMemo<GridColDef<Promocode, string | number, string >[]>(
const validity = (value: string | number) => {
if (value === 0) {
return "неоганичен";
} else {
return new Date(value).toLocaleString();
}
};
return useMemo<GridColDef<Promocode, string | number, string>[]>(
() => [
{
field: "id",
@ -50,6 +56,14 @@ export function usePromocodeGridColDef(
valueGetter: ({ row }) => row.dueTo * 1000,
valueFormatter: ({ value }) => `${validity(value)}`,
},
{
field: "description",
headerName: "Описание",
minWidth: 200,
flex: 1,
sortable: false,
valueGetter: ({ row }) => row.description,
},
{
field: "settings",
headerName: "",
@ -57,10 +71,12 @@ export function usePromocodeGridColDef(
sortable: false,
renderCell: (params) => {
return (
<IconButton onClick={() => {
setStatistics(params.row.id,)
promocodeApi.getPromocodeStatistics(params.row.id, 0, 0)
}}>
<IconButton
onClick={() => {
setStatistics(params.row.id);
promocodeApi.getPromocodeStatistics(params.row.id, 0, 0);
}}
>
<BarChart />
</IconButton>
);

@ -1,169 +0,0 @@
import { Table, TableBody, TableCell, TableHead, TableRow, useTheme, Typography, Box, TextField, Button } from '@mui/material';
import { useState } from 'react';
import moment from "moment";
import type { Moment } from "moment";
import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { useQuizStatistic } from '@root/utils/hooks/useQuizStatistic';
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'
export default () => {
const theme = useTheme()
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 { Registrations, Quizes, Results } = useQuizStatistic({
from,
to,
});
const resetTime = () => {
setFrom(moment(0));
setTo(moment(Date.now()));
};
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
const onAdornmentClick = () => {
setOpen((old) => !old);
if (isOpenEnd) {
handleCloseEnd();
}
};
const handleCloseEnd = () => {
setOpenEnd(false);
};
const handleOpenEnd = () => {
setOpenEnd(true);
};
const onAdornmentClickEnd = () => {
setOpenEnd((old) => !old);
if (isOpen) {
handleClose();
}
};
return <>
<LocalizationProvider dateAdapter={AdapterMoment}>
<Box>
<Typography
sx={{
fontSize: "16px",
marginBottom: "5px",
fontWeight: 500,
color: "4D4D4D",
}}
>
Дата начала
</Typography>
<DatePicker
inputFormat="DD/MM/YYYY"
value={from}
onChange={(date) => date && setFrom(date)}
renderInput={(params) => (
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{
sx: {
height: "40px",
color: theme.palette.secondary.main,
border: "1px solid",
borderColor: theme.palette.secondary.main,
"& .MuiSvgIcon-root": {
color: theme.palette.secondary.main,
},
},
}}
/>
</Box>
<Box>
<Typography
sx={{
fontSize: "16px",
marginBottom: "5px",
fontWeight: 500,
color: "4D4D4D",
}}
>
Дата окончания
</Typography>
<DatePicker
inputFormat="DD/MM/YYYY"
value={to}
onChange={(date) => date && setTo(date)}
renderInput={(params) => (
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{
sx: {
height: "40px",
color: theme.palette.secondary.main,
border: "1px solid",
borderColor: theme.palette.secondary.main,
"& .MuiSvgIcon-root": {
color: theme.palette.secondary.main,
},
},
}}
/>
</Box>
<Button
sx={{
m: '10px 0'
}}
onClick={resetTime}
>
Сбросить даты
</Button>
<Table
sx={{
width: "80%",
border: "2px solid",
borderColor: theme.palette.secondary.main,
bgcolor: theme.palette.content.main,
color: "white"
}}
>
<TableHead>
<TableRow
sx={{
borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main,
height: "100px",
}}
>
<TableCell sx={{color: "inherit"}} align="center">Регистраций</TableCell>
<TableCell sx={{color: "inherit"}} align="center">Quiz</TableCell>
<TableCell sx={{color: "inherit"}} align="center">Результаты</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableCell sx={{color: "inherit"}} align="center">{Registrations}</TableCell>
<TableCell sx={{color: "inherit"}} align="center">{Quizes}</TableCell>
<TableCell sx={{color: "inherit"}} align="center">{Results}</TableCell>
</TableBody>
</Table>
</LocalizationProvider>
</>
}

@ -0,0 +1,86 @@
import { Box, TextField, Typography, useTheme } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import type { Moment } from "moment";
type DateFilterProps = {
from: Moment | null;
to: Moment | null;
setFrom: (date: Moment | null) => void;
setTo: (date: Moment | null) => void;
};
export const DateFilter = ({ to, setTo, from, setFrom }: DateFilterProps) => {
const theme = useTheme();
return (
<>
<Box>
<Typography
sx={{
fontSize: "16px",
marginBottom: "5px",
fontWeight: 500,
color: "4D4D4D",
}}
>
Дата начала
</Typography>
<DatePicker
inputFormat="DD/MM/YYYY"
value={from}
onChange={(date) => date && setFrom(date.startOf('day'))}
renderInput={(params) => (
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{
sx: {
height: "40px",
color: theme.palette.secondary.main,
border: "1px solid",
borderColor: theme.palette.secondary.main,
"& .MuiSvgIcon-root": { color: theme.palette.secondary.main },
},
}}
/>
</Box>
<Box>
<Typography
sx={{
fontSize: "16px",
marginBottom: "5px",
fontWeight: 500,
color: "4D4D4D",
}}
>
Дата окончания
</Typography>
<DatePicker
inputFormat="DD/MM/YYYY"
value={to}
onChange={(date) => date && setTo(date.endOf('day'))}
renderInput={(params) => (
<TextField
{...params}
sx={{ background: "#1F2126", borderRadius: "5px" }}
/>
)}
InputProps={{
sx: {
height: "40px",
color: theme.palette.secondary.main,
border: "1px solid",
borderColor: theme.palette.secondary.main,
"& .MuiSvgIcon-root": {
color: theme.palette.secondary.main,
},
},
}}
/>
</Box>
</>
);
};

@ -0,0 +1,83 @@
import { useState } from "react";
import moment from "moment";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Button,
useTheme,
} from "@mui/material";
import { DateFilter } from "./DateFilter";
import { useQuizStatistic } from "@root/utils/hooks/useQuizStatistic";
import type { Moment } from "moment";
export const QuizInfo = () => {
const [from, setFrom] = useState<Moment | null>(null);
const [to, setTo] = useState<Moment | null>(moment());
const theme = useTheme();
const { Registrations, Quizes, Results } = useQuizStatistic({
from,
to,
});
const resetTime = () => {
setFrom(moment());
setTo(moment());
};
return (
<>
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
<Button sx={{ m: "10px 0" }} onClick={resetTime}>
Сбросить даты
</Button>
<Table
sx={{
width: "80%",
border: "2px solid",
borderColor: theme.palette.secondary.main,
bgcolor: theme.palette.content.main,
color: "white",
}}
>
<TableHead>
<TableRow
sx={{
borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main,
height: "100px",
}}
>
<TableCell sx={{ color: "inherit" }} align="center">
Регистраций
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
Quiz
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
Результаты
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell sx={{ color: "inherit" }} align="center">
{Registrations}
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
{Quizes}
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
{Results}
</TableCell>
</TableRow>
</TableBody>
</Table>
</>
);
};

@ -0,0 +1,80 @@
import { useState } from "react";
import moment from "moment";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
useTheme,
} from "@mui/material";
import { DateFilter } from "./DateFilter";
import { useAllPromocodes } from "@root/api/promocode/swr";
import { usePromocodeStatistics } from "@root/utils/hooks/usePromocodeStatistics";
import type { Moment } from "moment";
export const StatisticsPromocode = () => {
const [from, setFrom] = useState<Moment | null>(
moment(moment().subtract(4, "weeks"))
);
const [to, setTo] = useState<Moment | null>(moment());
const promocodes = useAllPromocodes();
const promocodeStatistics = usePromocodeStatistics({ to, from });
const theme = useTheme();
return (
<>
<Typography sx={{ marginTop: "30px" }}>Статистика промокодов</Typography>
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
<Table
sx={{
width: "80%",
border: "2px solid",
borderColor: theme.palette.secondary.main,
bgcolor: theme.palette.content.main,
color: "white",
marginTop: "30px",
}}
>
<TableHead>
<TableRow
sx={{
borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main,
height: "100px",
}}
>
<TableCell sx={{ color: "inherit" }} align="center">
Промокод
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
Регистации
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
Внесено
</TableCell>
</TableRow>
</TableHead>
{Object.entries(promocodeStatistics).map(([key, { Regs, Money }]) => (
<TableBody key={key}>
<TableRow>
<TableCell sx={{ color: "inherit" }} align="center">
{promocodes.find(({ id }) => id === key)?.codeword ?? ""}
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
{Regs}
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
{(Money / 100).toFixed(2)}
</TableCell>
</TableRow>
</TableBody>
))}
</Table>
</>
);
};

@ -0,0 +1,196 @@
import moment from "moment";
import { useEffect, useState } from "react";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
Button,
useTheme,
} from "@mui/material";
import { GridToolbar } from "@mui/x-data-grid";
import { enqueueSnackbar } from "notistack";
import DataGrid from "@kitUI/datagrid";
import ModalUser from "@pages/dashboard/ModalUser";
import { DateFilter } from "./DateFilter";
import { useSchildStatistics } from "@root/utils/hooks/useSchildStatistics";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import type { Moment } from "moment";
import type { QuizStatisticsItem } from "@root/api/quizStatistics/types";
import type { GridColDef } from "@mui/x-data-grid";
const COLUMNS: GridColDef<QuizStatisticsItem, string>[] = [
{
field: "user",
headerName: "Пользователь",
width: 250,
valueGetter: ({ row }) => row.ID,
},
{
field: "regs",
headerName: "Регистраций",
width: 200,
valueGetter: ({ row }) => String(row.Regs),
},
{
field: "money",
headerName: "Деньги",
width: 200,
valueGetter: ({ row }) => String(row.Money),
},
];
export const StatisticsSchild = () => {
const [openUserModal, setOpenUserModal] = useState<boolean>(false);
const [activeUserId, setActiveUserId] = useState<string>("");
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [from, setFrom] = useState<Moment | null>(
moment(moment().subtract(4, "weeks"))
);
const [to, setTo] = useState<Moment | null>(moment());
const theme = useTheme();
const statistics = useSchildStatistics(from, to)
.map((obj) => ({...obj, Money: Number((obj.Money / 100).toFixed(2))}));
useEffect(() => {
if (!openUserModal) {
setActiveUserId("");
}
}, [openUserModal]);
useEffect(() => {
if (activeUserId) {
setOpenUserModal(true);
return;
}
setOpenUserModal(false);
}, [activeUserId]);
const copyQuizLink = (quizId: string) => {
navigator.clipboard.writeText(
`https://${
window.location.href.includes("/admin.") ? "" : "s."
}hbpn.link/${quizId}`
);
enqueueSnackbar("Ссылка успешно скопирована");
};
return (
<>
<Typography sx={{ mt: "20px", mb: "20px" }}>
Статистика переходов с шильдика
</Typography>
<DateFilter from={from} to={to} setFrom={setFrom} setTo={setTo} />
<DataGrid
sx={{ marginTop: "30px", width: "80%" }}
getRowId={({ ID }) => ID}
checkboxSelection={true}
rows={statistics}
components={{ Toolbar: GridToolbar }}
rowCount={statistics.length}
rowsPerPageOptions={[1, 10, 25, 50, 100]}
paginationMode="client"
disableSelectionOnClick
page={page}
pageSize={pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
onCellClick={({ id, field }) =>
field === "user" && setActiveUserId(String(id))
}
getRowHeight={() => "auto"}
columns={[
...COLUMNS,
{
field: "quizes",
headerName: "Квизы",
flex: 1,
minWidth: 220,
valueGetter: ({ row }) => String(row.Quizes.length),
renderCell: ({ row }) => (
<Accordion sx={{ width: "100%" }}>
<AccordionSummary
sx={{ backgroundColor: "#26272c", color: "#FFFFFF" }}
expandIcon={<ExpandMoreIcon sx={{ color: "#FFFFFF" }} />}
aria-controls="panel1-content"
id="panel1-header"
>
Статистика по квизам
</AccordionSummary>
<AccordionDetails sx={{ backgroundColor: "#26272c" }}>
<Table
sx={{
width: "80%",
border: "2px solid",
borderColor: theme.palette.secondary.main,
bgcolor: theme.palette.content.main,
color: "white",
}}
>
<TableHead>
<TableRow
sx={{
borderBottom: "2px solid",
borderColor: theme.palette.grayLight.main,
height: "100px",
}}
>
<TableCell sx={{ color: "inherit" }} align="center">
QuizID
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
Регистрации
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
Деньги
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{row.Quizes.map(({ QuizID, Regs, Money }) => (
<TableRow key={QuizID}>
<TableCell sx={{ color: "inherit" }} align="center">
<Button onClick={() => copyQuizLink(QuizID)}>
{QuizID}
</Button>
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
{Regs}
</TableCell>
<TableCell sx={{ color: "inherit" }} align="center">
{(Money / 100).toFixed(2)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</AccordionDetails>
</Accordion>
),
},
]}
/>
<ModalUser
open={openUserModal}
onClose={() => setOpenUserModal(false)}
userId={activeUserId}
/>
</>
);
};

@ -0,0 +1,18 @@
import { Suspense } from "react";
import { Box } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { QuizInfo } from "./QuizInfo";
import { StatisticsSchild } from "./StatisticsSchild";
import { StatisticsPromocode } from "./StastisticsPromocode";
export const QuizStatistics = () => (
<LocalizationProvider dateAdapter={AdapterMoment}>
<QuizInfo />
<StatisticsSchild />
<Suspense fallback={<Box>Loading...</Box>}>
<StatisticsPromocode />
</Suspense>
</LocalizationProvider>
);

@ -0,0 +1,87 @@
import Modal from "@mui/material/Modal";
import {closeDeleteTariffDialog} from "@stores/tariffs";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import makeRequest from "@root/api/makeRequest";
import {parseAxiosError} from "@root/utils/parse-error";
interface Props{
ticketId: string | undefined,
openModal: boolean,
setOpenModal: (a: boolean) => void
}
export default function CloseTicketModal({ticketId, openModal, setOpenModal}: Props) {
const CloseTicket = async () => {
try {
const ticketCloseResponse = await makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + "/heruvym/close" ,
method: "post",
useToken: true,
body: {
"ticket": ticketId
},
});
return [ticketCloseResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось закрыть тикет. ${error}`];
}
}
return (
<Modal
open={openModal}
onClose={() => setOpenModal(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
border: "2px solid gray",
borderRadius: "6px",
boxShadow: 24,
p: 4,
}}
>
<Typography id="modal-modal-title" variant="h6" component="h2">
Вы уверены, что хотите закрыть тикет?
</Typography>
<Box
sx={{
mt: "20px",
display: "flex",
width: "332px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Button
onClick={async ()=>{
CloseTicket()
setOpenModal(false)
}}
sx={{width: "40px", height: "25px"}}
>
Да
</Button>
<Button
onClick={() => setOpenModal(false)}
sx={{width: "40px", height: "25px"}}
>
Нет
</Button>
</Box>
</Box>
</Modal>
)
}

@ -3,9 +3,13 @@ import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined";
import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { Ticket } from "@root/model/ticket";
import { incrementTicketsApiPage, useTicketStore } from "@root/stores/tickets";
import { useEffect, useRef } from "react";
import {useEffect, useRef, useState} from "react";
import TicketItem from "./TicketItem";
import { throttle } from "@frontend/kitui";
import makeRequest from "@root/api/makeRequest";
import {parseAxiosError} from "@root/utils/parse-error";
import {useParams} from "react-router-dom";
import CloseTicketModal from "@pages/dashboard/Content/Support/TicketList/CloseTicketModal";
type TicketListProps = {
closeCollapse?: () => void;
@ -21,6 +25,8 @@ export default function TicketList({
const tickets = useTicketStore((state) => state.tickets);
const ticketsFetchState = useTicketStore((state) => state.ticketsFetchState);
const ticketsBoxRef = useRef<HTMLDivElement>(null);
const ticketId = useParams().ticketId;
const [openModal, setOpenModal] = useState(false)
useEffect(
function updateCurrentPageOnScroll() {
@ -91,6 +97,7 @@ export default function TicketList({
<SearchOutlinedIcon />
</Button>
<Button
onClick={()=> setOpenModal(true)}
variant="text"
sx={{
width: "100%",
@ -109,6 +116,7 @@ export default function TicketList({
ЗАКРЫТЬ ТИКЕТ
<HighlightOffOutlinedIcon />
</Button>
<CloseTicketModal openModal={openModal} setOpenModal={setOpenModal} ticketId={ticketId}/>
</Box>
<Box
ref={ticketsBoxRef}

@ -52,19 +52,19 @@ export default function CreateTariff() {
};
const initialValues: Values = {
nameField: "",
descriptionField: "",
amountField: "",
customPriceField: "",
privilegeIdField: "",
orderField: 0,
privilege: null
};
const initialValues: Values = {
nameField: "",
descriptionField: "",
amountField: "",
customPriceField: "",
privilegeIdField: "",
orderField: 0,
privilege: null
};
const createTariffBackend = async (
values: Values,
formikHelpers: FormikHelpers<Values>
values: Values,
formikHelpers: FormikHelpers<Values>
) => {
if (values.privilege !== null) {
const [, createdTariffError] = await createTariff({
@ -111,241 +111,244 @@ export default function CreateTariff() {
// }
return (
<Formik
initialValues={initialValues}
validate={checkFulledFields}
onSubmit={createTariffBackend}
>
{(props) => (
<Form style={{width: "100%"}} >
<Container
sx={{
p: "20px",
border: "1px solid rgba(224, 224, 224, 1)",
borderRadius: "4px",
display: "flex",
flexDirection: "column",
gap: "12px",
<Formik
initialValues={initialValues}
validate={checkFulledFields}
onSubmit={createTariffBackend}
>
{(props) => (
<Form style={{ width: "100%" }} >
<Container
sx={{
p: "20px",
border: "1px solid rgba(224, 224, 224, 1)",
borderRadius: "4px",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<Typography variant="h6" sx={{ textAlign: "center", mb: "16px" }}>
Создание тарифа
</Typography>
<FormControl
fullWidth
sx={{
height: "52px",
color: theme.palette.secondary.main,
"& .MuiInputLabel-outlined": {
color: theme.palette.secondary.main,
},
"& .MuiInputLabel-outlined.MuiInputLabel-shrink": {
color: theme.palette.secondary.main,
},
}}
}}
>
<Typography variant="h6" sx={{ textAlign: "center", mb: "16px" }}>
Создание тарифа
</Typography>
<FormControl
fullWidth
sx={{
height: "52px",
color: theme.palette.secondary.main,
"& .MuiInputLabel-outlined": {
color: theme.palette.secondary.main,
},
"& .MuiInputLabel-outlined.MuiInputLabel-shrink": {
color: theme.palette.secondary.main,
},
}}
>
<InputLabel
id="privilege-select-label"
sx={{
color: theme.palette.secondary.main,
fontSize: "16px",
lineHeight: "19px",
}}
>
Привилегия
</InputLabel>
<Select
labelId="privilege-select-label"
id="privilege-select"
value={props.values.privilegeIdField}
label="Привилегия"
error={props.touched.privilegeIdField && !!props.errors.privilegeIdField}
onChange={(e) => {
console.log(e.target.value)
console.log(findPrivilegeById(e.target.value))
if (findPrivilegeById(e.target.value) === null) {
return enqueueSnackbar("Привилегия не найдена");
}
props.setFieldValue("privilegeIdField", e.target.value)
props.setFieldValue("privilege", findPrivilegeById(e.target.value))
}}
onBlur={props.handleBlur}
sx={{
color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.secondary.main,
border: "1px solid",
},
".MuiSvgIcon-root ": {
fill: theme.palette.secondary.main,
},
}}
inputProps={{ sx: { pt: "12px" } }}
>
{privileges.map((privilege) => (
<MenuItem
data-cy={`select-option-${privilege.description}`}
key={privilege._id}
value={privilege._id}
sx={{ whiteSpace: "normal", wordBreak: "break-world" }}
>
<InputLabel
id="privilege-select-label"
sx={{
color: theme.palette.secondary.main,
fontSize: "16px",
lineHeight: "19px",
}}
>
Привилегия
</InputLabel>
<Select
labelId="privilege-select-label"
id="privilege-select"
value={props.values.privilegeIdField}
label="Привилегия"
error={props.touched.privilegeIdField && !!props.errors.privilegeIdField}
onChange={(e) => {
props.setFieldValue("privilegeIdField", e.target.value)
props.setFieldValue("privilege", findPrivilegeById(e.target.value))
if (props.values.privilege === null)
return enqueueSnackbar("Привилегия не найдена");
}}
onBlur={props.handleBlur}
sx={{
color: theme.palette.secondary.main,
borderColor: theme.palette.secondary.main,
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.secondary.main,
border: "1px solid",
},
".MuiSvgIcon-root ": {
fill: theme.palette.secondary.main,
},
}}
inputProps={{ sx: { pt: "12px" } }}
>
{privileges.map((privilege) => (
<MenuItem
data-cy={`select-option-${privilege.description}`}
key={privilege._id}
value={privilege._id}
sx={{whiteSpace: "normal", wordBreak: "break-world"}}
>
{privilege.serviceKey}:{privilege.description}
</MenuItem>
))}
</Select>
</FormControl>
{props.values.privilege && (
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<Typography>
Имя: <span>{props.values.privilege.name}</span>
</Typography>
<Typography>
Сервис: <span>{props.values.privilege.serviceKey}</span>
</Typography>
<Typography>
Единица: <span>{props.values.privilege.type}</span>
</Typography>
<Typography>
Стандартная цена за единицу: <span>{currencyFormatter.format(props.values.privilege.price / 100)}</span>
</Typography>
</Box>
)}
<Field
as={TextField}
id="tariff-name"
name="nameField"
variant="filled"
label="Название тарифа"
type="text"
error={props.touched.nameField && !!props.errors.nameField}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.nameField}
</Typography>
}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="amountField"
name="amountField"
variant="filled"
onChange={(e) => {
props.setFieldValue("amountField", e.target.value.replace(/[^\d]/g,''))
}}
value={props.values.amountField}
onBlur={props.handleBlur}
label="Кол-во единиц привилегии"
error={props.touched.amountField && !!props.errors.amountField}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.amountField}
</Typography>
}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="tariff-custom-price"
name="customPriceField"
variant="filled"
onChange={(e) => {
props.setFieldValue("customPriceField", e.target.value.replace(/[^\d]/g,''))
}}
value={props.values.customPriceField}
onBlur={props.handleBlur}
label="Кастомная цена (не обязательно)"
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="tariff-dezcription"
name="descriptionField"
variant="filled"
onChange={(e) => {
props.setFieldValue("descriptionField", e.target.value)
}}
value={props.values.descriptionField}
onBlur={props.handleBlur}
label="Описание"
multiline={true}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="tariff-order"
name="orderField"
variant="filled"
onChange={(e) => {
props.setFieldValue("orderField", e.target.value)
}}
value={props.values.orderField}
onBlur={props.handleBlur}
label="порядковый номер"
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
type={'number'}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<Button
className="btn_createTariffBackend"
type="submit"
disabled={props.isSubmitting}
>
Создать
</Button>
</Container>
</Form>
)}
</Formik>
{privilege.serviceKey}:{privilege.description}
</MenuItem>
))}
</Select>
</FormControl>
{props.values.privilege && (
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<Typography>
Имя: <span>{props.values.privilege.name}</span>
</Typography>
<Typography>
Сервис: <span>{props.values.privilege.serviceKey}</span>
</Typography>
<Typography>
Единица: <span>{props.values.privilege.type}</span>
</Typography>
<Typography>
Стандартная цена за единицу: <span>{currencyFormatter.format(props.values.privilege.price / 100)}</span>
</Typography>
</Box>
)}
<Field
as={TextField}
id="tariff-name"
name="nameField"
variant="filled"
label="Название тарифа"
type="text"
error={props.touched.nameField && !!props.errors.nameField}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.nameField}
</Typography>
}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="amountField"
name="amountField"
variant="filled"
onChange={(e) => {
props.setFieldValue("amountField", e.target.value.replace(/[^\d]/g, ''))
}}
value={props.values.amountField}
onBlur={props.handleBlur}
label="Кол-во единиц привилегии"
error={props.touched.amountField && !!props.errors.amountField}
helperText={
<Typography sx={{ fontSize: "12px", width: "200px" }}>
{props.errors.amountField}
</Typography>
}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="tariff-custom-price"
name="customPriceField"
variant="filled"
onChange={(e) => {
props.setFieldValue("customPriceField", e.target.value.replace(/[^\d]/g, ''))
}}
value={props.values.customPriceField}
onBlur={props.handleBlur}
label="Кастомная цена (не обязательно)"
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="tariff-dezcription"
name="descriptionField"
variant="filled"
onChange={(e) => {
props.setFieldValue("descriptionField", e.target.value)
}}
value={props.values.descriptionField}
onBlur={props.handleBlur}
label="Описание"
multiline={true}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<TextField
id="tariff-order"
name="orderField"
variant="filled"
onChange={(e) => {
props.setFieldValue("orderField", e.target.value)
}}
value={props.values.orderField}
onBlur={props.handleBlur}
label="порядковый номер"
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
}
}}
type={'number'}
InputLabelProps={{
style: {
color: theme.palette.secondary.main
}
}}
/>
<Button
className="btn_createTariffBackend"
type="submit"
disabled={props.isSubmitting}
>
Создать
</Button>
</Container>
</Form>
)}
</Formik>
);
}

@ -45,7 +45,6 @@ export default function EditModal() {
updatedTariff.price = price;
updatedTariff.description = descriptionField;
updatedTariff.order = parseInt(orderField);
const [_, putedTariffError] = await putTariff(updatedTariff);
@ -99,20 +98,20 @@ export default function EditModal() {
sx={{ marginBottom: "10px" }}
/>
<Typography>
Цена: {tariff.price}
Цена: {Math.trunc((tariff.price ?? 0) / 100)}
</Typography>
<TextField
type="number"
onChange={(event) => setPriceField(event.target.value)}
onChange={({ target }) =>
setPriceField(String(+target.value * 100))
}
label="Цена"
name="price"
value={priceField}
value={Math.trunc(Number(priceField) / 100)}
sx={{ marginBottom: "10px" }}
/>
<Typography>
Описание: {tariff.description}
</Typography>
<Typography>Описание: {tariff.description}</Typography>
<TextField
type="text"
multiline={true}
@ -122,9 +121,7 @@ export default function EditModal() {
value={descriptionField}
sx={{ marginBottom: "10px" }}
/>
<Typography>
Порядок: {tariff.order}
</Typography>
<Typography>Порядок: {tariff.order}</Typography>
<TextField
type="number"
onChange={(event) => setOrderField(event.target.value)}

@ -100,7 +100,7 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open"
}));
const links: { path: string; element: JSX.Element; title: string; className: string }[] = [
{ path: "/quizStatistic", element: <>📝</>, title: "Статистика Quiz", className: "menu" },
{ path: "/quizStatistics", element: <>📝</>, title: "Статистика Quiz", className: "menu" },
{ path: "/users", element: <PersonOutlineOutlinedIcon />, title: "Информация о проекте", className: "menu" },
{ path: "/entities", element: <SettingsOutlinedIcon />, title: "Юридические лица", className: "menu" },
{ path: "/tariffs", element: <BathtubOutlinedIcon />, title: "Тарифы", className: "menu" },

@ -48,7 +48,7 @@ export const UserTab = ({ userId }: UserTabProps) => {
<Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Email</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{user?.email}
{user?.email || user?.login}
</Typography>
</Box>
<Box sx={{ marginBottom: "25px" }}>

@ -0,0 +1,35 @@
import { useEffect, useState } from "react";
import { getStatisticPromocode } from "@root/api/quizStatistics";
import type { Moment } from "moment";
import type { AllPromocodeStatistics } from "@root/api/quizStatistics/types";
import moment from "moment";
interface useStatisticProps {
to: Moment | null;
from: Moment | null;
}
export function usePromocodeStatistics({ to, from }: useStatisticProps) {
const formatTo = to?.unix();
const formatFrom = from?.unix() || moment().unix() - 604800;
const [promocodeStatistics, setPromocodeStatistics] = useState<
Record<string, AllPromocodeStatistics>
>({});
useEffect(() => {
const requestStatistics = async () => {
const gottenData = await getStatisticPromocode(
Number(formatFrom),
Number(formatTo)
);
setPromocodeStatistics(gottenData);
};
requestStatistics();
}, [formatTo, formatFrom]);
return promocodeStatistics;
}

@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import {
QuizStatisticResponse,
getStatistic
} from "@root/api/quizStatistic";
import { QuizStatisticResponse, getStatistic } from "@root/api/quizStatistics";
import type { Moment } from "moment";
@ -15,18 +12,23 @@ export function useQuizStatistic({ to, from }: useQuizStatisticProps) {
const formatTo = to?.unix();
const formatFrom = from?.unix();
const [data, setData] = useState<QuizStatisticResponse | null>({ Registrations: 0, Quizes: 0, Results: 0 });
const [data, setData] = useState<QuizStatisticResponse | null>({
Registrations: 0,
Quizes: 0,
Results: 0,
});
useEffect(() => {
const requestStatistics = async () => {
const gottenData = await getStatistic(Number(formatTo), Number(formatFrom));
setData(gottenData)
}
const gottenData = await getStatistic(
Number(formatTo),
Number(formatFrom)
);
setData(gottenData);
};
requestStatistics();
}, [to, from]);
}, [formatTo, formatFrom]);
return { ...data };
}

@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import { getStatisticSchild } from "@root/api/quizStatistics";
import type { Moment } from "moment";
import type { QuizStatisticsItem } from "@root/api/quizStatistics/types";
import moment from "moment";
export const useSchildStatistics = (from: Moment | null, to: Moment | null) => {
const formatTo = to?.unix();
const formatFrom = from?.unix() || moment().unix() - 604800;
const [statistics, setStatistics] = useState<QuizStatisticsItem[]>([]);
useEffect(() => {
const StatisticsShild = async () => {
const gottenData = await getStatisticSchild(
Number(formatFrom),
Number(formatTo)
);
setStatistics(gottenData);
};
StatisticsShild();
}, [formatTo, formatFrom]);
return statistics;
};

@ -16,9 +16,7 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["node"],
"types": ["node"]
},
"include": ["src", "**/*.ts"]
}