diff --git a/client/src/components/schedule-calendar-wrapper/localizer.js b/client/src/components/schedule-calendar-wrapper/localizer.js index 9935252b4..daaf484e7 100644 --- a/client/src/components/schedule-calendar-wrapper/localizer.js +++ b/client/src/components/schedule-calendar-wrapper/localizer.js @@ -1,11 +1,4 @@ -import isBetween from "dayjs/plugin/isBetween"; -import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; -import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; -import localeData from "dayjs/plugin/localeData"; -import localizedFormat from "dayjs/plugin/localizedFormat"; -import minMax from "dayjs/plugin/minMax"; -import utc from "dayjs/plugin/utc"; -import { DateLocalizer } from "react-big-calendar"; +import {DateLocalizer} from "react-big-calendar"; function arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; @@ -29,7 +22,9 @@ function iterableToArrayLimit(arr, i) { try { if (!_n && _i["return"] != null) _i["return"](); } finally { - if (_d) throw _e; + if (_d) { // noinspection ThrowInsideFinallyBlockJS + throw _e; + } } } return _arr; diff --git a/client/src/components/shop-info/shop-info.parts-scan.jsx b/client/src/components/shop-info/shop-info.parts-scan.jsx index 87ac86fbd..206bbcddf 100644 --- a/client/src/components/shop-info/shop-info.parts-scan.jsx +++ b/client/src/components/shop-info/shop-info.parts-scan.jsx @@ -1,75 +1,188 @@ -import { DeleteFilled } from "@ant-design/icons"; -import { Button, Form, Input, Space } from "antd"; -import React from "react"; -import { useTranslation } from "react-i18next"; +import {DeleteFilled} from "@ant-design/icons"; +import {Button, Col, Form, Input, Row, Select, Space, Switch} from "antd"; +import React, {useMemo} from "react"; +import {useTranslation} from "react-i18next"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -export default function ShopInfoPartsScan({ form }) { - const { t } = useTranslation(); +const predefinedPartTypes = [ + "PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG" +]; +const predefinedModLbrTypes = [ + "LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", + "LA1", "LA2", "LA3", "LA4" +]; + +const getFieldType = (field) => { + if (["line_desc", "part_number"].includes(field)) return "string"; + if (["act_price", "part_qty", "mod_lb_hrs"].includes(field)) return "number"; + if (["part_type", "mod_lbr_ty"].includes(field)) return "predefined"; + return null; +}; + +export default function ShopInfoPartsScan({form}) { + const {t} = useTranslation(); + + const watchedFields = Form.useWatch("md_parts_scan", form); + + const operationOptions = useMemo(() => ({ + string: [ + {label: t("bodyshop.operations.contains"), value: "contains"}, + {label: t("bodyshop.operations.equals"), value: "equals"}, + {label: t("bodyshop.operations.starts_with"), value: "startsWith"}, + {label: t("bodyshop.operations.ends_with"), value: "endsWith"}, + ], + number: [ + {label: t("bodyshop.operations.equals"), value: "="}, + {label: t("bodyshop.operations.greater_than"), value: ">"}, + {label: t("bodyshop.operations.less_than"), value: "<"}, + ], + }), [t]); return (
- {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - + {(fields, {add, remove, move}) => ( +
+ {fields.map((field, index) => { + const selectedField = watchedFields?.[index]?.field || "line_desc"; + const fieldType = getFieldType(selectedField); - - { - remove(field.name); - }} - /> - - - + return ( + + + {/* Select Field */} + + + + + + )} + + {/* Value */} + {fieldType && ( + + + {fieldType === "predefined" ? ( + + )} + + + )} + + {/* Case Sensitivity */} + {fieldType === "string" && ( + + + + + + )} + + {/* Actions */} + + + remove(field.name)}/> + + + + - ))} - - - -
- ); - }} + ); + })} + + + + +
+ )}
diff --git a/client/src/components/shop-info/shop-intellipay-config.component.jsx b/client/src/components/shop-info/shop-intellipay-config.component.jsx index 30c12cfa2..f40a404ee 100644 --- a/client/src/components/shop-info/shop-intellipay-config.component.jsx +++ b/client/src/components/shop-info/shop-intellipay-config.component.jsx @@ -1,31 +1,32 @@ -import { Alert, Form, InputNumber, Switch } from "antd"; +import {Alert, Form, Switch} from "antd"; import React from "react"; -import { useTranslation } from "react-i18next"; +import {useTranslation} from "react-i18next"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; +import {connect} from "react-redux"; +import {createStructuredSelector} from "reselect"; +import {selectBodyshop} from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); -const mapDispatchToProps = (dispatch) => ({ +const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay); -export function ShopInfoIntellipay({ bodyshop, form }) { - const { t } = useTranslation(); +// noinspection JSUnusedLocalSymbols +export function ShopInfoIntellipay({bodyshop, form}) { + const {t} = useTranslation(); return ( <> {() => { - const { intellipay_config } = form.getFieldsValue(); + const {intellipay_config} = form.getFieldsValue(); if (intellipay_config?.enable_cash_discount) - return ; + return ; }} @@ -35,7 +36,7 @@ export function ShopInfoIntellipay({ bodyshop, form }) { valuePropName="checked" name={["intellipay_config", "enable_cash_discount"]} > - + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 337047e1f..be7c095e3 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -256,6 +256,15 @@ } }, "bodyshop": { + "operations": { + "starts_with": "Starts With", + "contains": "Contains", + "ends_with": "Ends With", + "equals": "Equals", + "not_equals": "Not Equals", + "greater_than": "Greater Than", + "less_than": "Less Than" + }, "actions": { "add_task_preset": "Add Task Preset", "addapptcolor": "Add Appointment Color", @@ -379,8 +388,10 @@ "md_lost_sale_reasons": "Lost Sale Reasons", "md_parts_order_comment": "Parts Orders Comments", "md_parts_scan": { - "expression": "RegEX Expression", - "flags": "Flags" + "field": "Field", + "operation": "Operation", + "value": "Value", + "caseInsensitive": "Case Insensitive" }, "md_payment_types": "Payment Types", "md_referral_sources": "Referral Sources", @@ -1451,6 +1462,7 @@ "mod_lbr_ty": "Labor Type", "notes": "Notes", "oem_partno": "OEM Part #", + "alt_partno": "Alt Part #", "op_code_desc": "Op Code Description", "part_qty": "Qty.", "part_type": "Part Type", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 02c3dd2dd..d0e2b1687 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -256,6 +256,15 @@ } }, "bodyshop": { + "operations": { + "starts_with": "", + "contains": "", + "ends_with": "", + "equals": "", + "not_equals": "", + "greater_than": "", + "less_than": "" + }, "actions": { "add_task_preset": "", "addapptcolor": "", @@ -379,8 +388,10 @@ "md_lost_sale_reasons": "", "md_parts_order_comment": "", "md_parts_scan": { - "expression": "", - "flags": "" + "field": "", + "operation": "", + "value": "", + "caseInsensitive": "" }, "md_payment_types": "", "md_referral_sources": "", @@ -1451,6 +1462,7 @@ "mod_lbr_ty": "Tipo de trabajo", "notes": "", "oem_partno": "OEM parte #", + "alt_partno": "", "op_code_desc": "", "part_qty": "", "part_type": "Tipo de parte", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 7ea7ef02c..e499e4275 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -256,6 +256,15 @@ } }, "bodyshop": { + "operations": { + "starts_with": "", + "contains": "", + "ends_with": "", + "equals": "", + "not_equals": "", + "greater_than": "", + "less_than": "" + }, "actions": { "add_task_preset": "", "addapptcolor": "", @@ -379,8 +388,10 @@ "md_lost_sale_reasons": "", "md_parts_order_comment": "", "md_parts_scan": { - "expression": "", - "flags": "" + "field": "", + "operation": "", + "value": "", + "caseInsensitive": "" }, "md_payment_types": "", "md_referral_sources": "", @@ -1451,6 +1462,7 @@ "mod_lbr_ty": "Type de travail", "notes": "", "oem_partno": "Pièce OEM #", + "alt_partno": "", "op_code_desc": "", "part_qty": "", "part_type": "Type de pièce", diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index da2aaac89..d585a4255 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2234,18 +2234,25 @@ exports.QUERY_PARTS_SCAN = `query QUERY_PARTS_SCAN ($id: uuid!) { id line_desc critical + part_type + act_price + part_qty + mod_lbr_ty + mod_lb_hrs + oem_partno + alt_partno } } }`; -exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCritical:[uuid!]!, $jobid: uuid!){ - critical: update_joblines(where:{id:{_in:$IdsToMarkCritical}}, _set:{critical: true}){ +exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCritical:[uuid!]!, $jobid: uuid!) { + critical: update_joblines(where: {id: {_in: $IdsToMarkCritical}}, _set: {critical: true}) { affected_rows } - notcritical: update_joblines(where:{id:{_nin:$IdsToMarkCritical}, jobid: {_eq: $jobid}}, _set:{critical: false}){ + notcritical: update_joblines(where: {id: {_nin: $IdsToMarkCritical}, jobid: {_eq: $jobid}}, _set: {critical: false}) { affected_rows } -}`; +}` exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) { associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) { diff --git a/server/parts-scan/parts-scan.js b/server/parts-scan/parts-scan.js index 7769654ce..943c4e59a 100644 --- a/server/parts-scan/parts-scan.js +++ b/server/parts-scan/parts-scan.js @@ -1,50 +1,90 @@ -const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); const logger = require("../utils/logger"); -const { job } = require("../scheduling/scheduling-job"); -const _ = require("lodash"); -// Dinero.defaultCurrency = "USD"; -// Dinero.globalLocale = "en-CA"; +const predefinedPartTypes = [ + "PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG", +]; +const predefinedModLbrTypes = [ + "LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", + "LA1", "LA2", "LA3", "LA4", +]; exports.partsScan = async function (req, res) { + console.log('hello') const { jobid } = req.body; - const BearerToken = req.BearerToken; const client = req.userGraphQLClient; logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null); try { - //Query all jobline data using the user's authorization. - const data = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_PARTS_SCAN, { - id: jobid - }); + const data = await client.setHeaders({ Authorization: BearerToken }).request( + queries.QUERY_PARTS_SCAN, + { id: jobid } + ); - //Create RegExps once for better performance. - const IdsToMarkCritical = []; - const RegExpressions = data.jobs_by_pk.bodyshop.md_parts_scan.map((r) => new RegExp(r.expression, r.flags)); + const rules = data.jobs_by_pk.bodyshop.md_parts_scan || []; + if (!Array.isArray(rules)) { + // noinspection ExceptionCaughtLocallyJS + throw new Error("Invalid md_parts_scan format. Expected an array of rules."); + } - //Check each line against each regex rule. - data.jobs_by_pk.joblines.forEach((jobline) => { - RegExpressions.forEach((rExp) => { - if (jobline.line_desc.match(rExp)) { - IdsToMarkCritical.push(jobline); + const compiledRules = rules.map((rule) => ({ + ...rule, + regex: + typeof rule.value === "string" && rule.caseInsensitive + ? new RegExp(rule.value, "i") + : typeof rule.value === "string" + ? new RegExp(rule.value) + : null, + })); + + const criticalIds = new Set(); + + for (const jobline of data.jobs_by_pk.joblines) { + for (const { field, regex, operation, value } of compiledRules) { + if (criticalIds.has(jobline.id)) break; // Skip further evaluation if already critical + + let jobValue = jobline[field]; + let match = false; + + if (field === "part_number") { + match = regex + ? regex.test(jobline.oem_partno || '') || regex.test(jobline.alt_partno || '') + : jobline.oem_partno === value || jobline.alt_partno === value; + } else if (field === "part_type") { + match = predefinedPartTypes.includes(value) && value === jobValue; + } else if (field === "mod_lbr_ty") { + match = predefinedModLbrTypes.includes(value) && value === jobValue; + } else if (regex && typeof jobValue === "string") { + if (operation === "contains") match = regex.test(jobValue); + if (operation === "startsWith") match = new RegExp(`^${value}`).test(jobValue); + if (operation === "endsWith") match = new RegExp(`${value}$`).test(jobValue); + if (operation === "equals") match = jobValue === value; + } else if (typeof jobValue === "number") { + if (operation === ">") match = jobValue > value; + if (operation === "<") match = jobValue < value; + if (operation === "=") match = jobValue === value; } - }); - }); - const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_PARTS_CRITICAL, { - IdsToMarkCritical: _.uniqBy(IdsToMarkCritical, "id").map((i) => i.id), - jobid: jobid - }); + if (match) { + criticalIds.add(jobline.id); + break; // No need to evaluate further rules for this jobline + } + } + } + + const result = await client.setHeaders({ Authorization: BearerToken }).request( + queries.UPDATE_PARTS_CRITICAL, + { IdsToMarkCritical: Array.from(criticalIds), jobid } + ); res.status(200).json(result); } catch (error) { - logger.log("job-parts-scan-error", "ERROR", req.user.email, jobid, { + logger.log("job-parts-scan-error", "ERROR", req.user?.email, jobid, { jobid, - error + error: error.message, }); - res.status(400).json(JSON.stringify(error)); + res.status(400).json(JSON.stringify({ message: error?.message })); } };