389 lines
13 KiB
JavaScript
389 lines
13 KiB
JavaScript
import Icon, {SyncOutlined} from "@ant-design/icons";
|
|
import {gql, useMutation, useQuery} from "@apollo/client";
|
|
import {Button, Dropdown, notification, Space} from "antd";
|
|
import {PageHeader} from "@ant-design/pro-layout";
|
|
import i18next from "i18next";
|
|
import _ from "lodash";
|
|
import dayjs from "../../utils/day";
|
|
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 JobLifecycleDashboardComponent, {
|
|
JobLifecycleDashboardGQL
|
|
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.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 menuItems = Object.keys(componentList).map((key) => ({
|
|
key: key,
|
|
label: componentList[key].label,
|
|
value: key,
|
|
disabled: existingLayoutKeys.includes(key),
|
|
}));
|
|
|
|
const menu = {items: menuItems, onClick: handleAddComponent};
|
|
|
|
if (error) return <AlertComponent message={error.message} type="error"/>;
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
extra={
|
|
<Space>
|
|
<Button onClick={() => refetch()}>
|
|
<SyncOutlined/>
|
|
</Button>
|
|
<Dropdown menu={menu} 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" bodyshop={bodyshop} 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,
|
|
},
|
|
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
|
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"),
|
|
component: DashboardScheduledInToday,
|
|
gqlFragment: DashboardScheduledInTodayGql,
|
|
minW: 6,
|
|
minH: 2,
|
|
w: 10,
|
|
h: 3,
|
|
},
|
|
ScheduleOutToday: {
|
|
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
|
component: DashboardScheduledOutToday,
|
|
gqlFragment: DashboardScheduledOutTodayGql,
|
|
minW: 6,
|
|
minH: 2,
|
|
w: 10,
|
|
h: 3,
|
|
},
|
|
JobLifecycle: {
|
|
label: i18next.t("dashboard.titles.joblifecycle"),
|
|
component: JobLifecycleDashboardComponent,
|
|
gqlFragment: JobLifecycleDashboardGQL,
|
|
minW: 6,
|
|
minH: 3,
|
|
w: 6,
|
|
h: 3,
|
|
},
|
|
};
|
|
|
|
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: "${dayjs()
|
|
.startOf("month")
|
|
.startOf("day")
|
|
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs()
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`;
|
|
};
|