Added statistics and finished calculations for job costing. BOD-192
This commit is contained in:
@@ -12405,27 +12405,6 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
<concept_node>
|
|
||||||
<name>estimated</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>existing_jobs</name>
|
<name>existing_jobs</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -12930,6 +12909,48 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>sale_labor</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>sale_parts</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>
|
||||||
@@ -13014,6 +13035,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>total_cost</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>total_repairs</name>
|
<name>total_repairs</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -13035,6 +13077,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>total_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>totals</name>
|
<name>totals</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -1,21 +1,158 @@
|
|||||||
|
import Dinero from "dinero.js";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
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 { Row, Col } from "antd";
|
import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component";
|
||||||
|
|
||||||
const colSpan = {
|
const mapStateToProps = createStructuredSelector({
|
||||||
md: { span: 24 },
|
bodyshop: selectBodyshop,
|
||||||
lg: { span: 12 },
|
});
|
||||||
};
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JobCostingModalComponent({ bodyshop, job }) {
|
||||||
|
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
|
||||||
|
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
|
||||||
|
|
||||||
|
const jobLineTotalsByProfitCenter = job.joblines.reduce(
|
||||||
|
(acc, val) => {
|
||||||
|
const laborProfitCenter = defaultProfits[val.mod_lbr_ty];
|
||||||
|
if (!!!laborProfitCenter)
|
||||||
|
console.log(
|
||||||
|
"Unknown cost/profit center mapping for labor.",
|
||||||
|
val.mod_lbr_ty
|
||||||
|
);
|
||||||
|
|
||||||
|
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
|
||||||
|
const laborAmount = Dinero({
|
||||||
|
amount: Math.round((job[rateName] || 0) * 100),
|
||||||
|
}).multiply(val.mod_lb_hrs || 0);
|
||||||
|
if (!!!acc.labor[laborProfitCenter])
|
||||||
|
acc.labor[laborProfitCenter] = Dinero();
|
||||||
|
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
|
||||||
|
laborAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
const partsProfitCenter = defaultProfits[val.part_type];
|
||||||
|
if (!!!partsProfitCenter)
|
||||||
|
console.log(
|
||||||
|
"Unknown cost/profit center mapping for parts.",
|
||||||
|
val.part_type
|
||||||
|
);
|
||||||
|
const partsAmount = Dinero({
|
||||||
|
amount: Math.round((val.act_price || 0) * 100),
|
||||||
|
}).multiply(val.part_qty || 1);
|
||||||
|
if (!!!acc.parts[partsProfitCenter])
|
||||||
|
acc.parts[partsProfitCenter] = Dinero();
|
||||||
|
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
|
||||||
|
partsAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ parts: {}, labor: {} }
|
||||||
|
);
|
||||||
|
|
||||||
|
const invoiceTotalsByProfitCenter = job.invoices.reduce(
|
||||||
|
(inv_acc, inv_val) => {
|
||||||
|
//At the invoice level.
|
||||||
|
inv_val.invoicelines.map((line_val) => {
|
||||||
|
//At the invoice line level.
|
||||||
|
//console.log("JobCostingPartsTable -> line_val", line_val);
|
||||||
|
if (!!!inv_acc[line_val.cost_center])
|
||||||
|
inv_acc[line_val.cost_center] = Dinero();
|
||||||
|
|
||||||
|
inv_acc[line_val.cost_center] = inv_acc[line_val.cost_center].add(
|
||||||
|
Dinero({
|
||||||
|
amount: Math.round((line_val.actual_cost || 0) * 100),
|
||||||
|
})
|
||||||
|
.multiply(line_val.quantity)
|
||||||
|
.multiply(inv_val.is_credit_memo ? -1 : 1)
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return inv_acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const summaryData = {
|
||||||
|
totalLaborSales: Dinero({ amount: 0 }),
|
||||||
|
totalPartsSales: Dinero({ amount: 0 }),
|
||||||
|
totalSales: Dinero({ amount: 0 }),
|
||||||
|
totalCost: Dinero({ amount: 0 }),
|
||||||
|
gpdollars: Dinero({ amount: 0 }),
|
||||||
|
gppercent: null,
|
||||||
|
gppercentFormatted: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const costCenterData = Object.keys(defaultProfits).map((key, idx) => {
|
||||||
|
const ccVal = defaultProfits[key];
|
||||||
|
const sale_labor =
|
||||||
|
jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 });
|
||||||
|
const sale_parts =
|
||||||
|
jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({ amount: 0 });
|
||||||
|
const cost = invoiceTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||||
|
const totalSales = sale_labor.add(sale_parts);
|
||||||
|
const gpdollars = totalSales.subtract(cost);
|
||||||
|
const gppercent = (
|
||||||
|
(gpdollars.getAmount() / totalSales.getAmount()) *
|
||||||
|
100
|
||||||
|
).toFixed(2);
|
||||||
|
|
||||||
|
let gppercentFormatted;
|
||||||
|
if (isNaN(gppercent)) gppercentFormatted = "0%";
|
||||||
|
else if (!isFinite(gppercent)) gppercentFormatted = "-∞";
|
||||||
|
else {
|
||||||
|
gppercentFormatted = `${gppercent}%`;
|
||||||
|
}
|
||||||
|
//Push summary data to avoid extra loop.
|
||||||
|
summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor);
|
||||||
|
summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts);
|
||||||
|
summaryData.totalSales = summaryData.totalSales
|
||||||
|
.add(sale_labor)
|
||||||
|
.add(sale_parts);
|
||||||
|
summaryData.totalCost = summaryData.totalCost.add(cost);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: idx,
|
||||||
|
cost_center: ccVal,
|
||||||
|
sale_labor: sale_labor && sale_labor.toFormat(),
|
||||||
|
sale_parts: sale_parts && sale_parts.toFormat(),
|
||||||
|
cost: cost && cost.toFormat(),
|
||||||
|
gpdollars: gpdollars.toFormat(),
|
||||||
|
gppercent: gppercentFormatted,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//Final summary data massaging.
|
||||||
|
summaryData.gpdollars = summaryData.totalSales.subtract(
|
||||||
|
summaryData.totalCost
|
||||||
|
);
|
||||||
|
summaryData.gppercent = (
|
||||||
|
(summaryData.gpdollars.getAmount() / summaryData.totalSales.getAmount()) *
|
||||||
|
100
|
||||||
|
).toFixed(2);
|
||||||
|
if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0;
|
||||||
|
else if (!isFinite(summaryData.gppercent))
|
||||||
|
summaryData.gppercentFormatted = "-∞";
|
||||||
|
else {
|
||||||
|
summaryData.gppercentFormatted = summaryData.gppercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("JobCostingModalComponent -> summaryData", summaryData);
|
||||||
|
|
||||||
export default function JobCostingModalComponent({ job }) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row gutter={[32, 32]}>
|
<JobCostingStatistics job={job} summaryData={summaryData} />
|
||||||
<Col {...colSpan}>
|
<JobCostingPartsTable job={job} data={costCenterData} />
|
||||||
<JobCostingPartsTable job={job} />
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpan}></Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobCostingModalComponent);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function JobCostingModalContainer({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { visible, context, actions } = jobCostingModal;
|
const { visible, context } = jobCostingModal;
|
||||||
const { jobId } = context;
|
const { jobId } = context;
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
|
const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
|
||||||
@@ -38,10 +38,11 @@ export function JobCostingModalContainer({
|
|||||||
visible={visible}
|
visible={visible}
|
||||||
title={t("jobs.labels.jobcosting")}
|
title={t("jobs.labels.jobcosting")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
width='90%'
|
width="90%"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
forceRender>
|
forceRender
|
||||||
{error ? <AlertComponent message={error.message} type='error' /> : null}
|
>
|
||||||
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSpinner loading={loading} />
|
<LoadingSpinner loading={loading} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,120 +1,81 @@
|
|||||||
import React from "react";
|
import { Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import "./job-costing-parts-table.styles.scss";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import Dinero from "dinero.js";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
export default function JobCostingPartsTable({ job, data }) {
|
||||||
bodyshop: selectBodyshop,
|
const [state, setState] = useState({
|
||||||
});
|
sortedInfo: {},
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
});
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
export function JobCostingPartsTable({ bodyshop, job }) {
|
|
||||||
console.log("JobCostingPartsTable -> job", job);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
|
|
||||||
console.log("JobCostingPartsTable -> defaultProfits", defaultProfits);
|
|
||||||
const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
|
|
||||||
|
|
||||||
//Need to get the sums of the job lines by cost center.
|
const columns = [
|
||||||
const jobLineTotalsByProfitCenter = job.joblines.reduce(
|
{
|
||||||
(acc, val) => {
|
title: t("bodyshop.fields.responsibilitycenter"),
|
||||||
const laborProfitCenter = defaultProfits[val.mod_lbr_ty];
|
dataIndex: "cost_center",
|
||||||
if (!!!laborProfitCenter)
|
key: "cost_center",
|
||||||
console.log(
|
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||||
"Unknown cost/profit center mapping for labor.",
|
sortOrder:
|
||||||
val.mod_lbr_ty
|
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||||
);
|
|
||||||
|
|
||||||
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
|
|
||||||
const laborAmount = Dinero({
|
|
||||||
amount: Math.round((job[rateName] || 0) * 100),
|
|
||||||
}).multiply(val.mod_lb_hrs || 0);
|
|
||||||
if (!!!acc.labor[laborProfitCenter])
|
|
||||||
acc.labor[laborProfitCenter] = Dinero();
|
|
||||||
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
|
|
||||||
laborAmount
|
|
||||||
);
|
|
||||||
|
|
||||||
const partsProfitCenter = defaultProfits[val.part_type];
|
|
||||||
if (!!!partsProfitCenter)
|
|
||||||
console.log(
|
|
||||||
"Unknown cost/profit center mapping for parts.",
|
|
||||||
val.part_type
|
|
||||||
);
|
|
||||||
const partsAmount = Dinero({
|
|
||||||
amount: Math.round((val.act_price || 0) * 100),
|
|
||||||
}).multiply(val.part_qty || 1);
|
|
||||||
if (!!!acc.parts[partsProfitCenter])
|
|
||||||
acc.parts[partsProfitCenter] = Dinero();
|
|
||||||
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
|
|
||||||
partsAmount
|
|
||||||
);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
},
|
||||||
{ parts: {}, labor: {} }
|
{
|
||||||
);
|
title: t("jobs.labels.sale_labor"),
|
||||||
|
dataIndex: "sale_labor",
|
||||||
const invoiceTotalsByProfitCenter = job.invoices.reduce(
|
key: "sale_labor",
|
||||||
(inv_acc, inv_val) => {
|
sorter: (a, b) => alphaSort(a.sale_labor, b.sale_labor),
|
||||||
//At the invoice level.
|
sortOrder:
|
||||||
const t = inv_val.invoicelines.map((line_val) => {
|
state.sortedInfo.columnKey === "sale_labor" && state.sortedInfo.order,
|
||||||
//At the invoice line level.
|
|
||||||
console.log("JobCostingPartsTable -> line_val", line_val);
|
|
||||||
if (!!!inv_acc[line_val.cost_center])
|
|
||||||
inv_acc[line_val.cost_center] = Dinero();
|
|
||||||
|
|
||||||
inv_acc[line_val.cost_center] = inv_acc[line_val.cost_center].add(
|
|
||||||
Dinero({
|
|
||||||
amount: Math.round((line_val.actual_cost || 0) * 100),
|
|
||||||
})
|
|
||||||
.multiply(line_val.quantity)
|
|
||||||
.multiply(inv_val.is_credit_memo ? -1 : 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return inv_acc;
|
|
||||||
},
|
},
|
||||||
{}
|
{
|
||||||
);
|
title: t("jobs.labels.sale_parts"),
|
||||||
console.log(
|
dataIndex: "sale_parts",
|
||||||
"JobCostingPartsTable -> invoiceTotalsByProfitCenter",
|
key: "sale_parts",
|
||||||
invoiceTotalsByProfitCenter
|
sorter: (a, b) => alphaSort(a.sale_parts, b.sale_parts),
|
||||||
);
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "sale_parts" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.cost"),
|
||||||
|
dataIndex: "cost",
|
||||||
|
key: "cost",
|
||||||
|
sorter: (a, b) => a.cost - b.cost,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.gpdollars"),
|
||||||
|
dataIndex: "gpdollars",
|
||||||
|
key: "gpdollars",
|
||||||
|
sorter: (a, b) => a.gpdollars - b.gpdollars,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.gppercent"),
|
||||||
|
dataIndex: "gppercent",
|
||||||
|
key: "gppercent",
|
||||||
|
sorter: (a, b) => a.gppercent - b.gppercent,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='job-costing-parts-table-container'>
|
<div>
|
||||||
<table className='job-costing-parts-table'>
|
<Table
|
||||||
<thead>
|
size="small"
|
||||||
<tr>
|
scroll={{ x: "50%", y: "40rem" }}
|
||||||
<th>{t("bodyshop.fields.responsibilitycenter")}</th>
|
onChange={handleTableChange}
|
||||||
<th>{t("jobs.labels.estimated")}</th>
|
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||||
<th>{t("jobs.labels.cost")}</th>
|
columns={columns}
|
||||||
<th>{t("jobs.labels.gpdollars")}</th>
|
rowKey="id"
|
||||||
<th>{t("jobs.labels.gppercent")}</th>
|
dataSource={data}
|
||||||
</tr>
|
/>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Cost Center</td>
|
|
||||||
<td>$12345.45</td>
|
|
||||||
<td>$123.45</td>
|
|
||||||
<td>$1234.00</td>
|
|
||||||
<td>50.58%</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(JobCostingPartsTable);
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
.job-costing-parts-table-container {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-costing-parts-table {
|
|
||||||
border: black;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Statistic } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function JobCostingStatistics({ job, summaryData }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="imex-flex-row imex-flex-row__flex-space-around">
|
||||||
|
<Statistic
|
||||||
|
value={summaryData.totalLaborSales.toFormat()}
|
||||||
|
title={t("jobs.labels.sale_labor")}
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
value={summaryData.totalPartsSales.toFormat()}
|
||||||
|
title={t("jobs.labels.sale_parts")}
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
value={summaryData.totalSales.toFormat()}
|
||||||
|
title={t("jobs.labels.total_sales")}
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
value={summaryData.totalCost.toFormat()}
|
||||||
|
title={t("jobs.labels.total_cost")}
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
value={summaryData.gpdollars.toFormat()}
|
||||||
|
title={t("jobs.labels.gpdollars")}
|
||||||
|
/>
|
||||||
|
<Statistic
|
||||||
|
value={summaryData.gppercentFormatted}
|
||||||
|
suffix="%"
|
||||||
|
title={t("jobs.labels.gppercent")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -766,7 +766,6 @@
|
|||||||
"documents": "Documents",
|
"documents": "Documents",
|
||||||
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
|
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
|
||||||
"employeeassignments": "Employee Assignments",
|
"employeeassignments": "Employee Assignments",
|
||||||
"estimated": "Estimated",
|
|
||||||
"existing_jobs": "Existing Jobs",
|
"existing_jobs": "Existing Jobs",
|
||||||
"federal_tax_amt": "Federal Taxes",
|
"federal_tax_amt": "Federal Taxes",
|
||||||
"gpdollars": "$ G.P.",
|
"gpdollars": "$ G.P.",
|
||||||
@@ -791,11 +790,15 @@
|
|||||||
"rates": "Rates",
|
"rates": "Rates",
|
||||||
"rates_subtotal": "Rates Subtotal",
|
"rates_subtotal": "Rates Subtotal",
|
||||||
"reconciliationheader": "Parts & Sublet Reconciliation",
|
"reconciliationheader": "Parts & Sublet Reconciliation",
|
||||||
|
"sale_labor": "Sales - Labor",
|
||||||
|
"sale_parts": "Sales - Parts",
|
||||||
"state_tax_amt": "State/Provincial Taxes",
|
"state_tax_amt": "State/Provincial Taxes",
|
||||||
"subletstotal": "Sublets Total",
|
"subletstotal": "Sublets Total",
|
||||||
"subtotal": "Subtotal",
|
"subtotal": "Subtotal",
|
||||||
"suspense": "Suspense",
|
"suspense": "Suspense",
|
||||||
|
"total_cost": "Total Cost",
|
||||||
"total_repairs": "Total Repairs",
|
"total_repairs": "Total Repairs",
|
||||||
|
"total_sales": "Total Sales",
|
||||||
"totals": "Totals",
|
"totals": "Totals",
|
||||||
"vehicle_info": "Vehicle",
|
"vehicle_info": "Vehicle",
|
||||||
"viewallocations": "View Allocations"
|
"viewallocations": "View Allocations"
|
||||||
|
|||||||
@@ -766,7 +766,6 @@
|
|||||||
"documents": "documentos",
|
"documents": "documentos",
|
||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
"estimated": "",
|
|
||||||
"existing_jobs": "Empleos existentes",
|
"existing_jobs": "Empleos existentes",
|
||||||
"federal_tax_amt": "",
|
"federal_tax_amt": "",
|
||||||
"gpdollars": "",
|
"gpdollars": "",
|
||||||
@@ -791,11 +790,15 @@
|
|||||||
"rates": "Tarifas",
|
"rates": "Tarifas",
|
||||||
"rates_subtotal": "",
|
"rates_subtotal": "",
|
||||||
"reconciliationheader": "",
|
"reconciliationheader": "",
|
||||||
|
"sale_labor": "",
|
||||||
|
"sale_parts": "",
|
||||||
"state_tax_amt": "",
|
"state_tax_amt": "",
|
||||||
"subletstotal": "",
|
"subletstotal": "",
|
||||||
"subtotal": "",
|
"subtotal": "",
|
||||||
"suspense": "",
|
"suspense": "",
|
||||||
|
"total_cost": "",
|
||||||
"total_repairs": "",
|
"total_repairs": "",
|
||||||
|
"total_sales": "",
|
||||||
"totals": "",
|
"totals": "",
|
||||||
"vehicle_info": "Vehículo",
|
"vehicle_info": "Vehículo",
|
||||||
"viewallocations": ""
|
"viewallocations": ""
|
||||||
|
|||||||
@@ -766,7 +766,6 @@
|
|||||||
"documents": "Les documents",
|
"documents": "Les documents",
|
||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
"estimated": "",
|
|
||||||
"existing_jobs": "Emplois existants",
|
"existing_jobs": "Emplois existants",
|
||||||
"federal_tax_amt": "",
|
"federal_tax_amt": "",
|
||||||
"gpdollars": "",
|
"gpdollars": "",
|
||||||
@@ -791,11 +790,15 @@
|
|||||||
"rates": "Les taux",
|
"rates": "Les taux",
|
||||||
"rates_subtotal": "",
|
"rates_subtotal": "",
|
||||||
"reconciliationheader": "",
|
"reconciliationheader": "",
|
||||||
|
"sale_labor": "",
|
||||||
|
"sale_parts": "",
|
||||||
"state_tax_amt": "",
|
"state_tax_amt": "",
|
||||||
"subletstotal": "",
|
"subletstotal": "",
|
||||||
"subtotal": "",
|
"subtotal": "",
|
||||||
"suspense": "",
|
"suspense": "",
|
||||||
|
"total_cost": "",
|
||||||
"total_repairs": "",
|
"total_repairs": "",
|
||||||
|
"total_sales": "",
|
||||||
"totals": "",
|
"totals": "",
|
||||||
"vehicle_info": "Véhicule",
|
"vehicle_info": "Véhicule",
|
||||||
"viewallocations": ""
|
"viewallocations": ""
|
||||||
|
|||||||
Reference in New Issue
Block a user