204 lines
7.0 KiB
JavaScript
204 lines
7.0 KiB
JavaScript
import { Button, Tag, Modal, Typography } from "antd";
|
|
import axios from "axios";
|
|
import { useState } from "react";
|
|
import { FaWandMagicSparkles } from "react-icons/fa6";
|
|
import { connect } from "react-redux";
|
|
import { createStructuredSelector } from "reselect";
|
|
import { useNotification } from "../../contexts/Notifications/notificationContext";
|
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
billEnterModal: selectBillEnterModal,
|
|
bodyshop: selectBodyshop
|
|
});
|
|
|
|
function BillEnterAiScan({
|
|
billEnterModal,
|
|
bodyshop,
|
|
pollingIntervalRef,
|
|
setPollingIntervalRef,
|
|
form,
|
|
fileInputRef,
|
|
scanLoading,
|
|
setScanLoading,
|
|
setIsAiScan
|
|
}) {
|
|
const notification = useNotification();
|
|
const { t } = useTranslation();
|
|
const [showBetaModal, setShowBetaModal] = useState(false);
|
|
const BETA_ACCEPTANCE_KEY = "ai_scan_beta_acceptance";
|
|
const handleBetaAcceptance = () => {
|
|
localStorage.setItem(BETA_ACCEPTANCE_KEY, "true");
|
|
setShowBetaModal(false);
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const checkBetaAcceptance = () => {
|
|
const hasAccepted = localStorage.getItem(BETA_ACCEPTANCE_KEY);
|
|
if (hasAccepted) {
|
|
fileInputRef.current?.click();
|
|
} else {
|
|
setShowBetaModal(true);
|
|
}
|
|
};
|
|
|
|
// Polling function for multipage PDF status
|
|
const pollJobStatus = async (textractJobId) => {
|
|
try {
|
|
const { data } = await axios.get(`/ai/bill-ocr/status/${textractJobId}`);
|
|
|
|
if (data.status === "COMPLETED") {
|
|
// Stop polling
|
|
if (pollingIntervalRef.current) {
|
|
clearInterval(pollingIntervalRef.current);
|
|
setPollingIntervalRef(null);
|
|
}
|
|
setScanLoading(false);
|
|
|
|
// Update form with the extracted data
|
|
if (data?.data?.billForm) {
|
|
form.setFieldsValue(data.data.billForm);
|
|
await form.validateFields(["billlines"], { recursive: true });
|
|
notification.success({
|
|
title: t("bills.labels.ai.scancomplete")
|
|
});
|
|
}
|
|
} else if (data.status === "FAILED") {
|
|
// Stop polling on failure
|
|
if (pollingIntervalRef.current) {
|
|
clearInterval(pollingIntervalRef.current);
|
|
setPollingIntervalRef(null);
|
|
}
|
|
setScanLoading(false);
|
|
|
|
notification.error({
|
|
title: t("bills.labels.ai.scanfailed"),
|
|
description: data.error || ""
|
|
});
|
|
}
|
|
// If status is IN_PROGRESS, continue polling
|
|
} catch (error) {
|
|
// Stop polling on error
|
|
if (pollingIntervalRef.current) {
|
|
clearInterval(pollingIntervalRef.current);
|
|
setPollingIntervalRef(null);
|
|
}
|
|
setScanLoading(false);
|
|
|
|
notification.error({
|
|
title: t("bills.labels.ai.scanfailed"),
|
|
description: error.response?.data?.message || error.message || "Failed to check scan status"
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*,application/pdf"
|
|
style={{ display: "none" }}
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
setScanLoading(true);
|
|
setIsAiScan(true);
|
|
const formdata = new FormData();
|
|
formdata.append("billScan", file);
|
|
formdata.append("jobid", billEnterModal.context.job?.id);
|
|
formdata.append("bodyshopid", bodyshop.id);
|
|
formdata.append("partsorderid", billEnterModal.context.parts_order?.id);
|
|
|
|
try {
|
|
const { data, status } = await axios.post("/ai/bill-ocr", formdata);
|
|
|
|
// Add the scanned file to the upload field
|
|
const currentUploads = form.getFieldValue("upload") || [];
|
|
form.setFieldValue("upload", [
|
|
...currentUploads,
|
|
{
|
|
uid: `ai-scan-${Date.now()}`,
|
|
name: file.name,
|
|
originFileObj: file,
|
|
status: "done"
|
|
}
|
|
]);
|
|
if (status === 202) {
|
|
// Multipage PDF - start polling
|
|
notification.info({
|
|
title: t("bills.labels.ai.scanstarted"),
|
|
description: t("bills.labels.ai.multipage")
|
|
});
|
|
|
|
//Workaround needed to bypass react-compiler error about manipulating refs in child components. Refactor may be needed in the future to clean this up.
|
|
setPollingIntervalRef(
|
|
setInterval(() => {
|
|
pollJobStatus(data.textractJobId);
|
|
}, 3000)
|
|
);
|
|
|
|
// Initial poll
|
|
pollJobStatus(data.textractJobId);
|
|
} else if (status === 200) {
|
|
// Single page - immediate response
|
|
setScanLoading(false);
|
|
|
|
form.setFieldsValue(data.data.billForm);
|
|
await form.validateFields(["billlines"], { recursive: true });
|
|
|
|
notification.success({
|
|
title: t("bills.labels.ai.scancomplete")
|
|
});
|
|
}
|
|
} catch (error) {
|
|
setScanLoading(false);
|
|
notification.error({
|
|
title: t("bills.labels.ai.scanfailed"),
|
|
description: error.response?.data?.message || error.message || t("bills.labels.ai.generic_failure")
|
|
});
|
|
}
|
|
}
|
|
// Reset the input so the same file can be selected again
|
|
e.target.value = "";
|
|
}}
|
|
/>
|
|
|
|
<Button onClick={checkBetaAcceptance} icon={<FaWandMagicSparkles />} loading={scanLoading} disabled={scanLoading}>
|
|
{scanLoading ? t("bills.labels.ai.processing") : t("bills.labels.ai.scan")}
|
|
<Tag color="red">{t("general.labels.beta")}</Tag>
|
|
</Button>
|
|
|
|
<Modal
|
|
title={t("bills.labels.ai.disclaimer_title")}
|
|
open={showBetaModal}
|
|
onOk={handleBetaAcceptance}
|
|
onCancel={() => setShowBetaModal(false)}
|
|
okText={t("bills.labels.ai.accept_and_continue")}
|
|
cancelText={t("general.actions.cancel")}
|
|
>
|
|
{
|
|
//This is explicitly not translated.
|
|
}
|
|
<Typography.Text>
|
|
This AI scanning feature is currently in <strong>beta</strong>. While it can accelerate data entry, you{" "}
|
|
<strong>must carefully review all extracted results</strong> for accuracy.
|
|
</Typography.Text>
|
|
<Typography.Text>The AI may make mistakes or miss information. Always verify:</Typography.Text>
|
|
<ul>
|
|
<li>All line items and quantities</li>
|
|
<li>Prices and totals</li>
|
|
<li>Part numbers and descriptions</li>
|
|
<li>Any other critical invoice details</li>
|
|
</ul>
|
|
<Typography.Text>
|
|
By continuing, you acknowledge that you will review and verify all AI-generated data before posting.
|
|
</Typography.Text>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
export default connect(mapStateToProps, null)(BillEnterAiScan);
|