Added dashboard framework. Components are not yet created nor is the query finalized. BOD-79

Missed in previous commit. BOD-79
This commit is contained in:
Patrick Fic
2020-07-14 15:21:38 -07:00
parent e91751e20c
commit 2eb8360f5d
28 changed files with 753 additions and 85 deletions

View File

@@ -2,8 +2,8 @@ import axios from "axios";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { auth } from "../../firebase/firebase.utils";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -97,6 +97,13 @@ export default connect(
<button onClick={handle}>Hit with Header.</button>
<button onClick={handleLocal}>Hit Localhost with Header.</button>
<button onClick={handleQbxml}>Qbxml</button>
<button
onClick={() => {
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
}}
>
Log an ImEX Event.
</button>
</div>
);
});

View File

@@ -0,0 +1,82 @@
import { Card } from "antd";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import {
Area,
Bar,
CartesianGrid,
ComposedChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from "recharts";
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
const { t } = useTranslation();
const jobsByDate = {
"2020-07-5": [{ clm_total: 1224 }],
"2020-07-8": [{ clm_total: 987 }, { clm_total: 8755 }],
"2020-07-12": [{ clm_total: 684 }, { clm_total: 12022 }],
"2020-07-21": [{ clm_total: 15000 }],
"2020-07-28": [{ clm_total: 122 }, { clm_total: 4522 }],
};
const listOfDays = Utils.ListOfDaysInCurrentMonth();
const chartData = listOfDays.reduce((acc, val) => {
//Sum up the current day.
let dailySales;
if (!!jobsByDate[val]) {
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
return dayAcc + dayVal.clm_total;
}, 0);
} else {
dailySales = 0;
}
const theValue = {
date: moment(val).format("D dd"),
dailySales,
accSales:
acc.length > 0 ? acc[acc.length - 1].accSales + dailySales : dailySales,
};
return [...acc, theValue];
}, []);
return (
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={chartData}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Area
type="monotone"
name="Accumulated Sales"
dataKey="accSales"
fill="#8884d8"
stroke="#8884d8"
/>
<Bar
name="Daily Sales"
dataKey="dailySales"
//stackId="day"
barSize={20}
fill="#413ea0"
/>
</ComposedChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -0,0 +1,30 @@
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import { Card, Statistic } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
const { t } = useTranslation();
const aboveTargetMonthlySales = false;
return (
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.projectedmonthlysales")}
value={222000.0}
precision={2}
prefix={
<div>
{aboveTargetMonthlySales ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
/>
</Card>
);
}

View File

@@ -0,0 +1,33 @@
import React from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
export default function DashboardTotalProductionDollars({
data,
...cardProps
}) {
const { t } = useTranslation();
const aboveTargetProductionDollars = false;
return (
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.productiondollars")}
value={175000.0}
precision={2}
prefix={
<div>
{aboveTargetProductionDollars ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
/>
</Card>
);
}

View File

@@ -0,0 +1,19 @@
import React from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
export default function DashboardTotalProductionHours({ data, ...cardProps }) {
const { t } = useTranslation();
const aboveTargetHours = true;
return (
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.productionhours")}
value={750}
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
/>
</Card>
);
}

View File

@@ -1,64 +1,176 @@
import { Card } from "antd";
import Icon from "@ant-design/icons";
import { Button, Dropdown, Menu, notification } from "antd";
import React, { useState } from "react";
import { useMutation, useQuery } from "react-apollo";
import { Responsive, WidthProvider } from "react-grid-layout";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { MdClose } from "react-icons/md";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following:
// /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import "./dashboard-grid.styles.css";
const Sdiv = styled.div`
position: absolute;
height: 80%;
width: 80%;
top: 10%;
left: 10%;
// background-color: #ffcc00;
`;
import "./dashboard-grid.styles.scss";
const ResponsiveReactGridLayout = WidthProvider(Responsive);
export default function DashboardGridComponent() {
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function DashboardGridComponent({ currentUser, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS);
const { t } = useTranslation();
const [state, setState] = useState({
layout: [
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
{ i: "2", x: 2, y: 0, w: 2, h: 2 },
{ i: "3", x: 4, y: 0, w: 2, h: 2 }
]
layout: bodyshop.associations[0].user.dashboardlayout || [
{ i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 },
// { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 },
],
});
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const defaultProps = {
className: "layout",
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
// cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
// rowHeight: 100
const handleLayoutChange = async (newLayout) => {
setState({ ...state, layout: newLayout });
const result = await updateLayout({
variables: { email: currentUser.email, layout: newLayout },
});
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
};
const handleRemoveComponent = (key) => {
const idxToRemove = state.layout.findIndex((i) => i.i === key);
const newLayout = state.layout;
newLayout.splice(idxToRemove, 1);
console.log(newLayout);
handleLayoutChange(newLayout);
};
const handleAddComponent = (e) => {
handleLayoutChange([
...state.layout,
{
i: e.key,
x: (state.layout.length * 2) % (state.cols || 12),
y: Infinity, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
]);
};
// We're using the cols coming back from this to calculate where to add new items.
const onBreakpointChange = (breakpoint, cols) => {
console.log("breakpoint, cols", breakpoint, cols);
// setState({ ...state, breakpoint: breakpoint, cols: cols });
setState({ ...state, breakpoint: breakpoint, cols: cols });
};
if (true) return null;
const existingLayoutKeys = state.layout.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
return (
<Sdiv>
The Grid.
<div>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
<ResponsiveReactGridLayout
{...defaultProps}
className="layout"
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
width="100%"
onLayoutChange={handleLayoutChange}
onBreakpointChange={onBreakpointChange}
width='100%'
onLayoutChange={layout => {
console.log("layout", layout);
setState({ ...state, layout });
}}>
>
{state.layout.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<Card style={{ width: "100px" }} key={item.i} data-grid={item}>
A Card {index}
</Card>
<div key={item.i} data-grid={item}>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
right: ".25rem",
top: ".25rem",
cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent
className="dashboard-card"
size="small"
style={{ height: "100%", width: "100%" }}
/>
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</Sdiv>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(DashboardGridComponent);
const componentList = {
ProductionDollars: {
label: "Production Dollars",
component: DashboardTotalProductionDollars,
w: 2,
h: 1,
},
ProductionHours: {
label: "Production Hours",
component: DashboardTotalProductionHours,
w: 2,
h: 1,
},
ProjectedMonthlySales: {
label: "Projected Monthly Sales",
component: DashboardProjectedMonthlySales,
w: 2,
h: 1,
},
MonthlyRevenueGraph: {
label: "Monthly Sales Graph",
component: DashboardMonthlyRevenueGraph,
w: 2,
h: 2,
},
};

View File

@@ -124,3 +124,5 @@
.react-resizable-hide > .react-resizable-handle {
display: none;
}

View File

@@ -0,0 +1,12 @@
.dashboard-card {
// background-color: green;
.ant-card-body {
// background-color: red;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
}

View File

@@ -8,39 +8,45 @@ class ErrorBoundary extends React.Component {
this.state = {
hasErrored: false,
error: null,
info: null,
};
}
static getDerivedStateFromError(error) {
return { hasErrored: true, error };
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
return { hasErrored: true, error: error };
}
componentDidCatch(error, info) {
console.log("Exception Caught by Error Boundary.", error, info);
this.setState({ ...this.state, error, info });
}
render() {
console.log("this.state", this.state);
const { t } = this.props;
if (this.state.hasErrored === true) {
return (
<div>
<Result
status='500'
status="500"
title={t("general.labels.exceptiontitle")}
subTitle={t("general.messages.exception")}
extra={
<Space>
<Button
type='primary'
type="primary"
onClick={() => {
window.location.reload();
}}>
}}
>
{t("general.actions.refresh")}
</Button>
<Button
onClick={() => {
alert("Not implemented yet.");
}}>
}}
>
{t("general.actions.submitticket")}
</Button>
</Space>
@@ -50,7 +56,10 @@ class ErrorBoundary extends React.Component {
<Col offset={6} span={12}>
<Collapse bordered={false}>
<Collapse.Panel header={t("general.labels.errors")}>
{JSON.stringify(this.state.error || "")}
<div>
<strong>{this.state.error.message}</strong>
</div>
<div>{this.state.error.stack}</div>
</Collapse.Panel>
</Collapse>
</Col>

View File

@@ -4,5 +4,9 @@ import "./loading-skeleton.styles.scss";
import { Skeleton } from "antd";
export default function LoadingSkeleton(props) {
return <Skeleton {...props} className='loading-skeleton' active />;
return (
<Skeleton {...props} className="loading-skeleton" active>
{props.children}
</Skeleton>
);
}

View File

@@ -1,21 +1,33 @@
import moment from "moment";
import React from "react";
import { connect } from "react-redux";
import {
ComposedChart,
Line,
Area,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
CartesianGrid, ComposedChart,
Legend, Line,
ResponsiveContainer, Tooltip, XAxis,
YAxis
} from "recharts";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import moment from "moment";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,