IO-306 Further development of dashboard.

This commit is contained in:
Patrick Fic
2021-06-14 19:37:17 -07:00
parent db76992c70
commit a42da5b6da
15 changed files with 786 additions and 106 deletions

View File

@@ -10647,6 +10647,69 @@
<folder_node>
<name>titles</name>
<children>
<concept_node>
<name>monthlyjobcosting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>monthlylaborsales</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>monthlypartssales</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>monthlyrevenuegraph</name>
<definition_loaded>false</definition_loaded>
@@ -10668,6 +10731,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>prodhrssummary</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>productiondollars</name>
<definition_loaded>false</definition_loaded>

View File

@@ -0,0 +1,163 @@
import { Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../../utils/sorters";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import Dinero from "dinero.js";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [costingData, setcostingData] = useState(null);
const [searchText, setSearchText] = useState("");
const [state, setState] = useState({
sortedInfo: {},
});
useEffect(() => {
async function getCostingData() {
if (data && data.monthly_sales) {
setLoading(true);
const response = await axios.post("/job/costingmulti", {
jobids: data.monthly_sales.map((x) => x.id),
});
setcostingData(response.data);
setLoading(false);
}
}
getCostingData();
}, [data]);
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("bodyshop.fields.responsibilitycenter"),
dataIndex: "cost_center",
key: "cost_center",
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder:
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
},
{
title: t("jobs.labels.sales"),
dataIndex: "sales",
key: "sales",
sorter: (a, b) =>
parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
},
{
title: t("jobs.labels.costs"),
dataIndex: "costs",
key: "costs",
sorter: (a, b) =>
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
},
{
title: t("jobs.labels.gpdollars"),
dataIndex: "gpdollars",
key: "gpdollars",
sorter: (a, b) =>
parseFloat(a.gpdollars.substring(1)) -
parseFloat(b.gpdollars.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
},
{
title: t("jobs.labels.gppercent"),
dataIndex: "gppercent",
key: "gppercent",
sorter: (a, b) =>
parseFloat(a.gppercent.slice(0, -1) || 0) -
parseFloat(b.gppercent.slice(0, -1) || 0),
sortOrder:
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
},
];
const filteredData =
searchText === ""
? (costingData && costingData.allCostCenterData) || []
: costingData.allCostCenterData.filter((d) =>
(d.cost_center || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
);
return (
<Card
title={t("dashboard.titles.monthlyjobcosting")}
extra={
<Space wrap>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
}
{...cardProps}
>
<LoadingSkeleton loading={loading}>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
scroll={{ x: true, y: "calc(100% - 4em)" }}
rowKey="id"
style={{ height: "100%" }}
dataSource={filteredData}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>
{t("general.labels.totals")}
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(
costingData &&
costingData.allSummaryData &&
costingData.allSummaryData.totalSales
).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(
costingData &&
costingData.allSummaryData &&
costingData.allSummaryData.totalCost
).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(
costingData &&
costingData.allSummaryData &&
costingData.allSummaryData.gpdollars
).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</div>
</LoadingSkeleton>
</Card>
);
}

View File

@@ -0,0 +1,148 @@
import { Card } from "antd";
import Dinero from "dinero.js";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyLaborSales({ data, ...cardProps }) {
const { t } = useTranslation();
const [activeIndex, setActiveIndex] = useState(0);
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const laborData = {};
data.monthly_sales.forEach((job) => {
job.joblines.forEach((jobline) => {
if (!jobline.mod_lbr_ty) return;
if (!laborData[jobline.mod_lbr_ty])
laborData[jobline.mod_lbr_ty] = Dinero();
laborData[jobline.mod_lbr_ty] = laborData[jobline.mod_lbr_ty].add(
Dinero({
amount: Math.round(
(job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] || 0) * 100
),
}).multiply(jobline.mod_lb_hrs || 0)
);
});
});
const chartData = Object.keys(laborData).map((key) => {
return {
name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`),
value: laborData[key].getAmount() / 100,
// color: pieColor(i.status),
};
});
console.log(
"🚀 ~ file: monthly-parts-sales.component.jsx ~ line 34 ~ chartData",
chartData
);
return (
<Card title={t("dashboard.titles.monthlylaborsales")} {...cardProps}>
<div style={{ height: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart margin={0} padding={0}>
<Pie
data={chartData}
activeIndex={activeIndex}
activeShape={renderActiveShape}
cx="50%"
cy="50%"
innerRadius="60%"
// outerRadius={80}
fill="#8884d8"
dataKey="value"
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
</Card>
);
}
export const DashboardMonthlyRevenueGraphGql = `
`;
const renderActiveShape = (props) => {
const RADIAN = Math.PI / 180;
const {
cx,
cy,
midAngle,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
payload,
percent,
value,
} = props;
const sin = Math.sin(-RADIAN * midAngle);
const cos = Math.cos(-RADIAN * midAngle);
const sx = cx + (outerRadius + 10) * cos;
const sy = cy + (outerRadius + 10) * sin;
const mx = cx + (outerRadius + 30) * cos;
const my = cy + (outerRadius + 30) * sin;
const ex = mx + (cos >= 0 ? 1 : -1) * 22;
const ey = my;
const textAnchor = cos >= 0 ? "start" : "end";
return (
<g>
<text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>
{payload.name}
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={outerRadius + 6}
outerRadius={outerRadius + 10}
fill={fill}
/>
<path
d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
stroke={fill}
fill="none"
/>
<text
x={ex + (cos >= 0 ? 1 : -1) * 12}
y={ey}
textAnchor={textAnchor}
fill="#333"
>
{payload.name}
</text>
<text
x={ex + (cos >= 0 ? 1 : -1) * 12}
y={ey}
dy={18}
textAnchor={textAnchor}
fill="#999"
>
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
</text>
</g>
);
};

View File

@@ -0,0 +1,145 @@
import { Card } from "antd";
import Dinero from "dinero.js";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyPartsSales({ data, ...cardProps }) {
const { t } = useTranslation();
const [activeIndex, setActiveIndex] = useState(0);
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const partData = {};
data.monthly_sales.forEach((job) => {
job.joblines.forEach((jobline) => {
if (!jobline.part_type) return;
if (!partData[jobline.part_type]) partData[jobline.part_type] = Dinero();
partData[jobline.part_type] = partData[jobline.part_type].add(
Dinero({ amount: Math.round((jobline.act_price || 0) * 100) }).multiply(
jobline.part_qty || 0
)
);
});
});
const chartData = Object.keys(partData).map((key) => {
return {
name: t(`joblines.fields.part_types.${key.toUpperCase()}`),
value: partData[key].getAmount() / 100,
// color: pieColor(i.status),
};
});
console.log(
"🚀 ~ file: monthly-parts-sales.component.jsx ~ line 34 ~ chartData",
chartData
);
return (
<Card title={t("dashboard.titles.monthlypartssales")} {...cardProps}>
<div style={{ height: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart margin={0} padding={0}>
<Pie
data={chartData}
activeIndex={activeIndex}
activeShape={renderActiveShape}
cx="50%"
cy="50%"
innerRadius="60%"
// outerRadius={80}
fill="#8884d8"
dataKey="value"
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
</Card>
);
}
export const DashboardMonthlyRevenueGraphGql = `
`;
const renderActiveShape = (props) => {
const RADIAN = Math.PI / 180;
const {
cx,
cy,
midAngle,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
payload,
percent,
value,
} = props;
const sin = Math.sin(-RADIAN * midAngle);
const cos = Math.cos(-RADIAN * midAngle);
const sx = cx + (outerRadius + 10) * cos;
const sy = cy + (outerRadius + 10) * sin;
const mx = cx + (outerRadius + 30) * cos;
const my = cy + (outerRadius + 30) * sin;
const ex = mx + (cos >= 0 ? 1 : -1) * 22;
const ey = my;
const textAnchor = cos >= 0 ? "start" : "end";
return (
<g>
<text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>
{payload.name}
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={outerRadius + 6}
outerRadius={outerRadius + 10}
fill={fill}
/>
<path
d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
stroke={fill}
fill="none"
/>
<text
x={ex + (cos >= 0 ? 1 : -1) * 12}
y={ey}
textAnchor={textAnchor}
fill="#333"
>
{payload.name}
</text>
<text
x={ex + (cos >= 0 ? 1 : -1) * 12}
y={ey}
dy={18}
textAnchor={textAnchor}
fill="#999"
>
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
</text>
</g>
);
};

View File

@@ -16,18 +16,16 @@ import {
} from "recharts";
import Dinero from "dinero.js";
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
const { t } = useTranslation();
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
moment(item.date_invoiced).format("YYYY-MM-DD")
);
console.log(
"🚀 ~ file: monthly-revenue-graph.component.jsx ~ line 27 ~ jobsByDate",
jobsByDate
);
const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -56,44 +54,38 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
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>
<div style={{ height: "100%" }}>
<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>
</div>
</Card>
);
}
export const DashboardMonthlyRevenueGraphGql = `
monthly_sales: jobs(where: {_and: [{date_invoiced: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}) {
id
date_invoiced
job_totals
}
`;

View File

@@ -1,30 +1,40 @@
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
const { t } = useTranslation();
const aboveTargetMonthlySales = false;
if (!data) return null;
if (!data.projected_monthly_sales)
return <DashboardRefreshRequired {...cardProps} />;
const dollars =
data.projected_monthly_sales &&
data.projected_monthly_sales.reduce(
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
Dinero()
);
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 title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
<Statistic value={dollars.toFormat()} />
</Card>
);
}
export const DashboardProjectedMonthlySalesGql = `
projected_monthly_sales: jobs(where: {_or: [{_and: [{date_invoiced: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}, {_and: [{scheduled_completion: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {scheduled_completion: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}]}) {
id
date_invoiced
job_totals
}
`;

View File

@@ -0,0 +1,25 @@
import { Card, Typography } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import React from "react";
import { useTranslation } from "react-i18next";
export default function DashboardRefreshRequired(props) {
const { t } = useTranslation();
return (
<Card {...props}>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<SyncOutlined />
<Typography.Title level={4}>
{t("dashboard.errors.refreshrequired")}
</Typography.Title>
</div>
</Card>
);
}

View File

@@ -2,6 +2,7 @@ import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardTotalProductionDollars({
data,
@@ -9,18 +10,17 @@ export default function DashboardTotalProductionDollars({
}) {
const { t } = useTranslation();
if (!data) return null;
const dollars = data.production_jobs.reduce(
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
Dinero()
);
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
const dollars =
data.production_jobs &&
data.production_jobs.reduce(
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
Dinero()
);
return (
<Card {...cardProps}>
<Statistic
title={t("dashboard.labels.dollarsinproduction")}
value={dollars.toFormat()}
/>
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
<Statistic value={dollars.toFormat()} />
</Card>
);
}

View File

@@ -1,11 +1,11 @@
import React from "react";
import { Card, Space, Statistic } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import DashboardRefreshRequired from "../refresh-required.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -22,22 +22,25 @@ export function DashboardTotalProductionHours({
}) {
const { t } = useTranslation();
if (!data) return null;
const hours = data.production_jobs.reduce(
(acc, val) => {
return {
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
total:
acc.total +
val.labhrs.aggregate.sum.mod_lb_hrs +
val.larhrs.aggregate.sum.mod_lb_hrs,
};
},
{ body: 0, ref: 0, total: 0 }
);
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
const hours =
data.production_jobs &&
data.production_jobs.reduce(
(acc, val) => {
return {
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
total:
acc.total +
val.labhrs.aggregate.sum.mod_lb_hrs +
val.larhrs.aggregate.sum.mod_lb_hrs,
};
},
{ body: 0, ref: 0, total: 0 }
);
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
return (
<Card {...cardProps}>
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
<Space wrap style={{ flex: 1 }}>
<Statistic title={t("dashboard.labels.bodyhrs")} value={hours.body} />
<Statistic title={t("dashboard.labels.refhrs")} value={hours.ref} />

View File

@@ -1,6 +1,8 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
import { gql, useMutation, useQuery } from "@apollo/client";
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { useTranslation } from "react-i18next";
@@ -14,10 +16,15 @@ import {
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.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 from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.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,
@@ -44,8 +51,9 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout
: { items: [], layout: [], layouts: [] }),
: { items: [], layout: {}, layouts: [] }),
});
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state)
);
@@ -74,7 +82,12 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", { name: key });
const idxToRemove = state.items.findIndex((i) => i.i === key);
const items = state.items;
console.log(
"🚀 ~ file: dashboard-grid.component.jsx ~ line 81 ~ idxToRemove",
idxToRemove
);
const items = _.cloneDeep(state.items);
items.splice(idxToRemove, 1);
setState({ ...state, items });
};
@@ -88,7 +101,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: Infinity, // puts it at the bottom
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
@@ -100,11 +113,6 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
() => GenerateDashboardData(data),
[data]
);
// const onBreakpointChange = (breakpoint, cols) => {
// setState({ ...state, breakpoint: breakpoint, cols: cols });
// };
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
@@ -149,7 +157,14 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
{state.items.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<div key={item.i} data-grid={item}>
<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}
@@ -196,7 +211,7 @@ const componentList = {
ProjectedMonthlySales: {
label: "Projected Monthly Sales",
component: DashboardProjectedMonthlySales,
gqlFragment: null,
gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2,
h: 1,
},
@@ -204,6 +219,35 @@ const componentList = {
label: "Monthly Sales Graph",
component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4,
h: 2,
minW: 4,
minH: 2,
},
MonthlyJobCosting: {
label: "Monthly Job Costing",
component: DashboardMonthlyJobCosting,
gqlFragment: null,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
MonthlyPartsSales: {
label: "Monthly Parts Sales",
component: DashboardMonthlyPartsSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyLaborSales: {
label: "Monthly Parts Sales",
component: DashboardMonthlyLaborSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
@@ -216,6 +260,46 @@ const createDashboardQuery = (state) => {
return gql`
query QUERY_DASHBOARD_DETAILS {
${componentBasedAdditions}
monthly_sales: jobs(where: {_and: [{date_invoiced: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}) {
id
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

View File

@@ -127,13 +127,28 @@
.dashboard-card {
height: 100%;
width: 100%;
.ant-card-body {
// background-color: red;
height: 90%;
height: 80%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
// // background-color: red;
// height: 90%;
// width: 100%;
// padding: 8px;
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: center;
}
.ant-spin-nested-loading {
height: 100%;
.ant-spin-container {
height: 100%;
.ant-table {
height: 100%;
.ant-table-container {
height: 100%;
}
}
}
}
}

View File

@@ -3,6 +3,7 @@ import Icon, {
BarChartOutlined,
CarFilled,
ClockCircleFilled,
DashboardFilled,
DollarCircleFilled,
ExportOutlined,
FieldTimeOutlined,
@@ -18,7 +19,6 @@ import Icon, {
ScheduleOutlined,
SettingOutlined,
TeamOutlined,
DashboardFilled,
ToolFilled,
UnorderedListOutlined,
UserOutlined,
@@ -46,7 +46,6 @@ import {
import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,

View File

@@ -672,12 +672,16 @@
},
"labels": {
"bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production",
"dollarsinproduction": "Dollars in Production'",
"prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs"
},
"titles": {
"monthlyjobcosting": "Monthly Job Costing ",
"monthlylaborsales": "Monthly Labor Sales",
"monthlypartssales": "Monthly Parts Sales",
"monthlyrevenuegraph": "Monthly Revenue Graph",
"prodhrssummary": "Production Hours Summary",
"productiondollars": "Total dollars in production",
"productionhours": "Total hours in production",
"projectedmonthlysales": "Projected Monthly Sales"

View File

@@ -677,7 +677,11 @@
"refhrs": ""
},
"titles": {
"monthlyjobcosting": "",
"monthlylaborsales": "",
"monthlypartssales": "",
"monthlyrevenuegraph": "",
"prodhrssummary": "",
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": ""

View File

@@ -677,7 +677,11 @@
"refhrs": ""
},
"titles": {
"monthlyjobcosting": "",
"monthlylaborsales": "",
"monthlypartssales": "",
"monthlyrevenuegraph": "",
"prodhrssummary": "",
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": ""