diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index abd4507e6..e724e1926 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -4684,6 +4684,152 @@ + + dashboard + + + actions + + + addcomponent + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + errors + + + updatinglayout + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + titles + + + monthlyrevenuegraph + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + productiondollars + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + productionhours + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + projectedmonthlysales + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + documents diff --git a/client/src/components/_test/test.component.jsx b/client/src/components/_test/test.component.jsx index 6067540c9..a14db2124 100644 --- a/client/src/components/_test/test.component.jsx +++ b/client/src/components/_test/test.component.jsx @@ -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( + ); }); diff --git a/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx b/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx new file mode 100644 index 000000000..095c0d93d --- /dev/null +++ b/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx @@ -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 ( + + + + + + + + + + + + + + ); +} diff --git a/client/src/components/dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx b/client/src/components/dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx new file mode 100644 index 000000000..65d3f8edc --- /dev/null +++ b/client/src/components/dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx @@ -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 ( + + + {aboveTargetMonthlySales ? ( + + ) : ( + + )} + $ + + } + valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }} + /> + + ); +} diff --git a/client/src/components/dashboard-components/total-production-dollars/total-production-dollars.component.jsx b/client/src/components/dashboard-components/total-production-dollars/total-production-dollars.component.jsx new file mode 100644 index 000000000..46e3e8f2c --- /dev/null +++ b/client/src/components/dashboard-components/total-production-dollars/total-production-dollars.component.jsx @@ -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 ( + + + {aboveTargetProductionDollars ? ( + + ) : ( + + )} + $ + + } + valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }} + /> + + ); +} diff --git a/client/src/components/dashboard-components/total-production-hours/total-production-hours.component.jsx b/client/src/components/dashboard-components/total-production-hours/total-production-hours.component.jsx new file mode 100644 index 000000000..48b087cd8 --- /dev/null +++ b/client/src/components/dashboard-components/total-production-hours/total-production-hours.component.jsx @@ -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 ( + + : } + valueStyle={{ color: aboveTargetHours ? "green" : "red" }} + /> + + ); +} diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index 353799588..084840de1 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -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 = ( + + {Object.keys(componentList).map((key) => ( + + {componentList[key].label} + + ))} + + ); + return ( - - The Grid. +
+ + + { - console.log("layout", layout); - setState({ ...state, layout }); - }}> + > {state.layout.map((item, index) => { + const TheComponent = componentList[item.i].component; return ( - - A Card {index} - +
+ + handleRemoveComponent(item.i)} + /> + + +
); })}
- +
); } + +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, + }, +}; diff --git a/client/src/components/dashboard-grid/dashboard-grid.styles.css b/client/src/components/dashboard-grid/dashboard-grid.styles.css index 10969c599..6b74ec5ee 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.styles.css +++ b/client/src/components/dashboard-grid/dashboard-grid.styles.css @@ -124,3 +124,5 @@ .react-resizable-hide > .react-resizable-handle { display: none; } + + diff --git a/client/src/components/dashboard-grid/dashboard-grid.styles.scss b/client/src/components/dashboard-grid/dashboard-grid.styles.scss new file mode 100644 index 000000000..425ba3788 --- /dev/null +++ b/client/src/components/dashboard-grid/dashboard-grid.styles.scss @@ -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; + } +} diff --git a/client/src/components/error-boundary/error-boundary.component.jsx b/client/src/components/error-boundary/error-boundary.component.jsx index 7a28e5edd..8a15e923c 100644 --- a/client/src/components/error-boundary/error-boundary.component.jsx +++ b/client/src/components/error-boundary/error-boundary.component.jsx @@ -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 (
@@ -50,7 +56,10 @@ class ErrorBoundary extends React.Component { - {JSON.stringify(this.state.error || "")} +
+ {this.state.error.message} +
+
{this.state.error.stack}
diff --git a/client/src/components/loading-skeleton/loading-skeleton.component.jsx b/client/src/components/loading-skeleton/loading-skeleton.component.jsx index 45b7dc535..6d9af741a 100644 --- a/client/src/components/loading-skeleton/loading-skeleton.component.jsx +++ b/client/src/components/loading-skeleton/loading-skeleton.component.jsx @@ -4,5 +4,9 @@ import "./loading-skeleton.styles.scss"; import { Skeleton } from "antd"; export default function LoadingSkeleton(props) { - return ; + return ( + + {props.children} + + ); } diff --git a/client/src/components/scoreboard-chart/scoreboard-chart.component.jsx b/client/src/components/scoreboard-chart/scoreboard-chart.component.jsx index 6a0d781e0..7a2c32e0e 100644 --- a/client/src/components/scoreboard-chart/scoreboard-chart.component.jsx +++ b/client/src/components/scoreboard-chart/scoreboard-chart.component.jsx @@ -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, diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 01491c9e5..6c7c937e6 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -3,6 +3,13 @@ import gql from "graphql-tag"; export const QUERY_BODYSHOP = gql` query QUERY_BODYSHOP { bodyshops(where: { associations: { active: { _eq: true } } }) { + associations { + user { + authid + email + dashboardlayout + } + } address1 address2 city @@ -114,3 +121,36 @@ export const QUERY_STRIPE_ID = gql` } } `; + +export const QUERY_DASHBOARD_DETAILS = gql` + query QUERY_DASHBOARD_DETAILS { + query + QUERY_DASHBOARD_DETAILS { + jobs { + id + clm_total + scheduled_completion + date_invoiced + ins_co_nm + } + compJobs: jobs(where: { inproduction: { _eq: true } }) { + id + scheduled_completion + labhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAB" } }) { + aggregate { + sum { + mod_lb_hrs + } + } + } + larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) { + aggregate { + sum { + mod_lb_hrs + } + } + } + } + } + } +`; diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 694109c89..d633762c4 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -828,6 +828,8 @@ export const QUERY_ALL_JOBS_PAGINATED = gql` } `; + + export const QUERY_JOB_CLOSE_DETAILS = gql` query QUERY_JOB_CLOSE_DETAILS($id: uuid!) { jobs_by_pk(id: $id) { diff --git a/client/src/graphql/user.queries.js b/client/src/graphql/user.queries.js index 8452e7550..3b86be6a4 100644 --- a/client/src/graphql/user.queries.js +++ b/client/src/graphql/user.queries.js @@ -13,6 +13,18 @@ export const UPSERT_USER = gql` } `; +export const UPDATE_DASHBOARD_LAYOUT = gql` + mutation UPDATE_DASHBOARD_LAYOUT($email: String!, $layout: jsonb!) { + update_users_by_pk( + pk_columns: { email: $email } + _set: { dashboardlayout: $layout } + ) { + email + dashboardlayout + } + } +`; + export const UPDATE_FCM_TOKEN = gql` mutation UPDATE_FCM_TOKEN($authEmail: String!, $token: jsonb!) { update_users( diff --git a/client/src/pages/manage-root/manage-root.page.component.jsx b/client/src/pages/manage-root/manage-root.page.component.jsx index 28daf94c2..f6c88d2d8 100644 --- a/client/src/pages/manage-root/manage-root.page.component.jsx +++ b/client/src/pages/manage-root/manage-root.page.component.jsx @@ -1,34 +1,10 @@ import React from "react"; import DashboardGridComponent from "../../components/dashboard-grid/dashboard-grid.component"; -import Test from "../../components/_test/test.component"; -import { analytics, logImEXEvent } from "../../firebase/firebase.utils"; export default function ManageRootPageComponent() { //const client = useApolloClient(); return (
- - -
); diff --git a/client/src/pages/manage-root/manage-root.page.container.jsx b/client/src/pages/manage-root/manage-root.page.container.jsx index c8627fc7e..2616da67b 100644 --- a/client/src/pages/manage-root/manage-root.page.container.jsx +++ b/client/src/pages/manage-root/manage-root.page.container.jsx @@ -1,12 +1,29 @@ import React, { useEffect } from "react"; -import ManageRootPageComponent from "./manage-root.page.component"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setBreadcrumbs } from "../../redux/application/application.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import ManageRootPageComponent from "./manage-root.page.component"; -export default function ManageRootPageContainer() { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +const mapDispatchToProps = (dispatch) => ({ + setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), +}); + +export function ManageRootPageContainer({ setBreadcrumbs, bodyshop }) { const { t } = useTranslation(); useEffect(() => { document.title = t("titles.manageroot"); - }, [t]); + setBreadcrumbs([]); + }, [t, setBreadcrumbs]); return ; } +export default connect( + mapStateToProps, + mapDispatchToProps +)(ManageRootPageContainer); diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index 163786b0f..4c9ac14ae 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -20,6 +20,7 @@ import PrintCenterModalContainer from "../../components/print-center-modal/print import { QUERY_STRIPE_ID } from "../../graphql/bodyshop.queries"; import { selectInstanceConflict } from "../../redux/user/user.selectors"; import "./manage.page.styles.scss"; +import TestComponent from "../../components/_test/test.component"; const ManageRootPage = lazy(() => import("../manage-root/manage-root.page.container") @@ -167,6 +168,11 @@ export function Manage({ match, conflict }) { +