Merge remote-tracking branch 'origin/staging'
This commit is contained in:
commit
d49208b56f
@ -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 };
|
||||
}
|
||||
};
|
||||
89
src/api/quizStatistics/index.ts
Normal file
89
src/api/quizStatistics/index.ts
Normal file
@ -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 {};
|
||||
}
|
||||
};
|
||||
31
src/api/quizStatistics/types.ts
Normal file
31
src/api/quizStatistics/types.ts
Normal file
@ -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>
|
||||
</>
|
||||
}
|
||||
86
src/pages/dashboard/Content/QuizStatistics/DateFilter.tsx
Normal file
86
src/pages/dashboard/Content/QuizStatistics/DateFilter.tsx
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
83
src/pages/dashboard/Content/QuizStatistics/QuizInfo.tsx
Normal file
83
src/pages/dashboard/Content/QuizStatistics/QuizInfo.tsx
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
196
src/pages/dashboard/Content/QuizStatistics/StatisticsSchild.tsx
Normal file
196
src/pages/dashboard/Content/QuizStatistics/StatisticsSchild.tsx
Normal file
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
18
src/pages/dashboard/Content/QuizStatistics/index.tsx
Normal file
18
src/pages/dashboard/Content/QuizStatistics/index.tsx
Normal file
@ -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" }}>
|
||||
|
||||
35
src/utils/hooks/usePromocodeStatistics.ts
Normal file
35
src/utils/hooks/usePromocodeStatistics.ts
Normal file
@ -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 };
|
||||
}
|
||||
|
||||
28
src/utils/hooks/useSchildStatistics.ts
Normal file
28
src/utils/hooks/useSchildStatistics.ts
Normal file
@ -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"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user