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 ; return (
} /> {state.items.map((item, index) => { const TheComponent = componentList[item.i].component; return (
handleRemoveComponent(item.i)} />
); })}
); } 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 } } } } }`; };