Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3587-Commision-Cut-clean
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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";
|
||||
import { connect } from "react-redux";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
function BillAiFeedback({ billForm, rawAIData, bodyshop }) {
|
||||
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));
|
||||
formData.append("shopname", bodyshop?.shopname || "");
|
||||
|
||||
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 wrap align="top" size="small">
|
||||
<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>
|
||||
|
||||
<Space wrap size="small" orientation="vertical">
|
||||
<Form.Item name="comments">
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
style={{ minWidth: "400px" }}
|
||||
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>
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(BillAiFeedback);
|
||||
@@ -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({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useApolloClient, useMutation } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { Button, Checkbox, Form, Modal, Space } from "antd";
|
||||
import { Button, Checkbox, Divider, Form, Modal, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -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,26 +477,34 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
setLoading(false);
|
||||
}}
|
||||
footer={
|
||||
<Space>
|
||||
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
|
||||
{t("bills.labels.generatepartslabel")}
|
||||
</Checkbox>
|
||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||
<Button loading={loading} onClick={() => form.submit()} id="save-bill-enter-modal">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setEnterAgain(true);
|
||||
}}
|
||||
id="save-and-new-bill-enter-modal"
|
||||
>
|
||||
{t("general.actions.saveandnew")}
|
||||
</Button>
|
||||
<Space orientation="vertical">
|
||||
{isAiScan && (
|
||||
<>
|
||||
<BillAiFeedback billForm={form} rawAIData={rawAIData} />
|
||||
<Divider orientation="horizontal" />
|
||||
</>
|
||||
)}
|
||||
<Space wrap align="top">
|
||||
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
|
||||
{t("bills.labels.generatepartslabel")}
|
||||
</Checkbox>
|
||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||
<Button loading={loading} onClick={() => form.submit()} id="save-bill-enter-modal">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setEnterAgain(true);
|
||||
}}
|
||||
id="save-and-new-bill-enter-modal"
|
||||
>
|
||||
{t("general.actions.saveandnew")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
}
|
||||
destroyOnHidden
|
||||
|
||||
@@ -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",
|
||||
@@ -1076,36 +1079,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": {
|
||||
|
||||
@@ -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": "",
|
||||
@@ -1076,36 +1079,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": {
|
||||
|
||||
@@ -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": "",
|
||||
@@ -1076,36 +1079,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": {
|
||||
|
||||
Reference in New Issue
Block a user