diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index b52de475e..9fbb5f15f 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -5650,6 +5650,27 @@ md_tasks_presets + + enable_tasks + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + hourstype false @@ -5734,6 +5755,27 @@ + + use_approvals + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -45372,6 +45414,48 @@ + + commit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + commitone + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + enter false @@ -45414,6 +45498,27 @@ + + uncommit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -46337,6 +46442,27 @@ + + committed + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + created false @@ -48464,6 +48590,53 @@ + + labels + + + approval_queue_in_use + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + calculate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 2ed11cbc6..b57e94677 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -156,3 +156,11 @@ td.ant-table-column-sort { background-color: transparent; } + +.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { + background-color: #f4f4f4; +} + +.rowWithColor > td { + background-color: var(--bgColor) !important; +} diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index 7db03471d..362d973b4 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -1,28 +1,28 @@ import { UploadOutlined, UserAddOutlined } from "@ant-design/icons"; import { + Button, Divider, + Dropdown, Form, Input, + Menu, Select, + Space, Tabs, Upload, - Space, - Menu, - Dropdown, - Button, } from "antd"; +import _ from "lodash"; import React from "react"; import { useTranslation } from "react-i18next"; -import EmailDocumentsComponent from "../email-documents/email-documents.component"; -import _ from "lodash"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { selectEmailConfig } from "../../redux/email/email.selectors"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; import { CreateExplorerLinkForJob } from "../../utils/localmedia"; -import { selectEmailConfig } from "../../redux/email/email.selectors"; +import EmailDocumentsComponent from "../email-documents/email-documents.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -54,6 +54,15 @@ export function EmailOverlayComponent({ ]), }); }; + const handle_CC_Click = ({ item, key, keyPath }) => { + const email = item.props.value; + form.setFieldsValue({ + cc: _.uniq([ + ...(form.getFieldValue("cc") || ""), + ...(typeof email === "string" ? [email] : email), + ]), + }); + }; const menu = (
@@ -74,6 +83,25 @@ export function EmailOverlayComponent({
); + const menuCC = ( +
+ + {bodyshop.employees + .filter((e) => e.user_email) + .map((e, idx) => ( + + {`${e.first_name} ${e.last_name}`} + + ))} + {bodyshop.md_to_emails.map((e, idx) => ( + + {e.label} + + ))} + +
+ ); + return (
{ +const EmployeeSearchSelect = ({ options, ...props }) => { const { t } = useTranslation(); return ( @@ -39,4 +39,4 @@ const EmployeeSearchSelect = ({ options, ...props }, ref) => { ); }; -export default forwardRef(EmployeeSearchSelect); +export default EmployeeSearchSelect; diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index ee70e08cd..ecb12a1a4 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -262,11 +262,13 @@ function Header({ {t("menus.header.timetickets")} - }> - - {t("menus.header.ttapprovals")} - - + {bodyshop?.md_tasks_presets?.use_approvals && ( + }> + + {t("menus.header.ttapprovals")} + + + )} } 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 fb5c352bc..edb3f7dde 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 @@ -29,6 +29,7 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component"; +import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -250,21 +251,24 @@ export function JobsDetailHeaderActions({ > {t("timetickets.actions.enter")} - { - setTimeTicketTaskContext({ - actions: {}, - context: { jobid: job.id }, - }); - }} - > - {t("timetickets.actions.claimtasks")} - + {bodyshop.md_tasks_presets.enable_tasks && ( + { + setTimeTicketTaskContext({ + actions: {}, + context: { jobid: job.id }, + }); + }} + > + {t("timetickets.actions.claimtasks")} + + )} + {!jobRO && job.converted && ( - - e.stopPropagation()} - onConfirm={async () => { - //delete the job. - const result = await voidJob({ - variables: { - jobId: job.id, - job: { - status: bodyshop.md_ro_statuses.default_void, - voided: true, - scheduled_in: null, - scheduled_completion: null, - inproduction: false, - }, - note: [ - { - jobid: job.id, - created_by: currentUser.email, - audit: true, - text: t("jobs.labels.voidnote"), + + + e.stopPropagation()} + onConfirm={async () => { + //delete the job. + const result = await voidJob({ + variables: { + jobId: job.id, + job: { + status: bodyshop.md_ro_statuses.default_void, + voided: true, + scheduled_in: null, + scheduled_completion: null, + inproduction: false, }, - ], - }, - }); + note: [ + { + jobid: job.id, + created_by: currentUser.email, + audit: true, + text: t("jobs.labels.voidnote"), + }, + ], + }, + }); - if (!!!result.errors) { - notification["success"]({ - message: t("jobs.successes.voided"), - }); - //go back to jobs list. - history.push(`/manage/`); - } else { - notification["error"]({ - message: t("jobs.errors.voiding", { - error: JSON.stringify(result.errors), - }), - }); - } - }} - getPopupContainer={(trigger) => trigger.parentNode} - > - {t("menus.jobsactions.void")} - - + if (!!!result.errors) { + notification["success"]({ + message: t("jobs.successes.voided"), + }); + //go back to jobs list. + history.push(`/manage/`); + } else { + notification["error"]({ + message: t("jobs.errors.voiding", { + error: JSON.stringify(result.errors), + }), + }); + } + }} + getPopupContainer={(trigger) => trigger.parentNode} + > + {t("menus.jobsactions.void")} + + + )} ); diff --git a/client/src/components/print-center-jobs/print-center-jobs.component.jsx b/client/src/components/print-center-jobs/print-center-jobs.component.jsx index c96e3d3a5..ddb9a247d 100644 --- a/client/src/components/print-center-jobs/print-center-jobs.component.jsx +++ b/client/src/components/print-center-jobs/print-center-jobs.component.jsx @@ -23,17 +23,34 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) { const { id: jobId, job } = printCenterModal.context; const tempList = TemplateList("job", {}); const { t } = useTranslation(); - const JobsReportsList = Object.keys(tempList) - .map((key) => { - return tempList[key]; - }) - .filter( - (temp) => - !temp.regions || - (temp.regions && temp.regions[bodyshop.region_config]) || - (temp.regions && - bodyshop.region_config.includes(Object.keys(temp.regions)) === true) - ); + + const JobsReportsList = + bodyshop.cdk_dealerid === null && bodyshop.pbs_serialnumber === null + ? Object.keys(tempList) + .map((key) => { + return tempList[key]; + }) + .filter( + (temp) => + (!temp.regions || + (temp.regions && temp.regions[bodyshop.region_config]) || + (temp.regions && + bodyshop.region_config.includes(Object.keys(temp.regions)) === + true)) && + (!temp.dms || temp.dms === false) + ) + : Object.keys(tempList) + .map((key) => { + return tempList[key]; + }) + .filter( + (temp) => + !temp.regions || + (temp.regions && temp.regions[bodyshop.region_config]) || + (temp.regions && + bodyshop.region_config.includes(Object.keys(temp.regions)) === + true) + ); const filteredJobsReportsList = search !== "" 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 746d956ab..1585c6480 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 @@ -246,11 +246,21 @@ export function ProductionListTable({ (x) => x.status === record.status ); - if (!color) return null; + if (!color) { + if (index % 2 === 0) + return { + style: { + backgroundColor: `rgb(236, 236, 236)`, + }, + }; + + return null; + } return { + className: "rowWithColor", style: { - backgroundColor: `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`, + "--bgColor": `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`, }, }; }, diff --git a/client/src/components/rbac-wrapper/rbac-defaults.js b/client/src/components/rbac-wrapper/rbac-defaults.js index 1c2a779c4..24a564872 100644 --- a/client/src/components/rbac-wrapper/rbac-defaults.js +++ b/client/src/components/rbac-wrapper/rbac-defaults.js @@ -26,6 +26,8 @@ const ret = { "jobs:partsqueue": 4, "jobs:checklist-view": 2, "jobs:list-ready": 1, + "jobs:void": 5, + "bills:enter": 2, "bills:view": 2, "bills:list": 2, diff --git a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx index 837f5cbbe..7cd90bd14 100644 --- a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx +++ b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx @@ -12,7 +12,8 @@ import "./schedule-calendar.styles.scss"; import JobDetailCards from "../job-detail-cards/job-detail-cards.component"; import { selectProblemJobs } from "../../redux/application/application.selectors"; import { Alert, Collapse } from "antd"; -import { useTranslation } from "react-i18next"; +import { useTranslation, Trans } from "react-i18next"; +import { Link } from "react-router-dom"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -66,10 +67,21 @@ export function ScheduleCalendarWrapperComponent({ , + ]} + values={{ + ro_number: problem.ro_number, + code: problem.code, + }} + /> + } /> ))} @@ -79,10 +91,18 @@ export function ScheduleCalendarWrapperComponent({ , + ]} + values={{ + ro_number: problem.ro_number, + code: problem.code, + }} + /> + } /> )) )} diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx index 315d1f5dd..50301561a 100644 --- a/client/src/components/shop-info/shop-info.rbac.component.jsx +++ b/client/src/components/shop-info/shop-info.rbac.component.jsx @@ -1,12 +1,12 @@ +import { useTreatments } from "@splitsoftware/splitio-react"; import { Form, InputNumber } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; -import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; -import { useTreatments } from "@splitsoftware/splitio-react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -316,6 +316,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > + + + - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - - - - {t("joblines.fields.lbr_types.LAB")} - - - - - {t("joblines.fields.lbr_types.LAR")} - - - - - {t("joblines.fields.lbr_types.LAM")} - - - - - {t("joblines.fields.lbr_types.LAF")} - - - - - {t("joblines.fields.lbr_types.LAG")} - - - - - - - - - - - - - { - remove(field.name); - }} - /> - - - + <> + + + + + + + + + + + + {(fields, { add, remove, move }) => { + return ( +
+ {fields.map((field, index) => ( + + + + + + + + + + + {t("joblines.fields.lbr_types.LAB")} + + + + + {t("joblines.fields.lbr_types.LAR")} + + + + + {t("joblines.fields.lbr_types.LAM")} + + + + + {t("joblines.fields.lbr_types.LAF")} + + + + + {t("joblines.fields.lbr_types.LAG")} + + + + + + + + + + + + + { + remove(field.name); + }} + /> + + + + + ))} + + - ))} - - - -
- ); - }} -
-
+
+ ); + }} +
+ + ); } diff --git a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.component.jsx b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.component.jsx index 6c9f357e8..77bc795a8 100644 --- a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.component.jsx +++ b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.component.jsx @@ -33,6 +33,7 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
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 6cc2e3e3d..8f5cbba90 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 @@ -205,16 +205,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: "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"), dataIndex: "actions", @@ -282,16 +281,18 @@ export function TimeTicketList({ // context={{ jobId: jobId }} // /> } - + {bodyshop.md_tasks_presets.enable_tasks && ( + + )} {jobId && (techConsole ? null : ( {emps && emps.rates.map((item) => ( @@ -73,7 +74,7 @@ export function TimeTicketModalComponent({ ); }; @@ -127,7 +128,7 @@ export function TimeTicketModalComponent({ ]} > { const emps = @@ -279,6 +280,7 @@ export function TimeTicketModalComponent({ - {timeTicketModal.context && timeTicketModal.context.id ? null : ( @@ -198,6 +203,7 @@ export function TimeTicketModalContainer({ autoComplete={"off"} form={form} onFinishFailed={() => setEnterAgain(false)} + disabled={timeTicketModal.context?.timeticket?.committed_at} initialValues={ timeTicketModal.context.timeticket ? { @@ -218,6 +224,9 @@ export function TimeTicketModalContainer({ + @@ -241,14 +250,16 @@ export function TimeTicketModalContainer({ 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 index 5612931e1..8608a26da 100644 --- 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 @@ -23,6 +23,7 @@ import JobSearchSelectComponent from "../job-search-select/job-search-select.com import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility"; import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component"; import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -54,7 +55,8 @@ export function TimeTicketTaskModalComponent({ calculateTimeTickets={calculateTimeTickets} /> - + + - - - - - - Body - - - Refinish - - - Mechanical - - - Frame - - - Glass - - - - + + + + + {t("jobs.fields.lab")} + + + {t("jobs.fields.lar")} + + + {t("jobs.fields.lam")} + + + {t("jobs.fields.laf")} + + + {t("jobs.fields.lag")} + + + + + + + - - + {() => { const data = form.getFieldValue("timetickets"); @@ -167,11 +172,11 @@ export function TimeTicketTaskModalComponent({ dataIndex: "rate", key: "rate", }, - { - title: "Pay", - dataIndex: "pay", - key: "pay", - }, + // { + // title: "Pay", + // dataIndex: "pay", + // key: "pay", + // }, ]} /> ); @@ -231,11 +236,9 @@ export function TimeTicketTaskModalComponent({ ))}
{fields.map((field, index) => ( + {bodyshop?.md_tasks_presets?.use_approvals && ( + + + + )}
); } 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 index 09fffd9b2..f838f965a 100644 --- 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 @@ -52,12 +52,8 @@ export function TimeTickeTaskModalContainer({ }); async function handleFinish(values) { - console.log( - "🚀 ~ file: time-ticket-task-modal.container.jsx:55 ~ handleFinish ~ values:", - values - ); try { - if (true) { + if (bodyshop.md_tasks_presets.use_approvals) { const result = await insertTimeTicketApproval({ variables: { timeTicketInput: values.timetickets.map((ticket) => ({ @@ -88,6 +84,7 @@ export function TimeTickeTaskModalContainer({ _.omit(ticket, "pay") ), }, + refetchQueries: ["GET_LINE_TICKET_BY_PK"] }); if (result.errors) { notification.open({ @@ -105,6 +102,13 @@ export function TimeTickeTaskModalContainer({ } } } catch (error) { + console.log("🚀 ~ file: time-ticket-task-modal.container.jsx:104 ~ handleFinish ~ error:", error) + notification.open({ + type: "error", + message: t("timetickets.errors.creating", { + message: JSON.stringify(error), + }), + }); } finally { } } diff --git a/client/src/components/time-tickets-commit-toggle/time-tickets-commit-toggle.component.jsx b/client/src/components/time-tickets-commit-toggle/time-tickets-commit-toggle.component.jsx new file mode 100644 index 000000000..9a85f6507 --- /dev/null +++ b/client/src/components/time-tickets-commit-toggle/time-tickets-commit-toggle.component.jsx @@ -0,0 +1,107 @@ +import { useMutation } from "@apollo/client"; +import { Button, notification } from "antd"; +import moment from "moment"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + UPDATE_TIME_TICKET, + UPDATE_TIME_TICKETS, +} from "../../graphql/timetickets.queries"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; +import { setModalContext } from "../../redux/modals/modals.actions"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, +}); +const mapDispatchToProps = (dispatch) => ({ + setTimeTicketContext: (context) => + dispatch(setModalContext({ context: context, modal: "timeTicket" })), +}); +export function TimeTicketsCommit({ + bodyshop, + currentUser, + timeticket, + disabled, + refetch, + setTimeTicketContext, +}) { + const { t } = useTranslation(); + const [updateTimeTicket] = useMutation(UPDATE_TIME_TICKET); + + const [loading, setLoading] = useState(false); + const handleCommit = async () => { + setLoading(true); + try { + const ticketUpdate = timeticket.committed_at + ? { commited_by: null, committed_at: null } + : { + commited_by: currentUser.email, + committed_at: moment(), + }; + + const result = await updateTimeTicket({ + variables: { + timeticketId: timeticket.id, + timeticket: ticketUpdate, + }, + update(cache) { + cache.modify({ + fields: { + timeTickets(existingtickets, { readField }) { + return existingtickets.map((ticket) => { + if (timeticket.id === readField("id", ticket)) { + return { + ...ticket, + ...ticketUpdate, + }; + } + return ticket; + }); + }, + }, + }); + }, + }); + if (result.errors) { + notification.open({ + type: "error", + message: t("timetickets.errors.creating", { + message: JSON.stringify(result.errors), + }), + }); + } else { + setTimeTicketContext({ + context: { + id: timeticket.id, + timeticket: result.data.update_timetickets.returning[0], + }, + }); + notification.open({ + type: "success", + message: t("timetickets.successes.committed"), + }); + } + } catch (error) { + } finally { + setLoading(false); + } + }; + + if (!timeticket?.id) return null; + + return ( + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketsCommit); diff --git a/client/src/components/time-tickets-commit/time-tickets-commit.component.jsx b/client/src/components/time-tickets-commit/time-tickets-commit.component.jsx new file mode 100644 index 000000000..87f550360 --- /dev/null +++ b/client/src/components/time-tickets-commit/time-tickets-commit.component.jsx @@ -0,0 +1,95 @@ +import { useMutation } from "@apollo/client"; +import { Button, notification } from "antd"; +import moment from "moment"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { UPDATE_TIME_TICKETS } from "../../graphql/timetickets.queries"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, +}); + +export function TimeTicketsCommit({ + bodyshop, + currentUser, + timetickets, + disabled, + loadingCallback, + completedCallback, + refetch, +}) { + const { t } = useTranslation(); + const [updateTimeTickets] = useMutation(UPDATE_TIME_TICKETS); + + const [loading, setLoading] = useState(false); + const handleCommit = async () => { + setLoading(true); + try { + const result = await updateTimeTickets({ + variables: { + timeticketIds: timetickets.map((ticket) => ticket.id), + timeticket: { + commited_by: currentUser.email, + committed_at: moment(), + }, + }, + update(cache) { + cache.modify({ + fields: { + timeTickets(existingtickets, { readField }) { + const modifiedIds = timetickets.map((ticket) => ticket.id); + return existingtickets.map((ticket) => { + if (modifiedIds.includes(readField("id", ticket))) { + return { + ...ticket, + commited_by: currentUser.email, + committed_at: moment(), + }; + } + return ticket; + }); + }, + }, + }); + }, + }); + 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.committed"), + }); + if (!!completedCallback) completedCallback([]); + if (!!loadingCallback) loadingCallback(false); + } + } catch (error) { + } finally { + setLoading(false); + } + }; + + return ( + + ); +} + +export default connect(mapStateToProps, null)(TimeTicketsCommit); diff --git a/client/src/components/tt-approve-button/tt-approve-button.component.jsx b/client/src/components/tt-approve-button/tt-approve-button.component.jsx index 28aa3f47a..0ae9120b3 100644 --- a/client/src/components/tt-approve-button/tt-approve-button.component.jsx +++ b/client/src/components/tt-approve-button/tt-approve-button.component.jsx @@ -9,13 +9,16 @@ import { createStructuredSelector } from "reselect"; import { INSERT_TIME_TICKET_AND_APPROVE } from "../../graphql/timetickets.queries"; import { QUERY_TT_APPROVALS_BY_IDS } from "../../graphql/tt-approvals.queries"; import { + selectAuthLevel, selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser, + authLevel: selectAuthLevel, }); export function TtApproveButton({ @@ -23,6 +26,7 @@ export function TtApproveButton({ currentUser, selectedTickets, disabled, + authLevel, loadingCallback, completedCallback, refetch, @@ -64,7 +68,7 @@ export function TtApproveButton({ } else { notification.open({ type: "success", - message: t("timetickets.successes.createdg"), + message: t("timetickets.successes.created"), }); } } catch (error) { @@ -83,7 +87,14 @@ export function TtApproveButton({ }; return ( - ); diff --git a/client/src/graphql/timetickets.queries.js b/client/src/graphql/timetickets.queries.js index 2d8c0c452..a62032f40 100644 --- a/client/src/graphql/timetickets.queries.js +++ b/client/src/graphql/timetickets.queries.js @@ -15,6 +15,8 @@ export const QUERY_TICKETS_BY_JOBID = gql` memo jobid flat_rate + commited_by + committed_at employee { employee_number first_name @@ -44,6 +46,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql` memo jobid flat_rate + commited_by + committed_at job { id ro_number @@ -86,6 +90,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql` productivehrs memo jobid + commited_by + committed_at flat_rate job { id @@ -119,6 +125,8 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql` memo jobid flat_rate + commited_by + committed_at job { id ro_number @@ -161,6 +169,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` committed_at commited_by flat_rate + commited_by + committed_at job { id ro_number @@ -221,6 +231,8 @@ export const INSERT_NEW_TIME_TICKET = gql` date memo flat_rate + commited_by + committed_at } } } @@ -244,6 +256,8 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql` date memo flat_rate + commited_by + committed_at } } update_tt_approval_queue( @@ -254,6 +268,7 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql` id approved_at approved_at + } } } @@ -282,6 +297,38 @@ export const UPDATE_TIME_TICKET = gql` date flat_rate memo + committed_at + commited_by + } + } + } +`; + +export const UPDATE_TIME_TICKETS = gql` + mutation UPDATE_TIME_TICKETS( + $timeticketIds: [uuid!]! + $timeticket: timetickets_set_input! + ) { + update_timetickets( + where: { id: { _in: $timeticketIds } } + _set: $timeticket + ) { + returning { + id + clockon + clockoff + employeeid + productivehrs + actualhrs + ciecacode + created_at + updated_at + jobid + date + flat_rate + memo + committed_at + commited_by } } } @@ -306,6 +353,8 @@ export const QUERY_ACTIVE_TIME_TICKETS = gql` cost_center flat_rate jobid + commited_by + committed_at job { id ownr_fn diff --git a/client/src/pages/shop/shop.page.component.jsx b/client/src/pages/shop/shop.page.component.jsx index 92f535691..5f26142ce 100644 --- a/client/src/pages/shop/shop.page.component.jsx +++ b/client/src/pages/shop/shop.page.component.jsx @@ -54,9 +54,12 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) { - - - + { + bodyshop.md_tasks_presets.enable_tasks && + + + + } diff --git a/client/src/pages/time-tickets/time-tickets.container.jsx b/client/src/pages/time-tickets/time-tickets.container.jsx index 03cfdda50..0730fca95 100644 --- a/client/src/pages/time-tickets/time-tickets.container.jsx +++ b/client/src/pages/time-tickets/time-tickets.container.jsx @@ -19,6 +19,7 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; +import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component"; const mapStateToProps = createStructuredSelector({}); @@ -74,6 +75,7 @@ export function TimeTicketsContainer({ + } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 08f5efd37..8702d3161 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -49,7 +49,7 @@ "blocked": "Blocked", "cancelledappointment": "Canceled appointment for: ", "completingjobs": "Completing Jobs", - "dataconsistency": "{{ro_number}} has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.", + "dataconsistency": "<0>{{ro_number}} has a data consistency issue. It may have been excluded for scheduling purposes. CODE: {{code}}.", "expectedjobs": "Expected Jobs in Production: ", "expectedprodhrs": "Expected Production Hours:", "history": "History", @@ -343,10 +343,12 @@ "md_payment_types": "Payment Types", "md_referral_sources": "Referral Sources", "md_tasks_presets": { + "enable_tasks": "Enable Task Claiming", "hourstype": "Hour Types", "memo": "Time Ticket Memo", "name": "Preset Name", - "percent": "Percent" + "percent": "Percent", + "use_approvals": "Use Time Ticket Approval Queue" }, "messaginglabel": "Messaging Preset Label", "messagingtext": "Messaging Preset Text", @@ -405,7 +407,8 @@ "list-active": "Jobs -> List Active", "list-all": "Jobs -> List All", "list-ready": "Jobs -> List Ready", - "partsqueue": "Jobs -> Parts Queue" + "partsqueue": "Jobs -> Parts Queue", + "void": "Jobs -> Void" }, "owners": { "detail": "Owners -> Detail", @@ -2696,8 +2699,11 @@ "claimtasks": "Claim Tasks", "clockin": "Clock In", "clockout": "Clock Out", + "commit": "Commit Tickets ({{count}})", + "commitone": "Commit", "enter": "Enter New Time Ticket", - "printemployee": "Print Time Tickets" + "printemployee": "Print Time Tickets", + "uncommit": "Uncommit" }, "errors": { "clockingin": "Error while clocking in. {{message}}", @@ -2749,6 +2755,7 @@ "successes": { "clockedin": "Clocked in successfully.", "clockedout": "Clocked out successfully.", + "committed": "Time Tickets Committed Successfully", "created": "Time ticket entered successfully.", "deleted": "Time ticket deleted successfully." }, @@ -2859,6 +2866,10 @@ "tt_approvals": { "actions": { "approveselected": "Approve Selected" + }, + "labels": { + "approval_queue_in_use": "Time tickets will be added to the approval queue.", + "calculate": "Calculate" } }, "user": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f89d0ca21..38b6ee304 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -343,10 +343,12 @@ "md_payment_types": "", "md_referral_sources": "", "md_tasks_presets": { + "enable_tasks": "", "hourstype": "", "memo": "", "name": "", - "percent": "" + "percent": "", + "use_approvals": "" }, "messaginglabel": "", "messagingtext": "", @@ -405,7 +407,8 @@ "list-active": "", "list-all": "", "list-ready": "", - "partsqueue": "" + "partsqueue": "", + "void": "" }, "owners": { "detail": "", @@ -2692,8 +2695,11 @@ "claimtasks": "", "clockin": "", "clockout": "", + "commit": "", + "commitone": "", "enter": "", - "printemployee": "" + "printemployee": "", + "uncommit": "" }, "errors": { "clockingin": "", @@ -2745,6 +2751,7 @@ "successes": { "clockedin": "", "clockedout": "", + "committed": "", "created": "", "deleted": "" }, @@ -2855,6 +2862,10 @@ "tt_approvals": { "actions": { "approveselected": "" + }, + "labels": { + "approval_queue_in_use": "", + "calculate": "" } }, "user": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index aac36f368..c879a857c 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -343,10 +343,12 @@ "md_payment_types": "", "md_referral_sources": "", "md_tasks_presets": { + "enable_tasks": "", "hourstype": "", "memo": "", "name": "", - "percent": "" + "percent": "", + "use_approvals": "" }, "messaginglabel": "", "messagingtext": "", @@ -405,7 +407,8 @@ "list-active": "", "list-all": "", "list-ready": "", - "partsqueue": "" + "partsqueue": "", + "void": "" }, "owners": { "detail": "", @@ -2692,8 +2695,11 @@ "claimtasks": "", "clockin": "", "clockout": "", + "commit": "", + "commitone": "", "enter": "", - "printemployee": "" + "printemployee": "", + "uncommit": "" }, "errors": { "clockingin": "", @@ -2745,6 +2751,7 @@ "successes": { "clockedin": "", "clockedout": "", + "committed": "", "created": "", "deleted": "" }, @@ -2855,6 +2862,10 @@ "tt_approvals": { "actions": { "approveselected": "" + }, + "labels": { + "approval_queue_in_use": "", + "calculate": "" } }, "user": { diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 862323ba0..f2fa60c99 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -512,6 +512,7 @@ export const TemplateList = (type, context) => { key: "dms_posting_sheet", disabled: false, group: "financial", + dms: true, }, } : {}), diff --git a/server/cdk/cdk-job-export.js b/server/cdk/cdk-job-export.js index 37265a0a7..c20a899a1 100644 --- a/server/cdk/cdk-job-export.js +++ b/server/cdk/cdk-job-export.js @@ -729,9 +729,15 @@ async function InsertDmsVehicle(socket) { deliveryDate: moment() // .tz(socket.JobData.bodyshop.timezone) .format("YYYYMMDD"), - licensePlateNo: String(socket.JobData.plate_no) - .replace(/([^\w]|_)/g, "") - .toUpperCase(), + licensePlateNo: + socket.JobData.plate_no === null + ? null + : String(socket.JobData.plate_no).replace(/([^\w]|_)/g, "") + .length === 0 + ? null + : String(socket.JobData.plate_no) + .replace(/([^\w]|_)/g, "") + .toUpperCase(), make: socket.txEnvelope.dms_make, modelAbrev: socket.txEnvelope.dms_model, modelYear: socket.JobData.v_model_yr,