added overview & daily data

This commit is contained in:
Juthatip McDevitt 2024-04-03 10:59:31 -05:00
parent 39b2da4a00
commit 3f2495f210
12 changed files with 505 additions and 28 deletions

View file

@ -16,6 +16,7 @@
"@nivo/bar": "^0.85.1", "@nivo/bar": "^0.85.1",
"@nivo/core": "^0.85.1", "@nivo/core": "^0.85.1",
"@nivo/geo": "^0.85.1", "@nivo/geo": "^0.85.1",
"@nivo/line": "^0.85.1",
"@nivo/pie": "^0.85.1", "@nivo/pie": "^0.85.1",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "^2.2.3",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
@ -3900,6 +3901,27 @@
"react": ">= 16.14.0 < 19.0.0" "react": ">= 16.14.0 < 19.0.0"
} }
}, },
"node_modules/@nivo/line": {
"version": "0.85.1",
"resolved": "https://registry.npmjs.org/@nivo/line/-/line-0.85.1.tgz",
"integrity": "sha512-BLswEMiBiFxpHaRoiKp7d3S4P3gzj0OYVBojFEEG+g19lmIEeTTc7aZsXz2pTz/NdzM6fwZqTD3llIhl6LfXFg==",
"dependencies": {
"@nivo/annotations": "0.85.1",
"@nivo/axes": "0.85.1",
"@nivo/colors": "0.85.1",
"@nivo/core": "0.85.1",
"@nivo/legends": "0.85.1",
"@nivo/scales": "0.85.1",
"@nivo/tooltip": "0.85.1",
"@nivo/voronoi": "0.85.1",
"@react-spring/web": "9.4.5 || ^9.7.2",
"d3-shape": "^1.3.5",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": ">= 16.14.0 < 19.0.0"
}
},
"node_modules/@nivo/pie": { "node_modules/@nivo/pie": {
"version": "0.85.1", "version": "0.85.1",
"resolved": "https://registry.npmjs.org/@nivo/pie/-/pie-0.85.1.tgz", "resolved": "https://registry.npmjs.org/@nivo/pie/-/pie-0.85.1.tgz",
@ -3962,6 +3984,21 @@
"react": ">= 16.14.0 < 19.0.0" "react": ">= 16.14.0 < 19.0.0"
} }
}, },
"node_modules/@nivo/voronoi": {
"version": "0.85.1",
"resolved": "https://registry.npmjs.org/@nivo/voronoi/-/voronoi-0.85.1.tgz",
"integrity": "sha512-HJuc1Lhc7RhJyZCnn2eB1nqX6tsczUY4Z1YY3rl1Gy5HfW1vpoJZHQtWzelnvVcpj3qTrwI9QGLmDYE12HAeOQ==",
"dependencies": {
"@nivo/core": "0.85.1",
"@types/d3-delaunay": "^5.3.0",
"@types/d3-scale": "^4.0.8",
"d3-delaunay": "^5.3.0",
"d3-scale": "^4.0.2"
},
"peerDependencies": {
"react": ">= 16.14.0 < 19.0.0"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4883,6 +4920,11 @@
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
}, },
"node_modules/@types/d3-delaunay": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-5.3.4.tgz",
"integrity": "sha512-GEQuDXVKQvHulQ+ecKyCubOmVjXrifAj7VR26rWVAER/IbWemaT/Tmo84ESiTtoDghg5ILdMZH7pYXQEt/Vu9A=="
},
"node_modules/@types/d3-format": { "node_modules/@types/d3-format": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz",
@ -7714,6 +7756,14 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-delaunay": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
"integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==",
"dependencies": {
"delaunator": "4"
}
},
"node_modules/d3-format": { "node_modules/d3-format": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
@ -8015,6 +8065,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delaunator": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
"integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",

View file

@ -11,6 +11,7 @@
"@nivo/bar": "^0.85.1", "@nivo/bar": "^0.85.1",
"@nivo/core": "^0.85.1", "@nivo/core": "^0.85.1",
"@nivo/geo": "^0.85.1", "@nivo/geo": "^0.85.1",
"@nivo/line": "^0.85.1",
"@nivo/pie": "^0.85.1", "@nivo/pie": "^0.85.1",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "^2.2.3",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",

View file

@ -10,6 +10,8 @@ import Products from "pages/products/Products";
import Customers from "pages/customers/Customers"; import Customers from "pages/customers/Customers";
import Transactions from "pages/transactions/Transactions"; import Transactions from "pages/transactions/Transactions";
import Geography from "pages/geography/Geography"; import Geography from "pages/geography/Geography";
import Overview from "pages/overview/Overview";
import Daily from "pages/dailyStat/Daily";
function App() { function App() {
@ -31,6 +33,8 @@ function App() {
<Route path="/customers" element={<Customers/>} /> <Route path="/customers" element={<Customers/>} />
<Route path="/transactions" element={<Transactions/>} /> <Route path="/transactions" element={<Transactions/>} />
<Route path="/geography" element={<Geography/>} /> <Route path="/geography" element={<Geography/>} />
<Route path="/overview" element={<Overview/>} />
<Route path="/daily" element={<Daily/>}/>
</Route> </Route>
</Routes> </Routes>
</ThemeProvider> </ThemeProvider>

View file

@ -0,0 +1,157 @@
import React, { useMemo } from 'react'
import {ResponsiveLine} from "@nivo/line"
import { useTheme } from '@emotion/react'
import { useGetSalesQuery } from 'state/api';
const OverviewChart = ({ isDashboard = false, view }) => {
const theme = useTheme();
const {data, isLoading} = useGetSalesQuery();
const [totalSalesLine, totalUnitsLine] = useMemo(() => {
if(!data) return [];
const { monthlyData } = data;
const totalSalesLine = {
id: "totalSales",
color: theme.palette.secondary.main,
data: [],
};
const totalUnitsLine = {
id: "totalUnits",
color: theme.palette.secondary[600],
data: [],
};
Object.values(monthlyData).reduce(
(acc, {month, totalSales, totalUnits}) => {
const curSales = acc.sales + totalSales;
const curUnits = acc.units + totalUnits;
totalSalesLine.data = [...totalSalesLine.data, { x: month, y: curSales },];
totalUnitsLine.data = [...totalUnitsLine.data, { x: month, y: curUnits },];
return {sales: curSales, units: curUnits};
},
{sales: 0, units: 0}
);
return [[totalSalesLine], [totalUnitsLine]];
}, [data]);
if (!data || isLoading) return "Loading...";
return (
<ResponsiveLine data={view === "sales" ? totalSalesLine : totalUnitsLine}
theme={{
axis: {
domain: {
line: {
stroke: theme.palette.secondary[200],
},
},
legend: {
text: {
fill: theme.palette.secondary[200],
},
},
ticks: {
line: {
stroke: theme.palette.secondary[200],
strokeWidth: 1,
},
text: {
fill: theme.palette.secondary[200],
},
},
},
legends: {
text: {
fill: theme.palette.secondary[200],
},
},
tooltip: {
container: {
color: theme.palette.primary.main,
},
},
}}
margin={{ top: 20, right: 50, bottom: 50, left: 70 }}
xScale={{ type: "point" }}
yScale={{
type: "linear",
min: "auto",
max: "auto",
stacked: false,
reverse: false,
}}
yFormat=" >-.2f"
curve="catmullRom"
enableArea={isDashboard}
axisTop={null}
axisRight={null}
axisBottom={{
format: (v) => {
if (isDashboard) return v.slice(0, 3);
return v;
},
orient: "bottom",
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: isDashboard ? "" : "Month",
legendOffset: 36,
legendPosition: "middle",
}}
axisLeft={{
orient: "left",
tickValues: 5,
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: isDashboard
? ""
: `Total ${view === "sales" ? "Revenue" : "Units"} for Year`,
legendOffset: -60,
legendPosition: "middle",
}}
enableGridX={true}
enableGridY={true}
pointSize={10}
pointColor={{ theme: "background" }}
pointBorderWidth={2}
pointBorderColor={{ from: "serieColor" }}
pointLabelYOffset={-12}
useMesh={true}
legends={
!isDashboard
? [
{
anchor: "bottom-right",
direction: "column",
justify: false,
translateX: 30,
translateY: -40,
itemsSpacing: 0,
itemDirection: "left-to-right",
itemWidth: 80,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: "circle",
symbolBorderColor: "rgba(0, 0, 0, .5)",
effects: [
{
on: "hover",
style: {
itemBackground: "rgba(0, 0, 0, .03)",
itemOpacity: 1,
},
},
],
},
]
: undefined
}
/>
)
}
export default OverviewChart

View file

@ -0,0 +1,176 @@
import { Box, useTheme } from '@mui/material'
import Header from 'components/Header'
import React, { useState, useMemo } from 'react'
import { useGetSalesQuery } from 'state/api';
import DatePicker from 'react-datepicker';
import "react-datepicker/dist/react-datepicker.css";
import { ResponsiveLine } from '@nivo/line';
const Daily = () => {
const [startDate, setStartDate] = useState(new Date("2021-02-01"));
const [endDate, setEndDate] = useState(new Date("2021-03-01"));
const { data } = useGetSalesQuery();
const theme = useTheme();
const [formattedData] = useMemo(() => {
if (!data) return [];
const { dailyData } = data;
const totalSalesLine = {
id: "totalSales",
color: theme.palette.secondary.main,
data: [],
};
const totalUnitsLine = {
id: "totalUnits",
color: theme.palette.secondary[600],
data: [],
};
Object.values(dailyData).forEach(({ date, totalSales, totalUnits }) => {
const dateFormatted = new Date(date);
if (dateFormatted >= startDate && dateFormatted <= endDate) {
const splitDate = date.substring(date.indexOf("-") + 1);
totalSalesLine.data = [
...totalSalesLine.data,
{ x: splitDate, y: totalSales },
];
totalUnitsLine.data = [
...totalUnitsLine.data,
{ x: splitDate, y: totalUnits },
];
}
});
const formattedData = [totalSalesLine, totalUnitsLine];
return [formattedData];
}, [data, startDate, endDate]);
return (
<Box m="1.5rem 2.5rem">
<Header title="Daily sales" subtitle="Chart of daily sales"/>
<Box height="75vh">
<Box display="flex" justifyContent="flex-end">
<Box>
<DatePicker selected={startDate} onChange={(date) => setStartDate(date)} selectsStart startDate={startDate} endDate={endDate} />
</Box>
<Box>
<DatePicker selected={endDate} onChange={(date) => setEndDate(date)} selectsEnd startDate={startDate} endDate={endDate} minDate={startDate} />
</Box>
</Box>
{data ? (
<ResponsiveLine data={formattedData}
theme={{
axis: {
domain: {
line: {
stroke: theme.palette.secondary[200],
},
},
legend: {
text: {
fill: theme.palette.secondary[200],
},
},
ticks: {
line: {
stroke: theme.palette.secondary[200],
strokeWidth: 1,
},
text: {
fill: theme.palette.secondary[200],
},
},
},
legends: {
text: {
fill: theme.palette.secondary[200],
},
},
tooltip: {
container: {
color: theme.palette.primary.main,
},
},
}}
colors={{ datum: "color" }}
margin={{ top: 50, right: 50, bottom: 70, left: 60 }}
xScale={{ type: "point" }}
yScale={{
type: "linear",
min: "auto",
max: "auto",
stacked: false,
reverse: false,
}}
yFormat=" >-.2f"
curve="catmullRom"
axisTop={null}
axisRight={null}
axisBottom={{
orient: "bottom",
tickSize: 5,
tickPadding: 5,
tickRotation: 90,
legend: "Month",
legendOffset: 60,
legendPosition: "middle",
}}
axisLeft={{
orient: "left",
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: "Total",
legendOffset: -50,
legendPosition: "middle",
}}
enableGridX={false}
enableGridY={false}
pointSize={10}
pointColor={{ theme: "background" }}
pointBorderWidth={2}
pointBorderColor={{ from: "serieColor" }}
pointLabelYOffset={-12}
useMesh={true}
legends={[
{
anchor: "top-right",
direction: "column",
justify: false,
translateX: 50,
translateY: 0,
itemsSpacing: 0,
itemDirection: "left-to-right",
itemWidth: 80,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: "circle",
symbolBorderColor: "rgba(0, 0, 0, .5)",
effects: [
{
on: "hover",
style: {
itemBackground: "rgba(0, 0, 0, .03)",
itemOpacity: 1,
},
},
],
},
]}
/>
) : (
<>Loading...</>
)}
</Box>
</Box>
)
}
export default Daily

View file

@ -17,38 +17,38 @@ const Geography = () => {
{data? ( {data? (
<ResponsiveChoropleth data={data} <ResponsiveChoropleth data={data}
theme={{ theme={{
axis: { axis: {
domain: { domain: {
line: { line: {
stroke: theme.palette.secondary[200], stroke: theme.palette.secondary[200],
},
},
legend: {
text: {
fill: theme.palette.secondary[200],
},
},
ticks: {
line: {
stroke: theme.palette.secondary[200],
strokeWidth: 1,
},
text: {
fill: theme.palette.secondary[200],
},
}, },
}, },
legends: { legend: {
text: { text: {
fill: theme.palette.secondary[200], fill: theme.palette.secondary[200],
}, },
}, },
tooltip: { ticks: {
container: { line: {
color: theme.palette.primary.main, stroke: theme.palette.secondary[200],
strokeWidth: 1,
},
text: {
fill: theme.palette.secondary[200],
}, },
}, },
}} },
legends: {
text: {
fill: theme.palette.secondary[200],
},
},
tooltip: {
container: {
color: theme.palette.primary.main,
},
},
}}
features={geographyData.features} features={geographyData.features}
margin={{ top: 0, right: 0, bottom: 0, left: -50 }} margin={{ top: 0, right: 0, bottom: 0, left: -50 }}
colors="YlGnBu" colors="YlGnBu"

View file

@ -0,0 +1,27 @@
import { Box, FormControl, InputLabel, MenuItem, Select } from '@mui/material'
import Header from 'components/Header'
import OverviewChart from 'components/OverviewChart';
import React, { useState } from 'react'
const Overview = () => {
const [view, setView] = useState("units");
return (
<Box m="1.5rem 2.5rem">
<Header title="Overview" subtitle="Overview of general revenue and profit"/>
<Box height="75vh" mt="2rem">
<FormControl>
<InputLabel>View</InputLabel>
<Select label="View" value={view} onChange={(e) => setView(e.target.value)}>
<MenuItem value="sales">Sales</MenuItem>
<MenuItem value="units">Unit</MenuItem>
</Select>
</FormControl>
<OverviewChart view={view}/>
</Box>
</Box>
)
}
export default Overview

View file

@ -3,7 +3,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
export const api = createApi({ export const api = createApi({
baseQuery: fetchBaseQuery({baseUrl: process.env.REACT_APP_BASE_URL}), baseQuery: fetchBaseQuery({baseUrl: process.env.REACT_APP_BASE_URL}),
reducerPath: "adminApi", reducerPath: "adminApi",
tagTypes: ["User", "Products", "Customers", "Transactions", "Geography" ], tagTypes: ["User", "Products", "Customers", "Transactions", "Geography", "Sales" ],
endpoints: (build) => ({ endpoints: (build) => ({
getUser: build.query({ getUser: build.query({
query: (id) => `general/user/${id}`, query: (id) => `general/user/${id}`,
@ -28,8 +28,12 @@ export const api = createApi({
getGeography: build.query({ getGeography: build.query({
query: () => "client/geography", query: () => "client/geography",
providesTags: ["Geography"], providesTags: ["Geography"],
}),
getSales: build.query({
query: () => "sales/sales",
providesTags: ["Sales"]
}) })
}) })
}) })
export const {useGetUserQuery, useGetProductsQuery, useGetCustomersQuery, useGetTransactionsQuery, useGetGeographyQuery} = api; export const {useGetUserQuery, useGetProductsQuery, useGetCustomersQuery, useGetTransactionsQuery, useGetGeographyQuery, useGetSalesQuery} = api;

View file

@ -0,0 +1,14 @@
import Stat from "../models/Stat.js"
export const getSales =async(req, res) => {
try {
const stats = await Stat.find();
res.status(200).json(stats[0]);
} catch (error) {
res.status(404).json({message: error.message})
}
}

View file

@ -10,11 +10,11 @@ import generalRoutes from "./routes/general.js"
import managmentRoutes from "./routes/management.js" import managmentRoutes from "./routes/management.js"
import saleRoutes from "./routes/sales.js" import saleRoutes from "./routes/sales.js"
import User from "./models/User.js" import User from "./models/User.js"
import {dataUser, dataProduct, dataProductStat, dataTransaction} from "./data/index.js" import {dataUser, dataProduct, dataProductStat, dataTransaction, dataOverallStat} from "./data/index.js"
import Product from "./models/Product.js" import Product from "./models/Product.js"
import ProductStat from "./models/ProductStat.js" import ProductStat from "./models/ProductStat.js"
import Transaction from "./models/Transaction.js" import Transaction from "./models/Transaction.js"
import Stat from "./models/Stat.js"
//configuration setup //configuration setup
dotenv.config(); dotenv.config();
@ -48,6 +48,8 @@ mongoose.connect(process.env.MONGO_URL, {
//ProductStat.insertMany(dataProductStat); //ProductStat.insertMany(dataProductStat);
//User.insertMany(dataUser); //User.insertMany(dataUser);
//Transaction.insertMany(dataTransaction); //Transaction.insertMany(dataTransaction);
//Stat.insertMany(dataOverallStat);
}).catch((error) => console.log(`${error} didn't connect`)) }).catch((error) => console.log(`${error} didn't connect`))

View file

@ -0,0 +1,32 @@
import mongoose from "mongoose"
const StatSchema = new mongoose.Schema(
{
totalCustomers: Number,
yaerlySalesTotal: Number,
yearlyTotalSoldUnits: Number,
yaer: Number,
monthlyData: [
{
month: String,
totalSales: Number,
totalUnits: Number,
},
],
dailyData: [
{
date: String,
totalSales: Number,
totalUnits: Number,
},
],
salesByCategory:{
type: Map,
of: Number,
},
},
{timestamps: true}
);
const Stat = mongoose.model("Stat", StatSchema);
export default Stat;

View file

@ -1,5 +1,10 @@
import express from "express" import express from "express"
import {getSales} from "../controllers/sales.js"
const router = express.Router(); const router = express.Router();
router.get("/sales", getSales)
export default router; export default router;