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 */}
+
+
+
+
+
+ {/* Operation */}
+ {fieldType !== "predefined" && fieldType && (
+
+
+
+
+ )}
+
+ {/* 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 }));
}
};