From d7a1d5bbd277046aadb3661b592db30a8fc7e652 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 18 Jul 2023 12:59:06 -0700 Subject: [PATCH] Refactor task claiming and implement basic claim functionality. --- bodyshop_translations.babel | 126 ++++++ .../read-only-form-item.component.jsx | 25 +- ...or-allocations-table.payroll.component.jsx | 54 +-- .../shop-info.task-presets.component.jsx | 133 +++++- .../time-ticket-task-modal.component.jsx | 421 +++++------------- .../time-ticket-task-modal.container.jsx | 189 ++------ .../time-ticket-task-selector.component.jsx | 30 -- .../time-ticket-tasks-presets.component.jsx | 72 --- client/src/translations/en_us/common.json | 6 + client/src/translations/es/common.json | 6 + client/src/translations/fr/common.json | 6 + server.js | 1 + server/graphql-client/queries.js | 3 + server/payroll/claim-task.js | 88 ++++ server/payroll/pay-all.js | 116 ++--- server/payroll/payroll.js | 1 + 16 files changed, 623 insertions(+), 654 deletions(-) delete mode 100644 client/src/components/time-ticket-task-selector/time-ticket-task-selector.component.jsx delete mode 100644 client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx create mode 100644 server/payroll/claim-task.js diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 0f1df33fd..d3bc23ec4 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -4318,6 +4318,48 @@ dms + + apcontrol + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + appostingaccount + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + cashierid false @@ -5734,6 +5776,27 @@ + + nextstatus + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + percent false @@ -46488,6 +46551,27 @@ + + claimtaskpreview + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + clockhours false @@ -46677,6 +46761,27 @@ + + payrollclaimedtasks + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + pmbreak false @@ -46782,6 +46887,27 @@ + + task + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + timetickets false diff --git a/client/src/components/form-items-formatted/read-only-form-item.component.jsx b/client/src/components/form-items-formatted/read-only-form-item.component.jsx index fd50b9c94..c27944691 100644 --- a/client/src/components/form-items-formatted/read-only-form-item.component.jsx +++ b/client/src/components/form-items-formatted/read-only-form-item.component.jsx @@ -1,9 +1,26 @@ import Dinero from "dinero.js"; import React, { forwardRef } from "react"; -const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => { +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +const ReadOnlyFormItem = ( + { bodyshop, value, type = "text", onChange }, + ref +) => { if (!value) return null; switch (type) { + case "employee": + const emp = bodyshop.employees.find((e) => e.id === value); + return `${emp?.first_name} ${emp?.last_name}`; + case "text": return
{value}
; case "currency": @@ -14,4 +31,8 @@ const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => { return
{value}
; } }; -export default forwardRef(ReadOnlyFormItem); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(forwardRef(ReadOnlyFormItem)); 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 f7989ce83..16d23beec 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 @@ -1,5 +1,5 @@ -import { EditFilled } from "@ant-design/icons"; import { Button, Card, Col, Row, Space, Table, Typography } from "antd"; +import axios from "axios"; import _ from "lodash"; import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -8,12 +8,7 @@ import { createStructuredSelector } from "reselect"; import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import { alphaSort } from "../../utils/sorters"; -import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component"; 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, @@ -211,28 +206,33 @@ export function PayrollLaborAllocationsTable({ return ( - + + + } > - Pay All Test - - - `${record.cost_center} ${record.mod_lbr_ty}`} diff --git a/client/src/components/shop-info/shop-info.task-presets.component.jsx b/client/src/components/shop-info/shop-info.task-presets.component.jsx index aedf554bc..82d49227d 100644 --- a/client/src/components/shop-info/shop-info.task-presets.component.jsx +++ b/client/src/components/shop-info/shop-info.task-presets.component.jsx @@ -7,6 +7,7 @@ import { Input, InputNumber, Row, + Select, Space, Switch, } from "antd"; @@ -15,7 +16,21 @@ import { useTranslation } from "react-i18next"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -export default function ShopInfoTaskPresets({ form }) { +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ShopInfoTaskPresets); + +export function ShopInfoTaskPresets({ bodyshop, form }) { const { t } = useTranslation(); return ( @@ -59,6 +74,7 @@ export default function ShopInfoTaskPresets({ form }) { - + + + {t("joblines.fields.lbr_types.LAA")} + + + - + - {t("joblines.fields.lbr_types.LAR")} + {t("joblines.fields.lbr_types.LAD")} - + - {t("joblines.fields.lbr_types.LAM")} + {t("joblines.fields.lbr_types.LAE")} - + - + + + + {t("joblines.fields.lbr_types.LAM")} + + + + + {t("joblines.fields.lbr_types.LAM")} + + + + + {t("joblines.fields.lbr_types.LAR")} + + + + + {t("joblines.fields.lbr_types.LAS")} + + + + + {t("joblines.fields.lbr_types.LAU")} + + + + + {t("joblines.fields.lbr_types.LA1")} + + + + + {t("joblines.fields.lbr_types.LA2")} + + + + + {t("joblines.fields.lbr_types.LA3")} + + + + + {t("joblines.fields.lbr_types.LA4")} + + @@ -128,6 +230,17 @@ export default function ShopInfoTaskPresets({ form }) { > + +
+ + + + + + + + + + + + +
{t("bodyshop.fields.md_tasks_presets.percent")}{`${theTaskPreset.percent || 0}%`}
{t("bodyshop.fields.md_tasks_presets.hourstype")}{theTaskPreset.hourstype.join(", ")}
+ {t("bodyshop.fields.md_tasks_presets.nextstatus")} + {theTaskPreset.nextstatus}
+ ); + }} + - - {() => { - const data = form.getFieldValue("timetickets"); - return ( - { - const emp = bodyshop.employees.find( - (e) => e.id === record.employeeid - ); - return `${emp?.first_name} ${emp?.last_name}`; - }, - }, - { - title: t("timetickets.fields.cost_center"), - dataIndex: "cost_center", - key: "cost_center", - - render: (text, record) => - record.cost_center === "timetickets.labels.shift" - ? t(record.cost_center) - : record.cost_center, - }, - { - title: t("timetickets.fields.productivehrs"), - dataIndex: "productivehrs", - key: "productivehrs", - }, - { - title: "Percentage", - dataIndex: "percentage", - key: "percentage", - }, - { - title: "Rate", - dataIndex: "rate", - key: "rate", - }, - // { - // title: "Pay", - // dataIndex: "pay", - // key: "pay", - // }, - ]} - /> - ); - }} - - - { - //Check the cost center, - const totals = CalculateAllocationsTotals( - bodyshop, - lineTicketData.joblines, - lineTicketData.timetickets, - lineTicketData.jobs_by_pk.lbr_adjustments - ); - - const grouped = _.groupBy(value, "cost_center"); - let error = false; - Object.keys(grouped).forEach((key) => { - const totalProdTicketHours = grouped[key].reduce( - (acc, val) => acc + val.productivehrs, - 0 - ); - - const fieldTypeToCheck = "cost_center"; - // bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber - // ? "mod_lbr_ty" - // : "cost_center"; - - const costCenterDiff = - Math.round( - totals.find((total) => total[fieldTypeToCheck] === key) - ?.difference * 10 - ) / 10; - - if (totalProdTicketHours > costCenterDiff) error = true; - else { - // return Promise.resolve(); - } - }); - - if (!error) return Promise.resolve(); - return Promise.reject( - "Too many hours are being claimed as a part of this task" - ); - }, - }, - ]} - > - {(fields, { add, remove, move }, { errors }) => { - return ( -
- {errors.map((e, idx) => ( - - ))} -
- {fields.map((field, index) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - ))} -
-
- ); - }} -
+ {loading ? ( + + ) : ( + + {(fields, { add, remove, move }) => { + return ( + <> + + {t("timetickets.labels.claimtaskpreview")} + +
+ + + + + + + + + + {fields.map((field, index) => ( + + + + + + + ))} + +
{t("timetickets.fields.employee")}{t("timetickets.fields.cost_center")}{t("timetickets.fields.ciecacode")}{t("timetickets.fields.productivehrs")}
+ + + + + + + + + + + + + + + +
+ + + ); + }} + + )}
- - {() => { - const jobid = form.getFieldValue("jobid"); - if ( - (!lineTicketCalled && jobid) || - (jobid && - lineTicketData?.jobs_by_pk?.id !== jobid && - !lineTicketLoading) - ) { - queryJobInfo({ variables: { id: jobid } }).then(() => - calculateTimeTickets("") - ); - } - return ( - - ); - }} - {bodyshop?.md_tasks_presets?.use_approvals && ( - + ({ - ..._.omit(ticket, "pay"), - bodyshopid: bodyshop.id, - })), - }, - }); - if (result.errors) { - notification.open({ - type: "error", - message: t("timetickets.errors.creating", { - message: JSON.stringify(result.errors), - }), - }); - } else { - notification.open({ - type: "success", - message: t("timetickets.successes.created"), - }); - form.resetFields(); - toggleModalVisible(); - } - } else { - const result = await insertTimeTickets({ - variables: { - timeTicketInput: values.timetickets.map((ticket) => - _.omit(ticket, "pay") - ), - }, - refetchQueries: ["GET_LINE_TICKET_BY_PK"] - }); - if (result.errors) { - notification.open({ - type: "error", - message: t("timetickets.errors.creating", { - message: JSON.stringify(result.errors), - }), - }); - } else { - notification.open({ - type: "success", - message: t("timetickets.successes.created"), - }); - toggleModalVisible(); - } - } - } catch (error) { - console.log("🚀 ~ file: time-ticket-task-modal.container.jsx:104 ~ handleFinish ~ error:", error) - notification.open({ - type: "error", - message: t("timetickets.errors.creating", { - message: JSON.stringify(error), - }), - }); - } finally { + calculateTickets({ values, handleFinish: true }); + } + + async function handleValueChange(changedValues, allValues) { + if (allValues.jobid && allValues.task) { + calculateTickets({ values: allValues, handleFinish: false }); } } - useEffect(() => { - if (visible && context.jobid) { - queryJobInfo({ variables: { id: context.jobid } }); - } - }, [context.jobid, queryJobInfo, visible]); - - const calculateTimeTickets = (presetMemo) => { - const formData = form.getFieldsValue(); - if ( - !formData.jobid || - !formData.employeeteamid || - !formData.hourstype || - formData.hourstype.length === 0 || - !formData.percent || - !lineTicketData - ) { - return; - } - - let data = []; - let eligibleHours = 0; - - const theTeam = JSON.parse(formData.employeeteamid); - - if (theTeam) { - formData.hourstype.forEach((hourstype) => { - eligibleHours = - lineTicketData.joblines.reduce( - (acc, val) => - acc + (hourstype === val.mod_lbr_ty ? val.mod_lb_hrs : 0), - 0 - ) * (formData.percent / 100 || 0); - - theTeam.employee_team_members.forEach((e) => { - const newTicket = { - employeeid: e.employeeid, - bodyshopid: bodyshop.id, - date: moment().format("YYYY-MM-DD"), - jobid: formData.jobid, - rate: e.labor_rates[hourstype], - actualhrs: 0, - memo: typeof presetMemo === "string" ? presetMemo : "", - flat_rate: true, - ciecacode: hourstype, - cost_center: - bodyshop.md_responsibility_centers.defaults.costs[hourstype], - productivehrs: - Math.round(eligibleHours * 100 * (e.percentage / 100)) / 100, - pay: Dinero({ - amount: Math.round((e.labor_rates[hourstype] || 0) * 100), - }) - .multiply( - Math.round(eligibleHours * 100 * (e.percentage / 100)) / 100 - ) - .toFormat("$0.00"), - }; - data.push(newTicket); + const calculateTickets = async ({ values, handleFinish }) => { + setLoading(true); + try { + const { data, ...response } = await axios.post("/payroll/claimtask", { + jobid: values.jobid, + task: values.task, + calculateOnly: !handleFinish, + }); + if (response.status === 200 && handleFinish) { + //Close the modal + toggleModalVisible(); + } else if (handleFinish === false) { + form.setFieldsValue({ timetickets: data }); + } else { + notification.open({ + type: "error", + message: t("timetickets.errors.creating", { + message: JSON.stringify(data), + }), }); + } + } catch (error) { + notification.open({ + type: "error", + message: t("timetickets.errors.creating", { message: error.message }), }); - - form.setFieldsValue({ - timetickets: data.filter((d) => d.productivehrs > 0), - }); - form.validateFields(); + } finally { + setLoading(false); } }; @@ -197,18 +91,9 @@ export function TimeTickeTaskModalContainer({ layout="vertical" onFinish={handleFinish} initialValues={context} + onValuesChange={handleValueChange} > - + ); diff --git a/client/src/components/time-ticket-task-selector/time-ticket-task-selector.component.jsx b/client/src/components/time-ticket-task-selector/time-ticket-task-selector.component.jsx deleted file mode 100644 index c974d3fe3..000000000 --- a/client/src/components/time-ticket-task-selector/time-ticket-task-selector.component.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; - -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; -import { useTranslation } from "react-i18next"; -import { Button, Dropdown } from "antd"; - -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, -}); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); -export default connect( - mapStateToProps, - mapDispatchToProps -)(TimeTicketTaskCollector); - -export function TimeTicketTaskCollector({ form, bodyshop }) { - const { t } = useTranslation(); - - const items = []; - - return ( - - - - ); -} diff --git a/client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx b/client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx deleted file mode 100644 index b940ca972..000000000 --- a/client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Button, Dropdown } from "antd"; -import React from "react"; - -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; -const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, -}); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); -export default connect( - mapStateToProps, - mapDispatchToProps -)(TimeTicketsTasksPresets); - -export function TimeTicketsTasksPresets({ - bodyshop, - form, - calculateTimeTickets, -}) { - const handleClick = (props) => { - const preset = bodyshop.md_tasks_presets?.presets?.find((p) => { - return p.name === props.key; - }); - - if (preset) { - form.setFieldsValue({ - percent: preset.percent, - hourstype: preset.hourstype, - }); - - calculateTimeTickets(preset.memo); - } - }; - - return ( - ({ - label: p.name, - key: p.name, - })) - : [], - onClick: handleClick, - }} - > - - - ); -} - -// const samplePresets = [ -// { -// name: "Teardown", -// hourstype: ["LAB", "LAM"], -// percent: 10, -// memo: "Teardown Preset Task", -// }, -// { -// name: "Disassembly", -// hourstype: ["LAB", "LAD"], -// percent: 20, -// memo: "Disassy Preset Claim", -// }, -// { name: "Body", hourstype: ["LAB", "LAD"], percent: 20 }, -// { name: "Prep", hourstype: ["LAR"], percent: 20 }, -// ]; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 671ea674a..7bb871cf8 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -269,6 +269,8 @@ "templates": "Delivery Templates" }, "dms": { + "apcontrol": "", + "appostingaccount": "", "cashierid": "Cashier ID", "default_journal": "Default Journal", "disablebillwip": "Disable bill WIP for A/P Posting", @@ -347,6 +349,7 @@ "hourstype": "Hour Types", "memo": "Time Ticket Memo", "name": "Preset Name", + "nextstatus": "Next Status", "percent": "Percent", "use_approvals": "Use Time Ticket Approval Queue" }, @@ -2759,6 +2762,7 @@ "alreadyclockedon": "You are already clocked in to the following job(s):", "ambreak": "AM Break", "amshift": "AM Shift", + "claimtaskpreview": "Claimed Tasks Preview", "clockhours": "Shift Clock Hours Summary", "clockintojob": "Clock In to Job", "deleteconfirm": "Are you sure you want to delete this time ticket? This cannot be undone.", @@ -2768,11 +2772,13 @@ "jobhours": "Job Related Time Tickets Summary", "lunch": "Lunch", "new": "New Time Ticket", + "payrollclaimedtasks": "These time tickets will be automatically entered to the system as a part of claiming this task. These numbers are calculated using the jobs assigned lines. If lines are unassigned, they will be excluded from created tickets.", "pmbreak": "PM Break", "pmshift": "PM Shift", "shift": "Shift", "shiftalreadyclockedon": "Active Shift Time Tickets", "straight_time": "Straight Time", + "task": "Task", "timetickets": "Time Tickets", "zeroactualnegativeprod": "Actual hours must be 0 if entering negative productive hours." }, diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 3700aa408..cfc46e50d 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -269,6 +269,8 @@ "templates": "" }, "dms": { + "apcontrol": "", + "appostingaccount": "", "cashierid": "", "default_journal": "", "disablebillwip": "", @@ -347,6 +349,7 @@ "hourstype": "", "memo": "", "name": "", + "nextstatus": "", "percent": "", "use_approvals": "" }, @@ -2759,6 +2762,7 @@ "alreadyclockedon": "", "ambreak": "", "amshift": "", + "claimtaskpreview": "", "clockhours": "", "clockintojob": "", "deleteconfirm": "", @@ -2768,11 +2772,13 @@ "jobhours": "", "lunch": "", "new": "", + "payrollclaimedtasks": "", "pmbreak": "", "pmshift": "", "shift": "", "shiftalreadyclockedon": "", "straight_time": "", + "task": "", "timetickets": "", "zeroactualnegativeprod": "" }, diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 2764abafb..2c8160707 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -269,6 +269,8 @@ "templates": "" }, "dms": { + "apcontrol": "", + "appostingaccount": "", "cashierid": "", "default_journal": "", "disablebillwip": "", @@ -347,6 +349,7 @@ "hourstype": "", "memo": "", "name": "", + "nextstatus": "", "percent": "", "use_approvals": "" }, @@ -2759,6 +2762,7 @@ "alreadyclockedon": "", "ambreak": "", "amshift": "", + "claimtaskpreview": "", "clockhours": "", "clockintojob": "", "deleteconfirm": "", @@ -2768,11 +2772,13 @@ "jobhours": "", "lunch": "", "new": "", + "payrollclaimedtasks": "", "pmbreak": "", "pmshift": "", "shift": "", "shiftalreadyclockedon": "", "straight_time": "", + "task": "", "timetickets": "", "zeroactualnegativeprod": "" }, diff --git a/server.js b/server.js index 8a938a257..ad7e4eaff 100644 --- a/server.js +++ b/server.js @@ -268,6 +268,7 @@ app.post( payroll.calculatelabor ); app.post("/payroll/payall", fb.validateFirebaseIdToken, payroll.payall); +app.post("/payroll/claimtask", fb.validateFirebaseIdToken, payroll.claimtask); var ioevent = require("./server/ioevent/ioevent"); app.post("/ioevent", ioevent.default); diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index d7f76ff73..99567fa42 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1828,6 +1828,7 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) { bodyshop{ id md_responsibility_centers + md_tasks_presets employee_teams{ id name @@ -1909,6 +1910,8 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) { misc_amt misc_tax assigned_team + convertedtolbr + convertedtolbr_data } } }`; diff --git a/server/payroll/claim-task.js b/server/payroll/claim-task.js new file mode 100644 index 000000000..fd3efb2b7 --- /dev/null +++ b/server/payroll/claim-task.js @@ -0,0 +1,88 @@ +const Dinero = require("dinero.js"); +const queries = require("../graphql-client/queries"); +const GraphQLClient = require("graphql-request").GraphQLClient; +const logger = require("../utils/logger"); +const { + CalculateExpectedHoursForJob, + CalculateTicketsHoursForJob, +} = require("./pay-all"); + +// Dinero.defaultCurrency = "USD"; +// Dinero.globalLocale = "en-CA"; +Dinero.globalRoundingMode = "HALF_EVEN"; + +exports.claimtask = async function (req, res) { + const BearerToken = req.headers.authorization; + const { jobid, task, calculateOnly } = req.body; + logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null); + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { + headers: { + Authorization: BearerToken, + }, + }); + + try { + const { jobs_by_pk: job } = await client + .setHeaders({ Authorization: BearerToken }) + .request(queries.QUERY_JOB_PAYROLL_DATA, { + id: jobid, + }); + + const theTaskPreset = job.bodyshop.md_tasks_presets.presets.find( + (tp) => tp.name === task + ); + + //Get all of the assignments that are filtered. + const { employeeHash } = CalculateExpectedHoursForJob( + job, + theTaskPreset.hourstype + ); + const ticketsToInsert = []; + //Then add them in based on a percentage to each employee. + + 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] * + (theTaskPreset.percent / 100); + + ticketsToInsert.push({ + jobid: job.id, + bodyshopid: job.bodyshop.id, + employeeid: employeeIdKey, + productivehrs: expectedHours, + rate: rateKey, + ciecacode: laborTypeKey, + flat_rate: true, + cost_center: + job.bodyshop.md_responsibility_centers.defaults.costs[ + laborTypeKey + ], + memo: `*Claimed Task* ${theTaskPreset.memo}`, + }); + } + ); + }); + }); + if (!calculateOnly) { + //Insert the time ticekts if we're not just calculating them. + const insertResult = await client.request(queries.INSERT_TIME_TICKETS, { + timetickets: ticketsToInsert.filter( + (ticket) => ticket.productivehrs !== 0 + ), + }); + } + res.json(ticketsToInsert); + } catch (error) { + logger.log("job-payroll-claim-task-error", "ERROR", req.user.email, jobid, { + jobid: jobid, + error, + }); + res.status(503).send(); + } +}; diff --git a/server/payroll/pay-all.js b/server/payroll/pay-all.js index 877ad159c..df5108ec6 100644 --- a/server/payroll/pay-all.js +++ b/server/payroll/pay-all.js @@ -85,9 +85,6 @@ exports.payall = async function (req, res) { } else if (diff.op === "update") { } else { //Has to be a delete - - ////// - if ( typeof diff.oldVal === "object" && Object.keys(diff.oldVal).length > 1 @@ -211,58 +208,75 @@ function diffParser(diff) { return ret; } -function CalculateExpectedHoursForJob(job) { +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.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 + 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)) ); - - 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; - }); } - } - }); + }) + .forEach((jobline) => { + if (jobline.convertedtolbr) { + // Line has been converte to labor. Temporarily re-assign the hours. + jobline.mod_lbr_ty = + 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 }; } diff --git a/server/payroll/payroll.js b/server/payroll/payroll.js index b7a8b7087..58b92e207 100644 --- a/server/payroll/payroll.js +++ b/server/payroll/payroll.js @@ -1,2 +1,3 @@ exports.calculatelabor = require("./calculate-totals").calculatelabor; exports.payall = require("./pay-all").payall; +exports.claimtask = require("./claim-task").claimtask;