added dashboard data

This commit is contained in:
Juthatip McDevitt 2024-04-03 15:08:59 -05:00
parent 3f2495f210
commit 07abcc29d2
13 changed files with 469 additions and 20 deletions

View file

@ -12,6 +12,9 @@ import Transactions from "pages/transactions/Transactions";
import Geography from "pages/geography/Geography";
import Overview from "pages/overview/Overview";
import Daily from "pages/dailyStat/Daily";
import Monthly from "pages/monthlyStat/Monthly";
import Breakdown from "pages/breakdown/Breakdown";
function App() {
@ -35,6 +38,8 @@ function App() {
<Route path="/geography" element={<Geography/>} />
<Route path="/overview" element={<Overview/>} />
<Route path="/daily" element={<Daily/>}/>
<Route path="/monthly" element={<Monthly/>}/>
<Route path="/breakdown" element={<Breakdown/>}/>
</Route>
</Routes>
</ThemeProvider>

View file

@ -0,0 +1,124 @@
import React from 'react'
import { ResponsivePie } from '@nivo/pie'
import { Box, Typography, useTheme } from "@mui/material";
import { useGetSalesQuery } from 'state/api';
const BreakdownChart = ({ isDashboard = false }) => {
const theme = useTheme();
const {data, isLoading} = useGetSalesQuery();
if (!data || isLoading) return "Loading...";
const colors = [
theme.palette.secondary[500],
theme.palette.secondary[300],
theme.palette.secondary[300],
theme.palette.secondary[500],
];
const formattedData = Object.entries(data.salesByCategory).map(
([category, sales], i) => ({
id: category,
label: category,
value: sales,
color: colors[i],
})
);
return (
<Box height={isDashboard ? "400px" : "100%"} width={undefined} minHeight={isDashboard ? "325px" : undefined} minWidth={isDashboard ? "325px" : undefined} position="relative">
<ResponsivePie 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: "data.color" }}
margin={
isDashboard
? { top: 40, right: 80, bottom: 100, left: 50 }
: { top: 40, right: 80, bottom: 80, left: 80 }
}
sortByValue={true}
innerRadius={0.45}
activeOuterRadiusOffset={8}
borderWidth={1}
borderColor={{
from: "color",
modifiers: [["darker", 0.2]],
}}
enableArcLinkLabels={!isDashboard}
arcLinkLabelsTextColor={theme.palette.secondary[200]}
arcLinkLabelsThickness={2}
arcLinkLabelsColor={{ from: "color" }}
arcLabelsSkipAngle={10}
arcLabelsTextColor={{
from: "color",
modifiers: [["darker", 2]],
}}
legends={[
{
anchor: "bottom",
direction: "row",
justify: false,
translateX: isDashboard ? 20 : 0,
translateY: isDashboard ? 50 : 56,
itemsSpacing: 0,
itemWidth: 85,
itemHeight: 18,
itemTextColor: "#999",
itemDirection: "left-to-right",
itemOpacity: 1,
symbolSize: 18,
symbolShape: "circle",
effects: [
{
on: "hover",
style: {
itemTextColor: theme.palette.primary[500],
},
},
],
},
]}
/>
<Box position="absolute" top="50%" left="50%" color={theme.palette.secondary[400]} textAlign="center" pointerEvents="none" sx={{transform: isDashboard ? "translate(-75%, -170%)" : "translate(-50%, -100%)",}}>
<Typography variant="h6">
{!isDashboard && "Total:"} ${data.yearlySalesTotal}
</Typography>
</Box>
</Box>
)
}
export default BreakdownChart

View file

@ -1,7 +1,7 @@
import React from 'react'
import { Box, Divider, Drawer, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography, useTheme } from '@mui/material'
import { SettingsOutlined, ChevronLeft, ChevronRightOutlined, HomeOutlined, Groups2Outlined, ReceiptLongOutlined, PublicOutlined,
TodayOutlined, CalendarMonthOutlined, AdminPanelSettingsOutlined, TrendingUpOutlined, PieChartOutlined } from '@mui/icons-material'
TodayOutlined, CalendarMonthOutlined, PieChartOutlined } from '@mui/icons-material'
import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined';
import PlagiarismOutlinedIcon from '@mui/icons-material/PlagiarismOutlined';
import { useLocation, useNavigate } from 'react-router-dom'
@ -54,18 +54,6 @@ const navItems = [
{
text: "Breakdown",
icon: <PieChartOutlined />
},
{
text: "Management",
icon: null,
},
{
text: "Admin",
icon: <AdminPanelSettingsOutlined />
},
{
text: "Performance",
icon: <TrendingUpOutlined />
},
]
@ -95,8 +83,8 @@ const Sidebar = ({user, drawerWidth, sidebarOpen, setSidebarOpen, nonMobile,}) =
<Box m="1.5rem 2rem 2rem 3rem">
<FlexBetween color={theme.palette.secondary.main}>
<Box display="flex" alignItems="center" gap="0.5rem">
<Typography variant='h4' fontWeight="bold" >
Jtp Dashboard
<Typography variant='h5' fontWeight="bold">
Analytics Dashboard
</Typography>
</Box>
{!nonMobile && (

View file

@ -0,0 +1,24 @@
import React from 'react'
import { Box, Typography, useTheme } from "@mui/material";
import FlexBetween from "./FlexBetween";
const StatBox = ({ title, value, increase, icon, description }) => {
const theme = useTheme();
return (
<Box gridColumn="span 2" gridRow="span 1" display="flex" flexDirection="column" justifyContent="space-between" p="1.25rem 1rem" flex="1 1 100%" backgroundColor={theme.palette.background.alt} borderRadius="0.55rem">
<FlexBetween>
<Typography variant="h6" sx={{ color: theme.palette.secondary[100] }}>{title}</Typography>{icon}
</FlexBetween>
<Typography variant="h3" fontWeight="600" sx={{ color: theme.palette.secondary[200] }}>{value}</Typography>
<FlexBetween gap="1rem">
<Typography variant="h5" fontStyle="italic" sx={{ color: theme.palette.secondary.light }}>{increase}</Typography>
<Typography>{description}</Typography>
</FlexBetween>
</Box>
)
}
export default StatBox

View file

@ -0,0 +1,17 @@
import { Box } from '@mui/material'
import BreakdownChart from 'components/BreakdownChart'
import Header from 'components/Header'
import React from 'react'
const Breakdown = () => {
return (
<Box m="1.5rem 2.5rem">
<Header title="Breakdown" subtitle="Breakdown of Sales By Category"/>
<Box mt="40px" height="75vh">
<BreakdownChart />
</Box>
</Box>
)
}
export default Breakdown

View file

@ -1,8 +1,95 @@
import { Box, Button, Typography, useTheme, useMediaQuery, } from '@mui/material'
import FlexBetween from 'components/FlexBetween'
import Header from 'components/Header'
import React from 'react'
import { DownloadOutlined, Email, PointOfSale, PersonAdd, Traffic } from "@mui/icons-material";
import { DataGrid } from '@mui/x-data-grid';
import BreakdownChart from 'components/BreakdownChart';
import OverviewChart from 'components/OverviewChart';
import { useGetDashboardQuery } from 'state/api';
import StatBox from 'components/StatBox';
const Dashboard = () => {
const theme = useTheme();
const isNonMediumScreens = useMediaQuery("(min-width: 1200px)");
const { data, isLoading } = useGetDashboardQuery();
console.log(data)
const columns = [
{
field: "_id",
headerName: "ID",
flex: 1,
},
{
field: "userId",
headerName: "User ID",
flex: 1,
},
{
field: "createdAt",
headerName: "CreatedAt",
flex: 1,
},
{
field: "products",
headerName: "# of Products",
flex: 0.5,
sortable: false,
renderCell: (params) => params.value.length,
},
{
field: "cost",
headerName: "Cost",
flex: 1,
renderCell: (params) => `$${Number(params.value).toFixed(2)}`,
},
];
return (
<div>Dashboard</div>
<Box m="1.5rem 2.5rem">
<FlexBetween>
<Header title="Dashboard"/>
<Box>
<Button sx={{ backgroundColor: theme.palette.secondary.light, color: theme.palette.background.alt, fontSize: "14px", fontWeight: "bold", padding: "10px 20px", }}>
<DownloadOutlined sx={{ mr: "10px" }} /> Download Reports
</Button>
</Box>
</FlexBetween>
<Box mt="20px" display="grid" gridTemplateColumns="repeat(12, 1fr)" gridAutoRows="160px" gap="20px" sx={{"& > div": { gridColumn: isNonMediumScreens ? undefined : "span 12" }}}>
<StatBox title="Total Customers" value={data && data.totalCustomers} increase="+14%" description="Since last month"
icon={<Email sx={{ color: theme.palette.secondary[300], fontSize: "26px" }}/>}/>
<StatBox title="Sales Today" value={data && data.todayStats.totalSales} increase="+21%" description="Since last month"
icon={<PointOfSale sx={{ color: theme.palette.secondary[300], fontSize: "26px" }}/>}/>
<Box gridColumn="span 8" gridRow="span 2" backgroundColor={theme.palette.background.alt} p="1rem" borderRadius="0.55rem">
<OverviewChart view="sales" isDashboard={true} />
</Box>
<StatBox title="Monthly Sales" value={data && data.thisMonthStats.totalSales} increase="+5%" description="Since last month"
icon={<PersonAdd sx={{ color: theme.palette.secondary[300], fontSize: "26px" }}/>}/>
<StatBox title="Yearly Sales" value={data && data.yearlySalesTotal} increase="+43%" description="Since last month"
icon={<Traffic sx={{ color: theme.palette.secondary[300], fontSize: "26px" }}/>}/>
<Box gridColumn="span 8" gridRow="span 3" sx={{"& .MuiDataGrid-root": {border: "none", borderRadius: "5rem",},
"& .MuiDataGrid-cell": {borderBottom: "none",},
"& .MuiDataGrid-columnHeaders": {backgroundColor: theme.palette.background.alt, color: theme.palette.secondary[100], borderBottom: "none",},
"& .MuiDataGrid-virtualScroller": {backgroundColor: theme.palette.background.alt,},
"& .MuiDataGrid-footerContainer": {backgroundColor: theme.palette.background.alt, color: theme.palette.secondary[100], borderTop: "none",},
"& .MuiDataGrid-toolbarContainer .MuiButton-text": {color: `${theme.palette.secondary[200]} !important`,},}}>
<DataGrid loading={isLoading || !data} getRowId={(row) => row._id} rows={(data && data.transactions) || []} columns={columns}/>
</Box>
<Box gridColumn="span 4" gridRow="span 3" backgroundColor={theme.palette.background.alt} p="1.5rem" borderRadius="0.55rem">
<Typography variant="h6" sx={{ color: theme.palette.secondary[100] }}> Sales By Category</Typography>
<BreakdownChart isDashboard={true} />
<Typography p="0 0.6rem" fontSize="0.8rem" sx={{ color: theme.palette.secondary[200] }}>
Breakdown of real states and information via category for revenue made for this year and total sales.
</Typography>
</Box>
</Box>
</Box>
)
}

View file

@ -0,0 +1,157 @@
import { useTheme } from '@emotion/react'
import { Box } from '@mui/material'
import { ResponsiveLine } from '@nivo/line'
import Header from 'components/Header'
import React, { useMemo } from 'react'
import { useGetSalesQuery } from 'state/api'
const Monthly = () => {
const theme = useTheme();
const {data} = useGetSalesQuery();
const [formattedData] = 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).forEach(({ month, totalSales, totalUnits }) => {
totalSalesLine.data = [
...totalSalesLine.data,
{ x: month, y: totalSales },
];
totalUnitsLine.data = [
...totalUnitsLine.data,
{ x: month, y: totalUnits },
];
});
const formattedData = [totalSalesLine, totalUnitsLine];
return [formattedData];
}, [data]);
return (
<Box m="1.5rem 2.5rem">
<Header title="Monthly sales" subtitle="Chart of monthly sales"/>
<Box height="75vh">
{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 Monthly

View file

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

View file

@ -1,4 +1,6 @@
import User from "../models/User.js";
import Stat from "../models/Stat.js"
import Transaction from "../models/Transaction.js"
export const getUser = async(req, res) => {
try {
@ -7,6 +9,30 @@ export const getUser = async(req, res) => {
res.status(200).json(user);
} catch (error) {
res.status(404).json({message: error.message})
}
};
export const getDashboardStats = async(req, res) => {
try {
const currerntMonth = "November";
const currentYear = 2021;
const currenrtDay = "2021-04-15";
const transactions = await Transaction.find().limit(50).sort({createdOn: -1});
const stat = await Stat.find({year: currentYear});
const {totalCustomers, yearlyTotalSoldUnits, yearlySalesTotal, monthlyData, salesByCategory} = stat[0]
const thisMonthStats = stat[0].monthlyData.find(({month}) => {
return month === currerntMonth;
});
const todayStats = stat[0].dailyData.find(({date}) => {
return date === currenrtDay;
});
res.status(200).json({totalCustomers, yearlyTotalSoldUnits, yearlySalesTotal, monthlyData, salesByCategory, thisMonthStats, todayStats, transactions})
} catch (error) {
res.status(404).json({message: error.message})
}

View file

@ -0,0 +1,12 @@
import mongoose from "mongoose"
import User from "../models/User.js"
export const getAdmins = async(req, res) => {
try {
const admins = await User.find({ role: "admin"}).select("-password");
res.status(200).json(admins);
} catch (error) {
res.status(404).json({message: error.message})
}
}

View file

@ -3,7 +3,7 @@ import mongoose from "mongoose"
const StatSchema = new mongoose.Schema(
{
totalCustomers: Number,
yaerlySalesTotal: Number,
yearlySalesTotal: Number,
yearlyTotalSoldUnits: Number,
yaer: Number,
monthlyData: [

View file

@ -1,9 +1,10 @@
import express from "express"
import {getUser} from "../controllers/general.js"
import {getUser, getDashboardStats} from "../controllers/general.js"
const router = express.Router();
router.get("/user/:id", getUser);
router.get("/dashboard", getDashboardStats)
export default router;

View file

@ -1,5 +1,9 @@
import express from "express"
import {getAdmins} from "../controllers/management.js"
const router = express.Router();
router.get("/admin", getAdmins);
export default router;