add statistics

main
Anton Zhuravlev 1 year ago
parent 9100449a61
commit dc55a46cf9

@ -1,16 +1,21 @@
import { StrictMode } from 'react'; import {StrictMode} from 'react';
import * as ReactDOM from 'react-dom/client'; 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
); );
root.render( root.render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <Provider store={store}>
</BrowserRouter> <App/>
</StrictMode> </Provider>
</BrowserRouter>
</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,15 +1,19 @@
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/>
</div> <StatisticsCharts/>
); </div>
);
} }
export default Statistics; export default Statistics;

@ -1,23 +1,23 @@
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"moduleResolution": "node", "moduleResolution": "node",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"importHelpers": true, "importHelpers": true,
"target": "es2015", "target": "es2015",
"module": "esnext", "module": "esnext",
"lib": ["es2020", "dom"], "lib": ["es2020", "dom"],
"skipLibCheck": true, "skipLibCheck": true,
"skipDefaultLibCheck": true, "skipDefaultLibCheck": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@perm-hack/page": ["page/src/index.ts"], "@perm-hack/page": ["page/src/index.ts"],
"@perm-hack/ui": ["ui/src/index.ts"] "@perm-hack/ui": ["ui/src/index.ts"]
} }
}, },
"exclude": ["node_modules", "tmp"] "exclude": ["node_modules", "tmp"]
} }

@ -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,64 +1,111 @@
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 options = [ const [timeData, setTimeData] = useState<string>();
{ const [mutation, {data}] = useGetDateMutation();
label: "Дата", const dispatch = useAppDispatch();
value: "date",
selectedBackgroundColor: "#94BF5E" const options = [
}, {
{ label: "Дата",
label: "Период", value: "date",
value: "period", selectedBackgroundColor: "#94BF5E",
selectedBackgroundColor: "#94BF5E" },
}, {
] label: "Период",
value: "period",
selectedBackgroundColor: "#94BF5E",
},
];
dayjs.extend(customParseFormat);
dayjs.extend(utc);
const handleChange = (time: never, timeString: string) => { function convertToISO8601(timeString: string): string {
console.log(timeString) const parsedTime = dayjs(timeString, {format: "YYYY-MM-DD HH:mm:ss"});
} const iso8601Time = parsedTime.utc().format("YYYY-MM-DDTHH:mm:ss.SSS[Z]");
const onChange = (newValue: timeFormatEnum) => { return iso8601Time;
setTimeFormat(newValue) }
};
return ( const handleChange = (time: never, timeString: string) => {
<div className={cls.container}> setTimeData(timeString);
<div className="your-required-wrapper" style={{width: 200, height: 30}}> };
<SwitchSelector
onChange={onChange} const onChange = (newValue: timeFormatEnum) => {
options={options} setTimeFormat(newValue);
fontSize={16} dispatch(setType())
backgroundColor={"#fff"} };
fontColor={"#94BF5E"}
border={"1px solid #94BF5E"} async function handleData() {
/> if (timeFormat === timeFormatEnum.DATE) {
</div> mutation({start_date: convertToISO8601(timeData)});
<ConfigProvider theme={{ } else {
token: { mutation({
colorPrimary: "#94BF5E" start_date: convertToISO8601(timeData[0]),
end_date: convertToISO8601(timeData[1]),
});
} }
}}> }
{timeFormat === timeFormatEnum.PERIOD ?
<DatePicker.RangePicker placeholder={["Время начала", "Время конца"]} onChange={handleChange} showTime/> : useEffect(() => {
<DatePicker onChange={handleChange} showTime placeholder="Дата и время"/> if (data) {
dispatch(setData(data));
} }
</ConfigProvider> }, [data]);
<button className={cls.button}>Показать</button>
</div> return (
); <div className={cls.container}>
<div className="your-required-wrapper" style={{width: 200, height: 30}}>
<SwitchSelector
onChange={onChange}
options={options}
fontSize={16}
backgroundColor={"#fff"}
fontColor={"#94BF5E"}
border={"1px solid #94BF5E"}
/>
</div>
<ConfigProvider
theme={{
token: {
colorPrimary: "#94BF5E",
},
}}
>
{timeFormat === timeFormatEnum.PERIOD ? (
<DatePicker.RangePicker
placeholder={["Время начала", "Время конца"]}
onChange={handleChange}
showTime
/>
) : (
<DatePicker
onChange={handleChange}
showTime
placeholder="Дата и время"
/>
)}
</ConfigProvider>
<button disabled={!timeData} onClick={handleData} className={cls.button}>
Показать
</button>
</div>
);
} }
export default Filters; export default Filters;

@ -1,59 +1,64 @@
.item { .item {
background: var(--white); background: var(--white);
border-radius: var(--border-medium); border-radius: var(--border-medium);
padding: 10px 20px; padding: 10px 20px;
display: flex; display: flex;
gap: 0 8px; gap: 0 8px;
justify-content: space-between; justify-content: space-between;
} }
.line { .line {
width: 1px; width: 1px;
background-color: var(--black); background-color: var(--black);
} }
.heading { .heading {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0 10px; gap: 0 10px;
justify-content: center; justify-content: center;
.text { .text {
text-align: end; display: flex;
color: var(--gray); align-items: center;
font-size: 20px; color: var(--gray);
font-style: normal; font-size: 20px;
font-weight: 300; font-style: normal;
line-height: normal; font-weight: 300;
} line-height: normal;
& svg {
margin-right: 5px;
}
}
.percent { .percent {
font-size: 54px; font-size: 54px;
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
line-height: normal; line-height: normal;
} }
} }
.info { .info {
padding: 10px; padding: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
& p {
padding: 0;
margin: 0;
font-size: 36px;
font-style: normal;
font-weight: 300;
line-height: normal;
color: var(--black
);
}
& span { & p {
font-size: 20px; padding: 0;
font-style: normal; margin: 0;
font-weight: 300; font-size: 36px;
line-height: normal; font-style: normal;
} font-weight: 300;
line-height: normal;
color: var(--black
);
}
& span {
font-size: 20px;
font-style: normal;
font-weight: 300;
line-height: normal;
}
} }

@ -1,63 +1,65 @@
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 {
name: string name: string
percent: number | string percent: number | string
weight: number | string weight: number | string
href: string href: string
} }
export function SidebarItem() { export function SidebarItem() {
const obj: SidebarItemProps[] = [ const obj: SidebarItemProps[] = [
{ {
name: "Дерево", name: "Дерево",
href: "some", href: "some",
percent: "10%", percent: "10%",
weight: 200 weight: 200
}, },
{ {
name: "Металл", name: "Металл",
href: "some", href: "some",
percent: "10%", percent: "10%",
weight: 200 weight: 200
}, },
{ {
name: "Пластик", name: "Пластик",
href: "some", href: "some",
percent: "10%", percent: "10%",
weight: 200 weight: 200
}, },
{ {
name: "Стекло", name: "Стекло",
href: "some", href: "some",
percent: "10%", percent: "10%",
weight: 200 weight: 200
} }
] ]
return ( return (
<> <>
{obj.map((item, i) => ( {obj.map((item, i) => (
<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}>
{item.name} <ImLeaf color="#94BF5E"/>
</p> {item.name}
<p className={styles.percent}> </p>
{item.percent} <p className={styles.percent}>
</p> {item.percent}
</div> </p>
<span className={styles.line}></span> </div>
<div className={styles.info}> <span className={styles.line}></span>
<p>{item.weight}</p> <div className={styles.info}>
<span>Тонн</span> <p>{item.weight}</p>
<Button link={"some"} className={styles.link}>Аналитика</Button> <span>Тонн</span>
</div> <Button link={"some"} className={styles.link}>Аналитика</Button>
</div> </div>
))} </div>
</> ))}
); </>
);
} }
export default SidebarItem; export default SidebarItem;

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