- Below contains instructions to replicate the project
mkdir project project/server
cd project/server
npm init -y
npm i express body-parser cors dotenv helmet morgan mongoose nodemon
express --> framework for api's
body-parse --> parsing data coming in
cors --> cross origin data sharing
dotenv --> for enviornment variables
helmet --> protecting apis
morgan --> logging apis calls
mongoose --> handling mongodb calls
nodemon --> live server reload
import express from 'express';
import bodyParser from 'body-parser';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';
import helmet from 'helmet';
import morgan from 'morgan';
/*CONFIGURATION*/
dotenv.config();
const app = express();
app.use(express.json())
app.use(helmet());
app.use(helmet.crossOriginResourcePolicy({policy: "cross-origin"}));
app.use(morgan("common"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:false}));
app.use(cors());
import clientRoutes from "./routes/client.js";
import generalRoutes from "./routes/general.js";
import managementRoutes from "./routes/management.js";
import salesRoutes from "./routes/sales.js";
/*ROUTES*/
app.use("/client",clientRoutes);
app.use("/general",generalRoutes);
app.use("/management",managementRoutes);
app.use("sales",salesRoutes);
mkdir routes
touch routes/client.js routes/general.js routes/management.js
routes/sales.js
mkdir controllers
touch controllers/client.js controllers/general.js controllers/management.js controllers/sales.js
mkdir data models
.
└── server
├── controllers
│  ├── client.js
│  ├── general.js
│  ├── management.js
│  └── sales.js
├── node_modules
├── data
├── index.js
├── models
├── package.json
├── package-lock.json
└── routes
├── client.js
├── general.js
├── management.js
└── sales.js
- Create an account on
cloud.mongodb.com
- Make a Database and use
shared
database Option and leave configuration to default and clickCreate Cluster
. - Redirected to Security Quickstart .
- Create a username and password for who can access the database.
- Click on
Add my Current IP Address
- Click Finish and Close
- You can change the username and password from the database access under the security section.
- You can change the IP Address from the Network access under the security section.
- Click on connect on the cluster created.
- Select the Drivers Option or the option that lets you connect through native drivers
Node.js
. - Copy the Add your connection string into your application code.
- Select the Drivers Option or the option that lets you connect through native drivers
mongodb+srv://dummyuser:<password>@cluster0.5zsbzt8.mongodb.net/?retryWrites=true&w=majority
PORT:5001
- Insert the password of the database instead of the
<password>
/node_modules
.env
{
 "name": "server",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "type": "module",
 "scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "node index.js",
  "dev" : "nodemon index.js"
 },
 "keywords": [],
 "author": "",
 "license": "ISC",
 "dependencies": {
  "body-parser": "^1.20.2",
  "cors": "^2.8.5",
  "dotenv": "^16.3.1",
  "express": "^4.18.2",
  "helmet": "^7.0.0",
  "mongoose": "^7.4.4",
  "morgan": "^1.10.0",
  "nodemon": "^3.0.1"
 }
}
/*MONGOOSE SETUP*/
const PORT= process.env.PORT || 9000;
mongoose
 .connect(process.env.MONGO_URL, {
  useNewURLParser:true,
  useUnifiedTopology:true,
})
.then(() => {
  app.listen(PORT, () => console.log(`Server Port: ${PORT}`));
})
.catch((error) => console.log(`${error} did not connect`));
──$ npm run dev
> server@1.0.0 dev
> nodemon index.js
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node index.js`
Server Port: 5001
Creating a frontend for application
cd ..
npx create-react-app client
-
Check the client application folder is made outside the server folder
-
Download the following Dependencies :
cd client
npm i react-redux @reduxjs/toolkit react-datepicker react-router-dom@6 @mui/material @emotion/react @emotion/styled @mui/icons-material @mui/x-data-grid @nivo/core @nivo/bar @nivo/geo @nivo/pie
@nivo/line
{
  "compilerOptions":{
    "baseUrl":"src"
  },
  "include":["src"]
}
Delete the following files and create some new files
rm src/setupTests.js src/reportWebVitals.js src/App.test.js src/logo.svg src/app.css src/index.css src/App.js
touch src/index.css src/App.js src/theme.js
- install Tailwind shades extension
#666666
select the above the gradient and press the ctrl+k ctrl+g
to get the different shades
#21295c
#ffd166
transformation for light and dark mode and various other settings
// color design tokens export
export const tokensDark = {
grey: {
0: "#ffffff", // manually adjusted
10: "#f6f6f6", // manually adjusted
50: "#f0f0f0", // manually adjusted
100: "#e0e0e0",
200: "#c2c2c2",
300: "#a3a3a3",
400: "#858585",
500: "#666666",
600: "#525252",
700: "#3d3d3d",
800: "#292929",
900: "#141414",
1000: "#000000", // manually adjusted
},
primary: {
// blue
100: "#d3d4de",
200: "#a6a9be",
300: "#7a7f9d",
400: "#4d547d",
500: "#21295c",
600: "#191F45", // manually adjusted
700: "#141937",
800: "#0d1025",
900: "#070812",
},
secondary: {
// yellow
50: "#f0f0f0", // manually adjusted
100: "#fff6e0",
200: "#ffedc2",
300: "#ffe3a3",
400: "#ffda85",
500: "#ffd166",
600: "#cca752",
700: "#997d3d",
800: "#665429",
900: "#332a14",
},
};
// function that reverses the color palette
function reverseTokens(tokensDark) {
const reversedTokens = {};
Object.entries(tokensDark).forEach(([key, val]) => {
const keys = Object.keys(val);
const values = Object.values(val);
const length = keys.length;
const reversedObj = {};
for (let i = 0; i < length; i++) {
reversedObj[keys[i]] = values[length - i - 1];
}
reversedTokens[key] = reversedObj;
});
return reversedTokens;
}
export const tokensLight = reverseTokens(tokensDark);
// mui theme settings
export const themeSettings = (mode) => {
return {
palette: {
mode: mode,
...(mode === "dark"
? {
// palette values for dark mode
primary: {
...tokensDark.primary,
main: tokensDark.primary[400],
light: tokensDark.primary[400],
},
secondary: {
...tokensDark.secondary,
main: tokensDark.secondary[300],
},
neutral: {
...tokensDark.grey,
main: tokensDark.grey[500],
},
background: {
default: tokensDark.primary[600],
alt: tokensDark.primary[500],
},
}
: {
// palette values for light mode
primary: {
...tokensLight.primary,
main: tokensDark.grey[50],
light: tokensDark.grey[100],
},
secondary: {
...tokensLight.secondary,
main: tokensDark.secondary[600],
light: tokensDark.secondary[700],
},
neutral: {
...tokensLight.grey,
main: tokensDark.grey[500],
},
background: {
default: tokensDark.grey[0],
alt: tokensDark.grey[50],
},
}),
},
typography: {
fontFamily: ["Inter", "sans-serif"].join(","),
fontSize: 12,
h1: {
fontFamily: ["Inter", "sans-serif"].join(","),
fontSize: 40,
},
h2: {
fontFamily: ["Inter", "sans-serif"].join(","),
fontSize: 32,
},
h3: {
fontFamily: ["Inter", "sans-serif"].join(","),
fontSize: 24,
},
h4: {
fontFamily: ["Inter", "sans-serif"].join(","),
fontSize: 20,
},
h5: {
fontFamily: ["Inter", "sans-serif"].join(","),
fontSize: 16,
},
h6: {
fontFamily: ["Inter", "sans-serif"].join(","),
fontSize: 14,
},
},
};
};
Create a file index.js inside the server directory data folder
- Ii contains the user data and sales record.
adding the Inter google fonts import in the index.css
in the client directory src
folder
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap");
html,
body,
#root,
.app {
height: 100%;
width: 100%;
font-family: "Inter", sans-serif;
}
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background: #7a7f9d;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #21295c;
}
/* Handle on Hover */
::-webkit-scrollbar-track:hover {
background: #21295c;
}
Create a folder state
inside src
folder in the client
directory . Create a index.js
file inside it. It will initialize the dark and light in the file.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
mode: "dark",
userId: "63701cc1f03239b7f700000e",
};
export const globalSlice = createSlice({
name: "global",
initialState,
reducers: {
setMode: (state) => {
state.mode = state.mode === "light" ? "dark" : "light";
},
},
});
export const { setMode } = globalSlice.actions;
export default globalSlice.reducer;
Edit the following configuration to index.js
in the src
folder
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { configureStore } from "@reduxjs/toolkit";
import globalReducer from "state";
import { Provider } from "react-redux";
import { setupListeners } from "@reduxjs/toolkit/query";
import { api } from "state/api";
const store = configureStore({
reducer: {
global: globalReducer,
[api.reducerPath]: api.reducer,
},
middleware: (getDefault) => getDefault().concat(api.middleware),
});
setupListeners(store.dispatch);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
import { CssBaseline, ThemeProvider } from "@mui/material";
import { createTheme } from "@mui/material/styles";
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { themeSettings } from "theme";
import Layout from "scenes/layout";
import Dashboard from "scenes/dashboard";
import Products from "scenes/products";
import Customers from "scenes/customers";
import Transactions from "scenes/transactions";
import Geography from "scenes/geography";
import Overview from "scenes/overview";
import Daily from "scenes/daily";
import Monthly from "scenes/monthly";
import Breakdown from "scenes/breakdown";
import Admin from "scenes/admin";
import Performance from "scenes/performance";
function App() {
const mode = useSelector((state) => state.global.mode);
const theme = useMemo(() => createTheme(themeSettings(mode)), [mode]);
return (
<div className="app">
<BrowserRouter>
<ThemeProvider theme={theme}>
<CssBaseline />
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/products" element={<Products />} />
<Route path="/customers" element={<Customers />} />
<Route path="/transactions" element={<Transactions />} />
<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 path="/admin" element={<Admin />} />
<Route path="/performance" element={<Performance />} />
</Route>
</Routes>
</ThemeProvider>
</BrowserRouter>
</div>
);
}
export default App;
mkdir src/scenes src/scenes/dashboard
touch src/scenes/dashboard/index.jsx
mkdir src/scenes/layout
touch src/scenes/layout/index.jsx
mkdir src/components
touch src/components/FlexBetween.jsx
const { Box } = require("@mui/material");
const { styled } = require("@mui/system");
const FlexBetween = styled(Box)({
display: "flex",
justifyContent: "space-between",
alignItems: "center",
});
export default FlexBetween;
import React, { useState } from "react";
import { Box, useMediaQuery } from "@mui/material";
import { Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import Navbar from "components/Navbar";
import Sidebar from "components/Sidebar";
import { useGetUserQuery } from "state/api";
const Layout = () => {
const isNonMobile = useMediaQuery("(min-width: 600px)");
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const userId = useSelector((state) => state.global.userId);
const { data } = useGetUserQuery(userId);
return (
<Box display={isNonMobile ? "flex" : "block"} width="100%" height="100%">
<Sidebar
user={data || {}}
isNonMobile={isNonMobile}
drawerWidth="250px"
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
/>
<Box flexGrow={1}>
<Navbar
user={data || {}}
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
/>
<Outlet />
</Box>
</Box>
);
};
export default Layout;
import React, { useState } from "react";
import {
LightModeOutlined,
DarkModeOutlined,
Menu as MenuIcon,
Search,
SettingsOutlined,
ArrowDropDownOutlined,
} from "@mui/icons-material";
import FlexBetween from "components/FlexBetween";
import { useDispatch } from "react-redux";
import { setMode } from "state";
import profileImage from "assets/profile.jpeg";
import {
AppBar,
Button,
Box,
Typography,
IconButton,
InputBase,
Toolbar,
Menu,
MenuItem,
useTheme,
} from "@mui/material";
const Navbar = ({ user, isSidebarOpen, setIsSidebarOpen }) => {
const dispatch = useDispatch();
const theme = useTheme();
const [anchorEl, setAnchorEl] = useState(null);
const isOpen = Boolean(anchorEl);
const handleClick = (event) => setAnchorEl(event.currentTarget);
const handleClose = () => setAnchorEl(null);
return (
<AppBar
sx={{
position: "static",
background: "none",
boxShadow: "none",
}}
>
<Toolbar sx={{ justifyContent: "space-between" }}>
{/* LEFT SIDE */}
<FlexBetween>
<IconButton onClick={() => setIsSidebarOpen(!isSidebarOpen)}>
<MenuIcon />
</IconButton>
<FlexBetween
backgroundColor={theme.palette.background.alt}
borderRadius="9px"
gap="3rem"
p="0.1rem 1.5rem"
>
<InputBase placeholder="Search..." />
<IconButton>
<Search />
</IconButton>
</FlexBetween>
</FlexBetween>
{/* RIGHT SIDE */}
<FlexBetween gap="1.5rem">
<IconButton onClick={() => dispatch(setMode())}>
{theme.palette.mode === "dark" ? (
<DarkModeOutlined sx={{ fontSize: "25px" }} />
) : (
<LightModeOutlined sx={{ fontSize: "25px" }} />
)}
</IconButton>
<IconButton>
<SettingsOutlined sx={{ fontSize: "25px" }} />
</IconButton>
<FlexBetween>
<Button
onClick={handleClick}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
textTransform: "none",
gap: "1rem",
}}
>
<Box
component="img"
alt="profile"
src={profileImage}
height="32px"
width="32px"
borderRadius="50%"
sx={{ objectFit: "cover" }}
/>
<Box textAlign="left">
<Typography
fontWeight="bold"
fontSize="0.85rem"
sx={{ color: theme.palette.secondary[100] }}
>
{user.name}
</Typography>
<Typography
fontSize="0.75rem"
sx={{ color: theme.palette.secondary[200] }}
>
{user.occupation}
</Typography>
</Box>
<ArrowDropDownOutlined
sx={{ color: theme.palette.secondary[300], fontSize: "25px" }}
/>
</Button>
<Menu
anchorEl={anchorEl}
open={isOpen}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
>
<MenuItem onClick={handleClose}>Log Out</MenuItem>
</Menu>
</FlexBetween>
</FlexBetween>
</Toolbar>
</AppBar>
);
};
export default Navbar;
import React from "react";
import {
Box,
Divider,
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
useTheme,
} from "@mui/material";
import {
SettingsOutlined,
ChevronLeft,
ChevronRightOutlined,
HomeOutlined,
ShoppingCartOutlined,
Groups2Outlined,
ReceiptLongOutlined,
PublicOutlined,
PointOfSaleOutlined,
TodayOutlined,
CalendarMonthOutlined,
AdminPanelSettingsOutlined,
TrendingUpOutlined,
PieChartOutlined,
} from "@mui/icons-material";
import { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import FlexBetween from "./FlexBetween";
import profileImage from "assets/profile.jpeg";
const navItems = [
{
text: "Dashboard",
icon: <HomeOutlined />,
},
{
text: "Client Facing",
icon: null,
},
{
text: "Products",
icon: <ShoppingCartOutlined />,
},
{
text: "Customers",
icon: <Groups2Outlined />,
},
{
text: "Transactions",
icon: <ReceiptLongOutlined />,
},
{
text: "Geography",
icon: <PublicOutlined />,
},
{
text: "Sales",
icon: null,
},
{
text: "Overview",
icon: <PointOfSaleOutlined />,
},
{
text: "Daily",
icon: <TodayOutlined />,
},
{
text: "Monthly",
icon: <CalendarMonthOutlined />,
},
{
text: "Breakdown",
icon: <PieChartOutlined />,
},
{
text: "Management",
icon: null,
},
{
text: "Admin",
icon: <AdminPanelSettingsOutlined />,
},
{
text: "Performance",
icon: <TrendingUpOutlined />,
},
];
const Sidebar = ({
user,
drawerWidth,
isSidebarOpen,
setIsSidebarOpen,
isNonMobile,
}) => {
const { pathname } = useLocation();
const [active, setActive] = useState("");
const navigate = useNavigate();
const theme = useTheme();
useEffect(() => {
setActive(pathname.substring(1));
}, [pathname]);
return (
<Box component="nav">
{isSidebarOpen && (
<Drawer
open={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
variant="persistent"
anchor="left"
sx={{
width: drawerWidth,
"& .MuiDrawer-paper": {
color: theme.palette.secondary[200],
backgroundColor: theme.palette.background.alt,
boxSixing: "border-box",
borderWidth: isNonMobile ? 0 : "2px",
width: drawerWidth,
},
}}
>
<Box width="100%">
<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">
ADMINS BOARD
</Typography>
</Box>
{!isNonMobile && (
<IconButton onClick={() => setIsSidebarOpen(!isSidebarOpen)}>
<ChevronLeft />
</IconButton>
)}
</FlexBetween>
</Box>
<List>
{navItems.map(({ text, icon }) => {
if (!icon) {
return (
<Typography key={text} sx={{ m: "2.25rem 0 1rem 3rem" }}>
{text}
</Typography>
);
}
const lcText = text.toLowerCase();
return (
<ListItem key={text} disablePadding>
<ListItemButton
onClick={() => {
navigate(`/${lcText}`);
setActive(lcText);
}}
sx={{
backgroundColor:
active === lcText
? theme.palette.secondary[300]
: "transparent",
color:
active === lcText
? theme.palette.primary[600]
: theme.palette.secondary[100],
}}
>
<ListItemIcon
sx={{
ml: "2rem",
color:
active === lcText
? theme.palette.primary[600]
: theme.palette.secondary[200],
}}
>
{icon}
</ListItemIcon>
<ListItemText primary={text} />
{active === lcText && (
<ChevronRightOutlined sx={{ ml: "auto" }} />
)}
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
<Box position="absolute" bottom="2rem">
<Divider />
<FlexBetween textTransform="none" gap="1rem" m="1.5rem 2rem 0 3rem">
<Box
component="img"
alt="profile"
src={profileImage}
height="40px"
width="40px"
borderRadius="50%"
sx={{ objectFit: "cover" }}
/>
<Box textAlign="left">
<Typography
fontWeight="bold"
fontSize="0.9rem"
sx={{ color: theme.palette.secondary[100] }}
>
{user.name}
</Typography>
<Typography
fontSize="0.8rem"
sx={{ color: theme.palette.secondary[200] }}
>
{user.occupation}
</Typography>
</Box>
<SettingsOutlined
sx={{
color: theme.palette.secondary[300],
fontSize: "25px ",
}}
/>
</FlexBetween>
</Box>
</Drawer>
)}
</Box>
);
};
export default Sidebar;
import express from "express";
import { getUser, getDashboardStats } from "../controllers/general.js";
const router = express.Router();
router.get("/user/:id", getUser);
router.get("/dashboard", getDashboardStats);
export default router;
import mongoose from "mongoose";
const UserSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
min: 2,
max: 100,
},
email: {
type: String,
required: true,
max: 50,
unique: true,
},
password: {
type: String,
required: true,
min: 5,
},
city: String,
state: String,
country: String,
occupation: String,
phoneNumber: String,
transactions: Array,
role: {
type: String,
enum: ["user", "admin", "superadmin"],
default: "admin",
},
},
{ timestamps: true }
);
const User = mongoose.model("User", UserSchema);
export default User;
import User from "../models/User.js";
import OverallStat from "../models/OverallStat.js";
import Transaction from "../models/Transaction.js";
export const getUser = async (req, res) => {
try {
const { id } = req.params;
const user = await User.findById(id);
res.status(200).json(user);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
export const getDashboardStats = async (req, res) => {
try {
// hardcoded values
const currentMonth = "November";
const currentYear = 2021;
const currentDay = "2021-11-15";
/* Recent Transactions */
const transactions = await Transaction.find()
.limit(50)
.sort({ createdOn: -1 });
/* Overall Stats */
const overallStat = await OverallStat.find({ year: currentYear });
const {
totalCustomers,
yearlyTotalSoldUnits,
yearlySalesTotal,
monthlyData,
salesByCategory,
} = overallStat[0];
const thisMonthStats = overallStat[0].monthlyData.find(({ month }) => {
return month === currentMonth;
});
const todayStats = overallStat[0].dailyData.find(({ date }) => {
return date === currentDay;
});
res.status(200).json({
totalCustomers,
yearlyTotalSoldUnits,
yearlySalesTotal,
monthlyData,
salesByCategory,
thisMonthStats,
todayStats,
transactions,
});
} catch (error) {
res.status(404).json({ message: error.message });
}
};
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",
"Admins",
"Performance",
"Dashboard",
],
endpoints: (build) => ({
getUser: build.query({
query: (id) => `general/user/${id}`,
providesTags: ["User"],
}),
getProducts: build.query({
query: () => "client/products",
providesTags: ["Products"],
}),
getCustomers: build.query({
query: () => "client/customers",
providesTags: ["Customers"],
}),
getTransactions: build.query({
query: ({ page, pageSize, sort, search }) => ({
url: "client/transactions",
method: "GET",
params: { page, pageSize, sort, search },
}),
providesTags: ["Transactions"],
}),
getGeography: build.query({
query: () => "client/geography",
providesTags: ["Geography"],
}),
getSales: build.query({
query: () => "sales/sales",
providesTags: ["Sales"],
}),
getAdmins: build.query({
query: () => "management/admins",
providesTags: ["Admins"],
}),
getUserPerformance: build.query({
query: (id) => `management/performance/${id}`,
providesTags: ["Performance"],
}),
getDashboard: build.query({
query: () => "general/dashboard",
providesTags: ["Dashboard"],
}),
}),
});
export const {
useGetUserQuery,
useGetProductsQuery,
useGetCustomersQuery,
useGetTransactionsQuery,
useGetGeographyQuery,
useGetSalesQuery,
useGetAdminsQuery,
useGetUserPerformanceQuery,
useGetDashboardQuery,
} = api;
REACT_APP_BASE_URL=http://localhost:5001
Create a new file Product.js
ProductStat.js
file inside the models
folder in the client
directory
Product.js
import mongoose from "mongoose";
const ProductSchema = new mongoose.Schema(
{
name: String,
price: Number,
description: String,
category: String,
rating: Number,
supply: Number,
},
{ timestamps: true }
);
const Product = mongoose.model("Product", ProductSchema);
export default Product;
ProductStat.js
import mongoose from "mongoose";
const ProductStatSchema = new mongoose.Schema(
{
productId: String,
yearlySalesTotal: Number,
yearlyTotalSoldUnits: Number,
year: Number,
monthlyData: [
{
month: String,
totalSales: Number,
totalUnits: Number,
},
],
dailyData: [
{
date: String,
totalSales: Number,
totalUnits: Number,
},
],
},
{ timestamps: true }
);
const ProductStat = mongoose.model("ProductStat", ProductStatSchema);
export default ProductStat;
Edit the client.js
in the routes
folder in the server
directory
import express from "express";
import {
getProducts,
getCustomers,
getTransactions,
getGeography,
} from "../controllers/client.js";
const router = express.Router();
router.get("/products", getProducts);
router.get("/customers", getCustomers);
router.get("/transactions", getTransactions);
router.get("/geography", getGeography);
export default router;
Edit the client.js
in the controllers
folder in the server
directory
import Product from "../models/Product.js";
import ProductStat from "../models/ProductStat.js";
import User from "../models/User.js";
import Transaction from "../models/Transaction.js";
import getCountryIso3 from "country-iso-2-to-3";
export const getProducts = async (req, res) => {
try {
const products = await Product.find();
const productsWithStats = await Promise.all(
products.map(async (product) => {
const stat = await ProductStat.find({
productId: product._id,
});
return {
...product._doc,
stat,
};
})
);
res.status(200).json(productsWithStats);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
export const getCustomers = async (req, res) => {
try {
const customers = await User.find({ role: "user" }).select("-password");
res.status(200).json(customers);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
export const getTransactions = async (req, res) => {
try {
// sort should look like this: { "field": "userId", "sort": "desc"}
const { page = 1, pageSize = 20, sort = null, search = "" } = req.query;
// formatted sort should look like { userId: -1 }
const generateSort = () => {
const sortParsed = JSON.parse(sort);
const sortFormatted = {
[sortParsed.field]: (sortParsed.sort = "asc" ? 1 : -1),
};
return sortFormatted;
};
const sortFormatted = Boolean(sort) ? generateSort() : {};
const transactions = await Transaction.find({
$or: [
{ cost: { $regex: new RegExp(search, "i") } },
{ userId: { $regex: new RegExp(search, "i") } },
],
})
.sort(sortFormatted)
.skip(page * pageSize)
.limit(pageSize);
const total = await Transaction.countDocuments({
name: { $regex: search, $options: "i" },
});
res.status(200).json({
transactions,
total,
});
} catch (error) {
res.status(404).json({ message: error.message });
}
};
export const getGeography = async (req, res) => {
try {
const users = await User.find();
const mappedLocations = users.reduce((acc, { country }) => {
const countryISO3 = getCountryIso3(country);
if (!acc[countryISO3]) {
acc[countryISO3] = 0;
}
acc[countryISO3]++;
return acc;
}, {});
const formattedLocations = Object.entries(mappedLocations).map(
([country, count]) => {
return { id: country, value: count };
}
);
res.status(200).json(formattedLocations);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
create a file index.jsx
inside the product
folder in the scenes
folder in the src
folder in the client
directory
import React, { useState } from "react";
import {
Box,
Card,
CardActions,
CardContent,
Collapse,
Button,
Typography,
Rating,
useTheme,
useMediaQuery,
} from "@mui/material";
import Header from "components/Header";
import { useGetProductsQuery } from "state/api";
const Product = ({
_id,
name,
description,
price,
rating,
category,
supply,
stat,
}) => {
const theme = useTheme();
const [isExpanded, setIsExpanded] = useState(false);
return (
<Card
sx={{
backgroundImage: "none",
backgroundColor: theme.palette.background.alt,
borderRadius: "0.55rem",
}}
>
<CardContent>
<Typography
sx={{ fontSize: 14 }}
color={theme.palette.secondary[700]}
gutterBottom
>
{category}
</Typography>
<Typography variant="h5" component="div">
{name}
</Typography>
<Typography sx={{ mb: "1.5rem" }} color={theme.palette.secondary[400]}>
${Number(price).toFixed(2)}
</Typography>
<Rating value={rating} readOnly />
<Typography variant="body2">{description}</Typography>
</CardContent>
<CardActions>
<Button
variant="primary"
size="small"
onClick={() => setIsExpanded(!isExpanded)}
>
See More
</Button>
</CardActions>
<Collapse
in={isExpanded}
timeout="auto"
unmountOnExit
sx={{
color: theme.palette.neutral[300],
}}
>
<CardContent>
<Typography>id: {_id}</Typography>
<Typography>Supply Left: {supply}</Typography>
<Typography>
Yearly Sales This Year: {stat.yearlySalesTotal}
</Typography>
<Typography>
Yearly Units Sold This Year: {stat.yearlyTotalSoldUnits}
</Typography>
</CardContent>
</Collapse>
</Card>
);
};
const Products = () => {
const { data, isLoading } = useGetProductsQuery();
const isNonMobile = useMediaQuery("(min-width: 1000px)");
return (
<Box m="1.5rem 2.5rem">
<Header title="PRODUCTS" subtitle="See your list of products." />
{data || !isLoading ? (
<Box
mt="20px"
display="grid"
gridTemplateColumns="repeat(4, minmax(0, 1fr))"
justifyContent="space-between"
rowGap="20px"
columnGap="1.33%"
sx={{
"& > div": { gridColumn: isNonMobile ? undefined : "span 4" },
}}
>
{data.map(
({
_id,
name,
description,
price,
rating,
category,
supply,
stat,
}) => (
<Product
key={_id}
_id={_id}
name={name}
description={description}
price={price}
rating={rating}
category={category}
supply={supply}
stat={stat}
/>
)
)}
</Box>
) : (
<>Loading...</>
)}
</Box>
);
};
export default Products;
import { Typography, Box, useTheme } from "@mui/material";
import React from "react";
const Header = ({ title, subtitle }) => {
const theme = useTheme();
return (
<Box>
<Typography
variant="h2"
color={theme.palette.secondary[100]}
fontWeight="bold"
sx={{ mb: "5px" }}
>
{title}
</Typography>
<Typography variant="h5" color={theme.palette.secondary[300]}>
{subtitle}
</Typography>
</Box>
);
};
export default Header;
Create a file index.jsx
inside the customers
folder in the scenes
folder in the src
folder in the client
directory
import React from "react";
import { Box, useTheme } from "@mui/material";
import { useGetCustomersQuery } from "state/api";
import Header from "components/Header";
import { DataGrid } from "@mui/x-data-grid";
const Customers = () => {
const theme = useTheme();
const { data, isLoading } = useGetCustomersQuery();
console.log("data", data);
const columns = [
{
field: "_id",
headerName: "ID",
flex: 1,
},
{
field: "name",
headerName: "Name",
flex: 0.5,
},
{
field: "email",
headerName: "Email",
flex: 1,
},
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 0.5,
renderCell: (params) => {
return params.value.replace(/^(\d{3})(\d{3})(\d{4})/, "($1)$2-$3");
},
},
{
field: "country",
headerName: "Country",
flex: 0.4,
},
{
field: "occupation",
headerName: "Occupation",
flex: 1,
},
{
field: "role",
headerName: "Role",
flex: 0.5,
},
];
return (
<Box m="1.5rem 2.5rem">
<Header title="CUSTOMERS" subtitle="List of Customers" />
<Box
mt="40px"
height="75vh"
sx={{
"& .MuiDataGrid-root": {
border: "none",
},
"& .MuiDataGrid-cell": {
borderBottom: "none",
},
"& .MuiDataGrid-columnHeaders": {
backgroundColor: theme.palette.background.alt,
color: theme.palette.secondary[100],
borderBottom: "none",
},
"& .MuiDataGrid-virtualScroller": {
backgroundColor: theme.palette.primary.light,
},
"& .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 || []}
columns={columns}
/>
</Box>
</Box>
);
};
export default Customers;
import mongoose from "mongoose";
const TransactionSchema = new mongoose.Schema(
{
userId: String,
cost: String,
products: {
type: [mongoose.Types.ObjectId],
of: Number,
},
},
{ timestamps: true }
);
const Transaction = mongoose.model("Transaction", TransactionSchema);
export default Transaction;
import React, { useState } from "react";
import { Box, useTheme } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import { useGetTransactionsQuery } from "state/api";
import Header from "components/Header";
import DataGridCustomToolbar from "components/DataGridCustomToolbar";
const Transactions = () => {
const theme = useTheme();
// values to be sent to the backend
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(20);
const [sort, setSort] = useState({});
const [search, setSearch] = useState("");
const [searchInput, setSearchInput] = useState("");
const { data, isLoading } = useGetTransactionsQuery({
page,
pageSize,
sort: JSON.stringify(sort),
search,
});
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 (
<Box m="1.5rem 2.5rem">
<Header title="TRANSACTIONS" subtitle="Entire list of transactions" />
<Box
height="80vh"
sx={{
"& .MuiDataGrid-root": {
border: "none",
},
"& .MuiDataGrid-cell": {
borderBottom: "none",
},
"& .MuiDataGrid-columnHeaders": {
backgroundColor: theme.palette.background.alt,
color: theme.palette.secondary[100],
borderBottom: "none",
},
"& .MuiDataGrid-virtualScroller": {
backgroundColor: theme.palette.primary.light,
},
"& .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}
rowCount={(data && data.total) || 0}
rowsPerPageOptions={[20, 50, 100]}
pagination
page={page}
pageSize={pageSize}
paginationMode="server"
sortingMode="server"
onPageChange={(newPage) => setPage(newPage)}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
onSortModelChange={(newSortModel) => setSort(...newSortModel)}
components={{ Toolbar: DataGridCustomToolbar }}
componentsProps={{
toolbar: { searchInput, setSearchInput, setSearch },
}}
/>
</Box>
</Box>
);
};
export default Transactions;
import React from "react";
import { Search } from "@mui/icons-material";
import { IconButton, TextField, InputAdornment } from "@mui/material";
import {
GridToolbarDensitySelector,
GridToolbarContainer,
GridToolbarExport,
GridToolbarColumnsButton,
} from "@mui/x-data-grid";
import FlexBetween from "./FlexBetween";
const DataGridCustomToolbar = ({ searchInput, setSearchInput, setSearch }) => {
return (
<GridToolbarContainer>
<FlexBetween width="100%">
<FlexBetween>
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
<GridToolbarExport />
</FlexBetween>
<TextField
label="Search..."
sx={{ mb: "0.5rem", width: "15rem" }}
onChange={(e) => setSearchInput(e.target.value)}
value={searchInput}
variant="standard"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => {
setSearch(searchInput);
setSearchInput("");
}}
>
<Search />
</IconButton>
</InputAdornment>
),
}}
/>
</FlexBetween>
</GridToolbarContainer>
);
};
export default DataGridCustomToolbar;
npm i country-iso-2-to-3
import React from "react";
import { Box, useTheme } from "@mui/material";
import { useGetGeographyQuery } from "state/api";
import Header from "components/Header";
import { ResponsiveChoropleth } from "@nivo/geo";
import { geoData } from "state/geoData";
const Geography = () => {
const theme = useTheme();
const { data } = useGetGeographyQuery();
return (
<Box m="1.5rem 2.5rem">
<Header title="GEOGRAPHY" subtitle="Find where your users are located." />
<Box
mt="40px"
height="75vh"
border={`1px solid ${theme.palette.secondary[200]}`}
borderRadius="4px"
>
{data ? (
<ResponsiveChoropleth
data={data}
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,
},
},
}}
features={geoData.features}
margin={{ top: 0, right: 0, bottom: 0, left: -50 }}
domain={[0, 60]}
unknownColor="#666666"
label="properties.name"
valueFormat=".2s"
projectionScale={150}
projectionTranslation={[0.45, 0.6]}
projectionRotation={[0, 0, 0]}
borderWidth={1.3}
borderColor="#ffffff"
legends={[
{
anchor: "bottom-right",
direction: "column",
justify: true,
translateX: 0,
translateY: -125,
itemsSpacing: 0,
itemWidth: 94,
itemHeight: 18,
itemDirection: "left-to-right",
itemTextColor: theme.palette.secondary[200],
itemOpacity: 0.85,
symbolSize: 18,
effects: [
{
on: "hover",
style: {
itemTextColor: theme.palette.background.alt,
itemOpacity: 1,
},
},
],
},
]}
/>
) : (
<>Loading...</>
)}
</Box>
</Box>
);
};
export default Geography;
import mongoose from "mongoose";
const OverallStatSchema = new mongoose.Schema(
{
totalCustomers: Number,
yearlySalesTotal: Number,
yearlyTotalSoldUnits: Number,
year: Number,
monthlyData: [
{
month: String,
totalSales: Number,
totalUnits: Number,
},
],
dailyData: [
{
date: String,
totalSales: Number,
totalUnits: Number,
},
],
salesByCategory: {
type: Map,
of: Number,
},
},
{ timestamps: true }
);
const OverallStat = mongoose.model("OverallStat", OverallStatSchema);
export default OverallStat;
import OverallStat from "../models/OverallStat.js";
export const getSales = async (req, res) => {
try {
const overallStats = await OverallStat.find();
res.status(200).json(overallStats[0]);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
Create a file index.jsx
inside the overview
folder in the scenes
folder in the src
folder in the client
directory
import React, { useState } from "react";
import { FormControl, MenuItem, InputLabel, Box, Select } from "@mui/material";
import Header from "components/Header";
import OverviewChart from "components/OverviewChart";
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">
<FormControl sx={{ mt: "1rem" }}>
<InputLabel>View</InputLabel>
<Select
value={view}
label="View"
onChange={(e) => setView(e.target.value)}
>
<MenuItem value="sales">Sales</MenuItem>
<MenuItem value="units">Units</MenuItem>
</Select>
</FormControl>
<OverviewChart view={view} />
</Box>
</Box>
);
};
export default Overview;
import React, { useMemo } from "react";
import { ResponsiveLine } from "@nivo/line";
import { useTheme } from "@mui/material";
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]); // eslint-disable-line react-hooks/exhaustive-deps
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={false}
enableGridY={false}
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;
import React, { useMemo, useState } from "react";
import { Box, useTheme } from "@mui/material";
import Header from "components/Header";
import { ResponsiveLine } from "@nivo/line";
import { useGetSalesQuery } from "state/api";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
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]); // eslint-disable-line react-hooks/exhaustive-deps
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;
import React, { useMemo } from "react";
import { Box, useTheme } from "@mui/material";
import Header from "components/Header";
import { ResponsiveLine } from "@nivo/line";
import { useGetSalesQuery } from "state/api";
const Monthly = () => {
const { data } = useGetSalesQuery();
const theme = useTheme();
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]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<Box m="1.5rem 2.5rem">
<Header title="MONTHLY SALES" subtitle="Chart of monthlysales" />
<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;
import React from "react";
import { Box } from "@mui/material";
import Header from "components/Header";
import BreakdownChart from "components/BreakdownChart";
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;
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 { data, isLoading } = useGetSalesQuery();
const theme = useTheme();
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;
import express from "express";
import { getAdmins, getUserPerformance } from "../controllers/management.js";
const router = express.Router();
router.get("/admins", getAdmins);
router.get("/performance/:id", getUserPerformance);
export default router;
import mongoose from "mongoose";
import User from "../models/User.js";
import Transaction from "../models/Transaction.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 });
}
};
export const getUserPerformance = async (req, res) => {
try {
const { id } = req.params;
const userWithStats = await User.aggregate([
{ $match: { _id: new mongoose.Types.ObjectId(id) } },
{
$lookup: {
from: "affiliatestats",
localField: "_id",
foreignField: "userId",
as: "affiliateStats",
},
},
{ $unwind: "$affiliateStats" },
]);
const saleTransactions = await Promise.all(
userWithStats[0].affiliateStats.affiliateSales.map((id) => {
return Transaction.findById(id);
})
);
const filteredSaleTransactions = saleTransactions.filter(
(transaction) => transaction !== null
);
res
.status(200)
.json({ user: userWithStats[0], sales: filteredSaleTransactions });
} catch (error) {
res.status(404).json({ message: error.message });
}
};
import React from "react";
import { Box, useTheme } from "@mui/material";
import { useGetAdminsQuery } from "state/api";
import { DataGrid } from "@mui/x-data-grid";
import Header from "components/Header";
import CustomColumnMenu from "components/DataGridCustomColumnMenu";
const Admin = () => {
const theme = useTheme();
const { data, isLoading } = useGetAdminsQuery();
const columns = [
{
field: "_id",
headerName: "ID",
flex: 1,
},
{
field: "name",
headerName: "Name",
flex: 0.5,
},
{
field: "email",
headerName: "Email",
flex: 1,
},
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 0.5,
renderCell: (params) => {
return params.value.replace(/^(\d{3})(\d{3})(\d{4})/, "($1)$2-$3");
},
},
{
field: "country",
headerName: "Country",
flex: 0.4,
},
{
field: "occupation",
headerName: "Occupation",
flex: 1,
},
{
field: "role",
headerName: "Role",
flex: 0.5,
},
];
return (
<Box m="1.5rem 2.5rem">
<Header title="ADMINS" subtitle="Managing admins and list of admins" />
<Box
mt="40px"
height="75vh"
sx={{
"& .MuiDataGrid-root": {
border: "none",
},
"& .MuiDataGrid-cell": {
borderBottom: "none",
},
"& .MuiDataGrid-columnHeaders": {
backgroundColor: theme.palette.background.alt,
color: theme.palette.secondary[100],
borderBottom: "none",
},
"& .MuiDataGrid-virtualScroller": {
backgroundColor: theme.palette.primary.light,
},
"& .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 || []}
columns={columns}
components={{
ColumnMenu: CustomColumnMenu,
}}
/>
</Box>
</Box>
);
};
export default Admin;
import {
  GridColumnMenuContainer,
  GridColumnMenuFilterItem, //updated
  GridColumnMenuHideItem, //updated
 } from "@mui/x-data-grid";
 const CustomColumnMenu = (props) => {
  const { hideMenu, currentColumn, open } = props;
  return (
   <GridColumnMenuContainer
    hideMenu={hideMenu}
    currentColumn={currentColumn}
    open={open}
   >
    <GridColumnMenuFilterItem onClick={hideMenu} column={currentColumn} />
    <GridColumnMenuHideItem onClick={hideMenu} column={currentColumn} />
   </GridColumnMenuContainer>
  );
 };
 export default CustomColumnMenu;
import mongoose from "mongoose";
const AffiliateStatSchema = new mongoose.Schema(
{
userId: { type: mongoose.Types.ObjectId, ref: "User" },
affiliateSales: {
type: [mongoose.Types.ObjectId],
ref: "Transaction",
},
},
{ timestamps: true }
);
const AffiliateStat = mongoose.model("AffiliateStat", AffiliateStatSchema);
export default AffiliateStat;
import React from "react";
import { Box, useTheme } from "@mui/material";
import { useGetUserPerformanceQuery } from "state/api";
import { useSelector } from "react-redux";
import { DataGrid } from "@mui/x-data-grid";
import Header from "components/Header";
import CustomColumnMenu from "components/DataGridCustomColumnMenu";
const Performance = () => {
const theme = useTheme();
const userId = useSelector((state) => state.global.userId);
const { data, isLoading } = useGetUserPerformanceQuery(userId);
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 (
<Box m="1.5rem 2.5rem">
<Header
title="PERFORMANCE"
subtitle="Track your Affiliate Sales Performance Here"
/>
<Box
mt="40px"
height="75vh"
sx={{
"& .MuiDataGrid-root": {
border: "none",
},
"& .MuiDataGrid-cell": {
borderBottom: "none",
},
"& .MuiDataGrid-columnHeaders": {
backgroundColor: theme.palette.background.alt,
color: theme.palette.secondary[100],
borderBottom: "none",
},
"& .MuiDataGrid-virtualScroller": {
backgroundColor: theme.palette.primary.light,
},
"& .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.sales) || []}
columns={columns}
components={{
ColumnMenu: CustomColumnMenu,
}}
/>
</Box>
</Box>
);
};
export default Performance;
import React from "react";
import FlexBetween from "components/FlexBetween";
import Header from "components/Header";
import {
DownloadOutlined,
Email,
PointOfSale,
PersonAdd,
Traffic,
} from "@mui/icons-material";
import {
Box,
Button,
Typography,
useTheme,
useMediaQuery,
} from "@mui/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();
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 (
<Box m="1.5rem 2.5rem">
<FlexBetween>
<Header title="DASHBOARD" subtitle="Welcome to your 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" },
}}
>
{/* ROW 1 */}
<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" }}
/>
}
/>
{/* ROW 2 */}
<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>
);
};
export default Dashboard;
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;