Merged in release/2025-02-14 (pull request #2113)

Release/2025-02-14 into test-AIO - IO-3127 IO-3128 IO-3077 IO-3131
This commit is contained in:
Dave Richer
2025-02-12 19:07:08 +00:00
12 changed files with 11780 additions and 11400 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1"> <babeledit_project be_version="2.7.1" version="1.2">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -6453,6 +6453,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>mark_critical</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>operation</name> <name>operation</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -6474,6 +6495,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>update_field</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>update_value</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>value</name> <name>value</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -11943,6 +12006,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>shop_enabled_features</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>shopinfo</name> <name>shopinfo</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -12312,6 +12396,37 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>tooltips</name>
<children>
<folder_node>
<name>md_parts_scan</name>
<children>
<concept_node>
<name>update_value_tooltip</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node> <folder_node>
<name>validation</name> <name>validation</name>
<children> <children>
@@ -19091,6 +19206,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>ok</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>previous</name> <name>previous</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -19385,6 +19521,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>sharetoteams</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>submit</name> <name>submit</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -43090,6 +43247,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>parts_returns</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>print</name> <name>print</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -48557,6 +48735,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>unassigned</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>vertical</name> <name>vertical</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -52732,6 +52931,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>purchases_by_date_excel</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>purchases_by_date_range_detail</name> <name>purchases_by_date_range_detail</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -54483,6 +54703,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>

View File

@@ -42,8 +42,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, { const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
skip: chatVisible, // Skip when chat is visible pollInterval: 60 * 1000 // TODO: This is a fix for now, should be coming from sockets
...(pollInterval > 0 ? { pollInterval } : {})
}); });
// Socket connection status // Socket connection status
@@ -85,29 +84,25 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
// Get unread count from the cache // Get unread count from the cache
const unreadCount = (() => { const unreadCount = (() => {
if (chatVisible) { try {
try { const cachedData = client.readQuery({
const cachedData = client.readQuery({ query: CONVERSATION_LIST_QUERY,
query: CONVERSATION_LIST_QUERY, variables: { offset: 0 }
variables: { offset: 0 } });
});
if (!cachedData?.conversations) return 0; if (!cachedData?.conversations) {
return unreadData?.messages_aggregate?.aggregate?.count;
// Aggregate unread message count
return cachedData.conversations.reduce((total, conversation) => {
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
return total + unread;
}, 0);
} catch (error) {
console.warn("Unread count not found in cache:", error);
return 0; // Fallback if not in cache
} }
} else if (unreadData?.messages_aggregate?.aggregate?.count) {
// Use the unread count from the query result // Aggregate unread message count
return unreadData.messages_aggregate.aggregate.count; return cachedData.conversations.reduce((total, conversation) => {
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
return total + unread;
}, 0);
} catch (error) {
console.warn("Unread count not found in cache:", error);
return 0; // Fallback if not in cache
} }
return 0;
})(); })();
return ( return (

View File

@@ -62,6 +62,9 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
refetchQueries: ["GET_LINE_TICKET_BY_PK"] refetchQueries: ["GET_LINE_TICKET_BY_PK"]
}); });
if (!r.errors) { if (!r.errors) {
if (CriticalPartsScanning.treatment === "on") {
await CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
await Axios.post("/job/totalsssu", { await Axios.post("/job/totalsssu", {
id: jobLineEditModal.context.jobid id: jobLineEditModal.context.jobid
}); });
@@ -107,7 +110,9 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
}) })
}); });
} }
if (CriticalPartsScanning.treatment === "on") {
await CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
if (jobLineEditModal.actions.submit) { if (jobLineEditModal.actions.submit) {
jobLineEditModal.actions.submit(); jobLineEditModal.actions.submit();
} else { } else {
@@ -115,9 +120,7 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
} }
toggleModalVisible(); toggleModalVisible();
} }
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
setLoading(false); setLoading(false);
}; };

View File

@@ -172,13 +172,13 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
job: newJob job: newJob
} }
}); });
if (CriticalPartsScanning.treatment === "on") {
await CriticalPartsScan(r.data.insert_jobs.returning[0].id, notification);
}
await Axios.post("/job/totalsssu", { await Axios.post("/job/totalsssu", {
id: r.data.insert_jobs.returning[0].id id: r.data.insert_jobs.returning[0].id
}); });
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(r.data.insert_jobs.returning[0].id, notification);
}
notification["success"]({ notification["success"]({
message: t("jobs.successes.created"), message: t("jobs.successes.created"),
onClick: () => { onClick: () => {
@@ -281,6 +281,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
if (CriticalPartsScanning.treatment === "on") { if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id, notification); CriticalPartsScan(updateResult.data.update_jobs.returning[0].id, notification);
} }
if (updateResult.errors) { if (updateResult.errors) {
//error while inserting //error while inserting
notification["error"]({ notification["error"]({

View File

@@ -1,16 +1,27 @@
import {DeleteFilled} from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import {Button, Col, Form, Input, Row, Select, Space, Switch} from "antd"; import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd";
import React, {useMemo} from "react"; import { useMemo } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import i18n from "i18next";
const predefinedPartTypes = [ const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
"PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"
];
const predefinedModLbrTypes = [ const predefinedModLbrTypes = [
"LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", "LAA",
"LA1", "LA2", "LA3", "LA4" "LAB",
"LAD",
"LAE",
"LAF",
"LAG",
"LAM",
"LAR",
"LAS",
"LAU",
"LA1",
"LA2",
"LA3",
"LA4"
]; ];
const getFieldType = (field) => { const getFieldType = (field) => {
@@ -20,30 +31,46 @@ const getFieldType = (field) => {
return null; return null;
}; };
export default function ShopInfoPartsScan({form}) { const fieldSelectOptions = [
const {t} = useTranslation(); { label: i18n.t("joblines.fields.line_desc"), value: "line_desc" },
{ label: i18n.t("joblines.fields.part_type"), value: "part_type" },
{ label: i18n.t("joblines.fields.act_price"), value: "act_price" },
{ label: i18n.t("joblines.fields.part_qty"), value: "part_qty" },
{ label: i18n.t("joblines.fields.mod_lbr_ty"), value: "mod_lbr_ty" },
{
label: `${i18n.t("joblines.fields.oem_partno")} / ${i18n.t("joblines.fields.alt_partno")}`,
value: "part_number"
},
{ label: i18n.t("joblines.fields.op_code_desc"), value: "op_code_desc" }
];
export default function ShopInfoPartsScan({ form }) {
const { t } = useTranslation();
const watchedFields = Form.useWatch("md_parts_scan", form); const watchedFields = Form.useWatch("md_parts_scan", form);
const operationOptions = useMemo(() => ({ const operationOptions = useMemo(
string: [ () => ({
{label: t("bodyshop.operations.contains"), value: "contains"}, string: [
{label: t("bodyshop.operations.equals"), value: "equals"}, { label: t("bodyshop.operations.contains"), value: "contains" },
{label: t("bodyshop.operations.starts_with"), value: "startsWith"}, { label: t("bodyshop.operations.equals"), value: "equals" },
{label: t("bodyshop.operations.ends_with"), value: "endsWith"}, { label: t("bodyshop.operations.starts_with"), value: "startsWith" },
], { label: t("bodyshop.operations.ends_with"), value: "endsWith" }
number: [ ],
{label: t("bodyshop.operations.equals"), value: "="}, number: [
{label: t("bodyshop.operations.greater_than"), value: ">"}, { label: t("bodyshop.operations.equals"), value: "=" },
{label: t("bodyshop.operations.less_than"), value: "<"}, { label: t("bodyshop.operations.greater_than"), value: ">" },
], { label: t("bodyshop.operations.less_than"), value: "<" }
}), [t]); ]
}),
[t]
);
return ( return (
<div> <div>
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}> <LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}>
<Form.List name={["md_parts_scan"]}> <Form.List name={["md_parts_scan"]}>
{(fields, {add, remove, move}) => ( {(fields, { add, remove, move }) => (
<div> <div>
{fields.map((field, index) => { {fields.map((field, index) => {
const selectedField = watchedFields?.[index]?.field || "line_desc"; const selectedField = watchedFields?.[index]?.field || "line_desc";
@@ -61,28 +88,17 @@ export default function ShopInfoPartsScan({form}) {
{ {
required: true, required: true,
message: t("general.validation.required", { message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.field"), label: t("bodyshop.fields.md_parts_scan.field")
}), })
}, }
]} ]}
> >
<Select <Select
options={[ options={fieldSelectOptions}
{label: t("joblines.fields.line_desc"), value: "line_desc"},
{label: t("joblines.fields.part_type"), value: "part_type"},
{label: t("joblines.fields.act_price"), value: "act_price"},
{label: t("joblines.fields.part_qty"), value: "part_qty"},
{label: t("joblines.fields.mod_lbr_ty"), value: "mod_lbr_ty"},
{label: t("joblines.fields.mod_lb_hrs"), value: "mod_lb_hrs"},
{
label: `${t("joblines.fields.oem_partno")} / ${t("joblines.fields.alt_partno")}`,
value: "part_number"
},
]}
onChange={() => { onChange={() => {
form.setFields([ form.setFields([
{name: ["md_parts_scan", index, "operation"], value: "contains"}, { name: ["md_parts_scan", index, "operation"], value: "contains" },
{name: ["md_parts_scan", index, "value"], value: undefined}, { name: ["md_parts_scan", index, "value"], value: undefined }
]); ]);
}} }}
/> />
@@ -99,12 +115,12 @@ export default function ShopInfoPartsScan({form}) {
{ {
required: true, required: true,
message: t("general.validation.required", { message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.operation"), label: t("bodyshop.fields.md_parts_scan.operation")
}), })
}, }
]} ]}
> >
<Select options={operationOptions[fieldType]}/> <Select options={operationOptions[fieldType]} />
</Form.Item> </Form.Item>
</Col> </Col>
)} )}
@@ -119,9 +135,9 @@ export default function ShopInfoPartsScan({form}) {
{ {
required: true, required: true,
message: t("general.validation.required", { message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.value"), label: t("bodyshop.fields.md_parts_scan.value")
}), })
}, }
]} ]}
> >
{fieldType === "predefined" ? ( {fieldType === "predefined" ? (
@@ -129,17 +145,17 @@ export default function ShopInfoPartsScan({form}) {
options={ options={
selectedField === "part_type" selectedField === "part_type"
? predefinedPartTypes.map((type) => ({ ? predefinedPartTypes.map((type) => ({
label: type, label: type,
value: type value: type
})) }))
: predefinedModLbrTypes.map((type) => ({ : predefinedModLbrTypes.map((type) => ({
label: type, label: type,
value: type value: type
})) }))
} }
/> />
) : ( ) : (
<Input/> <Input />
)} )}
</Form.Item> </Form.Item>
</Col> </Col>
@@ -152,19 +168,70 @@ export default function ShopInfoPartsScan({form}) {
label={t("bodyshop.fields.md_parts_scan.caseInsensitive")} label={t("bodyshop.fields.md_parts_scan.caseInsensitive")}
name={[field.name, "caseInsensitive"]} name={[field.name, "caseInsensitive"]}
valuePropName="checked" valuePropName="checked"
labelCol={{span: 14}} initialValue={true}
wrapperCol={{span: 10}} labelCol={{ span: 14 }}
wrapperCol={{ span: 10 }}
> >
<Switch defaultChecked={true}/> <Switch />
</Form.Item> </Form.Item>
</Col> </Col>
)} )}
{/* Mark Line as Critical */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.mark_critical")}
name={[field.name, "mark_critical"]}
valuePropName="checked"
initialValue={true}
labelCol={{ span: 14 }}
wrapperCol={{ span: 10 }}
>
<Switch />
</Form.Item>
</Col>
{/* Update Field */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.update_field")}
name={[field.name, "update_field"]}
>
<Select
options={fieldSelectOptions}
allowClear
onClear={() =>
form.setFields([{ name: ["md_parts_scan", index, "update_field"], value: null }])
}
/>
</Form.Item>
</Col>
{/* Update Field */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.update_value")}
name={[field.name, "update_value"]}
dependencies={[["md_parts_scan", index, "update_field"]]}
tooltip={t("bodyshop.tooltips.md_parts_scan.update_value_tooltip")}
rules={[
{
required: form.getFieldValue(["md_parts_scan", index, "update_field"]),
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.update_value")
})
}
]}
>
<Input />
</Form.Item>
</Col>
{/* Actions */} {/* Actions */}
<Col span={2}> <Col span={2}>
<Space> <Space>
<DeleteFilled onClick={() => remove(field.name)}/> <DeleteFilled onClick={() => remove(field.name)} />
<FormListMoveArrows move={move} index={index} total={fields.length}/> <FormListMoveArrows move={move} index={index} total={fields.length} />
</Space> </Space>
</Col> </Col>
</Row> </Row>
@@ -175,8 +242,8 @@ export default function ShopInfoPartsScan({form}) {
<Form.Item> <Form.Item>
<Button <Button
type="dashed" type="dashed"
onClick={() => add({field: "line_desc", operation: "contains"})} onClick={() => add({ field: "line_desc", operation: "contains" })}
style={{width: "100%"}} style={{ width: "100%" }}
> >
{t("bodyshop.actions.addpartsrule")} {t("bodyshop.actions.addpartsrule")}
</Button> </Button>

View File

@@ -347,6 +347,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname }); window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
} }
}); });
payload.features?.allAccess === true
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
} catch (error) { } catch (error) {
console.error("Couldnt find $crisp."); console.error("Couldnt find $crisp.");
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -55,7 +55,7 @@ const sendServerEmail = async ({ subject, text }) => {
imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`, imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>` rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>`
}), }),
to: ["patrick@imexsystems.ca", "support@thinkimex.com"], to: ["support@thinkimex.com"],
subject: subject, subject: subject,
text: text, text: text,
ses: { ses: {

View File

@@ -2241,6 +2241,7 @@ exports.QUERY_PARTS_SCAN = `query QUERY_PARTS_SCAN ($id: uuid!) {
mod_lb_hrs mod_lb_hrs
oem_partno oem_partno
alt_partno alt_partno
op_code_desc
} }
} }
}`; }`;
@@ -2252,7 +2253,7 @@ exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCriti
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 affected_rows
} }
}` }`;
exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) { exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) { associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) {
@@ -2618,7 +2619,6 @@ exports.CREATE_CONVERSATION = `mutation CREATE_CONVERSATION($conversation: [conv
} }
`; `;
exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: timestamptz!) { exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: timestamptz!) {
bodyshops(where: { created_at: { _gte: $period } }) { bodyshops(where: { created_at: { _gte: $period } }) {
shopname shopname
@@ -2689,4 +2689,19 @@ exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: time
} }
} }
} }
` `;
exports.INSERT_AUDIT_TRAIL = `
mutation INSERT_AUDIT_TRAIL($auditObj: audit_trail_insert_input!) {
insert_audit_trail_one(object: $auditObj) {
id
jobid
billid
bodyshopid
created
operation
type
useremail
}
}
`;

View File

@@ -1,16 +1,25 @@
const queries = require("../graphql-client/queries"); const queries = require("../graphql-client/queries");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const predefinedPartTypes = [ const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
"PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG",
];
const predefinedModLbrTypes = [ const predefinedModLbrTypes = [
"LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", "LAA",
"LA1", "LA2", "LA3", "LA4", "LAB",
"LAD",
"LAE",
"LAF",
"LAG",
"LAM",
"LAR",
"LAS",
"LAU",
"LA1",
"LA2",
"LA3",
"LA4"
]; ];
exports.partsScan = async function (req, res) { exports.partsScan = async function (req, res) {
console.log('hello')
const { jobid } = req.body; const { jobid } = req.body;
const BearerToken = req.BearerToken; const BearerToken = req.BearerToken;
const client = req.userGraphQLClient; const client = req.userGraphQLClient;
@@ -18,10 +27,9 @@ exports.partsScan = async function (req, res) {
logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null); logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null);
try { try {
const data = await client.setHeaders({ Authorization: BearerToken }).request( const data = await client
queries.QUERY_PARTS_SCAN, .setHeaders({ Authorization: BearerToken })
{ id: jobid } .request(queries.QUERY_PARTS_SCAN, { id: jobid });
);
const rules = data.jobs_by_pk.bodyshop.md_parts_scan || []; const rules = data.jobs_by_pk.bodyshop.md_parts_scan || [];
if (!Array.isArray(rules)) { if (!Array.isArray(rules)) {
@@ -36,21 +44,22 @@ exports.partsScan = async function (req, res) {
? new RegExp(rule.value, "i") ? new RegExp(rule.value, "i")
: typeof rule.value === "string" : typeof rule.value === "string"
? new RegExp(rule.value) ? new RegExp(rule.value)
: null, : null
})); }));
const criticalIds = new Set(); const criticalIds = new Set();
for (const jobline of data.jobs_by_pk.joblines) { for (const jobline of data.jobs_by_pk.joblines) {
for (const { field, regex, operation, value } of compiledRules) { for (const { field, regex, operation, value, mark_critical, update_field, update_value } of compiledRules) {
if (criticalIds.has(jobline.id)) break; // Skip further evaluation if already critical // IO-3077 - Remove skip as this is extended to include line updates.
// if (criticalIds.has(jobline.id)) break; // Skip further evaluation if already critical
let jobValue = jobline[field]; let jobValue = jobline[field];
let match = false; let match = false;
if (field === "part_number") { if (field === "part_number") {
match = regex match = regex
? regex.test(jobline.oem_partno || '') || regex.test(jobline.alt_partno || '') ? regex.test(jobline.oem_partno || "") || regex.test(jobline.alt_partno || "")
: jobline.oem_partno === value || jobline.alt_partno === value; : jobline.oem_partno === value || jobline.alt_partno === value;
} else if (field === "part_type") { } else if (field === "part_type") {
match = predefinedPartTypes.includes(value) && value === jobValue; match = predefinedPartTypes.includes(value) && value === jobValue;
@@ -68,22 +77,44 @@ exports.partsScan = async function (req, res) {
} }
if (match) { if (match) {
criticalIds.add(jobline.id); if (mark_critical) {
break; // No need to evaluate further rules for this jobline //Could technically lead to duplicates, but they'd only be n positives, ultimately leading a positive.
criticalIds.add(jobline.id);
}
if (update_field && update_value) {
const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
lineId: jobline.id,
line: { [update_field]: update_value, manual_line: true }
});
const auditResult = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_AUDIT_TRAIL, {
auditObj: {
bodyshopid: data.jobs_by_pk.bodyshop.id,
jobid,
operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
type: "partscanupdate",
useremail: req.user.email
}
});
}
//break; // No need to evaluate further rules for this jobline
} }
} }
} }
const result = await client.setHeaders({ Authorization: BearerToken }).request( const result = await client
queries.UPDATE_PARTS_CRITICAL, .setHeaders({ Authorization: BearerToken })
{ IdsToMarkCritical: Array.from(criticalIds), jobid } .request(queries.UPDATE_PARTS_CRITICAL, { IdsToMarkCritical: Array.from(criticalIds), jobid });
);
res.status(200).json(result); res.status(200).json(result);
} catch (error) { } 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, jobid,
error: error.message, error: error.message,
stack: error.stack
}); });
res.status(400).json(JSON.stringify({ message: error?.message })); res.status(400).json(JSON.stringify({ message: error?.message }));
} }