import { Typography } from "antd"; import Dinero from "dinero.js"; import React from "react"; import { useTranslation } from "react-i18next"; 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 JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component"; import JobCostingPie from "./job-costing-modal.pie.component"; import _ from "lodash"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export function JobCostingModalComponent({ bodyshop, job }) { const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits; const allProfitCenters = _.union( bodyshop.md_responsibility_centers.profits.map((p) => p.name), bodyshop.md_responsibility_centers.costs.map((p) => p.name) ); // const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs; const { t } = useTranslation(); 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 (
{t("jobs.labels.sales")}
{t("jobs.labels.cost")}
); } export default connect( mapStateToProps, mapDispatchToProps )(JobCostingModalComponent);