const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); const GraphQLClient = require("graphql-request").GraphQLClient; const _ = require("lodash"); const rdiff = require("recursive-diff"); const logger = require("../utils/logger"); const { json } = require("body-parser"); // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; exports.payall = async function (req, res) { const { jobid, calculateOnly } = req.body; logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null); const BearerToken = req.BearerToken; const client = req.userGraphQLClient; try { const { jobs_by_pk: job } = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOB_PAYROLL_DATA, { id: jobid }); //iterate over each ticket, building a hash of team -> employee to calculate total assigned hours. const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job); const ticketHash = CalculateTicketsHoursForJob(job); if (assignmentHash.unassigned > 0) { res.json({ success: false, error: "Not all hours have been assigned." }); return; } //Calculate how much time each tech should have by labor type. //Doing this order creates a diff of changes on the ticket hash to make it the same as the employee hash. const recursiveDiff = rdiff.getDiff(ticketHash, employeeHash, true); const ticketsToInsert = []; recursiveDiff.forEach((diff) => { //Every iteration is what we would need to insert into the time ticket hash //so that it would match the employee hash exactly. const path = diffParser(diff); if (diff.op === "add") { console.log(Object.keys(diff.val)); if (typeof diff.val === "object" && Object.keys(diff.val).length > 1) { //Multiple values to add. Object.keys(diff.val).forEach((key) => { console.log("Hours", diff.val[key][Object.keys(diff.val[key])[0]]); console.log("Rate", Object.keys(diff.val[key])[0]); ticketsToInsert.push({ task_name: "Pay All", jobid: job.id, bodyshopid: job.bodyshop.id, employeeid: path.employeeid, productivehrs: diff.val[key][Object.keys(diff.val[key])[0]], rate: Object.keys(diff.val[key])[0], ciecacode: key, cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[key], flat_rate: true, memo: `Add unflagged hours. (${req.user.email})` }); }); } else { //Only the 1 value to add. ticketsToInsert.push({ task_name: "Pay All", jobid: job.id, bodyshopid: job.bodyshop.id, employeeid: path.employeeid, productivehrs: path.hours, rate: path.rate, ciecacode: path.mod_lbr_ty, flat_rate: true, cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[path.mod_lbr_ty], memo: `Add unflagged hours. (${req.user.email})` }); } } else if (diff.op === "update") { //An old ticket amount isn't sufficient //We can't modify the existing ticket, it might already be committed. So let's add a new one instead. ticketsToInsert.push({ task_name: "Pay All", jobid: job.id, bodyshopid: job.bodyshop.id, employeeid: path.employeeid, productivehrs: diff.val - diff.oldVal, rate: path.rate, ciecacode: path.mod_lbr_ty, flat_rate: true, cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[path.mod_lbr_ty], memo: `Adjust flagged hours per assignment. (${req.user.email})` }); } else { //Has to be a delete if (typeof diff.oldVal === "object" && Object.keys(diff.oldVal).length > 1) { //Multiple oldValues to add. Object.keys(diff.oldVal).forEach((key) => { ticketsToInsert.push({ task_name: "Pay All", jobid: job.id, bodyshopid: job.bodyshop.id, employeeid: path.employeeid, productivehrs: diff.oldVal[key][Object.keys(diff.oldVal[key])[0]] * -1, rate: Object.keys(diff.oldVal[key])[0], ciecacode: key, cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[key], flat_rate: true, memo: `Remove flagged hours per assignment. (${req.user.email})` }); }); } else { //Only the 1 value to add. ticketsToInsert.push({ task_name: "Pay All", jobid: job.id, bodyshopid: job.bodyshop.id, employeeid: path.employeeid, productivehrs: path.hours * -1, rate: path.rate, ciecacode: path.mod_lbr_ty, cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[path.mod_lbr_ty], flat_rate: true, memo: `Remove flagged hours per assignment. (${req.user.email})` }); } } }); const insertResult = await client.request(queries.INSERT_TIME_TICKETS, { timetickets: ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0) }); res.json(ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0)); } catch (error) { logger.log("job-payroll-labor-totals-error", "ERROR", req.user.email, jobid, { jobid: jobid, error: JSON.stringify(error) }); res.status(400).json({ error: error.message }); } }; function diffParser(diff) { const type = typeof diff.oldVal; let mod_lbr_ty, rate, hours; if (diff.path.length === 1) { if (diff.op === "add") { mod_lbr_ty = Object.keys(diff.val)[0]; rate = Object.keys(diff.val[mod_lbr_ty])[0]; // hours = diff.oldVal[mod_lbr_ty][rate]; } else { mod_lbr_ty = Object.keys(diff.oldVal)[0]; rate = Object.keys(diff.oldVal[mod_lbr_ty])[0]; // hours = diff.oldVal[mod_lbr_ty][rate]; } } else if (diff.path.length === 2) { mod_lbr_ty = diff.path[1]; if (diff.op === "add") { rate = Object.keys(diff.val)[0]; } else { rate = Object.keys(diff.oldVal)[0]; } } else if (diff.path.length === 3) { mod_lbr_ty = diff.path[1]; rate = diff.path[2]; //hours = 0; } //Set the hours if (typeof diff.val === "number" && diff.val !== null && diff.val !== undefined) { hours = diff.val; } else if (diff.val !== null && diff.val !== undefined) { if (diff.path.length === 1) { hours = diff.val[Object.keys(diff.val)[0]][Object.keys(diff.val[Object.keys(diff.val)[0]])]; } else { hours = diff.val[Object.keys(diff.val)[0]]; } } else if (typeof diff.oldVal === "number" && diff.oldVal !== null && diff.oldVal !== undefined) { hours = diff.oldVal; } else { hours = diff.oldVal[Object.keys(diff.oldVal)[0]]; } const ret = { multiVal: false, employeeid: diff.path[0], // Always True mod_lbr_ty, rate, hours }; return ret; } function CalculateExpectedHoursForJob(job, filterToLbrTypes) { const assignmentHash = { unassigned: 0 }; const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid. job.joblines .filter((jobline) => { if (!filterToLbrTypes) return true; else { return ( filterToLbrTypes.includes(jobline.mod_lbr_ty) || (jobline.convertedtolbr && filterToLbrTypes.includes(jobline.convertedtolbr_data.mod_lbr_ty)) ); } }) .forEach((jobline) => { if (jobline.convertedtolbr) { // Line has been converte to labor. Temporarily re-assign the hours. jobline.mod_lbr_ty = jobline.convertedtolbr_data.mod_lbr_ty; jobline.mod_lb_hrs += jobline.convertedtolbr_data.mod_lb_hrs; } if (jobline.mod_lb_hrs != 0) { //Check if the line is assigned. If not, keep track of it as an unassigned line by type. if (jobline.assigned_team === null) { assignmentHash.unassigned = assignmentHash.unassigned + jobline.mod_lb_hrs; } else { //Line is assigned. if (!assignmentHash[jobline.assigned_team]) { assignmentHash[jobline.assigned_team] = 0; } assignmentHash[jobline.assigned_team] = assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs; //Create the assignment breakdown. const theTeam = job.bodyshop.employee_teams.find((team) => team.id === jobline.assigned_team); theTeam.employee_team_members.forEach((tm) => { //Figure out how many hours they are owed at this line, and at what rate. if (!employeeHash[tm.employee.id]) { employeeHash[tm.employee.id] = {}; } if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty]) { employeeHash[tm.employee.id][jobline.mod_lbr_ty] = {}; } if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]]) { employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]] = 0; } const hoursOwed = (tm.percentage * jobline.mod_lb_hrs) / 100; employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]] = employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]] + hoursOwed; }); } } }); return { assignmentHash, employeeHash }; } function CalculateTicketsHoursForJob(job) { const ticketHash = {}; // employeeid => Cieca labor type => rate => hours. //Calculate how much each employee has been paid so far. job.timetickets.forEach((ticket) => { if (!ticketHash[ticket.employeeid]) { ticketHash[ticket.employeeid] = {}; } if (!ticketHash[ticket.employeeid][ticket.ciecacode]) { ticketHash[ticket.employeeid][ticket.ciecacode] = {}; } if (!ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate]) { ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] = 0; } ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] = ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] + ticket.productivehrs; }); return ticketHash; } exports.CalculateExpectedHoursForJob = CalculateExpectedHoursForJob; exports.CalculateTicketsHoursForJob = CalculateTicketsHoursForJob;