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 ( + + + + + + + + {(fields, { add, remove, move }) => { + return ( +
+ {fields.map((field, index) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + remove(field.name); + }} + /> + + + + + ))} + + + +
+ ); + }} +
+ + + ); +} +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 ( + + ); + }} + 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 + + )} + + )} + + + + + + + + + ); + + return ( + + + + ); +} 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)} + > +
+ + + {() => ( + + + + )} + + + + + + + + +
( + + + 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", + }, + ]} + /> + ); + }} + + + + + + ); +} + +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={ + { + // + } + {jobId && (techConsole ? null : ( { 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 ( + + + + ); +} 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, + }} + > + + + ); +} + +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,