diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index eafb597f7..f718f02c8 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -6116,6 +6116,32 @@ + + employee_teams + + + page + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + employees @@ -8864,6 +8890,27 @@ + + tt_enforce_hours_for_tech_console + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + use_fippa false @@ -9404,6 +9451,27 @@ + + employee_teams + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + employees false @@ -14583,6 +14651,84 @@ + + employee_teams + + + actions + + + new + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + fields + + + active + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + name + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + employees @@ -17310,6 +17456,27 @@ + + total + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + totals false @@ -33848,6 +34015,27 @@ errors + + deleting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + noaccess false @@ -34409,6 +34597,27 @@ + + deleteconfirm + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + existing_owners false @@ -34519,6 +34728,27 @@ successes + + delete + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + save false @@ -43968,6 +44198,27 @@ actions + + claimtasks + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + clockin false @@ -45022,6 +45273,27 @@ + + hoursenteredmorethanavailable + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -47334,6 +47606,27 @@ errors + + deleting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + noaccess false @@ -47958,6 +48251,27 @@ labels + + deleteconfirm + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + fromvehicle false @@ -48047,6 +48361,27 @@ successes + + delete + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + save false diff --git a/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx b/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx new file mode 100644 index 000000000..3166a35d0 --- /dev/null +++ b/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx @@ -0,0 +1,33 @@ +import { useQuery } from "@apollo/client"; +import { Select } from "antd"; +import React, { forwardRef } from "react"; +import { QUERY_TEAMS } from "../../graphql/employee_teams.queries"; +import AlertComponent from "../alert/alert.component"; + +//To be used as a form element only. + +const EmployeeTeamSearchSelect = ({ ...props }, ref) => { + const { loading, error, data } = useQuery(QUERY_TEAMS); + + if (error) return ; + return ( + ({ + value: JSON.stringify(e), + label: e.name, + })) + : [] + } + {...props} + /> + ); +}; +export default forwardRef(EmployeeTeamSearchSelect); diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx index ca83a0531..fb5c352bc 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx @@ -47,6 +47,8 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setModalContext({ context: context, modal: "jobCosting" })), setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })), + setTimeTicketTaskContext: (context) => + dispatch(setModalContext({ context: context, modal: "timeTicketTask" })), setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" })), }); @@ -62,6 +64,7 @@ export function JobsDetailHeaderActions({ setJobCostingContext, jobRO, setTimeTicketContext, + setTimeTicketTaskContext, setCardPaymentContext, }) { const { t } = useTranslation(); @@ -247,6 +250,21 @@ export function JobsDetailHeaderActions({ > {t("timetickets.actions.enter")} + { + setTimeTicketTaskContext({ + actions: {}, + context: { jobid: job.id }, + }); + }} + > + {t("timetickets.actions.claimtasks")} + { - console.log( - "🚀 ~ file: labor-allocations-table.utility.js ~ line 9 ~ adjustments", - adjustments - ); const responsibilitycenters = bodyshop.md_responsibility_centers; const jobCodes = joblines.map((item) => item.mod_lbr_ty); //.filter((value, index, self) => self.indexOf(value) === index && !!value); diff --git a/client/src/components/production-list-columns/production-list-columns.data.js b/client/src/components/production-list-columns/production-list-columns.data.js index bd2f15817..7d122200d 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.js +++ b/client/src/components/production-list-columns/production-list-columns.data.js @@ -91,11 +91,13 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { b.v_make_desc + b.v_model_desc ), sortOrder: - state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order, + state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, render: (text, record) => ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - } ${record.v_color || ""} ${record.plate_no || ""}`} + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ + record.v_color || "" + } ${record.plate_no || ""}`} ), }, { diff --git a/client/src/components/production-list-table/production-list-table.component.jsx b/client/src/components/production-list-table/production-list-table.component.jsx index 6595476d5..746d956ab 100644 --- a/client/src/components/production-list-table/production-list-table.component.jsx +++ b/client/src/components/production-list-table/production-list-table.component.jsx @@ -81,7 +81,7 @@ export function ProductionListTable({ state, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((e) => e.key === k.key), - width: k.width, + width: k.width ?? 100, }; })) || [] @@ -267,6 +267,8 @@ export function ProductionListTable({ sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order, title: headerItem(c), + ellipsis: true, + width: c.width ?? 100, onHeaderCell: (column) => ({ width: column.width, onResize: handleResize(index), @@ -276,11 +278,12 @@ export function ProductionListTable({ rowKey="id" loading={loading} dataSource={dataSource} - // scroll={{ x: true }} + scroll={{ x: 1000 }} onChange={handleTableChange} /> ); } + export default connect(mapStateToProps, null)(ProductionListTable); diff --git a/client/src/components/production-list-table/production-list-table.resizeable.component.jsx b/client/src/components/production-list-table/production-list-table.resizeable.component.jsx index 2f1324999..618e9e8cd 100644 --- a/client/src/components/production-list-table/production-list-table.resizeable.component.jsx +++ b/client/src/components/production-list-table/production-list-table.resizeable.component.jsx @@ -3,8 +3,26 @@ import { Resizable } from "react-resizable"; export default function ResizableComponent(props) { const { onResize, width, ...restProps } = props; + + if (!width) { + return ; + } + return ( - + { + e.stopPropagation(); + }} + /> + } + > ); diff --git a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx index 36b4a028b..cfd6d945d 100644 --- a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx +++ b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx @@ -1,4 +1,4 @@ -import { Card, Statistic } from "antd"; +import { Card, Divider, Statistic } from "antd"; import moment from "moment"; import React from "react"; import { connect } from "react-redux"; @@ -41,6 +41,9 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) { label="P" value={paintHrs.toFixed(1)} /> + + + ); } diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx index 6f22b3b8a..112d441f6 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx @@ -1,5 +1,5 @@ import { CalendarOutlined } from "@ant-design/icons"; -import { Card, Col, Row, Statistic } from "antd"; +import { Card, Col, Divider, Row, Statistic } from "antd"; import _ from "lodash"; import moment from "moment"; import React, { useMemo } from "react"; @@ -177,6 +177,9 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { + + + @@ -184,14 +187,53 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { value={(values.todayPaint + values.todayBody).toFixed(1)} /> - + + + - - + + + + + + { @@ -94,6 +95,7 @@ export default function ScoreboardTimeTickets() { totalLastMonth: 0, totalOverPeriod: 0, actualTotalOverPeriod: 0, + totalEffieciencyOverPeriod: 0, }; } @@ -221,6 +223,28 @@ export default function ScoreboardTimeTickets() { ret2.push(r); }); + + // Add total efficiency of employees + const totalActualAndProductive = Object.keys(ret.employees) + .map((key) => { + return { employee_number: key, ...ret.employees[key] }; + }) + .reduce( + (acc, e) => { + return { + totalOverPeriod: acc.totalOverPeriod + e.totalOverPeriod, + actualTotalOverPeriod: + acc.actualTotalOverPeriod + e.actualTotalOverPeriod, + }; + }, + { totalOverPeriod: 0, actualTotalOverPeriod: 0 } + ); + + ret.totalEffieciencyOverPeriod = + (totalActualAndProductive.totalOverPeriod / + totalActualAndProductive.actualTotalOverPeriod) * + 100; + roundObject(ret); roundObject(totals); roundObject(ret2); diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx index 0b1b9b6bf..e31cec3af 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx @@ -62,7 +62,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { key: "efficiencyoverperiod", render: (text, record) => `${( - (record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) * + (record.totalOverPeriod / (record.actualTotalOverPeriod || 0.1)) * 100 ).toFixed(1)} %`, }, @@ -113,6 +113,12 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { value={data.totalOverPeriod} /> + + + {t("scoreboard.labels.calendarperiod")} @@ -121,7 +127,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { ({ export function ShopEmployeesFormComponent({ bodyshop }) { const { t } = useTranslation(); - const [form] = useForm(); + const [form] = Form.useForm(); const history = useHistory(); const search = querystring.parse(useLocation().search); const [deleteVacation] = useMutation(DELETE_VACATION); diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 3e9b1ed2c..32ac5aa5d 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -589,6 +589,13 @@ export default function ShopInfoGeneral({ form }) { > + + + + + + ShopEmployeeTeamMember + ) +} 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 new file mode 100644 index 000000000..904acda7d --- /dev/null +++ b/client/src/components/shop-teams/shop-employee-teams.form.component.jsx @@ -0,0 +1,424 @@ +import { DeleteFilled } from "@ant-design/icons"; +import { useMutation, useQuery } from "@apollo/client"; +import { + Button, + Space, + Card, + Form, + Input, + InputNumber, + Switch, + notification, +} from "antd"; + +import querystring from "query-string"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useHistory, useLocation } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import AlertComponent from "../alert/alert.component"; +import CurrencyInput from "../form-items-formatted/currency-form-item.component"; +import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; + +import { + INSERT_EMPLOYEE_TEAM, + QUERY_EMPLOYEE_TEAM_BY_ID, + UPDATE_EMPLOYEE_TEAM, +} from "../../graphql/employee_teams.queries"; +import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export function ShopEmployeeTeamsFormComponent({ bodyshop }) { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const history = useHistory(); + const search = querystring.parse(useLocation().search); + + const { error, data } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, { + variables: { id: search.employeeTeamId }, + skip: !search.employeeTeamId || search.employeeTeamId === "new", + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + useEffect(() => { + if (data && data.employee_teams_by_pk) + form.setFieldsValue(data.employee_teams_by_pk); + else { + form.resetFields(); + } + }, [form, data, search.employeeTeamId]); + + const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM); + const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM); + + const handleFinish = async ({ employee_team_members, ...values }) => { + if (search.employeeTeamId && search.employeeTeamId !== "new") { + //Update a record. + logImEXEvent("shop_employee_update"); + + const result = await updateEmployeeTeam({ + variables: { + employeeTeamId: search.employeeTeamId, + employeeTeam: values, + teamMemberUpdates: employee_team_members + .filter((e) => e.id) + .map((e) => { + delete e.__typename; + return { where: { id: { _eq: e.id } }, _set: e }; + }), + teamMemberInserts: employee_team_members + .filter((e) => e.id === null || e.id === undefined) + .map((e) => ({ ...e, teamid: search.employeeTeamId })), + teamMemberDeletes: + data.employee_teams_by_pk.employee_team_members.filter( + (e) => !employee_team_members.find((etm) => etm.id === e.id) + ), + }, + }); + if (!result.errors) { + notification["success"]({ + message: t("employees.successes.save"), + }); + } else { + notification["error"]({ + message: t("employees.errors.save", { + message: JSON.stringify(error), + }), + }); + } + } else { + //New record, insert it. + logImEXEvent("shop_employee_insert"); + + insertEmployeeTeam({ + variables: { + employeeTeam: { + ...values, + employee_team_members: { data: employee_team_members }, + bodyshopid: bodyshop.id, + }, + }, + refetchQueries: ["QUERY_TEAMS"], + }).then((r) => { + search.employeeTeamId = r.data.insert_employee_teams_one.id; + history.push({ search: querystring.stringify(search) }); + notification["success"]({ + message: t("employees.successes.save"), + }); + }); + } + }; + + if (!search.employeeTeamId) return null; + if (error) return ; + + return ( + form.submit()}> + {t("general.actions.save")} + + } + > + + + + + + + + + + + {(fields, { add, remove, move }) => { + return ( + + {fields.map((field, index) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + remove(field.name); + }} + /> + + + + + ))} + + { + add(); + }} + style={{ width: "100%" }} + > + {t("employee_teams.actions.newmember")} + + + + ); + }} + + + + ); +} +export default connect( + mapStateToProps, + mapDispatchToProps +)(ShopEmployeeTeamsFormComponent); diff --git a/client/src/components/shop-teams/shop-employee-teams.list.jsx b/client/src/components/shop-teams/shop-employee-teams.list.jsx new file mode 100644 index 000000000..402f8c9bf --- /dev/null +++ b/client/src/components/shop-teams/shop-employee-teams.list.jsx @@ -0,0 +1,71 @@ +import { Button, Table } from "antd"; +import queryString from "query-string"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { useHistory, useLocation } from "react-router-dom"; + +export default function ShopEmployeeTeamsListComponent({ + loading, + employee_teams, +}) { + const { t } = useTranslation(); + const history = useHistory(); + const search = queryString.parse(useLocation().search); + + const handleOnRowClick = (record) => { + if (record) { + search.employeeTeamId = record.id; + history.push({ search: queryString.stringify(search) }); + } else { + delete search.employeeTeamId; + history.push({ search: queryString.stringify(search) }); + } + }; + const columns = [ + { + title: t("employee_teams.fields.name"), + dataIndex: "name", + key: "name", + }, + ]; + + return ( + + { + return ( + { + search.employeeTeamId = "new"; + history.push({ search: queryString.stringify(search) }); + }} + > + {t("employee_teams.actions.new")} + + ); + }} + loading={loading} + pagination={{ position: "top" }} + columns={columns} + rowKey="id" + dataSource={employee_teams} + rowSelection={{ + onSelect: (props) => { + search.employeeTeamId = props.id; + history.push({ search: queryString.stringify(search) }); + }, + type: "radio", + selectedRowKeys: [search.employeeTeamId], + }} + onRow={(record, rowIndex) => { + return { + onClick: (event) => { + handleOnRowClick(record); + }, + }; + }} + /> + + ); +} diff --git a/client/src/components/shop-teams/shop-teams.container.jsx b/client/src/components/shop-teams/shop-teams.container.jsx new file mode 100644 index 000000000..702f15e37 --- /dev/null +++ b/client/src/components/shop-teams/shop-teams.container.jsx @@ -0,0 +1,43 @@ +import { useQuery } from "@apollo/client"; +import React from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { QUERY_TEAMS } from "../../graphql/employee_teams.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import AlertComponent from "../alert/alert.component"; +import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; +import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list"; +import ShopEmployeeTeamsFormComponent from "./shop-employee-teams.form.component"; +import { Col, Row } from "antd"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +function ShopTeamsContainer({ bodyshop }) { + const { loading, error, data } = useQuery(QUERY_TEAMS, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + if (error) return ; + + return ( + + + + + + + + + + + + + ); +} +export default connect(mapStateToProps, null)(ShopTeamsContainer); diff --git a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx index 85589b13d..5c0830d5e 100644 --- a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx +++ b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx @@ -1,4 +1,4 @@ -import { useMutation } from "@apollo/client"; +import { useMutation, useQuery } from "@apollo/client"; import { Button, Card, @@ -21,6 +21,8 @@ import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component"; import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component"; +import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries"; +import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -39,7 +41,17 @@ export function TechClockOffButton({ const [loading, setLoading] = useState(false); const [updateTimeticket] = useMutation(UPDATE_TIME_TICKET); const [form] = Form.useForm(); - + const { queryLoading, data: lineTicketData } = useQuery( + GET_LINE_TICKET_BY_PK, + { + variables: { + id: jobId, + }, + skip: !jobId, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + } + ); const { t } = useTranslation(); const emps = bodyshop.employees.filter( (e) => e.id === (technician && technician.id) @@ -129,6 +141,54 @@ export function TechClockOffButton({ required: true, //message: t("general.validation.required"), }, + ({ getFieldValue }) => ({ + validator(rule, value) { + console.log( + bodyshop.tt_enforce_hours_for_tech_console + ); + if (!bodyshop.tt_enforce_hours_for_tech_console) { + return Promise.resolve(); + } + if ( + !value || + getFieldValue("cost_center") === null || + !lineTicketData + ) + return Promise.resolve(); + + //Check the cost center, + const totals = CalculateAllocationsTotals( + bodyshop, + lineTicketData.joblines, + lineTicketData.timetickets, + lineTicketData.jobs_by_pk.lbr_adjustments + ); + + const fieldTypeToCheck = + bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber + ? "mod_lbr_ty" + : "cost_center"; + + const costCenterDiff = + Math.round( + totals.find( + (total) => + total[fieldTypeToCheck] === + getFieldValue("cost_center") + )?.difference * 10 + ) / 10; + + if (value > costCenterDiff) + return Promise.reject( + t( + "timetickets.validation.hoursenteredmorethanavailable" + ) + ); + else { + return Promise.resolve(); + } + }, + }), ]} > @@ -178,7 +238,11 @@ export function TechClockOffButton({ {!isShiftTicket && ( - + )} diff --git a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx new file mode 100644 index 000000000..a36f1dd49 --- /dev/null +++ b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx @@ -0,0 +1,142 @@ +import { DownOutlined } from "@ant-design/icons"; +import { + Button, + Checkbox, + Col, + Form, + InputNumber, + Popover, + Radio, + Row, + Space, + Spin, +} from "antd"; +import React, { useState } from "react"; +import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries"; +import { useQuery } from "@apollo/client"; + +export default function TimeTicketCalculatorComponent({ + setProductiveHours, + + jobid, +}) { + const { loading, data: lineTicketData } = useQuery(GET_JOB_INFO_DRAW_CALCULATIONS, { + variables: { id: jobid }, + skip: !jobid, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + const [visible, setVisible] = useState(false); + const handleOpenChange = (flag) => setVisible(flag); + const handleFinish = ({ type, hourstype, percent }) => { + //setProductiveHours(values); + //setVisible(false); + const eligibleHours = Array.isArray(hourstype) + ? lineTicketData.joblines.reduce( + (acc, val) => + acc + (hourstype.includes(val.mod_lbr_ty) ? val.mod_lb_hrs : 0), + 0 + ) + : lineTicketData.joblines.reduce( + (acc, val) => + acc + (hourstype === val.mod_lbr_ty ? val.mod_lb_hrs : 0), + 0 + ); + if (type === "draw") { + setProductiveHours(eligibleHours * (percent / 100)); + } else if (type === "cut") { + setProductiveHours(eligibleHours * (percent / 100)); + console.log( + "Cut selected, rate set to: ", + lineTicketData.jobs_by_pk[`rate_${hourstype.toLowerCase()}`] + ); + } + }; + + const popContent = ( + + + + + Draw + Cut of Sale + + + + + {({ getFieldValue }) => ( + + {getFieldValue("type") === "draw" ? ( + + + + + Body + + + + + Refinish + + + + + Mechanical + + + + + Frame + + + + + Glass + + + + + ) : ( + + Body + + Refinish + + Mechanical + + Frame + + Glass + + )} + + )} + + + + + + Calculate + + + ); + + return ( + + e.preventDefault()}> + + Draw Calculator + + + + + ); +} diff --git a/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx new file mode 100644 index 000000000..3c3375036 --- /dev/null +++ b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx @@ -0,0 +1,277 @@ +import { useQuery } from "@apollo/client"; +import { + Button, + Form, + InputNumber, + Modal, + Radio, + Select, + Space, + Table, + Typography, +} from "antd"; +import Dinero from "dinero.js"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import FormDatePicker from "../form-date-picker/form-date-picker.component"; +import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(TimeTicketListTeamPay); + +export function TimeTicketListTeamPay({ bodyshop, context, actions }) { + //const { refetch } = actions; + const { jobId } = context; + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const { t } = useTranslation(); + const { + //loading, + data: lineTicketData, + } = useQuery(GET_JOB_INFO_DRAW_CALCULATIONS, { + variables: { id: jobId }, + skip: !jobId, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + const handleOk = () => { + setVisible(false); + }; + + return ( + <> + setVisible(false)} + > + + + + {() => ( + + + + )} + + + + + + + + + ({ + value: team.name, + label: team.name, + }))} + /> + + + + + Body + Refinish + Mechanical + Frame + Glass + + + + + + + + + + {({ getFieldsValue }) => { + const formData = getFieldsValue(); + + let data = []; + let eligibleHours = 0; + const theTeam = Teams.find((team) => team.name === formData.team); + if (theTeam) { + eligibleHours = + lineTicketData.joblines.reduce( + (acc, val) => + acc + + (formData.hourstype === val.mod_lbr_ty + ? val.mod_lb_hrs + : 0), + 0 + ) * (formData.percent / 100 || 0); + + data = theTeam.employees.map((e) => { + return { + employeeid: e.employeeid, + percentage: e.percentage, + rate: e.rates[formData.hourstype], + cost_center: + bodyshop.md_responsibility_centers.defaults.costs[ + formData.hourstype + ], + productivehrs: + Math.round(eligibleHours * 100 * (e.percentage / 100)) / + 100, + pay: Dinero({ + amount: Math.round( + (e.rates[formData.hourstype] || 0) * 100 + ), + }) + .multiply( + Math.round(eligibleHours * 100 * (e.percentage / 100)) / + 100 + ) + .toFormat("$0.00"), + }; + }); + } + + return ( + ( + + + Tickets to be Created + + {`(${eligibleHours} hours to split)`} + + )} + columns={[ + { + title: t("timetickets.fields.employee"), + dataIndex: "employee", + key: "employee", + render: (text, record) => { + 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", + }, + ]} + /> + ); + }} + + + + setVisible(true)}>Assign Team Pay + > + ); +} + +const Teams = [ + { + name: "Team A", + employees: [ + { + employeeid: "9f1bdc23-8dc2-4b6a-8ca1-5f83a6fdcc22", + percentage: 50, + rates: { + LAB: 10, + LAR: 15, + }, + }, + { + employeeid: "201db66c-96c7-41ec-bed4-76842ba93087", + percentage: 50, + rates: { + LAB: 20, + LAR: 25, + }, + }, + ], + }, + { + name: "Team B", + employees: [ + { + employeeid: "9f1bdc23-8dc2-4b6a-8ca1-5f83a6fdcc22", + percentage: 75, + rates: { + LAB: 100, + LAR: 150, + }, + }, + { + employeeid: "201db66c-96c7-41ec-bed4-76842ba93087", + percentage: 25, + rates: { + LAB: 200, + LAR: 250, + }, + }, + ], + }, +]; diff --git a/client/src/components/time-ticket-list/time-ticket-list.component.jsx b/client/src/components/time-ticket-list/time-ticket-list.component.jsx index 7cebf2672..ba4aeda2f 100644 --- a/client/src/components/time-ticket-list/time-ticket-list.component.jsx +++ b/client/src/components/time-ticket-list/time-ticket-list.component.jsx @@ -1,17 +1,19 @@ import { EditFilled } from "@ant-design/icons"; -import { Card, Space, Table } from "antd"; +import { Button, Card, Space, Table } from "antd"; +import Dinero from "dinero.js"; import moment from "moment"; import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { setModalContext } from "../../redux/modals/modals.actions"; import { selectAuthLevel, selectBodyshop, } from "../../redux/user/user.selectors"; -import { onlyUnique } from "../../utils/arrayHelper"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; +import { onlyUnique } from "../../utils/arrayHelper"; import { alphaSort, dateSort } from "../../utils/sorters"; import RbacWrapper, { HasRbacAccess, @@ -22,12 +24,14 @@ const mapStateToProps = createStructuredSelector({ authLevel: selectAuthLevel, }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + setTimeTicketTaskContext: (context) => + dispatch(setModalContext({ context: context, modal: "timeTicketTask" })), }); export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList); export function TimeTicketList({ bodyshop, + setTimeTicketTaskContext, authLevel, disabled, loading, @@ -193,6 +197,15 @@ export function TimeTicketList({ } }, }, + { + title: "Pay", + dataIndex: "pay", + key: "pay", + render: (text, record) => + Dinero({ amount: Math.round(record.rate * 100) }) + .multiply(record.flat_rate ? record.productivehrs : record.actualhrs) + .toFormat("$0.00"), + }, { title: t("general.labels.actions"), @@ -250,6 +263,23 @@ export function TimeTicketList({ title={t("timetickets.labels.timetickets")} extra={ + { + // + } + { + setTimeTicketTaskContext(); + setTimeTicketTaskContext({ + actions: {}, + context: { jobid: jobId }, + }); + }} + > + {t("timetickets.actions.claimtasks")} + {jobId && (techConsole ? null : ( { return ( {() => ( - - - + <> + ({ + validator(rule, value) { + if (!bodyshop.tt_enforce_hours_for_tech_console) { + return Promise.resolve(); + } + if ( + !value || + getFieldValue("cost_center") === null || + !lineTicketData + ) + return Promise.resolve(); + + //Check the cost center, + const totals = CalculateAllocationsTotals( + bodyshop, + lineTicketData.joblines, + lineTicketData.timetickets, + lineTicketData.jobs_by_pk.lbr_adjustments + ); + + const fieldTypeToCheck = + bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber + ? "mod_lbr_ty" + : "cost_center"; + + const costCenterDiff = + Math.round( + totals.find( + (total) => + total[fieldTypeToCheck] === + getFieldValue("cost_center") + )?.difference * 10 + ) / 10; + + if (value > costCenterDiff) + return Promise.reject( + t( + "timetickets.validation.hoursenteredmorethanavailable" + ) + ); + else { + return Promise.resolve(); + } + }, + }), + { + required: + form.getFieldValue("cost_center") !== + "timetickets.labels.shift", + //message: t("general.validation.required"), + }, + ]} + > + + + + form.setFieldsValue({ productivehrs }) + } + /> + > )} - {() => ( - - )} + {() => { + const jobid = form.getFieldValue("jobid"); + if ( + (!called && jobid) || + (jobid && lineTicketData?.jobs_by_pk?.id !== jobid && !loading) + ) { + loadLineTicketData({ variables: { id: jobid } }); + } + return ( + + ); + }} ); } -export function LaborAllocationContainer({ jobid }) { - const { loading, data: lineTicketData } = useQuery(GET_LINE_TICKET_BY_PK, { - variables: { id: jobid }, - skip: !jobid, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); +export function LaborAllocationContainer({ + jobid, + loading, + lineTicketData, + hideTimeTickets = false, +}) { if (loading) return ; if (!lineTicketData) return null; return ( @@ -318,12 +387,13 @@ export function LaborAllocationContainer({ jobid }) { timetickets={lineTicketData.timetickets} adjustments={lineTicketData.jobs_by_pk.lbr_adjustments} /> - - + {!hideTimeTickets && ( + + )} ); } diff --git a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.component.jsx b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.component.jsx new file mode 100644 index 000000000..0884b656a --- /dev/null +++ b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.component.jsx @@ -0,0 +1,376 @@ +import { + Alert, + Button, + Checkbox, + Col, + Form, + Input, + InputNumber, + Row, + Space, + Table, +} from "antd"; +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 EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component"; +import EmployeeTeamSearchSelectComponent from "../employee-team-search-select/employee-team-search-select.component"; +import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; +import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; +import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component"; +import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component"; +import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility"; +import _ from "lodash"; + +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(TimeTicketTaskModalComponent); + +export function TimeTicketTaskModalComponent({ + bodyshop, + form, + employeeAutoCompleteOptions, + lineTicketCalled, + calculateTimeTickets, + lineTicketLoading, + lineTicketData, + queryJobInfo, +}) { + const { t } = useTranslation(); + + return ( + + + + + + + + + + { + const emps = + employeeAutoCompleteOptions && + employeeAutoCompleteOptions.filter((e) => e.id === value)[0]; + form.setFieldsValue({ flat_rate: emps && emps.flat_rate }); + }} + /> + + + + + + + + + + Body + + + Refinish + + + Mechanical + + + Frame + + + Glass + + + + + + + + + + Calculate + + + + {() => { + 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) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + + + ); + }} + + + + + {() => { + const jobid = form.getFieldValue("jobid"); + + if ( + (!lineTicketCalled && jobid) || + (jobid && + lineTicketData?.jobs_by_pk?.id !== jobid && + !lineTicketLoading) + ) { + queryJobInfo({ variables: { id: jobid } }).then(() => + calculateTimeTickets() + ); + } + return ( + + ); + }} + + + ); +} diff --git a/client/src/components/time-ticket-task-modal/time-ticket-task-modal.container.jsx b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.container.jsx new file mode 100644 index 000000000..44e17185d --- /dev/null +++ b/client/src/components/time-ticket-task-modal/time-ticket-task-modal.container.jsx @@ -0,0 +1,181 @@ +import React, { useEffect } from "react"; + +import { useLazyQuery, useMutation, useQuery } from "@apollo/client"; +import { Form, Modal, notification } from "antd"; +import Dinero from "dinero.js"; +import _ from "lodash"; +import moment from "moment"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries"; +import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries"; +import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectTimeTicketTasks } from "../../redux/modals/modals.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import TimeTicketTaskModalComponent from "./time-ticket-task-modal.component"; + +const mapStateToProps = createStructuredSelector({ + timeTicketTasksModal: selectTimeTicketTasks, + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + toggleModalVisible: () => dispatch(toggleModalVisible("timeTicketTask")), +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(TimeTickeTaskModalContainer); + +export function TimeTickeTaskModalContainer({ + bodyshop, + timeTicketTasksModal, + toggleModalVisible, +}) { + const [form] = Form.useForm(); + const { context, visible } = timeTicketTasksModal; + const { data: EmployeeAutoCompleteData } = useQuery(QUERY_ACTIVE_EMPLOYEES, { + skip: !visible, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + const { t } = useTranslation(); + const [insertTimeTickets] = useMutation(INSERT_NEW_TIME_TICKET); + const [queryJobInfo, { called, loading, data: lineTicketData }] = + useLazyQuery(GET_JOB_INFO_DRAW_CALCULATIONS, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + async function handleFinish(values) { + console.log( + "🚀 ~ file: time-ticket-task-modal.container.jsx:52 ~ handleFinish ~ values:", + values + ); + try { + const result = await insertTimeTickets({ + variables: { + timeTicketInput: values.timetickets.map((ticket) => + _.omit(ticket, "pay") + ), + }, + }); + 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) { + } finally { + } + } + + useEffect(() => { + if (context.jobid) { + console.log("UE Fired."); + queryJobInfo({ variables: { id: context.jobid } }); + } + }, [context.jobid, queryJobInfo]); + + 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) { + data = []; + + 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: presetMemo, + flat_rate: true, + 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); + }); + }); + + form.setFieldsValue({ + timetickets: data.filter((d) => d.productivehrs > 0), + }); + form.validateFields(); + } + }; + + return ( + toggleModalVisible()} + width="80%" + onOk={() => form.submit()} + > + + + + + ); +} 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 new file mode 100644 index 000000000..c974d3fe3 --- /dev/null +++ b/client/src/components/time-ticket-task-selector/time-ticket-task-selector.component.jsx @@ -0,0 +1,30 @@ +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 ( + + {t("timetickets.actions.tasks")} + + ); +} 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 new file mode 100644 index 000000000..f88ba4d1a --- /dev/null +++ b/client/src/components/time-ticket-tasks-presets/time-ticket-tasks-presets.component.jsx @@ -0,0 +1,50 @@ +import { Button, Dropdown } from "antd"; +import React from "react"; + +export default function TimeTicketsTasksPresets({ + form, + calculateTimeTickets, +}) { + const handleClick = (props) => { + const preset = samplePresets.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, + }} + > + Presets + + ); +} + +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/components/time-tickets-summary-employees/time-tickets-summary-employees.component.jsx b/client/src/components/time-tickets-summary-employees/time-tickets-summary-employees.component.jsx index b51d45680..1a1d05dc9 100644 --- a/client/src/components/time-tickets-summary-employees/time-tickets-summary-employees.component.jsx +++ b/client/src/components/time-tickets-summary-employees/time-tickets-summary-employees.component.jsx @@ -10,7 +10,7 @@ import { onlyUnique } from "../../utils/arrayHelper"; import { alphaSort } from "../../utils/sorters"; import { TemplateList } from "../../utils/TemplateConstants"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; - +import Dinero from "dinero.js"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -129,6 +129,16 @@ const JobRelatedTicketsTable = ({ return acc; }, 0); + const pay = item.tickets + .filter((ticket) => ticket.cost_center === costCenter) + .reduce((acc, val) => { + return acc.add( + Dinero({ amount: Math.round(val.rate * 100) }).multiply( + val.flat_rate ? val.productivehrs : val.actualhrs + ) + ); + }, Dinero()); + return { id: `${item.jobKey}${costCenter}`, costCenter, @@ -136,6 +146,7 @@ const JobRelatedTicketsTable = ({ actHrs: actHrs.toFixed(1), prodHrs: prodHrs.toFixed(1), clockHrs, + pay, }; }); }) @@ -195,6 +206,15 @@ const JobRelatedTicketsTable = ({ state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order, render: (text, record) => record.clockHrs.toFixed(2), }, + { + title: "Pay", + dataIndex: "Pay", + key: "Pay", + sorter: (a, b) => a.clockHrs - b.clockHrs, + sortOrder: + state.sortedInfo.columnKey === "clockHrs" && state.sortedInfo.order, + render: (text, record) => record.pay.toFormat("$0.00"), + }, { title: t("general.labels.actions"), dataIndex: "actions", diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 956bf9dd4..c934c9514 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -116,6 +116,7 @@ export const QUERY_BODYSHOP = gql` md_lost_sale_reasons md_parts_scan enforce_conversion_category + tt_enforce_hours_for_tech_console employees { user_email id @@ -230,6 +231,7 @@ export const UPDATE_SHOP = gql` md_lost_sale_reasons md_parts_scan enforce_conversion_category + tt_enforce_hours_for_tech_console employees { id first_name diff --git a/client/src/graphql/employee_teams.queries.js b/client/src/graphql/employee_teams.queries.js new file mode 100644 index 000000000..5d80abad2 --- /dev/null +++ b/client/src/graphql/employee_teams.queries.js @@ -0,0 +1,91 @@ +import { gql } from "@apollo/client"; + +export const QUERY_TEAMS = gql` + query QUERY_TEAMS { + employee_teams(order_by: { name: asc }) { + id + name + employee_team_members { + id + employeeid + labor_rates + percentage + } + } + } +`; + +export const UPDATE_EMPLOYEE_TEAM = gql` + mutation UPDATE_EMPLOYEE_TEAM( + $employeeTeamId: uuid! + $employeeTeam: employee_teams_set_input + $teamMemberDeletes: [uuid!] + $teamMemberUpdates: [employee_team_members_updates!]! + $teamMemberInserts: [employee_team_members_insert_input!]! + ) { + update_employee_team_members_many(updates: $teamMemberUpdates) { + returning { + id + labor_rates + employeeid + percentage + } + } + delete_employee_team_members(where: { id: { _in: $teamMemberDeletes } }) { + affected_rows + } + insert_employee_team_members(objects: $teamMemberInserts) { + returning { + id + labor_rates + employeeid + percentage + } + } + update_employee_teams_by_pk( + pk_columns: { id: $employeeTeamId } + _set: $employeeTeam + ) { + active + name + id + employee_team_members { + percentage + labor_rates + id + } + } + } +`; + +export const INSERT_EMPLOYEE_TEAM = gql` + mutation INSERT_EMPLOYEE_TEAM($employeeTeam: employee_teams_insert_input!) { + insert_employee_teams_one(object: $employeeTeam) { + active + name + id + employee_team_members { + employeeid + percentage + labor_rates + id + } + } + } +`; + +export const QUERY_EMPLOYEE_TEAM_BY_ID = gql` + query QUERY_EMPLOYEE_TEAM_BY_ID($id: uuid!) { + employee_teams_by_pk(id: $id) { + id + name + active + employee_team_members { + employeeid + percentage + labor_rates + id + } + } + } +`; diff --git a/client/src/graphql/jobs-lines.queries.js b/client/src/graphql/jobs-lines.queries.js index 1f51cfbb1..9ad82c7b0 100644 --- a/client/src/graphql/jobs-lines.queries.js +++ b/client/src/graphql/jobs-lines.queries.js @@ -76,6 +76,65 @@ export const GET_LINE_TICKET_BY_PK = gql` } `; +export const GET_JOB_INFO_DRAW_CALCULATIONS = gql` + query GET_JOB_INFO_DRAW_CALCULATIONS($id: uuid!) { + jobs_by_pk(id: $id) { + id + lbr_adjustments + converted + rate_lab + rate_lad + rate_laa + rate_la1 + rate_la2 + rate_la3 + rate_la4 + rate_lau + rate_lar + rate_lag + rate_laf + rate_lam + } + timetickets(where: { jobid: { _eq: $id } }) { + actualhrs + ciecacode + cost_center + date + id + jobid + employeeid + memo + flat_rate + clockon + clockoff + rate + employee { + id + first_name + last_name + employee_number + } + productivehrs + } + joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) { + id + line_desc + part_type + oem_partno + db_price + act_price + part_qty + mod_lbr_ty + db_hrs + mod_lb_hrs + lbr_op + lbr_amt + op_code_desc + convertedtolbr + convertedtolbr_data + } + } +`; export const UPDATE_JOB_LINE_STATUS = gql` mutation UPDATE_JOB_LINE_STATUS( $ids: [uuid!]! diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 2f5dc056a..411293fc0 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -284,6 +284,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` clm_no v_make_desc v_color + vehicleid plate_no actual_in scheduled_completion diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index edf77292d..351f447bb 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -98,6 +98,11 @@ const BillEnterModalContainer = lazy(() => const TimeTicketModalContainer = lazy(() => import("../../components/time-ticket-modal/time-ticket-modal.container") ); +const TimeTicketModalTask = lazy(() => + import( + "../../components/time-ticket-task-modal/time-ticket-task-modal.container" + ) +); const PaymentModalContainer = lazy(() => import("../../components/payment-modal/payment-modal.container") ); @@ -207,6 +212,7 @@ export function Manage({ match, conflict, bodyshop }) { + diff --git a/client/src/pages/shop/shop.page.component.jsx b/client/src/pages/shop/shop.page.component.jsx index 7c9e4d5b2..a6e2f71ef 100644 --- a/client/src/pages/shop/shop.page.component.jsx +++ b/client/src/pages/shop/shop.page.component.jsx @@ -14,6 +14,8 @@ import { } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component"; +import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container"; + const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -44,6 +46,9 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) { + + + diff --git a/client/src/redux/modals/modals.reducer.js b/client/src/redux/modals/modals.reducer.js index 3209ae53d..ad08c88e3 100644 --- a/client/src/redux/modals/modals.reducer.js +++ b/client/src/redux/modals/modals.reducer.js @@ -16,6 +16,7 @@ const INITIAL_STATE = { schedule: { ...baseModal }, partsOrder: { ...baseModal }, timeTicket: { ...baseModal }, + timeTicketTask: { ...baseModal }, printCenter: { ...baseModal }, reconciliation: { ...baseModal }, payment: { ...baseModal }, diff --git a/client/src/redux/modals/modals.selectors.js b/client/src/redux/modals/modals.selectors.js index 8a4cfee77..f8b8aa6c2 100644 --- a/client/src/redux/modals/modals.selectors.js +++ b/client/src/redux/modals/modals.selectors.js @@ -36,6 +36,10 @@ export const selectTimeTicket = createSelector( [selectModals], (modals) => modals.timeTicket ); +export const selectTimeTicketTasks = createSelector( + [selectModals], + (modals) => modals.timeTicketTask +); export const selectPrintCenter = createSelector( [selectModals], diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index bc130866b..f6d4623a6 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -374,6 +374,9 @@ "export": "CSI -> Export", "page": "CSI -> Page" }, + "employee_teams": { + "page": "Employee Teams -> List" + }, "employees": { "page": "Employees -> List" }, @@ -541,6 +544,7 @@ "target_touchtime": "Target Touch Time", "timezone": "Timezone", "tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs", + "tt_enforce_hours_for_tech_console": "Restrict Claimable hours from Tech Console", "use_fippa": "Use FIPPA for Names on Generated Documents?", "uselocalmediaserver": "Use Local Media Server?", "website": "Website", @@ -572,6 +576,7 @@ "title": "DMS" }, "emaillater": "Email Later", + "employee_teams": "Employee Teams", "employees": "Employees", "estimators": "Estimators", "filehandlers": "File Handlers", @@ -905,6 +910,15 @@ "sent": "Email sent successfully." } }, + "employee_teams": { + "actions": { + "new": "New Team" + }, + "fields": { + "active": "Active", + "name": "Team Name" + } + }, "employees": { "actions": { "addvacation": "Add Vacation", @@ -1063,6 +1077,7 @@ "sunday": "Sunday", "text": "Text", "thursday": "Thursday", + "total": "Total", "totals": "Totals", "tuesday": "Tuesday", "unknown": "Unknown", @@ -2605,6 +2620,7 @@ }, "timetickets": { "actions": { + "claimtasks": "Claim Tasks", "clockin": "Clock In", "clockout": "Clock Out", "enter": "Enter New Time Ticket", @@ -2663,7 +2679,8 @@ }, "validation": { "clockoffmustbeafterclockon": "Clock off time must be the same or after clock in time.", - "clockoffwithoutclockon": "Clock off time cannot be set without a clock in time." + "clockoffwithoutclockon": "Clock off time cannot be set without a clock in time.", + "hoursenteredmorethanavailable": "The number of hours entered is more than what is available for this cost center." } }, "titles": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 6bc95309b..80735a76a 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -374,6 +374,9 @@ "export": "", "page": "" }, + "employee_teams": { + "page": "" + }, "employees": { "page": "" }, @@ -541,6 +544,7 @@ "target_touchtime": "", "timezone": "", "tt_allow_post_to_invoiced": "", + "tt_enforce_hours_for_tech_console": "", "use_fippa": "", "uselocalmediaserver": "", "website": "", @@ -572,6 +576,7 @@ "title": "" }, "emaillater": "", + "employee_teams": "", "employees": "", "estimators": "", "filehandlers": "", @@ -905,6 +910,15 @@ "sent": "Correo electrónico enviado con éxito." } }, + "employee_teams": { + "actions": { + "new": "" + }, + "fields": { + "active": "", + "name": "" + } + }, "employees": { "actions": { "addvacation": "", @@ -1063,6 +1077,7 @@ "sunday": "", "text": "", "thursday": "", + "total": "", "totals": "", "tuesday": "", "unknown": "Desconocido", @@ -2605,6 +2620,7 @@ }, "timetickets": { "actions": { + "claimtasks": "", "clockin": "", "clockout": "", "enter": "", @@ -2663,7 +2679,8 @@ }, "validation": { "clockoffmustbeafterclockon": "", - "clockoffwithoutclockon": "" + "clockoffwithoutclockon": "", + "hoursenteredmorethanavailable": "" } }, "titles": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 5272e9a65..4873881fc 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -374,6 +374,9 @@ "export": "", "page": "" }, + "employee_teams": { + "page": "" + }, "employees": { "page": "" }, @@ -541,6 +544,7 @@ "target_touchtime": "", "timezone": "", "tt_allow_post_to_invoiced": "", + "tt_enforce_hours_for_tech_console": "", "use_fippa": "", "uselocalmediaserver": "", "website": "", @@ -572,6 +576,7 @@ "title": "" }, "emaillater": "", + "employee_teams": "", "employees": "", "estimators": "", "filehandlers": "", @@ -905,6 +910,15 @@ "sent": "E-mail envoyé avec succès." } }, + "employee_teams": { + "actions": { + "new": "" + }, + "fields": { + "active": "", + "name": "" + } + }, "employees": { "actions": { "addvacation": "", @@ -1063,6 +1077,7 @@ "sunday": "", "text": "", "thursday": "", + "total": "", "totals": "", "tuesday": "", "unknown": "Inconnu", @@ -2605,6 +2620,7 @@ }, "timetickets": { "actions": { + "claimtasks": "", "clockin": "", "clockout": "", "enter": "", @@ -2663,7 +2679,8 @@ }, "validation": { "clockoffmustbeafterclockon": "", - "clockoffwithoutclockon": "" + "clockoffwithoutclockon": "", + "hoursenteredmorethanavailable": "" } }, "titles": { diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index d18918d04..a456a9a0f 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -761,6 +761,13 @@ table: name: email_audit_trail schema: public + - name: employee_teams + using: + foreign_key_constraint_on: + column: bodyshopid + table: + name: employee_teams + schema: public - name: employees using: foreign_key_constraint_on: @@ -928,6 +935,7 @@ - textid - timezone - tt_allow_post_to_invoiced + - tt_enforce_hours_for_tech_console - updated_at - use_fippa - uselocalmediaserver @@ -1016,6 +1024,7 @@ - target_touchtime - timezone - tt_allow_post_to_invoiced + - tt_enforce_hours_for_tech_console - updated_at - use_fippa - uselocalmediaserver @@ -1945,6 +1954,165 @@ - active: _eq: true check: null +- table: + name: employee_team_members + schema: public + object_relationships: + - name: employee + using: + foreign_key_constraint_on: employeeid + - name: employee_team + using: + foreign_key_constraint_on: teamid + insert_permissions: + - role: user + permission: + check: + employee_team: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + columns: + - labor_rates + - percentage + - created_at + - updated_at + - employeeid + - id + - teamid + select_permissions: + - role: user + permission: + columns: + - labor_rates + - percentage + - created_at + - updated_at + - employeeid + - id + - teamid + filter: + employee_team: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + update_permissions: + - role: user + permission: + columns: + - labor_rates + - percentage + - created_at + - updated_at + - employeeid + - id + - teamid + filter: + employee_team: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + check: null + delete_permissions: + - role: user + permission: + backend_only: false + filter: + employee_team: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true +- table: + name: employee_teams + schema: public + object_relationships: + - name: bodyshop + using: + foreign_key_constraint_on: bodyshopid + array_relationships: + - name: employee_team_members + using: + foreign_key_constraint_on: + column: teamid + table: + name: employee_team_members + schema: public + insert_permissions: + - role: user + permission: + check: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + columns: + - active + - name + - created_at + - updated_at + - bodyshopid + - id + select_permissions: + - role: user + permission: + columns: + - active + - name + - created_at + - updated_at + - bodyshopid + - id + filter: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + update_permissions: + - role: user + permission: + columns: + - active + - bodyshopid + - name + - updated_at + filter: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + check: null - table: name: employee_vacation schema: public @@ -2045,6 +2213,13 @@ table: name: allocations schema: public + - name: employee_team_members + using: + foreign_key_constraint_on: + column: employeeid + table: + name: employee_team_members + schema: public - name: employee_vacations using: foreign_key_constraint_on: @@ -2218,30 +2393,32 @@ - active: _eq: true columns: - - id - - created_at - - updated_at - - jobid - billid + - bodyshopid + - created_at + - id + - jobid + - message + - metadata - paymentid - successful - - message - - bodyshopid + - updated_at - useremail select_permissions: - role: user permission: columns: - - successful - - message - - useremail - - created_at - - updated_at - billid - bodyshopid + - created_at - id - jobid + - message + - metadata - paymentid + - successful + - updated_at + - useremail filter: bodyshop: associations: diff --git a/hasura/migrations/1681155844658_create_table_public_employee_teams/down.sql b/hasura/migrations/1681155844658_create_table_public_employee_teams/down.sql new file mode 100644 index 000000000..1639caf73 --- /dev/null +++ b/hasura/migrations/1681155844658_create_table_public_employee_teams/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."employee_teams"; diff --git a/hasura/migrations/1681155844658_create_table_public_employee_teams/up.sql b/hasura/migrations/1681155844658_create_table_public_employee_teams/up.sql new file mode 100644 index 000000000..994e15280 --- /dev/null +++ b/hasura/migrations/1681155844658_create_table_public_employee_teams/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public"."employee_teams" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "name" text NOT NULL, "active" boolean NOT NULL DEFAULT true, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE cascade ON DELETE cascade); +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_employee_teams_updated_at" +BEFORE UPDATE ON "public"."employee_teams" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_employee_teams_updated_at" ON "public"."employee_teams" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/1681156265693_create_table_public_employee_team_members/down.sql b/hasura/migrations/1681156265693_create_table_public_employee_team_members/down.sql new file mode 100644 index 000000000..14ab424ce --- /dev/null +++ b/hasura/migrations/1681156265693_create_table_public_employee_team_members/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."employee_team_members"; diff --git a/hasura/migrations/1681156265693_create_table_public_employee_team_members/up.sql b/hasura/migrations/1681156265693_create_table_public_employee_team_members/up.sql new file mode 100644 index 000000000..e6a32938f --- /dev/null +++ b/hasura/migrations/1681156265693_create_table_public_employee_team_members/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public"."employee_team_members" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "teamid" uuid NOT NULL, "employeeid" uuid NOT NULL, "labor_rates" jsonb NOT NULL DEFAULT jsonb_build_object(), "percentage" numeric NOT NULL DEFAULT 0, PRIMARY KEY ("id") , FOREIGN KEY ("teamid") REFERENCES "public"."employee_teams"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("employeeid") REFERENCES "public"."employees"("id") ON UPDATE cascade ON DELETE cascade); +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_employee_team_members_updated_at" +BEFORE UPDATE ON "public"."employee_team_members" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_employee_team_members_updated_at" ON "public"."employee_team_members" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/1681329377686_alter_table_public_exportlog_add_column_metadata/down.sql b/hasura/migrations/1681329377686_alter_table_public_exportlog_add_column_metadata/down.sql new file mode 100644 index 000000000..3c175e747 --- /dev/null +++ b/hasura/migrations/1681329377686_alter_table_public_exportlog_add_column_metadata/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."exportlog" add column "metadata" jsonb +-- null; diff --git a/hasura/migrations/1681329377686_alter_table_public_exportlog_add_column_metadata/up.sql b/hasura/migrations/1681329377686_alter_table_public_exportlog_add_column_metadata/up.sql new file mode 100644 index 000000000..621b8481c --- /dev/null +++ b/hasura/migrations/1681329377686_alter_table_public_exportlog_add_column_metadata/up.sql @@ -0,0 +1,2 @@ +alter table "public"."exportlog" add column "metadata" jsonb + null; diff --git a/hasura/migrations/1681331289298_alter_table_public_bodyshops_add_column_tt_enforce_hours_for_tech_console/down.sql b/hasura/migrations/1681331289298_alter_table_public_bodyshops_add_column_tt_enforce_hours_for_tech_console/down.sql new file mode 100644 index 000000000..53e7608da --- /dev/null +++ b/hasura/migrations/1681331289298_alter_table_public_bodyshops_add_column_tt_enforce_hours_for_tech_console/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "tt_enforce_hours_for_tech_console" boolean +-- not null default 'false'; diff --git a/hasura/migrations/1681331289298_alter_table_public_bodyshops_add_column_tt_enforce_hours_for_tech_console/up.sql b/hasura/migrations/1681331289298_alter_table_public_bodyshops_add_column_tt_enforce_hours_for_tech_console/up.sql new file mode 100644 index 000000000..86e0f5973 --- /dev/null +++ b/hasura/migrations/1681331289298_alter_table_public_bodyshops_add_column_tt_enforce_hours_for_tech_console/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "tt_enforce_hours_for_tech_console" boolean + not null default 'false'; diff --git a/server/accounting/pbs/pbs-job-export.js b/server/accounting/pbs/pbs-job-export.js index 45289e652..001099a7e 100644 --- a/server/accounting/pbs/pbs-job-export.js +++ b/server/accounting/pbs/pbs-job-export.js @@ -249,10 +249,10 @@ async function QueryCustomersFromDms(socket) { SerialNumber: socket.JobData.bodyshop.pbs_serialnumber, //ContactId: "00000000000000000000000000000000", // ContactCode: socket.JobData.owner.accountingid, - FirstName: socket.JobData.ownr_co_nm + FirstName: socket.JobData.ownr_fn, + LastName: socket.JobData.ownr_co_nm ? socket.JobData.ownr_co_nm - : socket.JobData.ownr_fn, - LastName: socket.JobData.ownr_ln, + : socket.JobData.ownr_ln, PhoneNumber: socket.JobData.ownr_ph1, EmailAddress: socket.JobData.ownr_ea, // ModifiedSince: "0001-01-01T00:00:00.0000000Z", diff --git a/server/cdk/cdk-job-export.js b/server/cdk/cdk-job-export.js index d5fb6b295..3987d21bc 100644 --- a/server/cdk/cdk-job-export.js +++ b/server/cdk/cdk-job-export.js @@ -224,6 +224,7 @@ async function CdkSelectedCustomer(socket, selectedCustomerId) { } finally { //Ensure we always insert logEvents //GQL to insert logevents. + CdkBase.createLogEvent( socket, "DEBUG", @@ -1213,6 +1214,7 @@ async function GenerateTransWips(socket) { wips.push(item); }); + socket.transWips = wips; return wips; } @@ -1388,6 +1390,7 @@ async function MarkJobExported(socket, jobid) { jobid: jobid, successful: true, useremail: socket.user.email, + metadata: socket.transWips, }, bill: { exported: true,