Added pie to job costinng, combined columns on job costing IO-569

This commit is contained in:
Patrick Fic
2021-01-11 12:13:50 -08:00
parent ae7f67461b
commit 32cbe95d0b
8 changed files with 161 additions and 35 deletions

View File

@@ -4040,6 +4040,15 @@
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
"optional": true "optional": true
}, },
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bluebird": { "bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -7153,6 +7162,12 @@
} }
} }
}, },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"filesize": { "filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@@ -10752,6 +10767,12 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
}, },
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"optional": true
},
"nanoid": { "nanoid": {
"version": "3.1.16", "version": "3.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz",
@@ -16724,7 +16745,11 @@
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true "optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
}, },
"glob-parent": { "glob-parent": {
"version": "3.1.0", "version": "3.1.0",
@@ -17326,7 +17351,11 @@
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true "optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
}, },
"glob-parent": { "glob-parent": {
"version": "3.1.0", "version": "3.1.0",

View File

@@ -18265,6 +18265,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>sales</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> <concept_node>
<name>state_tax_amt</name> <name>state_tax_amt</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -1,10 +1,13 @@
import { Typography } from "antd";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component"; import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component";
import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component"; import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component";
import JobCostingPie from "./job-costing-modal.pie.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -16,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
export function JobCostingModalComponent({ bodyshop, job }) { export function JobCostingModalComponent({ bodyshop, job }) {
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits; const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs; // const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
const { t } = useTranslation();
const jobLineTotalsByProfitCenter = const jobLineTotalsByProfitCenter =
job && job &&
job.joblines.reduce( job.joblines.reduce(
@@ -115,11 +118,11 @@ export function JobCostingModalComponent({ bodyshop, job }) {
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }); ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }); const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
const cost = (billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })).add( const costs = (
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }) billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
); ).add(ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }));
const totalSales = sale_labor.add(sale_parts); const totalSales = sale_labor.add(sale_parts);
const gpdollars = totalSales.subtract(cost); const gpdollars = totalSales.subtract(costs);
const gppercent = ( const gppercent = (
(gpdollars.getAmount() / totalSales.getAmount()) * (gpdollars.getAmount() / totalSales.getAmount()) *
100 100
@@ -139,16 +142,19 @@ export function JobCostingModalComponent({ bodyshop, job }) {
.add(sale_parts); .add(sale_parts);
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor); summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts); summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
summaryData.totalCost = summaryData.totalCost.add(cost); summaryData.totalCost = summaryData.totalCost.add(costs);
return { return {
id: idx, id: idx,
cost_center: ccVal, cost_center: ccVal,
sale_labor: sale_labor && sale_labor.toFormat(), sale_labor: sale_labor && sale_labor.toFormat(),
sale_parts: sale_parts && sale_parts.toFormat(), sale_parts: sale_parts && sale_parts.toFormat(),
sales: sale_labor.add(sale_parts).toFormat(),
sales_dinero: sale_labor.add(sale_parts),
cost_parts: cost_parts && cost_parts.toFormat(), cost_parts: cost_parts && cost_parts.toFormat(),
cost_labor: cost_labor && cost_labor.toFormat(), cost_labor: cost_labor && cost_labor.toFormat(),
cost: cost && cost.toFormat(), costs: cost_parts.add(cost_labor).toFormat(),
costs_dinero: cost_parts.add(cost_labor),
gpdollars: gpdollars.toFormat(), gpdollars: gpdollars.toFormat(),
gppercent: gppercentFormatted, gppercent: gppercentFormatted,
}; };
@@ -173,6 +179,18 @@ export function JobCostingModalComponent({ bodyshop, job }) {
<div> <div>
<JobCostingStatistics job={job} summaryData={summaryData} /> <JobCostingStatistics job={job} summaryData={summaryData} />
<JobCostingPartsTable job={job} data={costCenterData} /> <JobCostingPartsTable job={job} data={costCenterData} />
<div className="imex-flex-row">
<div style={{ flex: 1 }}>
<Typography.Title level={4}>
{t("jobs.labels.sales")}
</Typography.Title>
<JobCostingPie type="sales" costCenterData={costCenterData} />
</div>
<div style={{ flex: 1 }}>
<Typography.Title level={4}>{t("jobs.labels.cost")}</Typography.Title>
<JobCostingPie type="cost" costCenterData={costCenterData} />
</div>
</div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,69 @@
import React, { useCallback, useMemo } from "react";
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
export default function JobCostingPieComponent({
type = "sales",
costCenterData,
}) {
const Calculatedata = useCallback(
(data) => {
if (data && data.length > 0) {
return data.reduce((acc, i) => {
const value =
type === "sales"
? i.sales_dinero.getAmount()
: i.costs_dinero.getAmount();
if (value > 0) {
acc.push({
name: i.cost_center,
color: "#" + Math.floor(Math.random() * 16777215).toString(16),
label: `${i.cost_center} - ${
type === "sales"
? i.sales_dinero.toFormat()
: i.costs_dinero.toFormat()
}`,
value:
type === "sales"
? i.sales_dinero.getAmount()
: i.costs_dinero.getAmount(),
});
}
return acc;
}, []);
} else {
return [];
}
},
[type]
);
const memoizedData = useMemo(() => Calculatedata(costCenterData), [
costCenterData,
Calculatedata,
]);
console.log(type, memoizedData);
return (
<ResponsiveContainer width="100%" height={175}>
<PieChart>
<Pie
data={memoizedData}
innerRadius={40}
outerRadius={50}
fill="#8884d8"
paddingAngle={5}
dataKey="value"
label={(entry) => entry.label}
labelLine
>
{memoizedData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
}

View File

@@ -24,37 +24,23 @@ export default function JobCostingPartsTable({ job, data }) {
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order, state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.labels.sale_labor"), title: t("jobs.labels.sales"),
dataIndex: "sale_labor", dataIndex: "sales",
key: "sale_labor", key: "sales",
sorter: (a, b) => alphaSort(a.sale_labor, b.sale_labor), sorter: (a, b) => alphaSort(a.sales, b.sales),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "sale_labor" && state.sortedInfo.order, state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.labels.sale_parts"), title: t("jobs.labels.costs"),
dataIndex: "sale_parts", dataIndex: "costs",
key: "sale_parts", key: "costs",
sorter: (a, b) => alphaSort(a.sale_parts, b.sale_parts), sorter: (a, b) => a.costs - b.costs,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "sale_parts" && state.sortedInfo.order, state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
},
{
title: t("jobs.labels.cost_labor"),
dataIndex: "cost_labor",
key: "cost_labor",
sorter: (a, b) => a.cost_labor - b.cost_labor,
sortOrder:
state.sortedInfo.columnKey === "cost_labor" && state.sortedInfo.order,
},
{
title: t("jobs.labels.cost_parts"),
dataIndex: "cost_parts",
key: "cost_parts",
sorter: (a, b) => a.cost_parts - b.cost_parts,
sortOrder:
state.sortedInfo.columnKey === "cost_parts" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.labels.gpdollars"), title: t("jobs.labels.gpdollars"),
dataIndex: "gpdollars", dataIndex: "gpdollars",

View File

@@ -1112,6 +1112,7 @@
"reconciliationheader": "Parts & Sublet Reconciliation", "reconciliationheader": "Parts & Sublet Reconciliation",
"sale_labor": "Sales - Labor", "sale_labor": "Sales - Labor",
"sale_parts": "Sales - Parts", "sale_parts": "Sales - Parts",
"sales": "Sales",
"state_tax_amt": "State/Provincial Taxes", "state_tax_amt": "State/Provincial Taxes",
"subletstotal": "Sublets Total", "subletstotal": "Sublets Total",
"subtotal": "Subtotal", "subtotal": "Subtotal",

View File

@@ -1112,6 +1112,7 @@
"reconciliationheader": "", "reconciliationheader": "",
"sale_labor": "", "sale_labor": "",
"sale_parts": "", "sale_parts": "",
"sales": "",
"state_tax_amt": "", "state_tax_amt": "",
"subletstotal": "", "subletstotal": "",
"subtotal": "", "subtotal": "",

View File

@@ -1112,6 +1112,7 @@
"reconciliationheader": "", "reconciliationheader": "",
"sale_labor": "", "sale_labor": "",
"sale_parts": "", "sale_parts": "",
"sales": "",
"state_tax_amt": "", "state_tax_amt": "",
"subletstotal": "", "subletstotal": "",
"subtotal": "", "subtotal": "",