diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel
index cfb312ab1..2449aeb52 100644
--- a/bodyshop_translations.babel
+++ b/bodyshop_translations.babel
@@ -2696,6 +2696,27 @@
+
+ oem_partno
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
quantity
false
@@ -3684,6 +3705,48 @@
+
+ feedback_placeholder
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ feedback_prompt
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
generic_failure
false
@@ -3831,6 +3894,27 @@
+
+ submit_feedback
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
@@ -8641,6 +8725,27 @@
+
+ manual-line
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
partsqueue
false
@@ -17816,6 +17921,468 @@
labels
+
+ banner_message
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ banner_status_connected
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ banner_status_disconnected
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ clear_logs
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ collapse_all
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ color_json
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ copied
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ copy
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ copy_request
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ copy_response
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ details
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ expand_all
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ hide_details
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ log_level
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ plain_json
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ provider_cdk
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ provider_dms
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ provider_fortellis
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ provider_pbs
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ provider_reynolds
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ reconnect
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ reconnected_export_service
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
refreshallocations
false
@@ -17837,6 +18404,153 @@
+
+ request_xml
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ response_xml
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ rr_validation_message
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ rr_validation_notice_description
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ rr_validation_notice_title
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ transport_ws
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ transport_wss
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
@@ -20590,6 +21304,27 @@
+
+ done
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
download
false
@@ -23009,6 +23744,27 @@
+
+ validationerror
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
view
false
@@ -23423,6 +24179,27 @@
validation
+
+ array
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
dateRangeExceeded
false
@@ -57452,6 +58229,27 @@
+
+ work_in_progress_labour_summary
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
work_in_progress_payables
false
@@ -57473,6 +58271,27 @@
+
+ work_in_progress_payables_summary
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
diff --git a/client/src/components/bill-ai-feedback/bill-ai-feedback.component.jsx b/client/src/components/bill-ai-feedback/bill-ai-feedback.component.jsx
new file mode 100644
index 000000000..0693937b1
--- /dev/null
+++ b/client/src/components/bill-ai-feedback/bill-ai-feedback.component.jsx
@@ -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 (
+
+ );
+}
+export default BillAiFeedback;
diff --git a/client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx b/client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx
index 453895b59..f4f9362d6 100644
--- a/client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx
+++ b/client/src/components/bill-enter-ai-scan/bill-enter-ai-scan.component.jsx
@@ -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({
diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
index a73b19663..734b401a6 100644
--- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
+++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
@@ -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}
/>
)}
@@ -471,7 +477,8 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
setLoading(false);
}}
footer={
-
+
+ {isAiScan && }
setGenerateLabel(e.target.checked)}>
{t("bills.labels.generatepartslabel")}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index e216b9cd5..a24453ae0 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -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": {
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 50b336d37..f16ca40e5 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -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": {
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 727d689f8..8004d5e6b 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -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": {
diff --git a/localstack/init/10-bootstrap.sh b/localstack/init/10-bootstrap.sh
old mode 100644
new mode 100755
diff --git a/server/ai/bill-ai-feedback.js b/server/ai/bill-ai-feedback.js
new file mode 100644
index 000000000..e136c60f4
--- /dev/null
+++ b/server/ai/bill-ai-feedback.js
@@ -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
+};
diff --git a/server/ai/bill-ocr/bill-ocr.js b/server/ai/bill-ocr/bill-ocr.js
index 26e77033a..6c1101207 100644
--- a/server/ai/bill-ocr/bill-ocr.js
+++ b/server/ai/bill-ocr/bill-ocr.js
@@ -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()
}
diff --git a/server/email/sendemail.js b/server/email/sendemail.js
index e39a2093c..bcef44c0a 100644
--- a/server/email/sendemail.js
+++ b/server/email/sendemail.js
@@ -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: [
diff --git a/server/routes/aiRoutes.js b/server/routes/aiRoutes.js
index f06125718..b92b1b69d 100644
--- a/server/routes/aiRoutes.js
+++ b/server/routes/aiRoutes.js
@@ -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;