387 lines
11 KiB
JavaScript
387 lines
11 KiB
JavaScript
import Icon, { SyncOutlined } from "@ant-design/icons";
|
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
|
import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
|
|
import i18next from "i18next";
|
|
import _ from "lodash";
|
|
import moment from "moment";
|
|
import React, { useState } from "react";
|
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
|
import { useTranslation } from "react-i18next";
|
|
import { MdClose } from "react-icons/md";
|
|
import { connect } from "react-redux";
|
|
import { createStructuredSelector } from "reselect";
|
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
|
import {
|
|
selectBodyshop,
|
|
selectCurrentUser,
|
|
} from "../../redux/user/user.selectors";
|
|
import AlertComponent from "../alert/alert.component";
|
|
import DashboardMonthlyEmployeeEfficiency, {
|
|
DashboardMonthlyEmployeeEfficiencyGql,
|
|
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
|
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
|
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
|
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
|
import DashboardMonthlyRevenueGraph, {
|
|
DashboardMonthlyRevenueGraphGql,
|
|
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
|
import DashboardProjectedMonthlySales, {
|
|
DashboardProjectedMonthlySalesGql,
|
|
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
|
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
|
import DashboardTotalProductionHours, {
|
|
DashboardTotalProductionHoursGql,
|
|
} 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 DashboardScheduledInToday, {
|
|
DashboardScheduledInTodayGql,
|
|
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
|
|
import DashboardScheduledOutToday, {
|
|
DashboardScheduledOutTodayGql,
|
|
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
|
|
import "./dashboard-grid.styles.scss";
|
|
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
|
|
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
currentUser: selectCurrentUser,
|
|
bodyshop: selectBodyshop,
|
|
});
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
});
|
|
|
|
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|
const { t } = useTranslation();
|
|
const [state, setState] = useState({
|
|
...(bodyshop.associations[0].user.dashboardlayout
|
|
? bodyshop.associations[0].user.dashboardlayout
|
|
: { items: [], layout: {}, layouts: [] }),
|
|
});
|
|
|
|
const { loading, error, data, refetch } = useQuery(
|
|
createDashboardQuery(state),
|
|
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
|
|
);
|
|
|
|
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
|
|
|
const handleLayoutChange = async (layout, layouts) => {
|
|
logImEXEvent("dashboard_change_layout");
|
|
|
|
setState({ ...state, layout, layouts });
|
|
|
|
const result = await updateLayout({
|
|
variables: {
|
|
email: currentUser.email,
|
|
layout: { ...state, layout, layouts },
|
|
},
|
|
});
|
|
if (!!result.errors) {
|
|
notification["error"]({
|
|
message: t("dashboard.errors.updatinglayout", {
|
|
message: JSON.stringify(result.errors),
|
|
}),
|
|
});
|
|
}
|
|
};
|
|
const handleRemoveComponent = (key) => {
|
|
logImEXEvent("dashboard_remove_component", { name: key });
|
|
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
|
|
|
const items = _.cloneDeep(state.items);
|
|
|
|
items.splice(idxToRemove, 1);
|
|
setState({ ...state, items });
|
|
};
|
|
|
|
const handleAddComponent = (e) => {
|
|
logImEXEvent("dashboard_add_component", { name: e });
|
|
setState({
|
|
...state,
|
|
items: [
|
|
...state.items,
|
|
{
|
|
i: e.key,
|
|
x: (state.items.length * 2) % (state.cols || 12),
|
|
y: 99, // puts it at the bottom
|
|
w: componentList[e.key].w || 2,
|
|
h: componentList[e.key].h || 2,
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const dashboarddata = React.useMemo(
|
|
() => GenerateDashboardData(data),
|
|
[data]
|
|
);
|
|
const existingLayoutKeys = state.items.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>
|
|
);
|
|
|
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
extra={
|
|
<Space>
|
|
<Button onClick={() => refetch()}>
|
|
<SyncOutlined />
|
|
</Button>
|
|
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
|
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
|
</Dropdown>
|
|
</Space>
|
|
}
|
|
/>
|
|
|
|
<ResponsiveReactGridLayout
|
|
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%"
|
|
layouts={state.layouts}
|
|
onLayoutChange={handleLayoutChange}
|
|
// onBreakpointChange={onBreakpointChange}
|
|
>
|
|
{state.items.map((item, index) => {
|
|
const TheComponent = componentList[item.i].component;
|
|
return (
|
|
<div
|
|
key={item.i}
|
|
data-grid={{
|
|
...item,
|
|
minH: componentList[item.i].minH || 1,
|
|
minW: componentList[item.i].minW || 1,
|
|
}}
|
|
>
|
|
<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" data={dashboarddata} />
|
|
</LoadingSkeleton>
|
|
</div>
|
|
);
|
|
})}
|
|
</ResponsiveReactGridLayout>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(DashboardGridComponent);
|
|
|
|
const componentList = {
|
|
ProductionDollars: {
|
|
label: i18next.t("dashboard.titles.productiondollars"),
|
|
component: DashboardTotalProductionDollars,
|
|
gqlFragment: null,
|
|
w: 1,
|
|
h: 1,
|
|
minW: 2,
|
|
minH: 1,
|
|
},
|
|
ProductionHours: {
|
|
label: i18next.t("dashboard.titles.productionhours"),
|
|
component: DashboardTotalProductionHours,
|
|
gqlFragment: DashboardTotalProductionHoursGql,
|
|
w: 3,
|
|
h: 1,
|
|
minW: 3,
|
|
minH: 1,
|
|
},
|
|
ProjectedMonthlySales: {
|
|
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
|
component: DashboardProjectedMonthlySales,
|
|
gqlFragment: DashboardProjectedMonthlySalesGql,
|
|
w: 2,
|
|
h: 1,
|
|
minW: 2,
|
|
minH: 1,
|
|
},
|
|
MonthlyRevenueGraph: {
|
|
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
|
component: DashboardMonthlyRevenueGraph,
|
|
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
|
w: 4,
|
|
h: 2,
|
|
minW: 4,
|
|
minH: 2,
|
|
},
|
|
MonthlyJobCosting: {
|
|
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
|
component: DashboardMonthlyJobCosting,
|
|
gqlFragment: null,
|
|
minW: 6,
|
|
minH: 3,
|
|
w: 6,
|
|
h: 3,
|
|
},
|
|
MonthlyPartsSales: {
|
|
label: i18next.t("dashboard.titles.monthlypartssales"),
|
|
component: DashboardMonthlyPartsSales,
|
|
gqlFragment: null,
|
|
minW: 2,
|
|
minH: 2,
|
|
w: 2,
|
|
h: 2,
|
|
},
|
|
MonthlyLaborSales: {
|
|
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
|
component: DashboardMonthlyLaborSales,
|
|
gqlFragment: null,
|
|
minW: 2,
|
|
minH: 2,
|
|
w: 2,
|
|
h: 2,
|
|
},
|
|
MonthlyEmployeeEfficency: {
|
|
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
|
component: DashboardMonthlyEmployeeEfficiency,
|
|
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
|
minW: 2,
|
|
minH: 2,
|
|
w: 2,
|
|
h: 2,
|
|
},
|
|
ScheduleInToday: {
|
|
label: i18next.t("dashboard.titles.scheduledintoday", {
|
|
date: moment().startOf("day").format("MM/DD/YYYY"),
|
|
}),
|
|
component: DashboardScheduledInToday,
|
|
gqlFragment: DashboardScheduledInTodayGql,
|
|
minW: 10,
|
|
minH: 2,
|
|
w: 10,
|
|
h: 2,
|
|
},
|
|
ScheduleOutToday: {
|
|
label: i18next.t("dashboard.titles.scheduledouttoday", {
|
|
date: moment().startOf("day").format("MM/DD/YYYY"),
|
|
}),
|
|
component: DashboardScheduledOutToday,
|
|
gqlFragment: DashboardScheduledOutTodayGql,
|
|
minW: 10,
|
|
minH: 2,
|
|
w: 10,
|
|
h: 2,
|
|
},
|
|
};
|
|
|
|
const createDashboardQuery = (state) => {
|
|
const componentBasedAdditions =
|
|
state &&
|
|
Array.isArray(state.layout) &&
|
|
state.layout
|
|
.map((item, index) => componentList[item.i].gqlFragment || "")
|
|
.join("");
|
|
return gql`
|
|
query QUERY_DASHBOARD_DETAILS {
|
|
${componentBasedAdditions || ""}
|
|
monthly_sales: jobs(where: {_and: [
|
|
{ voided: {_eq: false}},
|
|
{date_invoiced: {_gte: "${moment()
|
|
.startOf("month")
|
|
.startOf("day")
|
|
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
|
|
.endOf("month")
|
|
.endOf("day")
|
|
.toISOString()}"}}]}) {
|
|
id
|
|
ro_number
|
|
date_invoiced
|
|
job_totals
|
|
rate_la1
|
|
rate_la2
|
|
rate_la3
|
|
rate_la4
|
|
rate_laa
|
|
rate_lab
|
|
rate_lad
|
|
rate_lae
|
|
rate_laf
|
|
rate_lag
|
|
rate_lam
|
|
rate_lar
|
|
rate_las
|
|
rate_lau
|
|
rate_ma2s
|
|
rate_ma2t
|
|
rate_ma3s
|
|
rate_mabl
|
|
rate_macs
|
|
rate_mahw
|
|
rate_mapa
|
|
rate_mash
|
|
rate_matd
|
|
joblines(where: { removed: { _eq: false } }) {
|
|
id
|
|
mod_lbr_ty
|
|
mod_lb_hrs
|
|
act_price
|
|
part_qty
|
|
part_type
|
|
}
|
|
}
|
|
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
|
id
|
|
ro_number
|
|
ins_co_nm
|
|
job_totals
|
|
joblines(where: { removed: { _eq: false } }) {
|
|
id
|
|
mod_lbr_ty
|
|
mod_lb_hrs
|
|
act_price
|
|
part_qty
|
|
part_type
|
|
}
|
|
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
|
aggregate {
|
|
sum {
|
|
mod_lb_hrs
|
|
}
|
|
}
|
|
}
|
|
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
|
aggregate {
|
|
sum {
|
|
mod_lb_hrs
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
};
|