const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); //const client = require("../graphql-client/graphql-client").client; const _ = require("lodash"); const GraphQLClient = require("graphql-request").GraphQLClient; // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; async function JobCosting(req, res) { const { jobid } = req.body; console.log("🚀 ~ file: job-costing.js ~ line 13 ~ jobid", jobid); const BearerToken = req.headers.authorization; const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: { Authorization: BearerToken, }, }); try { const resp = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOB_COSTING_DETAILS, { id: jobid, }); const ret = GenerateCostingData(resp.jobs_by_pk); res.status(200).json(ret); } catch (error) { console.log("error", error); res.status(400).send(JSON.stringify(error)); } } async function JobCostingMulti(req, res) { const { jobids } = req.body; console.log("🚀 ~ file: job-costing.js ~ line 13 ~ jobids", jobids); const BearerToken = req.headers.authorization; const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: { Authorization: BearerToken, }, }); try { const resp = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOB_COSTING_DETAILS_MULTI, { ids: jobids, }); //for Each!*************** const ret = {}; resp.jobs.map((job) => { 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;