IO-3515 Bill OCR Feedback.

This commit is contained in:
Patrick Fic
2026-03-19 14:47:22 -07:00
parent 8af8c8039c
commit b2231007b6
12 changed files with 1103 additions and 79 deletions

View File

@@ -2696,6 +2696,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>oem_partno</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>quantity</name>
<definition_loaded>false</definition_loaded>
@@ -3684,6 +3705,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>feedback_placeholder</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>feedback_prompt</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>generic_failure</name>
<definition_loaded>false</definition_loaded>
@@ -3831,6 +3894,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>submit_feedback</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>
<concept_node>
@@ -8641,6 +8725,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>manual-line</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>partsqueue</name>
<definition_loaded>false</definition_loaded>
@@ -17816,6 +17921,468 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>banner_message</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>banner_status_connected</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>banner_status_disconnected</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>clear_logs</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>collapse_all</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>color_json</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>copied</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>copy</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>copy_request</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>copy_response</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>details</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>expand_all</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>hide_details</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>log_level</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>plain_json</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>provider_cdk</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>provider_dms</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>provider_fortellis</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>provider_pbs</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>provider_reynolds</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>reconnect</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>reconnected_export_service</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>refreshallocations</name>
<definition_loaded>false</definition_loaded>
@@ -17837,6 +18404,153 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>request_xml</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>response_xml</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>rr_validation_message</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>rr_validation_notice_description</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>rr_validation_notice_title</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>transport_ws</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>transport_wss</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>
@@ -20590,6 +21304,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>done</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>download</name>
<definition_loaded>false</definition_loaded>
@@ -23009,6 +23744,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>validationerror</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>view</name>
<definition_loaded>false</definition_loaded>
@@ -23423,6 +24179,27 @@
<folder_node>
<name>validation</name>
<children>
<concept_node>
<name>array</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>dateRangeExceeded</name>
<definition_loaded>false</definition_loaded>
@@ -57452,6 +58229,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>work_in_progress_labour_summary</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>work_in_progress_payables</name>
<definition_loaded>false</definition_loaded>
@@ -57473,6 +58271,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>work_in_progress_payables_summary</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>

View File

@@ -0,0 +1,104 @@
import { DislikeOutlined, LikeOutlined } from "@ant-design/icons";
import { Button, Form, Input, Radio, Space } from "antd";
import axios from "axios";
import { useState } from "react";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { useTranslation } from "react-i18next";
function BillAiFeedback({ billForm, rawAIData }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const notification = useNotification();
//Need to sanitize becuase we pass as form data to include the attachment.
const sanitizeBillFormValues = (value) => {
const seen = new WeakSet();
return JSON.stringify(
value,
(key, v) => {
if (key === "originFileObj") return undefined;
if (key === "thumbUrl") return undefined;
if (key === "preview") return undefined;
if (typeof v === "function") return undefined;
if (v && typeof v === "object") {
if (seen.has(v)) return "[Circular]";
seen.add(v);
}
return v;
},
0
);
};
const getAttachmentFromBillFormUpload = () => {
const uploads = billForm?.getFieldValue?.("upload") || [];
const files = uploads.map((u) => u?.originFileObj).filter(Boolean);
return (
files.find((f) => f?.type === "application/pdf") ||
files.find((f) => isString(f?.name) && f.name.toLowerCase().endsWith(".pdf")) ||
files[0] ||
null
);
};
const submitFeedback = async ({ rating, comments }) => {
setSubmitting(true);
try {
const billFormValues = billForm.getFieldsValue(true);
const formData = new FormData();
formData.append("rating", rating);
formData.append("comments", comments || "");
formData.append("billFormValues", sanitizeBillFormValues(billFormValues));
formData.append("rawAIData", sanitizeBillFormValues(rawAIData));
const attachmentFile = getAttachmentFromBillFormUpload();
if (attachmentFile) {
formData.append("billPdf", attachmentFile, attachmentFile.name || "bill.pdf");
}
await axios.post("/ai/bill-feedback", formData);
notification.success({
title: "Thanks — feedback submitted"
});
form.resetFields();
} catch (error) {
notification.error({
title: "Failed to submit feedback",
description: error?.response?.data?.message || error?.message
});
} finally {
setSubmitting(false);
}
};
const isString = (v) => typeof v === "string";
return (
<Form form={form} onFinish={submitFeedback} requiredMark={false}>
<Space align="top" size="large">
<Form.Item name="rating" label={t("bills.labels.ai.feedback_prompt")} rules={[{ required: true }]}>
<Radio.Group optionType="button" buttonStyle="solid">
<Radio.Button value="up">
<LikeOutlined />
</Radio.Button>
<Radio.Button value="down">
<DislikeOutlined />
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item name="comments">
<Input.TextArea rows={1} placeholder={t("bills.labels.ai.feedback_placeholder")} />
</Form.Item>
<Button onClick={() => form.submit()} loading={submitting} disabled={submitting}>
{t("bills.labels.ai.submit_feedback")}
</Button>
</Space>
</Form>
);
}
export default BillAiFeedback;

View File

@@ -23,7 +23,8 @@ function BillEnterAiScan({
fileInputRef,
scanLoading,
setScanLoading,
setIsAiScan
setIsAiScan,
setRawAIData
}) {
const notification = useNotification();
const { t } = useTranslation();
@@ -57,6 +58,7 @@ function BillEnterAiScan({
}
setScanLoading(false);
setRawAIData(data.data);
// Update form with the extracted data
if (data?.data?.billForm) {
form.setFieldsValue(data.data.billForm);
@@ -147,6 +149,7 @@ function BillEnterAiScan({
setScanLoading(false);
form.setFieldsValue(data.data.billForm);
setRawAIData(data.data);
await form.validateFields(["billlines"], { recursive: true });
notification.success({

View File

@@ -28,6 +28,7 @@ import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import BillAiFeedback from "../bill-ai-feedback/bill-ai-feedback.component.jsx";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -53,6 +54,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const [loading, setLoading] = useState(false);
const [scanLoading, setScanLoading] = useState(false);
const [isAiScan, setIsAiScan] = useState(false);
const [rawAIData, setRawAIData] = useState(null);
const client = useApolloClient();
const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
const notification = useNotification();
@@ -387,6 +389,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
billlines: []
});
setIsAiScan(false);
setRawAIData(null);
// form.resetFields();
} else {
toggleModalVisible();
@@ -404,6 +407,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
}
setScanLoading(false);
setIsAiScan(false);
setRawAIData(null);
toggleModalVisible();
}
};
@@ -429,6 +433,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
}
setScanLoading(false);
setIsAiScan(false);
setRawAIData(null);
}
}, [billEnterModal.open, form, formValues]);
@@ -456,6 +461,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
scanLoading={scanLoading}
setScanLoading={setScanLoading}
setIsAiScan={setIsAiScan}
setRawAIData={setRawAIData}
/>
)}
</Space>
@@ -471,7 +477,8 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
setLoading(false);
}}
footer={
<Space>
<Space align="top">
{isAiScan && <BillAiFeedback billForm={form} rawAIData={rawAIData} />}
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
{t("bills.labels.generatepartslabel")}
</Checkbox>

View File

@@ -231,13 +231,16 @@
"overall": "Overall"
},
"disclaimer_title": "AI Scan Beta Disclaimer",
"feedback_placeholder": "Tell us what worked, what didn't, and what could be better.",
"feedback_prompt": "Was this AI scan helpful?",
"generic_failure": "Failed to process invoice.",
"multipage": "The is a multi-page document. Processing will take a few moments.",
"processing": "Analyzing Bill",
"scan": "AI Bill Scanner",
"scancomplete": "AI Scan Complete",
"scanfailed": "AI Scan Failed",
"scanstarted": "AI Scan Started"
"scanstarted": "AI Scan Started",
"submit_feedback": "Submit Feedback"
},
"bill_lines": "Bill Lines",
"bill_total": "Bill Total Amount",
@@ -1075,36 +1078,36 @@
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
},
"labels": {
"refreshallocations": "Refresh to see DMS Allocations.",
"provider_reynolds": "Reynolds",
"provider_fortellis": "Fortellis",
"provider_cdk": "CDK",
"provider_pbs": "PBS",
"provider_dms": "DMS",
"transport_wss": "(WSS)",
"transport_ws": "(WS)",
"banner_message": "Posting to {{provider}} | {{transport}} | {{status}}",
"banner_status_connected": "Connected",
"banner_status_disconnected": "Disconnected",
"banner_message": "Posting to {{provider}} | {{transport}} | {{status}}",
"reconnected_export_service": "Reconnected to {{provider}} Export Service",
"rr_validation_message": "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
"rr_validation_notice_title": "Reynolds RO created",
"rr_validation_notice_description": "Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
"color_json": "Color JSON",
"plain_json": "Plain JSON",
"collapse_all": "Collapse All",
"expand_all": "Expand All",
"log_level": "Log Level",
"clear_logs": "Clear Logs",
"reconnect": "Reconnect",
"details": "Details",
"hide_details": "Hide details",
"copy": "Copy",
"collapse_all": "Collapse All",
"color_json": "Color JSON",
"copied": "Copied",
"copy": "Copy",
"copy_request": "Copy Request",
"copy_response": "Copy Response",
"details": "Details",
"expand_all": "Expand All",
"hide_details": "Hide details",
"log_level": "Log Level",
"plain_json": "Plain JSON",
"provider_cdk": "CDK",
"provider_dms": "DMS",
"provider_fortellis": "Fortellis",
"provider_pbs": "PBS",
"provider_reynolds": "Reynolds",
"reconnect": "Reconnect",
"reconnected_export_service": "Reconnected to {{provider}} Export Service",
"refreshallocations": "Refresh to see DMS Allocations.",
"request_xml": "Request XML",
"response_xml": "Response XML"
"response_xml": "Response XML",
"rr_validation_message": "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
"rr_validation_notice_description": "Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
"rr_validation_notice_title": "Reynolds RO created",
"transport_ws": "(WS)",
"transport_wss": "(WSS)"
}
},
"documents": {

View File

@@ -231,13 +231,16 @@
"overall": ""
},
"disclaimer_title": "",
"feedback_placeholder": "",
"feedback_prompt": "",
"generic_failure": "",
"multipage": "",
"processing": "",
"scan": "",
"scancomplete": "",
"scanfailed": "",
"scanstarted": ""
"scanstarted": "",
"submit_feedback": ""
},
"bill_lines": "",
"bill_total": "",
@@ -1075,36 +1078,36 @@
"earlyrorequired.message": ""
},
"labels": {
"refreshallocations": "",
"provider_reynolds": "",
"provider_fortellis": "",
"provider_cdk": "",
"provider_pbs": "",
"provider_dms": "",
"transport_wss": "",
"transport_ws": "",
"banner_message": "",
"banner_status_connected": "",
"banner_status_disconnected": "",
"banner_message": "",
"reconnected_export_service": "",
"rr_validation_message": "",
"rr_validation_notice_title": "",
"rr_validation_notice_description": "",
"color_json": "",
"plain_json": "",
"collapse_all": "",
"expand_all": "",
"log_level": "",
"clear_logs": "",
"reconnect": "",
"details": "",
"hide_details": "",
"copy": "",
"collapse_all": "",
"color_json": "",
"copied": "",
"copy": "",
"copy_request": "",
"copy_response": "",
"details": "",
"expand_all": "",
"hide_details": "",
"log_level": "",
"plain_json": "",
"provider_cdk": "",
"provider_dms": "",
"provider_fortellis": "",
"provider_pbs": "",
"provider_reynolds": "",
"reconnect": "",
"reconnected_export_service": "",
"refreshallocations": "",
"request_xml": "",
"response_xml": ""
"response_xml": "",
"rr_validation_message": "",
"rr_validation_notice_description": "",
"rr_validation_notice_title": "",
"transport_ws": "",
"transport_wss": ""
}
},
"documents": {

View File

@@ -231,13 +231,16 @@
"overall": ""
},
"disclaimer_title": "",
"feedback_placeholder": "",
"feedback_prompt": "",
"generic_failure": "",
"multipage": "",
"processing": "",
"scan": "",
"scancomplete": "",
"scanfailed": "",
"scanstarted": ""
"scanstarted": "",
"submit_feedback": ""
},
"bill_lines": "",
"bill_total": "",
@@ -1075,36 +1078,36 @@
"earlyrorequired.message": ""
},
"labels": {
"refreshallocations": "",
"provider_reynolds": "",
"provider_fortellis": "",
"provider_cdk": "",
"provider_pbs": "",
"provider_dms": "",
"transport_wss": "",
"transport_ws": "",
"banner_message": "",
"banner_status_connected": "",
"banner_status_disconnected": "",
"banner_message": "",
"reconnected_export_service": "",
"rr_validation_message": "",
"rr_validation_notice_title": "",
"rr_validation_notice_description": "",
"color_json": "",
"plain_json": "",
"collapse_all": "",
"expand_all": "",
"log_level": "",
"clear_logs": "",
"reconnect": "",
"details": "",
"hide_details": "",
"copy": "",
"collapse_all": "",
"color_json": "",
"copied": "",
"copy": "",
"copy_request": "",
"copy_response": "",
"details": "",
"expand_all": "",
"hide_details": "",
"log_level": "",
"plain_json": "",
"provider_cdk": "",
"provider_dms": "",
"provider_fortellis": "",
"provider_pbs": "",
"provider_reynolds": "",
"reconnect": "",
"reconnected_export_service": "",
"refreshallocations": "",
"request_xml": "",
"response_xml": ""
"response_xml": "",
"rr_validation_message": "",
"rr_validation_notice_description": "",
"rr_validation_notice_title": "",
"transport_ws": "",
"transport_wss": ""
}
},
"documents": {

0
localstack/init/10-bootstrap.sh Normal file → Executable file
View File

View File

@@ -0,0 +1,72 @@
const { isString } = require("lodash");
const { sendServerEmail } = require("../email/sendemail");
const logger = require("../utils/logger");
const { raw } = require("express");
const SUPPORT_EMAIL = "patrick@imexsystems.ca";
const safeJsonParse = (maybeJson) => {
if (!isString(maybeJson)) return null;
try {
return JSON.parse(maybeJson);
} catch {
return null;
}
};
const handleBillAiFeedback = async (req, res) => {
try {
const rating = req.body?.rating;
const comments = isString(req.body?.comments) ? req.body?.comments?.trim() : "";
const billFormValues = safeJsonParse(req.body?.billFormValues);
const rawAIData = safeJsonParse(req.body?.rawAIData);
const jobid = billFormValues?.jobid || billFormValues?.jobId || "unknown";
const subject = `Bill AI Feedback (${rating === "up" ? "+" : "-"}) jobid=${jobid}`;
const text = [
`User: ${req?.user?.email || "unknown"}`,
`Rating: ${rating}`,
comments ? `Comments: ${comments}` : "Comments: (none)",
"",
"Form Values (User):",
JSON.stringify(billFormValues, null, 4),
"",
"Raw AI Data:",
JSON.stringify(rawAIData, null, 4)
]
.filter(Boolean)
.join("\n");
const attachments = [];
if (req.file?.buffer) {
attachments.push({
filename: req.file.originalname || `bill-${jobid}.pdf`,
content: req.file.buffer,
contentType: req.file.mimetype || "application/pdf"
});
}
await sendServerEmail({
to: [SUPPORT_EMAIL],
subject,
type: "text",
text,
attachments
});
return res.json({ success: true });
} catch (error) {
logger.log("bill-ai-feedback-error", "ERROR", req?.user?.email, null, {
message: error?.message,
stack: error?.stack
});
return res.status(500).json({ message: "Failed to submit feedback" });
}
};
module.exports = {
handleBillAiFeedback
};

View File

@@ -212,7 +212,8 @@ async function processSinglePageDocument(pdfBuffer) {
return {
...processedData,
originalTextractResponse: result
//Removed as this is a large object that provides minimal value to send to client.
// originalTextractResponse: result
};
}
@@ -392,7 +393,8 @@ async function handleTextractNotification(message) {
status: 'COMPLETED',
data: {
...processedData,
originalTextractResponse: originalResponse
//Removed as this is a large object that provides minimal value to send to client.
// originalTextractResponse: originalResponse
},
completedAt: new Date().toISOString()
}

View File

@@ -44,7 +44,7 @@ const logEmail = async (req, email) => {
}
};
const sendServerEmail = async ({ subject, text, to = [] }) => {
const sendServerEmail = async ({ subject, text, to = [], attachments }) => {
if (process.env.NODE_ENV === undefined) return;
try {
@@ -57,6 +57,7 @@ const sendServerEmail = async ({ subject, text, to = [] }) => {
to: ["support@imexsystems.ca", ...to],
subject: subject,
text: text,
attachments: attachments,
ses: {
// optional extra arguments for SendRawEmail
Tags: [

View File

@@ -4,9 +4,14 @@ const multer = require("multer");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
const { handleBillOcr, handleBillOcrStatus } = require("../ai/bill-ocr/bill-ocr");
const { handleBillAiFeedback } = require("../ai/bill-ai-feedback");
// Configure multer for form data parsing
const upload = multer();
// Configure multer for form data parsing (memory storage)
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
router.use(validateFirebaseIdTokenMiddleware);
router.use(withUserGraphQLClientMiddleware);
@@ -14,4 +19,6 @@ router.use(withUserGraphQLClientMiddleware);
router.post("/bill-ocr", upload.single('billScan'), handleBillOcr);
router.get("/bill-ocr/status/:textractJobId", handleBillOcrStatus);
router.post("/bill-feedback", upload.single("billPdf"), handleBillAiFeedback);
module.exports = router;