add statistics

main
Anton Zhuravlev 1 year ago
parent 9100449a61
commit dc55a46cf9

@ -3,6 +3,9 @@ import * as ReactDOM from 'react-dom/client';
import App from './app/app'; import App from './app/app';
import {BrowserRouter} from "react-router-dom"; import {BrowserRouter} from "react-router-dom";
import {Provider} from "react-redux";
import store from "./store";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
@ -10,7 +13,9 @@ const root = ReactDOM.createRoot(
root.render( root.render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<Provider store={store}>
<App/> <App/>
</Provider>
</BrowserRouter> </BrowserRouter>
</StrictMode> </StrictMode>
); );

@ -0,0 +1,32 @@
import {createSlice} from "@reduxjs/toolkit";
import {timeFormatEnum} from "@perm-hack/ui";
export interface IStatFilters {
data: {
start_date: string
} | {
start_data: string,
end_date: string
} | {}
type: "date" | "period"
}
const initialState: IStatFilters = {
data: {},
type: "date"
}
const statisticsFilterSlice = createSlice({
name: "statFilter",
initialState,
reducers: {
setData: (state, action) => {
state.data = action.payload;
},
setType: (state, action) => {
state.type = state.type === "date" ? "period" : "date"
}
}
})
export const {setData, setType} = statisticsFilterSlice.actions;
export default statisticsFilterSlice.reducer

@ -0,0 +1,20 @@
import {configureStore} from "@reduxjs/toolkit"
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
import statisticsFilterSlice from "./features/statisticsFilterSlice";
import {statisticsFilterApi} from "./services/statisticsFilterApi";
const store = configureStore({
reducer: {
statFilter: statisticsFilterSlice,
[statisticsFilterApi.reducerPath]: statisticsFilterApi.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(statisticsFilterApi.middleware),
})
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: (data: any) => AppDispatch = useDispatch // Export a hook that can be reused to resolve types
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

@ -0,0 +1,16 @@
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
export const statisticsFilterApi = createApi({
reducerPath: "statFilterApi",
baseQuery: fetchBaseQuery({baseUrl: "https://ayin.k-lab.su/api/analytics"}),
endpoints: (build) => ({
getDate: build.mutation({
query: (data) => ({
url: '/all',
method: 'POST',
body: data,
}),
}),
}),
})
export const {useGetDateMutation} = statisticsFilterApi

1083
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -8,15 +8,18 @@
"@nestjs/common": "^10.0.2", "@nestjs/common": "^10.0.2",
"@nestjs/core": "^10.0.2", "@nestjs/core": "^10.0.2",
"@nestjs/platform-express": "^10.0.2", "@nestjs/platform-express": "^10.0.2",
"@reduxjs/toolkit": "^1.9.7",
"@swc/helpers": "~0.5.2", "@swc/helpers": "~0.5.2",
"@tanstack/react-query": "^5.0.5",
"@types/classnames": "^2.3.1", "@types/classnames": "^2.3.1",
"@types/react-icons": "^3.0.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"antd": "^5.10.2", "antd": "^5.10.2",
"axios": "^1.0.0", "axios": "^1.0.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.11.0",
"react-redux": "^8.1.3",
"react-router-dom": "^6.17.0", "react-router-dom": "^6.17.0",
"react-switch-selector": "^2.2.1", "react-switch-selector": "^2.2.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@ -24,6 +27,7 @@
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/plots": "^1.2.5",
"@babel/core": "^7.14.5", "@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.14.5",
"@nestjs/schematics": "^10.0.1", "@nestjs/schematics": "^10.0.1",

@ -1,5 +1,3 @@
.container { .container {
padding: 20px; padding: 20px;
display: flex;
flex-direction: column;
} }

@ -1,13 +1,17 @@
import cls from './statistics.module.scss'; import cls from './statistics.module.scss';
import {Filters} from "@perm-hack/ui"; // eslint-disable-next-line @nx/enforce-module-boundaries
import {Filters, Sidebar, StatisticsCharts} from "@perm-hack/ui";
import {useState} from "react";
/* eslint-disable-next-line */ /* eslint-disable-next-line */
export interface StatisticsProps {} export interface StatisticsProps {
}
export function Statistics(props: StatisticsProps) { export function Statistics(props: StatisticsProps) {
return ( return (
<div className={cls.container}> <div className={cls.container}>
<Filters/> <Filters/>
<StatisticsCharts/>
</div> </div>
); );
} }

@ -1,3 +1,4 @@
export * from './lib/statistics-charts/statistics-charts';
export * from './lib/filters/filters'; export * from './lib/filters/filters';
export * from './lib/button/button'; export * from './lib/button/button';
export * from './lib/sidebar-item/sidebar-item'; export * from './lib/sidebar-item/sidebar-item';

@ -1,7 +1,7 @@
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: end; align-items: flex-end;
flex: 1 1 auto; flex: 1 1 auto;
padding: 20px; padding: 20px;
background-color: var(--white); background-color: var(--white);

@ -1,12 +1,18 @@
.container { .container {
grid-area: 1 / 1 / 2 / 6;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 30px; gap: 30px;
} }
.button { .button {
font-size: 14px; font-size: 14px;
border-radius: 20px; border-radius: 20px;
padding: 5px 10px; padding: 5px 10px;
color: var(--white); color: var(--white);
background-color: #94BF5E; background-color: #94BF5E;
&:disabled {
background-color: #eee;
}
} }

@ -1,39 +1,73 @@
import cls from './filters.module.scss'; import React, {useEffect, useState} from "react";
import {ConfigProvider, DatePicker} from "antd"; import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import utc from "dayjs/plugin/utc";
import SwitchSelector from "react-switch-selector"; import SwitchSelector from "react-switch-selector";
import dayjs from 'dayjs'; import {ConfigProvider, DatePicker} from "antd";
import customParseFormat from 'dayjs/plugin/customParseFormat'; import cls from "./filters.module.scss";
import {useState} from "react"; import {useGetDateMutation} from "../../../../apps/crud/src/store/services/statisticsFilterApi";
import {useAppDispatch} from "../../../../apps/crud/src/store";
/* eslint-disable-next-line */ import {setData, setType} from "../../../../apps/crud/src/store/features/statisticsFilterSlice";
enum timeFormatEnum { export enum timeFormatEnum {
DATE = "date", DATE = "date",
PERIOD = "period" PERIOD = "period",
} }
export function Filters() { export function Filters() {
const [timeFormat, setTimeFormat] = useState<timeFormatEnum>(timeFormatEnum.DATE) const [timeFormat, setTimeFormat] = useState<timeFormatEnum>(timeFormatEnum.DATE);
const [timeData, setTimeData] = useState<string>();
const [mutation, {data}] = useGetDateMutation();
const dispatch = useAppDispatch();
const options = [ const options = [
{ {
label: "Дата", label: "Дата",
value: "date", value: "date",
selectedBackgroundColor: "#94BF5E" selectedBackgroundColor: "#94BF5E",
}, },
{ {
label: "Период", label: "Период",
value: "period", value: "period",
selectedBackgroundColor: "#94BF5E" selectedBackgroundColor: "#94BF5E",
}, },
] ];
const handleChange = (time: never, timeString: string) => { dayjs.extend(customParseFormat);
console.log(timeString) dayjs.extend(utc);
function convertToISO8601(timeString: string): string {
const parsedTime = dayjs(timeString, {format: "YYYY-MM-DD HH:mm:ss"});
const iso8601Time = parsedTime.utc().format("YYYY-MM-DDTHH:mm:ss.SSS[Z]");
return iso8601Time;
} }
const handleChange = (time: never, timeString: string) => {
setTimeData(timeString);
};
const onChange = (newValue: timeFormatEnum) => { const onChange = (newValue: timeFormatEnum) => {
setTimeFormat(newValue) setTimeFormat(newValue);
dispatch(setType())
}; };
async function handleData() {
if (timeFormat === timeFormatEnum.DATE) {
mutation({start_date: convertToISO8601(timeData)});
} else {
mutation({
start_date: convertToISO8601(timeData[0]),
end_date: convertToISO8601(timeData[1]),
});
}
}
useEffect(() => {
if (data) {
dispatch(setData(data));
}
}, [data]);
return ( return (
<div className={cls.container}> <div className={cls.container}>
<div className="your-required-wrapper" style={{width: 200, height: 30}}> <div className="your-required-wrapper" style={{width: 200, height: 30}}>
@ -46,17 +80,30 @@ export function Filters() {
border={"1px solid #94BF5E"} border={"1px solid #94BF5E"}
/> />
</div> </div>
<ConfigProvider theme={{ <ConfigProvider
theme={{
token: { token: {
colorPrimary: "#94BF5E" colorPrimary: "#94BF5E",
} },
}}> }}
{timeFormat === timeFormatEnum.PERIOD ? >
<DatePicker.RangePicker placeholder={["Время начала", "Время конца"]} onChange={handleChange} showTime/> : {timeFormat === timeFormatEnum.PERIOD ? (
<DatePicker onChange={handleChange} showTime placeholder="Дата и время"/> <DatePicker.RangePicker
} placeholder={["Время начала", "Время конца"]}
onChange={handleChange}
showTime
/>
) : (
<DatePicker
onChange={handleChange}
showTime
placeholder="Дата и время"
/>
)}
</ConfigProvider> </ConfigProvider>
<button className={cls.button}>Показать</button> <button disabled={!timeData} onClick={handleData} className={cls.button}>
Показать
</button>
</div> </div>
); );
} }

@ -18,12 +18,16 @@
gap: 0 10px; gap: 0 10px;
justify-content: center; justify-content: center;
.text { .text {
text-align: end; display: flex;
align-items: center;
color: var(--gray); color: var(--gray);
font-size: 20px; font-size: 20px;
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
line-height: normal; line-height: normal;
& svg {
margin-right: 5px;
}
} }
.percent { .percent {
@ -39,6 +43,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
& p { & p {
padding: 0; padding: 0;
margin: 0; margin: 0;

@ -1,5 +1,6 @@
import styles from './sidebar-item.module.scss'; import styles from './sidebar-item.module.scss';
import Button from "../button/button"; import Button from "../button/button";
import {ImLeaf} from "react-icons/im"
/* eslint-disable-next-line */ /* eslint-disable-next-line */
export interface SidebarItemProps { export interface SidebarItemProps {
@ -42,6 +43,7 @@ export function SidebarItem() {
<div className={styles.item} key={i * 2}> <div className={styles.item} key={i * 2}>
<div className={styles.heading}> <div className={styles.heading}>
<p className={styles.text}> <p className={styles.text}>
<ImLeaf color="#94BF5E"/>
{item.name} {item.name}
</p> </p>
<p className={styles.percent}> <p className={styles.percent}>

@ -1,5 +1,5 @@
.container { .container {
flex: 0 0 20%; flex: 0 0 25%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 30px; gap: 30px;

Loading…
Cancel
Save