diff --git a/apps/OpenSign/public/locales/de/translation.json b/apps/OpenSign/public/locales/de/translation.json index e72a6d6b4e..e22aec5b4f 100644 --- a/apps/OpenSign/public/locales/de/translation.json +++ b/apps/OpenSign/public/locales/de/translation.json @@ -152,7 +152,8 @@ "Duplicate Template": "Vorlage duplizieren", "Duplicate": "Duplikat", "daily-mail-quota": "Tägliches E-Mail-Kontingent", - "Save as template": "Als Vorlage speichern" + "Save as template": "Als Vorlage speichern", + "Fix & resend": "Korrigieren und erneut senden" }, "report-heading": { "Sr.No": "Nr.", @@ -881,5 +882,10 @@ "contact-billing-at-opensign": "Um weitere Plätze hinzuzufügen, kontaktieren Sie bitte OpenSign™ unter <1>billing@opensignlabs.com1> für Unterstützung.", "title-length-alert": "Der Titel darf höchstens 250 Zeichen lang sein.", "note-length-alert": "Die Notiz darf höchstens 200 Zeichen lang sein.", - "description-length-alert": "Die Beschreibung darf höchstens 500 Zeichen lang sein." + "description-length-alert": "Die Beschreibung darf höchstens 500 Zeichen lang sein.", + "fix-&-resend-document": "Dokument korrigieren und erneut senden", + "do-you-want-recreate-document?": "Dadurch wird ein Entwurf aus diesem Dokument mit allen vorhandenen Feldern erstellt. Sind Sie sicher, dass Sie dieses Dokument neu erstellen möchten?", + "start-editing": "Bearbeitung starten", + "unsaved-changes-discard-them?": "Sie haben ungespeicherte Änderungen. Verwerfen?", + "yes-discard": "Ja, verwerfen" } diff --git a/apps/OpenSign/public/locales/en/translation.json b/apps/OpenSign/public/locales/en/translation.json index 2b2c3cd8e5..79b2635a82 100644 --- a/apps/OpenSign/public/locales/en/translation.json +++ b/apps/OpenSign/public/locales/en/translation.json @@ -152,7 +152,8 @@ "Duplicate Template": "Duplicate template", "Duplicate": "Duplicate", "daily-mail-quota": "Daily Email Quota", - "Save as template": "Save as template" + "Save as template": "Save as template", + "Fix & resend": "Fix & Resend" }, "report-heading": { "Sr.No": "Sr.No", @@ -881,5 +882,10 @@ "contact-billing-at-opensign": "To add more seats, please contact OpenSign™ at <1>billing@opensignlabs.com1> for assistance", "title-length-alert": "Title must be at most 250 characters long.", "note-length-alert": "Note must be at most 200 characters long.", - "description-length-alert": "Description must be at most 500 characters long." + "description-length-alert": "Description must be at most 500 characters long.", + "fix-&-resend-document": "Fix & Resend Document", + "do-you-want-recreate-document?": "This will create a draft from this document with all fields intact. Are you sure you want to recreate this document?", + "start-editing": "Start Editing", + "unsaved-changes-discard-them?": "You have unsaved changes. Discard them?", + "yes-discard": "Yes, Discard" } diff --git a/apps/OpenSign/public/locales/es/translation.json b/apps/OpenSign/public/locales/es/translation.json index c9605eb4b3..64bcb350f7 100644 --- a/apps/OpenSign/public/locales/es/translation.json +++ b/apps/OpenSign/public/locales/es/translation.json @@ -152,7 +152,8 @@ "Duplicate Template": "Plantilla duplicada", "Duplicate": "Duplicada", "daily-mail-quota": "Cuota diaria de correos electrónicos", - "Save as template": "Guardar como plantilla" + "Save as template": "Guardar como plantilla", + "Fix & resend": "Corregir y reenviar" }, "report-heading": { "Sr.No": "Nº", @@ -881,5 +882,10 @@ "contact-billing-at-opensign": "Para agregar más asientos, comuníquese con OpenSign™ a <1>billing@opensignlabs.com1> para obtener ayuda.", "title-length-alert": "El título debe tener como máximo 250 caracteres.", "note-length-alert": "La nota debe tener como máximo 200 caracteres.", - "description-length-alert": "La descripción debe tener como máximo 500 caracteres." + "description-length-alert": "La descripción debe tener como máximo 500 caracteres.", + "fix-&-resend-document": "Corregir y reenviar el documento", + "do-you-want-recreate-document?": "Esto creará un borrador a partir de este documento con todos los campos intactos. ¿Está seguro de que desea recrear este documento?", + "start-editing": "Comenzar a editar", + "unsaved-changes-discard-them?": "Tienes cambios sin guardar. ¿Deseas descartarlos?", + "yes-discard": "Sí, descartar" } diff --git a/apps/OpenSign/public/locales/fr/translation.json b/apps/OpenSign/public/locales/fr/translation.json index 7459aa7d92..e1b1883caa 100644 --- a/apps/OpenSign/public/locales/fr/translation.json +++ b/apps/OpenSign/public/locales/fr/translation.json @@ -173,7 +173,8 @@ "Duplicate Template": "dupliquer le modèle", "Duplicate": "Double", "daily-mail-quota": "Quota d'e-mails quotidien", - "Save as template": "Enregistrer comme modèle" + "Save as template": "Enregistrer comme modèle", + "Fix & resend": "Corriger et renvoyer" }, "report-help": { "Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.", @@ -881,5 +882,10 @@ "contact-billing-at-opensign": "Pour ajouter plus de places, veuillez contacter OpenSign™ à l'adresse <1>billing@opensignlabs.com1> pour obtenir de l'aide.", "title-length-alert": "Le titre doit comporter au maximum 250 caractères.", "note-length-alert": "La note doit comporter au maximum 200 caractères.", - "description-length-alert": "La description doit comporter au maximum 500 caractères" + "description-length-alert": "La description doit comporter au maximum 500 caractères", + "fix-&-resend-document": "Corriger et renvoyer le document", + "do-you-want-recreate-document?": "Cela créera un brouillon à partir de ce document avec tous les champs intacts. Êtes-vous sûr de vouloir recréer ce document ?", + "start-editing": "Commencer l'édition", + "unsaved-changes-discard-them?": "Vous avez des modifications non enregistrées. Les supprimer ?", + "yes-discard": "Oui, supprimer" } diff --git a/apps/OpenSign/public/locales/it/translation.json b/apps/OpenSign/public/locales/it/translation.json index ea2f9e4dfc..2d81a2fad7 100644 --- a/apps/OpenSign/public/locales/it/translation.json +++ b/apps/OpenSign/public/locales/it/translation.json @@ -152,7 +152,8 @@ "Duplicate Template": "Duplica modello", "Duplicate": "Duplica", "daily-mail-quota": "Quota e-mail giornaliera", - "Save as template": "Salva come modello" + "Save as template": "Salva come modello", + "Fix & resend": "Correggi e reinvia" }, "report-heading": { "Sr.No": "Nr.", @@ -881,5 +882,10 @@ "contact-billing-at-opensign": " Per aggiungere altri posti, contattare OpenSign™ all'indirizzo <1>billing@opensignlabs.com1> per assistenza.", "title-length-alert": "Il titolo può contenere al massimo 250 caratteri.", "note-length-alert": "La nota può contenere al massimo 200 caratteri.", - "description-length-alert": " La descrizione può contenere al massimo 500 caratteri." + "description-length-alert": " La descrizione può contenere al massimo 500 caratteri.", + "fix-&-resend-document": "Correggi e reinvia il documento", + "do-you-want-recreate-document?": "Questo creerà una bozza da questo documento con tutti i campi intatti. Sei sicuro di voler ricreare questo documento?", + "start-editing": "Inizia a modificare", + "unsaved-changes-discard-them?": "Hai modifiche non salvate. Vuoi scartarle?", + "yes-discard": "Sì, scarta" } diff --git a/apps/OpenSign/src/assets/images/recreatedoc.png b/apps/OpenSign/src/assets/images/recreatedoc.png new file mode 100644 index 0000000000..3348345cc9 Binary files /dev/null and b/apps/OpenSign/src/assets/images/recreatedoc.png differ diff --git a/apps/OpenSign/src/components/pdf/EditTemplate.js b/apps/OpenSign/src/components/pdf/EditTemplate.js index 9fd9660d5b..7212886ecf 100644 --- a/apps/OpenSign/src/components/pdf/EditTemplate.js +++ b/apps/OpenSign/src/components/pdf/EditTemplate.js @@ -1,5 +1,10 @@ -import React, { useState } from "react"; -import { getFileName } from "../../constant/Utils"; +import React, { useState, useRef } from "react"; +import { + base64ToArrayBuffer, + convertBase64ToFile, + generatePdfName, + getFileName +} from "../../constant/Utils"; import { maxDescriptionLength, maxNoteLength, @@ -8,10 +13,22 @@ import { import { useTranslation } from "react-i18next"; import { Tooltip } from "react-tooltip"; import SignersInput from "../shared/fields/SignersInput"; +import { PDFDocument } from "pdf-lib"; +import ModalUi from "../../primitives/ModalUi"; +import { SaveFileSize } from "../../constant/saveFileSize"; -const EditTemplate = ({ template, onSuccess }) => { +const EditTemplate = ({ + title, + handleClose, + pdfbase64, + template, + onSuccess, + setPdfArrayBuffer, + setPdfBase64Url +}) => { const appName = "OpenSign™"; const { t } = useTranslation(); + const inputFileRef = useRef(null); const [formData, setFormData] = useState({ Name: template?.Name || "", Note: template?.Note || "", @@ -32,6 +49,31 @@ const EditTemplate = ({ template, onSuccess }) => { AllowModifications: template?.AllowModifications || false, TimeToCompleteDays: template?.TimeToCompleteDays || 15 }); + const [isUpdate, setIsUpdate] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const [uploadPdf, setUploadPdf] = useState({ + name: "", + base64: "", + url: "" + }); + const handleDrop = (e) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + handleFile(file); + }; + + const handleFile = (file) => { + if (file && file.type === "application/pdf") { + handleReplaceFileValdition(file); + // You can handle the file here + } else { + alert("Only pdf files are allowed."); + if (inputFileRef.current) inputFileRef.current.value = ""; + } + }; + const handleDragOver = (e) => { + e.preventDefault(); + }; // `isValidURL` is used to check valid webhook url function isValidURL(value) { @@ -44,9 +86,63 @@ const EditTemplate = ({ template, onSuccess }) => { } const handleStrInput = (e) => { + setIsUpdate(true); setFormData({ ...formData, [e.target.name]: e.target.value }); }; + const getPdfMetadataHash = async (pdfBytes) => { + const pdfDoc = await PDFDocument.load(pdfBytes); + const pages = pdfDoc.getPages(); + const metaString = pages + .map((page, index) => { + const { width, height } = page.getSize(); + return `${index + 1}:${Math.round(width)}x${Math.round(height)}`; + }) + .join("|"); + const encoder = new TextEncoder(); + const data = encoder.encode(metaString); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + }; + + const handleFileInput = async (e) => { + const file = e.target.files[0]; + if (!file) return; + handleReplaceFileValdition(file); + }; + const handleReplaceFileValdition = async (file) => { + try { + const basePdfBytes = base64ToArrayBuffer(pdfbase64); + const expectedHash = await getPdfMetadataHash(basePdfBytes); + const fileReader = new FileReader(); + fileReader.onload = async (event) => { + const uploadedPdfBytes = event.target.result; + const uploadedHash = await getPdfMetadataHash(uploadedPdfBytes); + + if (expectedHash === uploadedHash) { + const arrayBuffer = uploadedPdfBytes; + const uint8Array = new Uint8Array(arrayBuffer); + const binaryString = Array.from(uint8Array) + .map((b) => String.fromCharCode(b)) + .join(""); + const base64 = btoa(binaryString); + const pdfName = generatePdfName(16); + setIsUpdate(true); + setUploadPdf((prev) => ({ ...prev, name: pdfName, base64: base64 })); + // alert("✅ PDFs match (based on page number, width, height)"); + } else { + alert("❌ PDF do NOT match based on page number, width, height"); + if (inputFileRef.current) inputFileRef.current.value = ""; + } + }; + fileReader.readAsArrayBuffer(file); + } catch (err) { + alert("Error: " + err.message); + if (inputFileRef.current) inputFileRef.current.value = ""; + } + }; // Define a function to handle form submission const handleSubmit = async (e) => { e.preventDefault(); @@ -67,6 +163,19 @@ const EditTemplate = ({ template, onSuccess }) => { alert(t("description-length-alert")); return; } + let pdfUrl; + if (uploadPdf?.base64) { + pdfUrl = await convertBase64ToFile(uploadPdf.name, uploadPdf.base64); + setUploadPdf((prev) => ({ ...prev, url: pdfUrl })); + const pdfBuffer = base64ToArrayBuffer(uploadPdf.base64); + setPdfArrayBuffer && setPdfArrayBuffer(pdfBuffer); + setPdfBase64Url && setPdfBase64Url(uploadPdf.base64); + const tenantId = + localStorage.getItem("TenantId") || + template?.ExtUserPtr?.TenantId?.objectId; + const buffer = atob(uploadPdf.base64); + SaveFileSize(buffer.length, pdfUrl, tenantId); + } const isChecked = formData.SendinOrder === "true" ? true : false; const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true; const AutoReminder = formData?.AutomaticReminders || false; @@ -88,6 +197,7 @@ const EditTemplate = ({ template, onSuccess }) => { } const data = { ...formData, + ...(pdfUrl ? { URL: pdfUrl } : {}), SendinOrder: isChecked, IsEnableOTP: IsEnableOTP, IsTourEnabled: isTourEnabled, @@ -99,6 +209,7 @@ const EditTemplate = ({ template, onSuccess }) => { // `handleNotifySignChange` is trigger when user change radio of notify on signatures const handleNotifySignChange = (value) => { + setIsUpdate(true); setFormData((obj) => ({ ...obj, NotifyOnSignatures: value })); }; const handleBcc = (data) => { @@ -108,226 +219,304 @@ const EditTemplate = ({ template, onSuccess }) => { Name: item?.label, Email: item?.email })); + setIsUpdate(true); setFormData((prev) => ({ ...prev, Bcc: trimEmail })); } }; + + const handleEditTemplateClose = () => { + if (isUpdate) { + setShowConfirm(true); + } else { + handleClose(); + } + }; + const discardChanges = () => { + setShowConfirm(false); + handleClose(); + }; return ( -