@@ -204,7 +30,3 @@ export function JobCostingModalComponent({ bodyshop, job }) {
);
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(JobCostingModalComponent);
diff --git a/client/src/components/job-costing-modal/job-costing-modal.container.jsx b/client/src/components/job-costing-modal/job-costing-modal.container.jsx
index ace532ba4..8a1c1dc40 100644
--- a/client/src/components/job-costing-modal/job-costing-modal.container.jsx
+++ b/client/src/components/job-costing-modal/job-costing-modal.container.jsx
@@ -1,13 +1,11 @@
-import { useQuery } from "@apollo/client";
import { Modal } from "antd";
-import React from "react";
+import axios from "axios";
+import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
-import { QUERY_JOB_COSTING_DETAILS } from "../../graphql/jobs.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobCosting } from "../../redux/modals/modals.selectors";
-import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobCostingModalComponent from "./job-costing-modal.component";
@@ -24,14 +22,25 @@ export function JobCostingModalContainer({
toggleModalVisible,
}) {
const { t } = useTranslation();
-
+ const [costingData, setCostingData] = useState(null);
const { visible, context } = jobCostingModal;
const { jobId } = context;
- const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
- variables: { id: jobId },
- skip: !jobId,
- });
+ // const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
+ // variables: { id: jobId },
+ // skip: !jobId,
+ // });
+
+ useEffect(() => {
+ async function getData() {
+ if (jobId && visible) {
+ const { data } = await axios.post("/job/costing", { jobid: jobId });
+ console.log(data);
+ setCostingData(data);
+ }
+ }
+ getData();
+ }, [jobId, visible]);
return (
- {error ? : null}
- {loading ? (
-
+ {!costingData ? (
+
) : (
-
+
)}
);
diff --git a/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx b/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx
index d4dbe2c30..8b483d113 100644
--- a/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx
+++ b/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from "react";
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
-
+import Dinero from "dinero.js";
export default function JobCostingPieComponent({
type = "sales",
costCenterData,
@@ -11,8 +11,8 @@ export default function JobCostingPieComponent({
return data.reduce((acc, i) => {
const value =
type === "sales"
- ? i.sales_dinero.getAmount()
- : i.costs_dinero.getAmount();
+ ? Dinero(i.sales_dinero).getAmount()
+ : Dinero(i.costs_dinero).getAmount();
if (value > 0) {
acc.push({
@@ -21,13 +21,13 @@ export default function JobCostingPieComponent({
label: `${i.cost_center} - ${
type === "sales"
- ? i.sales_dinero.toFormat()
- : i.costs_dinero.toFormat()
+ ? Dinero(i.sales_dinero).toFormat()
+ : Dinero(i.costs_dinero).toFormat()
}`,
value:
type === "sales"
- ? i.sales_dinero.getAmount()
- : i.costs_dinero.getAmount(),
+ ? Dinero(i.sales_dinero).getAmount()
+ : Dinero(i.costs_dinero).getAmount(),
});
}
return acc;
diff --git a/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx b/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx
index 3f7e576ec..acc81ef91 100644
--- a/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx
+++ b/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx
@@ -1,9 +1,9 @@
-import { Input, Table, Typography } from "antd";
+import { Input, Space, Table, Typography } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
-
-export default function JobCostingPartsTable({ job, data, summaryData }) {
+import Dinero from "dinero.js";
+export default function JobCostingPartsTable({ data, summaryData }) {
const [searchText, setSearchText] = useState("");
const [state, setState] = useState({
sortedInfo: {},
@@ -75,18 +75,16 @@ export default function JobCostingPartsTable({ job, data, summaryData }) {
{
return (
-
-
- {
- e.preventDefault();
- setSearchText(e.target.value);
- }}
- />
-
-
+
+ {
+ e.preventDefault();
+ setSearchText(e.target.value);
+ }}
+ />
+
);
}}
scroll={{ x: "50%", y: "40rem" }}
@@ -103,13 +101,13 @@ export default function JobCostingPartsTable({ job, data, summaryData }) {
- {summaryData.totalSales.toFormat()}
+ {Dinero(summaryData.totalSales).toFormat()}
- {summaryData.totalCost.toFormat()}
+ {Dinero(summaryData.totalCost).toFormat()}
- {summaryData.gpdollars.toFormat()}
+ {Dinero(summaryData.gpdollars).toFormat()}
diff --git a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx
index 730fc3d76..2eac049a1 100644
--- a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx
+++ b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx
@@ -1,39 +1,39 @@
import { Statistic } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
-
-export default function JobCostingStatistics({ job, summaryData }) {
+import Dinero from "dinero.js";
+export default function JobCostingStatistics({ summaryData }) {
const { t } = useTranslation();
return (
{
+ ret[job.id] = GenerateCostingData(job);
+ });
+
+ res.status(200).json(ret);
+ } catch (error) {
+ console.log("error", error);
+ res.status(400).send(JSON.stringify(error));
+ }
+}
+
+function GenerateCostingData(job) {
+ const defaultProfits =
+ job.bodyshop.md_responsibility_centers.defaults.profits;
+ const allProfitCenters = _.union(
+ job.bodyshop.md_responsibility_centers.profits.map((p) => p.name),
+ job.bodyshop.md_responsibility_centers.costs.map((p) => p.name)
+ );
+
+ //Massage the data.
+ const jobLineTotalsByProfitCenter =
+ job &&
+ job.joblines.reduce(
+ (acc, val) => {
+ const laborProfitCenter = defaultProfits[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 billTotalsByProfitCenter = job.bills.reduce((bill_acc, bill_val) => {
+ //At the invoice level.
+ bill_val.billlines.map((line_val) => {
+ //At the invoice line level.
+ //console.log("JobCostingPartsTable -> line_val", line_val);
+ if (!bill_acc[line_val.cost_center])
+ bill_acc[line_val.cost_center] = Dinero();
+
+ bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
+ Dinero({
+ amount: Math.round((line_val.actual_cost || 0) * 100),
+ })
+ .multiply(line_val.quantity)
+ .multiply(bill_val.is_credit_memo ? -1 : 1)
+ );
+
+ return null;
+ });
+ return bill_acc;
+ }, {});
+
+ const ticketTotalsByProfitCenter = job.timetickets.reduce(
+ (ticket_acc, ticket_val) => {
+ //At the invoice level.
+ if (!ticket_acc[ticket_val.cost_center])
+ ticket_acc[ticket_val.cost_center] = Dinero();
+
+ ticket_acc[ticket_val.cost_center] = ticket_acc[
+ ticket_val.cost_center
+ ].add(
+ Dinero({
+ amount: Math.round((ticket_val.rate || 0) * 100),
+ }).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0)
+ );
+
+ return ticket_acc;
+ },
+ {}
+ );
+
+ const summaryData = {
+ totalLaborSales: Dinero({ amount: 0 }),
+ totalPartsSales: Dinero({ amount: 0 }),
+ totalSales: Dinero({ amount: 0 }),
+ totalLaborCost: Dinero({ amount: 0 }),
+ totalPartsCost: Dinero({ amount: 0 }),
+ totalCost: Dinero({ amount: 0 }),
+ gpdollars: Dinero({ amount: 0 }),
+ gppercent: null,
+ gppercentFormatted: null,
+ };
+
+ const costCenterData = allProfitCenters.map((key, idx) => {
+ const ccVal = key; // defaultProfits[key];
+ const sale_labor =
+ jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 });
+ const sale_parts =
+ jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({ amount: 0 });
+
+ const cost_labor =
+ ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
+ const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
+
+ const costs = (
+ billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
+ ).add(ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }));
+ const totalSales = sale_labor.add(sale_parts);
+ const gpdollars = totalSales.subtract(costs);
+ 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.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
+ summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
+ summaryData.totalCost = summaryData.totalCost.add(costs);
+
+ return {
+ id: idx,
+ cost_center: ccVal,
+ sale_labor: sale_labor && sale_labor.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_labor: cost_labor && cost_labor.toFormat(),
+ costs: cost_parts.add(cost_labor).toFormat(),
+ costs_dinero: cost_parts.add(cost_labor),
+ 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;
+ }
+
+ return { summaryData, costCenterData };
+}
+
+exports.JobCosting = JobCosting;
+exports.JobCostingMulti = JobCostingMulti;
diff --git a/server/job/job.js b/server/job/job.js
index b41c1dab0..975342045 100644
--- a/server/job/job.js
+++ b/server/job/job.js
@@ -1,2 +1,4 @@
exports.totals = require("./job-totals").default;
exports.totalsSsu = require("./job-totals").totalsSsu;
+exports.costing = require("./job-costing").JobCosting;
+exports.costingmulti = require("./job-costing").JobCostingMulti;
From 5b17bfaaa0475f0c7e2d8fb6ec4914503112e41a Mon Sep 17 00:00:00 2001
From: Patrick Fic <>
Date: Tue, 6 Apr 2021 14:29:44 -0700
Subject: [PATCH 3/4] IO-834 IO-835 Totals table formatting.
---
bodyshop_translations.babel | 21 ++++++++++++++++
.../job-totals-table.component.jsx | 24 +++++++++++--------
.../job-totals.table.labor.component.jsx | 14 +++++------
.../job-totals.table.other.component.jsx | 4 ++--
.../job-totals.table.parts.component.jsx | 4 +++-
client/src/translations/en_us/common.json | 1 +
client/src/translations/es/common.json | 1 +
client/src/translations/fr/common.json | 1 +
client/src/utils/TemplateConstants.js | 8 +++++++
server/job/job-costing.js | 3 ---
10 files changed, 58 insertions(+), 23 deletions(-)
diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel
index 10c74fa83..b01b34c82 100644
--- a/bodyshop_translations.babel
+++ b/bodyshop_translations.babel
@@ -28239,6 +28239,27 @@
templates
+
+ credits_not_received_date
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
estimator_detail
false
diff --git a/client/src/components/job-totals-table/job-totals-table.component.jsx b/client/src/components/job-totals-table/job-totals-table.component.jsx
index 6262e2835..d021fbecd 100644
--- a/client/src/components/job-totals-table/job-totals-table.component.jsx
+++ b/client/src/components/job-totals-table/job-totals-table.component.jsx
@@ -47,17 +47,21 @@ export function JobsTotalsTableComponent({ jobRO, job }) {
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
diff --git a/client/src/components/job-totals-table/job-totals.table.labor.component.jsx b/client/src/components/job-totals-table/job-totals.table.labor.component.jsx
index 47a4a9969..55b41aaf6 100644
--- a/client/src/components/job-totals-table/job-totals.table.labor.component.jsx
+++ b/client/src/components/job-totals-table/job-totals.table.labor.component.jsx
@@ -103,11 +103,11 @@ export default function JobTotalsTableLabor({ job }) {
<>
- {t("jobs.labels.labor_rates_subtotal")}
+ {t("jobs.labels.labor_rates_subtotal")}
-
+
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
@@ -121,7 +121,7 @@ export default function JobTotalsTableLabor({ job }) {
{job.job_totals.rates.mapa.hours.toFixed(2)}
-
+
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
@@ -133,19 +133,19 @@ export default function JobTotalsTableLabor({ job }) {
{job.job_totals.rates.mash.hours.toFixed(2)}
-
+
{Dinero(job.job_totals.rates.mash.total).toFormat()}
- {t("jobs.labels.labor_rates_subtotal")}
+ {t("jobs.labels.rates_subtotal")}
-
+
- {Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
+ {Dinero(job.job_totals.rates.subtotal).toFormat()}
diff --git a/client/src/components/job-totals-table/job-totals.table.other.component.jsx b/client/src/components/job-totals-table/job-totals.table.other.component.jsx
index 00925007c..c06cd7232 100644
--- a/client/src/components/job-totals-table/job-totals.table.other.component.jsx
+++ b/client/src/components/job-totals-table/job-totals.table.other.component.jsx
@@ -79,7 +79,7 @@ export default function JobTotalsTableOther({ job }) {
<>
- {t("jobs.labels.additionaltotal")}
+ {t("jobs.labels.additionaltotal")}
@@ -90,7 +90,7 @@ export default function JobTotalsTableOther({ job }) {
- {t("jobs.labels.subletstotal")}
+ {t("jobs.labels.subletstotal")}
diff --git a/client/src/components/job-totals-table/job-totals.table.parts.component.jsx b/client/src/components/job-totals-table/job-totals.table.parts.component.jsx
index ca1eef5b3..d71e033b4 100644
--- a/client/src/components/job-totals-table/job-totals.table.parts.component.jsx
+++ b/client/src/components/job-totals-table/job-totals.table.parts.component.jsx
@@ -70,7 +70,9 @@ export default function JobTotalsTableParts({ job }) {
}}
summary={() => (
- {t("jobs.labels.partstotal")}
+
+ {t("jobs.labels.partstotal")}
+
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 99da29928..ad3484ac4 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -1705,6 +1705,7 @@
"vendor": "Vendor"
},
"templates": {
+ "credits_not_received_date": "Credits not Received by Date",
"estimator_detail": "Jobs by Estimator (Detail)",
"estimator_summary": "Jobs by Estimator (Summary)",
"hours_sold_detail_closed": "Hours Sold Detail - Closed",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 225b47151..4d77ac2f3 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -1705,6 +1705,7 @@
"vendor": ""
},
"templates": {
+ "credits_not_received_date": "",
"estimator_detail": "",
"estimator_summary": "",
"hours_sold_detail_closed": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 6ca6cbff4..c308bc2f7 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -1705,6 +1705,7 @@
"vendor": ""
},
"templates": {
+ "credits_not_received_date": "",
"estimator_detail": "",
"estimator_summary": "",
"hours_sold_detail_closed": "",
diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js
index e6f5d9453..1a6b49802 100644
--- a/client/src/utils/TemplateConstants.js
+++ b/client/src/utils/TemplateConstants.js
@@ -519,6 +519,14 @@ export const TemplateList = (type, context) => {
//idtype: "vendor",
disabled: false,
},
+ credits_not_received_date: {
+ title: i18n.t("reportcenter.templates.credits_not_received_date"),
+ description: "",
+ subject: i18n.t("reportcenter.templates.credits_not_received_date"),
+ key: "credits_not_received_date",
+ //idtype: "vendor",
+ disabled: false,
+ },
}
: {}),
...(!type || type === "courtesycarcontract"
diff --git a/server/job/job-costing.js b/server/job/job-costing.js
index b85e87f66..5d7fba619 100644
--- a/server/job/job-costing.js
+++ b/server/job/job-costing.js
@@ -3,9 +3,6 @@ const queries = require("../graphql-client/queries");
//const client = require("../graphql-client/graphql-client").client;
const _ = require("lodash");
const GraphQLClient = require("graphql-request").GraphQLClient;
-const {
- ExportCustomJobInstance,
-} = require("twilio/lib/rest/bulkexports/v1/export/exportCustomJob");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
From 09ce789d4cf8bc60c5c76810dea18d036ec8adf7 Mon Sep 17 00:00:00 2001
From: Patrick Fic <>
Date: Tue, 6 Apr 2021 15:57:14 -0700
Subject: [PATCH 4/4] IO-837 Fix Tech Clocking issue
---
.../tech-job-clock-out-button.component.jsx | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
index d6d782556..a36c9a856 100644
--- a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
+++ b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
@@ -31,17 +31,10 @@ export function TechClockOffButton({
const { t } = useTranslation();
const emps = bodyshop.employees.filter(
- (e) => e.id === technician && technician.id
+ (e) => e.id === (technician && technician.id)
)[0];
- console.log(
- "emps :>> ",
- emps,
- "e",
- bodyshop && bodyshop.employees,
- "tech",
- technician
- );
+ console.log(emps && emps.rates);
const handleFinish = async (values) => {
logImEXEvent("tech_clock_out_job");