diff --git a/client/src/components/jobs-detail-labor/jobs-detail-labor.component.jsx b/client/src/components/jobs-detail-labor/jobs-detail-labor.component.jsx index 2b0428772..c03d9d2d4 100644 --- a/client/src/components/jobs-detail-labor/jobs-detail-labor.component.jsx +++ b/client/src/components/jobs-detail-labor/jobs-detail-labor.component.jsx @@ -88,6 +88,7 @@ export function JobsDetailLaborContainer({ jobId={jobId} joblines={joblines} timetickets={timetickets} + refetch={refetch} adjustments={adjustments} /> @@ -97,6 +98,7 @@ export function JobsDetailLaborContainer({ jobId={jobId} joblines={joblines} timetickets={timetickets} + refetch={refetch} adjustments={adjustments} /> diff --git a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx index 7e0add2de..f7989ce83 100644 --- a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx +++ b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx @@ -13,6 +13,7 @@ import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit import "./labor-allocations-table.styles.scss"; import { CalculateAllocationsTotals } from "./labor-allocations-table.utility"; import axios from "axios"; +import { onlyUnique } from "../../utils/arrayHelper"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -26,6 +27,7 @@ export function PayrollLaborAllocationsTable({ bodyshop, adjustments, technician, + refetch, }) { const { t } = useTranslation(); const [totals, setTotals] = useState([]); @@ -39,11 +41,16 @@ export function PayrollLaborAllocationsTable({ }); useEffect(() => { - if (!!joblines && !!timetickets && !!bodyshop); + async function CalculateTotals() { + const { data } = await axios.post("/payroll/calculatelabor", { + jobid: jobId, + }); + setTotals(data); + } - setTotals( - CalculateAllocationsTotals(bodyshop, joblines, timetickets, adjustments) - ); + if (!!joblines && !!timetickets && !!bodyshop) { + CalculateTotals(); + } if (!jobId) setTotals([]); }, [joblines, timetickets, bodyshop, adjustments, jobId]); @@ -54,55 +61,46 @@ export function PayrollLaborAllocationsTable({ const columns = [ { - title: t("timetickets.fields.cost_center"), - dataIndex: "cost_center", - key: "cost_center", - defaultSortOrder: "cost_center", - sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), - sortOrder: - state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order, - render: (text, record) => `${record.cost_center} (${record.mod_lbr_ty})`, + title: t("timetickets.fields.employee"), + dataIndex: "employeeid", + key: "employeeid", + render: (text, record) => { + if (record.employeeid === undefined) { + return "Unassigned"; + } + const emp = bodyshop.employees.find((e) => e.id === record.employeeid); + return `${emp?.first_name} ${emp?.last_name}`; + }, + }, + { + title: t("joblines.fields.mod_lbr_ty"), + dataIndex: "mod_lbr_ty", + key: "mod_lbr_ty", + }, + { + title: t("timetickets.fields.rate"), + dataIndex: "rate", + key: "rate", }, { title: t("jobs.labels.hrs_total"), - dataIndex: "total", - key: "total", - sorter: (a, b) => a.total - b.total, + dataIndex: "expectedHours", + key: "expectedHours", + sorter: (a, b) => a.expectedHours - b.expectedHours, sortOrder: - state.sortedInfo.columnKey === "total" && state.sortedInfo.order, - render: (text, record) => record.total.toFixed(2), + state.sortedInfo.columnKey === "expectedHours" && + state.sortedInfo.order, + render: (text, record) => record.expectedHours.toFixed(5), }, { title: t("jobs.labels.hrs_claimed"), - dataIndex: "hrs_claimed", - key: "hrs_claimed", - sorter: (a, b) => a.claimed - b.claimed, + dataIndex: "claimedHours", + key: "claimedHours", + sorter: (a, b) => a.claimedHours - b.claimedHours, sortOrder: - state.sortedInfo.columnKey === "claimed" && state.sortedInfo.order, - render: (text, record) => record.claimed && record.claimed.toFixed(2), - }, - { - title: t("jobs.labels.adjustments"), - dataIndex: "adjustments", - key: "adjustments", - sorter: (a, b) => a.adjustments - b.adjustments, - sortOrder: - state.sortedInfo.columnKey === "adjustments" && state.sortedInfo.order, - render: (text, record) => ( - - {record.adjustments.toFixed(2)} - {!technician && ( - - - - )} - - ), + state.sortedInfo.columnKey === "claimedHours" && state.sortedInfo.order, + render: (text, record) => + record.claimedHours && record.claimedHours.toFixed(5), }, { title: t("jobs.labels.difference"), @@ -112,15 +110,22 @@ export function PayrollLaborAllocationsTable({ sorter: (a, b) => a.difference - b.difference, sortOrder: state.sortedInfo.columnKey === "difference" && state.sortedInfo.order, - render: (text, record) => ( - = 0 ? "green" : "red", - }} - > - {_.round(record.difference, 1)} - - ), + render: (text, record) => { + const difference = _.round( + record.expectedHours - record.claimedHours, + 5 + ); + + return ( + = 0 ? "green" : "red", + }} + > + {difference} + + ); + }, }, ]; const convertedTableCols = [ @@ -182,7 +187,7 @@ export function PayrollLaborAllocationsTable({ render: (text, record) => record.convertedtolbr_data && record.convertedtolbr_data.mod_lb_hrs && - record.convertedtolbr_data.mod_lb_hrs.toFixed(2), + record.convertedtolbr_data.mod_lb_hrs.toFixed(5), }, ]; @@ -194,10 +199,10 @@ export function PayrollLaborAllocationsTable({ totals && totals.reduce( (acc, val) => { - acc.hrs_total += val.total; - acc.hrs_claimed += val.claimed; - acc.adjustments += val.adjustments; - acc.difference += val.difference; + acc.hrs_total += val.expectedHours; + acc.hrs_claimed += val.claimedHours; + // acc.adjustments += val.adjustments; + acc.difference += val.expectedHours - val.claimedHours; return acc; }, { hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 } @@ -211,18 +216,21 @@ export function PayrollLaborAllocationsTable({ const { data } = await axios.post("/payroll/payall", { jobid: jobId, }); - - setTotals( - CalculateAllocationsTotals( - bodyshop, - joblines, - [...timetickets, ...data], - adjustments - ) - ); + refetch(); }} > - Calc + Pay All Test + + + + - {summary.hrs_total.toFixed(2)} + {summary.hrs_total.toFixed(5)} - {summary.hrs_claimed.toFixed(2)} + {summary.hrs_claimed.toFixed(5)} + - {summary.adjustments.toFixed(2)} - - - {summary.difference.toFixed(2)} + {summary.difference.toFixed(5)} )} diff --git a/client/src/components/shop-teams/shop-employee-teams.form.component.jsx b/client/src/components/shop-teams/shop-employee-teams.form.component.jsx index 904acda7d..d811eb794 100644 --- a/client/src/components/shop-teams/shop-employee-teams.form.component.jsx +++ b/client/src/components/shop-teams/shop-employee-teams.form.component.jsx @@ -199,7 +199,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) { }, ]} > - + employee to calculate total assigned hours. + const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job); + const ticketHash = CalculateTicketsHoursForJob(job); - const assignmentHash = { unassigned: 0 }; - job.joblines.forEach((jobline) => { - 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[jobline.mod_lbr_ty] = - assignmentHash.unassigned[jobline.mod_lbr_ty] + jobline.mod_lb_hrs; - } else { - //Line is assigned. - if (!assignmentHash[jobline.assigned_team]) { - assignmentHash[jobline.assigned_team] = 0; + const totals = []; + + //Iteratively go through all 4 levels of the object and create an array that can be presented. + // use the employee hash as the golden record (i.e. what they should have), and add what they've claimed. + //While going through, delete items from ticket hash. + //Anything left in ticket hash is an extra entered item. + + Object.keys(employeeHash).forEach((employeeIdKey) => { + //At the employee level. + Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => { + //At the labor level + Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach( + (rateKey) => { + //At the rate level. + const expectedHours = + employeeHash[employeeIdKey][laborTypeKey][rateKey]; + //Will the following line fail? Probably if it doesn't exist. + const claimedHours = get( + ticketHash, + `${employeeIdKey}.${laborTypeKey}.${rateKey}` + ); + if (claimedHours) { + delete ticketHash[employeeIdKey][laborTypeKey][rateKey]; + } + + totals.push({ + employeeid: employeeIdKey, + rate: rateKey, + mod_lbr_ty: laborTypeKey, + expectedHours, + claimedHours: claimedHours || 0, + }); } - assignmentHash[jobline.assigned_team] = - assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs; - } - } + ); + }); }); - res.json(assignmentHash); + + Object.keys(ticketHash).forEach((employeeIdKey) => { + //At the employee level. + Object.keys(ticketHash[employeeIdKey]).forEach((laborTypeKey) => { + //At the labor level + Object.keys(ticketHash[employeeIdKey][laborTypeKey]).forEach( + (rateKey) => { + //At the rate level. + const expectedHours = 0; + //Will the following line fail? Probably if it doesn't exist. + const claimedHours = get( + ticketHash, + `${employeeIdKey}.${laborTypeKey}.${rateKey}` + ); + if (claimedHours) { + delete ticketHash[employeeIdKey][laborTypeKey][rateKey]; + } + + totals.push({ + employeeid: employeeIdKey, + rate: rateKey, + mod_lbr_ty: laborTypeKey, + expectedHours, + claimedHours: claimedHours || 0, + }); + } + ); + }); + }); + if (assignmentHash.unassigned > 0) { + totals.push({ + employeeid: undefined, + //rate: rateKey, + //mod_lbr_ty: laborTypeKey, + expectedHours: assignmentHash.unassigned, + claimedHours: 0, + }); + } + res.json(totals); + //res.json(assignmentHash); } catch (error) { logger.log( - "job-payroll-labor-totals-error", + "job-payroll-calculate-labor-error", "ERROR", req.user.email, jobid, @@ -57,3 +122,9 @@ exports.calculateLaborTotals = async function (req, res) { res.status(503).send(); } }; + +get = function (obj, key) { + return key.split(".").reduce(function (o, x) { + return typeof o == "undefined" || o === null ? o : o[x]; + }, obj); +}; diff --git a/server/payroll/pay-all.js b/server/payroll/pay-all.js index 4fbf3f7af..877ad159c 100644 --- a/server/payroll/pay-all.js +++ b/server/payroll/pay-all.js @@ -5,13 +5,14 @@ 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 BearerToken = req.headers.authorization; - const { jobid } = req.body; + const { jobid, calculateOnly } = req.body; logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null); const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: { @@ -28,187 +29,117 @@ exports.payall = async function (req, res) { //iterate over each ticket, building a hash of team -> employee to calculate total assigned hours. - const assignmentHash = { unassigned: 0 }; - const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid. - job.joblines.forEach((jobline) => { - 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; - }); - } - } - }); - - 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; - //Add the rate - }); - + const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job); + const ticketHash = CalculateTicketsHoursForJob(job); if (assignmentHash.unassigned > 0) { res.json({ success: false, error: "Unassigned hours." }); return; } //Calculate how much time each tech should have by labor type. - // const comparison = compare(employeeHash, ticketHash); - const recursiveDiff = rdiff.getDiff(ticketHash, employeeHash, true); //Doing this order creates a diff of changes on the ticket hash to make it the same as the employee hash. + //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") { - if (typeof diff.val === "object") { + 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({ 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 hours.", + memo: "*System* Hours added during Pay All function. (multi)", }); }); } else { //Only the 1 value to add. ticketsToInsert.push({ jobid: job.id, + bodyshopid: job.bodyshop.id, employeeid: path.employeeid, productivehrs: path.hours, rate: path.rate, ciecacode: path.mod_lbr_ty, flat_rate: true, - memo: "Add hours.", + cost_center: + job.bodyshop.md_responsibility_centers.defaults.costs[ + path.mod_lbr_ty + ], + memo: "*System* Hours added during Pay All function.", }); } } else if (diff.op === "update") { } else { //Has to be a delete - ticketsToInsert.push({ - jobid: job.id, - employeeid: path.employeeid, - productivehrs: diff.oldVal * -1, - rate: path.rate, - ciecacode: path.mod_lbr_ty, - flat_rate: true, - memo: "Remove unneeded hours. (Rate/Unassigned).", - }); + + ////// + + if ( + typeof diff.oldVal === "object" && + Object.keys(diff.oldVal).length > 1 + ) { + //Multiple oldValues to add. + Object.keys(diff.oldVal).forEach((key) => { + console.log( + "Hours", + diff.oldVal[key][Object.keys(diff.oldVal[key])[0]] + ); + console.log("Rate", Object.keys(diff.oldVal[key])[0]); + ticketsToInsert.push({ + 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: "*System* Hours removed during Pay All function. (Change in rate, unassignment, etc.) *multi*", + }); + }); + } else { + //Only the 1 value to add. + ticketsToInsert.push({ + 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: "*System* Hours removed during Pay All function. (Change in rate, unassignment, etc.)", + }); + } } }); - // //Check the ones that are different first. Source of truth will be the employee hash. - // comparison.different.forEach((differentKey) => { - // const empVal = fetchFromObject(employeeHash, differentKey) || 0; - // const ticketVal = fetchFromObject(ticketHash, differentKey) || 0; - // const splitKey = splitEmployeeKey(differentKey); + const insertResult = await client.request(queries.INSERT_TIME_TICKETS, { + timetickets: ticketsToInsert.filter( + (ticket) => ticket.productivehrs !== 0 + ), + }); - // ticketsToInsert.push({ - // jobid: job.id, - // employeeid: splitKey.employeeid, - // productivehrs: empVal - ticketVal, - // rate: splitKey.rate, - // ciecacode: splitKey.mod_lbr_ty, - // flat_rate: true, - // memo: "Adjustment between expected and entered values.", - // }); - // }); - - // comparison.missing_from_first //Missing from the employee hash, but present in the ticket hash. - // .filter((differentKey) => differentKey.split(".").length == 3) - // .forEach((differentKey) => { - // const empVal = fetchFromObject(employeeHash, differentKey) || 0; - // const ticketVal = fetchFromObject(ticketHash, differentKey) || 0; - // const splitKey = splitEmployeeKey(differentKey); - - // ticketsToInsert.push({ - // jobid: job.id, - // employeeid: differentKey.split(".")[0], - // productivehrs: empVal - ticketVal * -1, - // rate: splitKey.rate, - // ciecacode: splitKey.mod_lbr_ty, - // flat_rate: true, - // memo: "Entered ticket reversed to match system payroll.", - // }); - // }); - // comparison.missing_from_second //Missing from the ticket hash, but present in the employee hash. - // .filter((differentKey) => differentKey.split(".").length == 3) - // .forEach((differentKey) => { - // const empVal = fetchFromObject(employeeHash, differentKey) || 0; - // const ticketVal = fetchFromObject(ticketHash, differentKey) || 0; - // const splitKey = splitEmployeeKey(differentKey); - - // ticketsToInsert.push({ - // jobid: job.id, - // employeeid: differentKey.split(".")[0], - // productivehrs: empVal - ticketVal * -1, - // rate: splitKey.rate, - // ciecacode: splitKey.mod_lbr_ty, - // flat_rate: true, - // memo: "Entered ticket reversed to match system payroll.", - // }); - // }); - - res.json(ticketsToInsert); + res.json(ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0)); } catch (error) { logger.log( "job-payroll-labor-totals-error", @@ -224,102 +155,137 @@ exports.payall = async function (req, res) { } }; -// var compare = function (a, b) { -// var result = { -// different: [], -// missing_from_first: [], -// missing_from_second: [], -// }; - -// _.reduce( -// a, -// function (result, value, key) { -// if (b.hasOwnProperty(key)) { -// if (_.isEqual(value, b[key])) { -// return result; -// } else { -// if (typeof a[key] != typeof {} || typeof b[key] != typeof {}) { -// //dead end. -// result.different.push(key); -// return result; -// } else { -// var deeper = compare(a[key], b[key]); -// result.different = result.different.concat( -// _.map(deeper.different, (sub_path) => { -// return key + "." + sub_path; -// }) -// ); - -// result.missing_from_second = result.missing_from_second.concat( -// _.map(deeper.missing_from_second, (sub_path) => { -// return key + "." + sub_path; -// }) -// ); - -// result.missing_from_first = result.missing_from_first.concat( -// _.map(deeper.missing_from_first, (sub_path) => { -// return key + "." + sub_path; -// }) -// ); -// return result; -// } -// } -// } else { -// result.missing_from_second.push(key); -// return result; -// } -// }, -// result -// ); - -// _.reduce( -// b, -// function (result, value, key) { -// if (a.hasOwnProperty(key)) { -// return result; -// } else { -// result.missing_from_first.push(key); -// return result; -// } -// }, -// result -// ); - -// return result; -// }; - -// function fetchFromObject(obj, prop) { -// if (typeof obj === "undefined") { -// return false; -// } - -// var _index = prop.indexOf("."); -// if (_index > -1) { -// return fetchFromObject( -// obj[prop.substring(0, _index)], -// prop.substr(_index + 1) -// ); -// } - -// return obj[prop]; -// } - -// function splitEmployeeKey(key) { -// const splitArray = differentKey.split("."); -// // employeeid => Cieca labor type => rate -// return { -// employeeid: splitArray[0], -// mod_lbr_ty: splitArray[1], -// rate: splitArray[2], -// hours: splitArray[3], -// }; -// } - function diffParser(diff) { - return { - employeeid: diff.path[0], - mod_lbr_ty: diff.path[1], - rate: diff.path[2], - hours: diff.val, + 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) { + 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) { + const assignmentHash = { unassigned: 0 }; + const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid. + job.joblines.forEach((jobline) => { + 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; diff --git a/server/payroll/payroll.js b/server/payroll/payroll.js index c069976c9..b7a8b7087 100644 --- a/server/payroll/payroll.js +++ b/server/payroll/payroll.js @@ -1,2 +1,2 @@ -exports.calculateLaborTotals = require("./calculate-totals").calculateLaborTotals; +exports.calculatelabor = require("./calculate-totals").calculatelabor; exports.payall = require("./pay-all").payall;