From 794f64dfba45db14d4607463c0603a90e2039fbf Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 30 Mar 2026 11:41:24 -0700 Subject: [PATCH] Add custom document signing. --- .../esignature-custom-document.component.jsx | 87 +++++++ .../esignature-modal.container.jsx | 14 +- .../print-center-item.component.jsx | 4 +- .../print-center-jobs.component.jsx | 2 + .../jobs-detail.page.component.jsx | 2 + client/src/translations/en_us/common.json | 7 +- client/src/translations/es/common.json | 7 +- client/src/translations/fr/common.json | 7 +- server/esign/esign-new.js | 231 ++++++++++++------ server/routes/esignRoutes.js | 10 +- 10 files changed, 284 insertions(+), 87 deletions(-) create mode 100644 client/src/components/esignature-custom-document/esignature-custom-document.component.jsx diff --git a/client/src/components/esignature-custom-document/esignature-custom-document.component.jsx b/client/src/components/esignature-custom-document/esignature-custom-document.component.jsx new file mode 100644 index 000000000..b0c09aeea --- /dev/null +++ b/client/src/components/esignature-custom-document/esignature-custom-document.component.jsx @@ -0,0 +1,87 @@ +import { UploadOutlined } from "@ant-design/icons"; +import { Button, Upload } from "antd"; +import axios from "axios"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = (dispatch) => ({ + setEsignatureContext: (context) => + dispatch( + setModalContext({ + context, + modal: "esignature" + }) + ) +}); + +export function EsignatureCustomDocument({ bodyshop, jobId, setEsignatureContext }) { + const [loading, setLoading] = useState(false); + const notification = useNotification(); + const { t } = useTranslation(); + + const uploadCustomDocument = async ({ file, onError, onSuccess }) => { + const formData = new FormData(); + formData.append("document", file); + formData.append("jobid", jobId); + formData.append("bodyshop", JSON.stringify(bodyshop)); + + setLoading(true); + + try { + const { + data: { token, documentId, envelopeId } + } = await axios.post("/esign/new-custom", formData, { + headers: { + "Content-Type": "multipart/form-data" + } + }); + + setEsignatureContext({ context: { token, documentId, envelopeId, jobid: jobId } }); + onSuccess?.({ token, documentId, envelopeId }); + } catch (error) { + notification.error({ + title: t("esignature.errors.upload_title"), + description: error?.response?.data?.error || error?.response?.data?.message || error.message + }); + onError?.(error); + } finally { + setLoading(false); + } + }; + + return ( + { + if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) { + return true; + } + + notification.error({ + title: t("esignature.errors.upload_title"), + description: t("esignature.errors.pdf_only") + }); + return Upload.LIST_IGNORE; + }} + customRequest={uploadCustomDocument} + maxCount={1} + showUploadList={false} + multiple={false} + > + + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(EsignatureCustomDocument); diff --git a/client/src/components/esignature-modal/esignature-modal.container.jsx b/client/src/components/esignature-modal/esignature-modal.container.jsx index 97d7c88e8..58460d4d3 100644 --- a/client/src/components/esignature-modal/esignature-modal.container.jsx +++ b/client/src/components/esignature-modal/esignature-modal.container.jsx @@ -31,16 +31,15 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, onOk={async () => { try { setDistributing(true); - const distResult = await axios.post("/esign/distribute", { + await axios.post("/esign/distribute", { documentId, envelopeId, jobid, bodyshopid: bodyshop.id }); - console.log("Distribution result:", distResult); + toggleModalVisible(); } catch (error) { - console.error("Error distributing document:", error); notification.error({ message: t("esignature.distribute_error"), description: error?.response?.data?.message || error.message @@ -50,14 +49,13 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, }} onCancel={async () => { try { - const cancelResult = await axios.post("/esign/delete", { + await axios.post("/esign/delete", { documentId, envelopeId }); - console.log("Cancel result:", cancelResult); + toggleModalVisible(); } catch (error) { - console.error("Error cancelling document:", error); notification.error({ message: t("esignature.cancel_error"), description: error?.response?.data?.message || error.message @@ -67,7 +65,7 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, okButtonProps={{ loading: distributing }} okText={t("esignature.actions.distribute")} destroyOnHidden - width={800} + width={"80%"} >
{token ? ( @@ -78,7 +76,7 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, externalId={`${jobid}|${currentUser?.email}`} className="esignature-embed" onDocumentUpdated={(data) => { - console.log("Document updated:", data.documentId); + console.log("Document updated:", data); }} /> ) : ( diff --git a/client/src/components/print-center-item/print-center-item.component.jsx b/client/src/components/print-center-item/print-center-item.component.jsx index 191f6b811..2cfa1b60c 100644 --- a/client/src/components/print-center-item/print-center-item.component.jsx +++ b/client/src/components/print-center-item/print-center-item.component.jsx @@ -61,7 +61,7 @@ export function PrintCenterItemComponent({ setLoading(true); try { const { - data: { token, documentId, evnelopeId } + data: { token, documentId, envelopeId } } = await axios.post("/esign/new", { name: item.key, jobid: id, @@ -73,7 +73,7 @@ export function PrintCenterItemComponent({ } }); - setEsignatureContext({ context: { token, documentId, evnelopeId, jobid: id } }); + setEsignatureContext({ context: { token, documentId, envelopeId, jobid: id } }); } catch (error) { console.log(error); } finally { diff --git a/client/src/components/print-center-jobs/print-center-jobs.component.jsx b/client/src/components/print-center-jobs/print-center-jobs.component.jsx index a33a387e1..e8e665743 100644 --- a/client/src/components/print-center-jobs/print-center-jobs.component.jsx +++ b/client/src/components/print-center-jobs/print-center-jobs.component.jsx @@ -9,6 +9,7 @@ import { selectPrintCenter } from "../../redux/modals/modals.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { TemplateList } from "../../utils/TemplateConstants"; import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component"; +import EsignatureCustomDocument from "../esignature-custom-document/esignature-custom-document.component"; import PrintCenterItem from "../print-center-item/print-center-item.component"; import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component"; import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component"; @@ -94,6 +95,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia extra={ + setSearch(e.target.value)} value={search} enterButton /> diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index ea7adf655..fe41869a6 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -20,6 +20,7 @@ import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa import { connect } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import EsignatureCustomDocument from "../../components/esignature-custom-document/esignature-custom-document.component.jsx"; import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component"; import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component"; import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container"; @@ -285,6 +286,7 @@ export function JobsDetailPage({ > {t("general.labels.refresh")} +