mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-20 06:35:54 +02:00
v2.18.0
New Features: 1. Added support to recreate a new document from declined document without having to redo everything from scratch. 2. Added replace pdf file feature for Templates. PDF underlying a Template can now be replaced while keeping all the fields the same. 3. Introduced the ability to replace pdf file for in UpdateTemplate and CreateDocumentfromtemplate API. 4. Revamped the preferences menu UI. Bug Fixes: 1. Fixed issue with capital letters in contact emails via API. 2. Better validations & error handling in APIs
This commit is contained in:
@@ -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.com</1> 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"
|
||||
}
|
||||
|
||||
@@ -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.com</1> 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"
|
||||
}
|
||||
|
||||
@@ -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.com</1> 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"
|
||||
}
|
||||
|
||||
@@ -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.com</1> 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"
|
||||
}
|
||||
|
||||
@@ -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.com</1> 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"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -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 (
|
||||
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
|
||||
<div className="text-base-content">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
{t("report-heading.File")}
|
||||
</label>
|
||||
<div className="op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs">
|
||||
{getFileName(template.URL)}
|
||||
</div>
|
||||
<ModalUi
|
||||
isOpen
|
||||
title={isUpdate ? `${title} (unsaved)` : title}
|
||||
handleClose={handleEditTemplateClose}
|
||||
>
|
||||
<ModalUi isOpen={showConfirm} showClose={false}>
|
||||
<div className="p-[20px]">
|
||||
<p className="text-base font-normal text-base-content py-[5px] md:py-[6px] px-[5px]">
|
||||
{t("unsaved-changes-discard-them?")}
|
||||
</p>
|
||||
<div className="flex items-center mt-2.5 gap-2 md:gap-3 text-white">
|
||||
<button
|
||||
className="op-btn op-btn-primary px-6"
|
||||
onClick={discardChanges}
|
||||
>
|
||||
{t("yes-discard")}
|
||||
</button>
|
||||
<button
|
||||
className="op-btn op-btn-secondary px-4 md:px-6"
|
||||
onClick={() => setShowConfirm(false)}
|
||||
>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
{t("Title")}
|
||||
<span className="text-[13px] text-[red]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Name"
|
||||
value={formData.Name}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Note" className="text-[13px]">
|
||||
{t("report-heading.Note")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Note"
|
||||
id="Note"
|
||||
value={formData.Note}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Description" className="text-[13px]">
|
||||
{t("description")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Description"
|
||||
id="Description"
|
||||
value={formData.Description}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label className="text-[13px]">{t("send-in-order")}</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"true"}
|
||||
className="op-radio op-radio-xs"
|
||||
name="SendinOrder"
|
||||
checked={formData.SendinOrder === "true"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">{t("yes")}</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
|
||||
<div className="text-base-content">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
{t("report-heading.File")}
|
||||
</label>
|
||||
<div
|
||||
className="border-[1.5px] border-dashed border-gray-300 rounded-lg px-4 py-6 text-center text-gray-500 bg-white cursor-pointer hover:border-base-content transition"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onClick={() => inputFileRef?.current?.click()}
|
||||
>
|
||||
<label
|
||||
htmlFor="fileUpload"
|
||||
className="cursor-pointer text-center mb-0"
|
||||
>
|
||||
Browse or drag & drop a new file to replace the existng one
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"false"}
|
||||
name="SendinOrder"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={formData.SendinOrder === "false"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">{t("no")}</div>
|
||||
<input
|
||||
ref={inputFileRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="application/pdf"
|
||||
onChange={(e) => handleFileInput(e)}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
{uploadPdf?.name && (
|
||||
<div
|
||||
onClick={() => inputFileRef?.current?.click()}
|
||||
className="mt-2 cursor-pointer op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs"
|
||||
>
|
||||
selected:{" "}
|
||||
{uploadPdf?.url
|
||||
? `${uploadPdf?.name}.pdf`
|
||||
: getFileName(template.URL)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
{t("Title")}
|
||||
<span className="text-[13px] text-[red]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Name"
|
||||
value={formData.Name}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Note" className="text-[13px]">
|
||||
{t("report-heading.Note")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Note"
|
||||
id="Note"
|
||||
value={formData.Note}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Description" className="text-[13px]">
|
||||
{t("description")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Description"
|
||||
id="Description"
|
||||
value={formData.Description}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label className="text-[13px]">{t("send-in-order")}</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"true"}
|
||||
className="op-radio op-radio-xs"
|
||||
name="SendinOrder"
|
||||
checked={formData.SendinOrder === "true"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">{t("yes")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"false"}
|
||||
name="SendinOrder"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={formData.SendinOrder === "false"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<label className="block">
|
||||
<span>
|
||||
{t("enable-tour")}
|
||||
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
|
||||
<div className="text-xs mt-3">
|
||||
<label className="block">
|
||||
<span>
|
||||
{t("enable-tour")}
|
||||
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>{" "}
|
||||
</span>
|
||||
<Tooltip id="istourenabled-tooltip" className="z-50">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("enable-tour")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">{t("yes")}: </span>
|
||||
<span>{t("istourenabled-help.p1")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">{t("no")}: </span>
|
||||
<span>{t("istourenabled-help.p2")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={"true"}
|
||||
className="op-radio op-radio-xs"
|
||||
name="IsTourEnabled"
|
||||
checked={formData.IsTourEnabled === "true"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={"false"}
|
||||
name="IsTourEnabled"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={formData.IsTourEnabled === "false"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<label>
|
||||
{t("notify-on-signatures")}
|
||||
<a data-tooltip-id="nos-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>{" "}
|
||||
</span>
|
||||
<Tooltip id="istourenabled-tooltip" className="z-50">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("enable-tour")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">{t("yes")}: </span>
|
||||
<span>{t("istourenabled-help.p1")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">{t("no")}: </span>
|
||||
<span>{t("istourenabled-help.p2")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<Tooltip id="nos-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
|
||||
<p className="font-bold">{t("notify-on-signatures")}</p>
|
||||
<p>{t("notify-on-signatures-help.p1")}</p>
|
||||
<p>{t("notify-on-signatures-help.note")}</p>
|
||||
</div>
|
||||
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className={`flex items-center gap-2 ml-2 mb-1`}>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(true)}
|
||||
checked={formData.NotifyOnSignatures === true}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 ml-2 mb-1`}>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(false)}
|
||||
checked={formData.NotifyOnSignatures === false}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={"true"}
|
||||
className="op-radio op-radio-xs"
|
||||
name="IsTourEnabled"
|
||||
checked={formData.IsTourEnabled === "true"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={"false"}
|
||||
name="IsTourEnabled"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={formData.IsTourEnabled === "false"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<label>
|
||||
{t("notify-on-signatures")}
|
||||
<a data-tooltip-id="nos-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>{" "}
|
||||
<Tooltip id="nos-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
|
||||
<p className="font-bold">{t("notify-on-signatures")}</p>
|
||||
<p>{t("notify-on-signatures-help.p1")}</p>
|
||||
<p>{t("notify-on-signatures-help.note")}</p>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className={`flex items-center gap-2 ml-2 mb-1`}>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(true)}
|
||||
checked={formData.NotifyOnSignatures === true}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 ml-2 mb-1`}>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(false)}
|
||||
checked={formData.NotifyOnSignatures === false}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<SignersInput
|
||||
label={t("Bcc")}
|
||||
initialData={template?.Bcc}
|
||||
onChange={handleBcc}
|
||||
helptextZindex={50}
|
||||
helpText={t("bcc-help")}
|
||||
isCaptureAllData
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<SignersInput
|
||||
label={t("Bcc")}
|
||||
initialData={template?.Bcc}
|
||||
onChange={handleBcc}
|
||||
helptextZindex={50}
|
||||
helpText={t("bcc-help")}
|
||||
isCaptureAllData
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs mt-2">
|
||||
<label className="block">Redirect Url</label>
|
||||
<input
|
||||
name="RedirectUrl"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
value={formData.RedirectUrl}
|
||||
onChange={handleStrInput}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs mt-2">
|
||||
<label className="block">
|
||||
{t("time-to-complete")}
|
||||
<span className="text-red-500 text-[13px]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="TimeToCompleteDays"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
value={formData.TimeToCompleteDays}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-[1rem] flex justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-xs mt-2">
|
||||
<label className="block">Redirect Url</label>
|
||||
<input
|
||||
name="RedirectUrl"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
value={formData.RedirectUrl}
|
||||
onChange={handleStrInput}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs mt-2">
|
||||
<label className="block">
|
||||
{t("time-to-complete")}
|
||||
<span className="text-red-500 text-[13px]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="TimeToCompleteDays"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
value={formData.TimeToCompleteDays}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-[1rem] flex justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const DateFormatSelector = (props) => {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-col md:flex-row md:gap-4 mt-[0.75rem] text-[12px]">
|
||||
<div className="flex flex-row gap-4 mt-[0.75rem] text-[12px]">
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<input
|
||||
type="radio"
|
||||
|
||||
@@ -219,7 +219,6 @@ export default function reportJson(id) {
|
||||
btnId: "44534",
|
||||
hoverLabel: "option",
|
||||
btnColor: "",
|
||||
restrictBtn: true,
|
||||
textColor: "black",
|
||||
btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
|
||||
action: "option",
|
||||
@@ -228,9 +227,18 @@ export default function reportJson(id) {
|
||||
btnId: "0940",
|
||||
btnLabel: "Save as template",
|
||||
hoverLabel: "Save as template",
|
||||
restrictBtn: true,
|
||||
btnIcon: "fa-light fa-envelope",
|
||||
redirectUrl: "",
|
||||
action: "saveastemplate"
|
||||
},
|
||||
{
|
||||
btnId: "8440",
|
||||
btnLabel: "Fix & resend",
|
||||
hoverLabel: "Fix & resend",
|
||||
btnIcon: "fa-light fa-paper-plane",
|
||||
redirectUrl: "",
|
||||
action: "recreatedocument"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ const Preferences = () => {
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<h1 className="ml-4 mt-3 mb-2 font-semibold">
|
||||
<h1 className="ml-4 mt-3 text-lg mb-2 font-semibold text-base-content">
|
||||
{appName} {t("Preferences")}{" "}
|
||||
<span>
|
||||
<Tooltip message={`${appName} ${t("Preferences")}`} />
|
||||
@@ -362,7 +362,9 @@ const Preferences = () => {
|
||||
role="tab"
|
||||
className={`${
|
||||
activeTab === ind ? "op-tab-active" : ""
|
||||
} op-tab text-xs md:text-base pb-2 md:pb-0 }`}
|
||||
} op-tab text-xs md:text-base pb-2 md:pb-0 transition-all `}
|
||||
aria-selected={activeTab === ind}
|
||||
aria-controls={`panel-${tabData.title}`}
|
||||
>
|
||||
<i className={tabData.icon}></i>
|
||||
<span className="ml-1 md:ml-2">{tabData.title}</span>
|
||||
@@ -370,275 +372,330 @@ const Preferences = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center md:justify-start mt-3 md:mt-4 break-all">
|
||||
<div
|
||||
id="panel-general"
|
||||
className="px-6 pt-4 pb-6"
|
||||
aria-labelledby="tab-general"
|
||||
role="tabpanel"
|
||||
>
|
||||
{activeTab === 0 ? (
|
||||
<div className="ml-4 mt-1 mb-2 flex flex-col">
|
||||
<div className="mb-[0.75rem]">
|
||||
<label
|
||||
className="mb-[0.7rem] text-[12px]"
|
||||
htmlFor="signaturetype"
|
||||
>
|
||||
<span className="font-medium text-[14px]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 md:gap-x-8">
|
||||
{/* Left Column - Signature Settings */}
|
||||
<div className="md:col-span-5 flex flex-col">
|
||||
{/* Signature Types Section */}
|
||||
<div className="mb-6">
|
||||
<label
|
||||
className="text-[14px] mb-[0.7rem] font-medium"
|
||||
htmlFor="signaturetype"
|
||||
>
|
||||
{t("allowed-signature-types")}
|
||||
</span>
|
||||
<a data-tooltip-id="signtypes-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id="signtypes-tooltip"
|
||||
className="z-[999]"
|
||||
>
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">
|
||||
{t("allowed-signature-types")}
|
||||
</p>
|
||||
<p>{t("allowed-signature-types-help.p1")}</p>
|
||||
<div className="p-[5px] ml-2">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">Draw: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l1")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Type: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l2")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Upload: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l3")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Default: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l4")}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
|
||||
{signatureType.map((type, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
<a
|
||||
data-tooltip-id="signtypes-tooltip"
|
||||
className="ml-1"
|
||||
>
|
||||
<input
|
||||
className="mr-[2px] op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
name="signaturetype"
|
||||
onChange={() => handleCheckboxChange(i)}
|
||||
checked={type.enabled}
|
||||
/>
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id="signtypes-tooltip"
|
||||
className="z-[999]"
|
||||
>
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">
|
||||
{t("allowed-signature-types")}
|
||||
</p>
|
||||
<p>{t("allowed-signature-types-help.p1")}</p>
|
||||
<div className="p-[5px] ml-2">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">Draw: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l1")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Type: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l2")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Upload: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l3")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Default: </span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l4")}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row gap-3 mb-2">
|
||||
{signatureType.map((type, i) => (
|
||||
<div
|
||||
className="text-[13px] font-medium hover:underline underline-offset-2 cursor-default capitalize"
|
||||
title={`Enabling this allow signers to ${type.name} signature`}
|
||||
key={i}
|
||||
className="flex flex-row gap-2 items-center"
|
||||
>
|
||||
{type?.name === "typed" ? "type" : type?.name}
|
||||
<input
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
id={`signature-type-${type.name}`}
|
||||
name="signaturetype"
|
||||
onChange={() => handleCheckboxChange(i)}
|
||||
checked={type.enabled}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`signature-type-${type.name}`}
|
||||
className="text-sm font-medium text-base-content hover:underline underline-offset-2 cursor-pointer capitalize mb-0"
|
||||
title={`Enabling this allows signers to ${type.name} signature`}
|
||||
>
|
||||
{type?.name === "typed" ? "type" : type?.name}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[0.75rem]">
|
||||
<label className="mb-[0.7rem] text-[12px]">
|
||||
<span className="text-[14px] font-medium">
|
||||
|
||||
{/* Notify on Signatures Section */}
|
||||
<div className="mb-6">
|
||||
<label className="text-[14px] mb-[0.7rem] font-medium">
|
||||
{t("notify-on-signatures")}
|
||||
</span>
|
||||
<a data-tooltip-id="nos-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip id="nos-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">
|
||||
{t("notify-on-signatures")}
|
||||
</p>
|
||||
<p>{t("notify-on-signatures-help.p1")}</p>
|
||||
<p>{t("notify-on-signatures-help.note")}</p>
|
||||
<a data-tooltip-id="nos-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip id="nos-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">
|
||||
{t("notify-on-signatures")}
|
||||
</p>
|
||||
<p>{t("notify-on-signatures-help.p1")}</p>
|
||||
<p>{t("notify-on-signatures-help.note")}</p>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="flex flex-row gap-6">
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<input
|
||||
id="notify-yes"
|
||||
className="op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(true)}
|
||||
checked={isNotifyOnSignatures === true}
|
||||
/>
|
||||
<label
|
||||
htmlFor="notify-yes"
|
||||
className="text-sm text-base-content cursor-pointer mb-0"
|
||||
>
|
||||
{t("yes")}
|
||||
</label>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className={"flex items-center gap-2 ml-2 mb-1"}>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(true)}
|
||||
checked={isNotifyOnSignatures === true}
|
||||
/>
|
||||
<div className="text-[13px] cursor-default capitalize">
|
||||
{t("yes")}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex items-center gap-2 ml-2 mb-1"}>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(false)}
|
||||
checked={isNotifyOnSignatures === false}
|
||||
/>
|
||||
<div className="text-[13px] cursor-default capitalize">
|
||||
{t("no")}
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<input
|
||||
id="notify-no"
|
||||
className="op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(false)}
|
||||
checked={isNotifyOnSignatures === false}
|
||||
/>
|
||||
<label
|
||||
htmlFor="notify-no"
|
||||
className="text-sm text-base-content cursor-pointer mb-0"
|
||||
>
|
||||
{t("no")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[0.75rem]">
|
||||
<TimezoneSelector
|
||||
timezone={timezone}
|
||||
setTimezone={setTimezone}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.75rem]">
|
||||
<DateFormatSelector
|
||||
timezone={timezone}
|
||||
dateFormat={dateFormat}
|
||||
is12HourTime={is12HourTime}
|
||||
setIs12HourTime={setIs12HourTime}
|
||||
setDateFormat={setDateFormat}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.75rem] text-[12px]">
|
||||
<label className="block mb-[0.7rem]">
|
||||
<span className="text-[14px] font-medium">
|
||||
|
||||
{/* Send in Order Section */}
|
||||
<div className="mb-6">
|
||||
<label className="text-[14px] mb-[0.7rem] font-medium">
|
||||
{t("send-in-order")}
|
||||
</span>
|
||||
<a
|
||||
data-tooltip-id="sendInOrder-tooltip"
|
||||
className="ml-1"
|
||||
>
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id="sendInOrder-tooltip"
|
||||
className="z-[999]"
|
||||
>
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("send-in-order")}</p>
|
||||
<p>{t("send-in-order-help.p1")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">
|
||||
{t("yes")}:{" "}
|
||||
</span>
|
||||
<span>{t("send-in-order-help.p2")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">{t("no")}: </span>
|
||||
<span>{t("send-in-order-help.p3")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<a
|
||||
data-tooltip-id="sendInOrder-tooltip"
|
||||
className="ml-1"
|
||||
>
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id="sendInOrder-tooltip"
|
||||
className="z-[999]"
|
||||
>
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("send-in-order")}</p>
|
||||
<p>{t("send-in-order-help.p1")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">
|
||||
{t("yes")}:{" "}
|
||||
</span>
|
||||
<span>{t("send-in-order-help.p2")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">
|
||||
{t("no")}:{" "}
|
||||
</span>
|
||||
<span>{t("send-in-order-help.p3")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>{t("send-in-order-help.p4")}</p>
|
||||
</div>
|
||||
<p>{t("send-in-order-help.p4")}</p>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="flex flex-row gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="order-yes"
|
||||
type="radio"
|
||||
value={true}
|
||||
className="op-radio op-radio-xs"
|
||||
name="SendinOrder"
|
||||
checked={sendinOrder}
|
||||
onChange={handleSendinOrderInput}
|
||||
/>
|
||||
<label
|
||||
htmlFor="order-yes"
|
||||
className="text-sm text-base-content cursor-pointer mb-0"
|
||||
>
|
||||
{t("yes")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="order-no"
|
||||
type="radio"
|
||||
value={false}
|
||||
name="SendinOrder"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={!sendinOrder}
|
||||
onChange={handleSendinOrderInput}
|
||||
/>
|
||||
<label
|
||||
htmlFor="order-no"
|
||||
className="text-sm text-base-content cursor-pointer mb-0"
|
||||
>
|
||||
{t("no")}
|
||||
</label>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={true}
|
||||
className="op-radio op-radio-xs"
|
||||
name="SendinOrder"
|
||||
checked={sendinOrder}
|
||||
onChange={handleSendinOrderInput}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={false}
|
||||
name="SendinOrder"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={!sendinOrder}
|
||||
onChange={handleSendinOrderInput}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[0.75rem] text-[12px]">
|
||||
<label className="block mb-[0.7rem]">
|
||||
<span className="text-[14px] font-medium">
|
||||
|
||||
{/* Enable Tour Section */}
|
||||
<div className="mb-6">
|
||||
<label className="text-[14px] mb-[0.7rem] font-medium">
|
||||
{t("enable-tour")}
|
||||
</span>
|
||||
<a
|
||||
data-tooltip-id="istourenabled-tooltip"
|
||||
className="ml-1"
|
||||
>
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id="istourenabled-tooltip"
|
||||
className="z-[999]"
|
||||
>
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("enable-tour")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">
|
||||
{t("yes")}:{" "}
|
||||
</span>
|
||||
<span>{t("istourenabled-help.p1")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">{t("no")}: </span>
|
||||
<span>{t("istourenabled-help.p2")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<a
|
||||
data-tooltip-id="istourenabled-tooltip"
|
||||
className="ml-1"
|
||||
>
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip
|
||||
id="istourenabled-tooltip"
|
||||
className="z-[999]"
|
||||
>
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("enable-tour")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">
|
||||
{t("yes")}:{" "}
|
||||
</span>
|
||||
<span>{t("istourenabled-help.p1")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">
|
||||
{t("no")}:{" "}
|
||||
</span>
|
||||
<span>{t("istourenabled-help.p2")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
{t("istourenabled-help.p3", {
|
||||
appName: appName
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
{t("istourenabled-help.p3", {
|
||||
appName: appName
|
||||
})}
|
||||
</p>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="flex flex-row gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="tour-yes"
|
||||
type="radio"
|
||||
value={true}
|
||||
className="op-radio op-radio-xs"
|
||||
name="IsTourEnabled"
|
||||
checked={isTourEnabled}
|
||||
onChange={handleTourInput}
|
||||
/>
|
||||
<label
|
||||
htmlFor="tour-yes"
|
||||
className="text-sm text-base-content cursor-pointer mb-0"
|
||||
>
|
||||
{t("yes")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="tour-no"
|
||||
type="radio"
|
||||
value={false}
|
||||
name="IsTourEnabled"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={!isTourEnabled}
|
||||
onChange={handleTourInput}
|
||||
/>
|
||||
<label
|
||||
htmlFor="tour-no"
|
||||
className="text-sm text-base-content cursor-pointer mb-0"
|
||||
>
|
||||
{t("no")}
|
||||
</label>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={true}
|
||||
className="op-radio op-radio-xs"
|
||||
name="IsTourEnabled"
|
||||
checked={isTourEnabled}
|
||||
onChange={handleTourInput}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={false}
|
||||
name="IsTourEnabled"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={!isTourEnabled}
|
||||
onChange={handleTourInput}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[0.75rem]">
|
||||
|
||||
{/* Right Column - Timezone & Date Settings */}
|
||||
<div className="md:col-span-7 flex flex-col">
|
||||
<div className="mb-6">
|
||||
<TimezoneSelector
|
||||
timezone={timezone}
|
||||
setTimezone={setTimezone}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<DateFormatSelector
|
||||
timezone={timezone}
|
||||
dateFormat={dateFormat}
|
||||
is12HourTime={is12HourTime}
|
||||
setIs12HourTime={setIs12HourTime}
|
||||
setDateFormat={setDateFormat}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button - Full Width */}
|
||||
<div className="md:col-span-12 flex justify-start mt-2">
|
||||
<button
|
||||
className="op-btn op-btn-primary w-[110px]"
|
||||
onClick={handleSave}
|
||||
@@ -648,7 +705,7 @@ const Preferences = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col mx-4 mb-4">
|
||||
<div className="flex flex-col mb-4">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-[14px] mb-[0.7rem] font-medium">
|
||||
{t("request-email")}
|
||||
@@ -660,7 +717,7 @@ const Preferences = () => {
|
||||
>
|
||||
<div className="text-lg font-normal">
|
||||
<label className="text-sm">
|
||||
{t("subject")}
|
||||
{t("subject")}{" "}
|
||||
<Tooltip
|
||||
id={"request-sub-tooltip"}
|
||||
message={`${t(
|
||||
@@ -682,7 +739,7 @@ const Preferences = () => {
|
||||
</div>
|
||||
<div className="text-lg font-normal py-2">
|
||||
<label className="text-sm mt-3">
|
||||
{t("body")}
|
||||
{t("body")}{" "}
|
||||
<Tooltip
|
||||
id={"request-body-tooltip"}
|
||||
message={`${t(
|
||||
@@ -730,7 +787,7 @@ const Preferences = () => {
|
||||
>
|
||||
<div className="text-lg font-normal">
|
||||
<label className="text-sm">
|
||||
{t("subject")}
|
||||
{t("subject")}{" "}
|
||||
<Tooltip
|
||||
id={"complete-sub-tooltip"}
|
||||
message={`${t(
|
||||
@@ -752,7 +809,7 @@ const Preferences = () => {
|
||||
</div>
|
||||
<div className="text-lg font-normal py-2">
|
||||
<label className="text-sm mt-3">
|
||||
{t("body")}
|
||||
{t("body")}{" "}
|
||||
<Tooltip
|
||||
id={"complete-body-tooltip"}
|
||||
message={`${t(
|
||||
|
||||
@@ -733,20 +733,18 @@ const TemplatePlaceholder = () => {
|
||||
let isLabel = false;
|
||||
let unfilledTextWidgetId = "";
|
||||
//condition is used to check text widget data is empty or have response
|
||||
if (getPrefill) {
|
||||
if (prefillPlaceholder) {
|
||||
prefillPlaceholder.map((data) => {
|
||||
if (!isLabel) {
|
||||
const unfilledTextWidgets = data.pos.find(
|
||||
(position) => !position.options.response
|
||||
);
|
||||
if (unfilledTextWidgets) {
|
||||
isLabel = true;
|
||||
unfilledTextWidgetId = unfilledTextWidgets.key;
|
||||
}
|
||||
if (getPrefill && prefillPlaceholder) {
|
||||
prefillPlaceholder.map((data) => {
|
||||
if (!isLabel) {
|
||||
const unfilledTextWidgets = data.pos.find(
|
||||
(position) => !position.options.response
|
||||
);
|
||||
if (unfilledTextWidgets) {
|
||||
isLabel = true;
|
||||
unfilledTextWidgetId = unfilledTextWidgets.key;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (getPrefill && isLabel) {
|
||||
setUnSignedWidgetId(unfilledTextWidgetId);
|
||||
@@ -1250,6 +1248,7 @@ const TemplatePlaceholder = () => {
|
||||
? { RedirectUrl: updateTemplate?.[0]?.RedirectUrl }
|
||||
: {};
|
||||
const data = {
|
||||
...(updateTemplate?.[0]?.URL ? { URL: updateTemplate?.[0]?.URL } : {}),
|
||||
Name: updateTemplate?.[0]?.Name || "",
|
||||
Note: updateTemplate?.[0]?.Note || "",
|
||||
Description: updateTemplate?.[0]?.Description || "",
|
||||
@@ -1964,16 +1963,17 @@ const TemplatePlaceholder = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ModalUi
|
||||
title={t("edit-template")}
|
||||
isOpen={isEditTemplate}
|
||||
handleClose={handleEditTemplateModal}
|
||||
>
|
||||
{isEditTemplate && (
|
||||
<EditTemplate
|
||||
title={t("edit-template")}
|
||||
handleClose={handleEditTemplateModal}
|
||||
pdfbase64={pdfBase64Url}
|
||||
template={pdfDetails?.[0]}
|
||||
onSuccess={handleEditTemplateForm}
|
||||
setPdfArrayBuffer={setPdfArrayBuffer}
|
||||
setPdfBase64Url={setPdfBase64Url}
|
||||
/>
|
||||
</ModalUi>
|
||||
)}
|
||||
<WidgetNameModal
|
||||
signatureType={signatureType}
|
||||
widgetName={widgetName}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import pad from "../assets/images/pad.svg";
|
||||
import recreatedoc from "../assets/images/recreatedoc.png";
|
||||
import { Link, useLocation, useNavigate } from "react-router";
|
||||
import axios from "axios";
|
||||
import ModalUi from "./ModalUi";
|
||||
@@ -19,7 +20,8 @@ import {
|
||||
getTenantDetails,
|
||||
handleSignatureType,
|
||||
replaceMailVaribles,
|
||||
signatureTypes
|
||||
signatureTypes,
|
||||
openInNewTab
|
||||
} from "../constant/Utils";
|
||||
import EditorToolbar, {
|
||||
module1,
|
||||
@@ -412,6 +414,8 @@ const ReportTable = (props) => {
|
||||
setIsModal({ [`edit_${item.objectId}`]: true });
|
||||
} else if (act.action === "saveastemplate") {
|
||||
setIsModal({ [`saveastemplate_${item.objectId}`]: true });
|
||||
} else if (act.action === "recreatedocument") {
|
||||
setIsModal({ [`recreatedocument_${item.objectId}`]: true });
|
||||
} else if (act.action) {
|
||||
setIsModal({ [`extendexpiry_${item.objectId}`]: true });
|
||||
}
|
||||
@@ -1352,6 +1356,24 @@ const ReportTable = (props) => {
|
||||
setUserDetails({});
|
||||
};
|
||||
|
||||
const handleRecreateDoc = async (item) => {
|
||||
setActLoader({ [item.objectId]: true });
|
||||
try {
|
||||
const res = await Parse.Cloud.run("recreatedoc", {
|
||||
docId: item.objectId
|
||||
});
|
||||
if (res) {
|
||||
openInNewTab(`/placeHolderSign/${res.objectId}`, "_self");
|
||||
}
|
||||
} catch (err) {
|
||||
handleCloseModal();
|
||||
showAlert("danger", err.message);
|
||||
// showAlert("danger", t("something-went-wrong-mssg"));
|
||||
console.log("Err while create duplicate template", err);
|
||||
} finally {
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
{Object.keys(actLoader)?.length > 0 && (
|
||||
@@ -1826,6 +1848,45 @@ const ReportTable = (props) => {
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{isModal["recreatedocument_" + item.objectId] && (
|
||||
<ModalUi isOpen handleClose={handleCloseModal}>
|
||||
{actLoader[item.objectId] && (
|
||||
<div className="absolute h-full w-full flex justify-center items-center rounded-box bg-black/30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-base-content font-bold text-lg pt-[15px] px-[20px]">
|
||||
{t("fix-&-resend-document")}
|
||||
</h3>
|
||||
<div className="p-[15px] md:p-[20px]">
|
||||
<div className="text-lg font-normal text-center">
|
||||
<img
|
||||
src={recreatedoc}
|
||||
alt="recreate-doc"
|
||||
className="mx-auto w-[200px] h-auto"
|
||||
/>
|
||||
<p className="text-sm md:text-base md:px-2 mt-2">
|
||||
{t("do-you-want-recreate-document?")}
|
||||
</p>
|
||||
</div>
|
||||
<hr className="bg-[#ccc] mt-2.5" />
|
||||
<div className="flex items-center justify-center mt-[14px] md:mt-[16px] gap-2 text-white">
|
||||
<button
|
||||
onClick={() => handleRecreateDoc(item)}
|
||||
className="op-btn op-btn-primary focus:outline-none text-sm relative px-4"
|
||||
>
|
||||
{t("start-editing")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="op-btn op-btn-secondary focus:outline-none text-sm relative px-8"
|
||||
>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
)}
|
||||
{isModal["saveastemplate_" + item.objectId] && (
|
||||
<ModalUi
|
||||
isOpen
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
FROM node:22.14.0
|
||||
|
||||
# install java
|
||||
RUN wget https://download.oracle.com/java/23/latest/jdk-23_linux-x64_bin.deb \
|
||||
RUN wget https://downloads.opensign.me/jdk-23_linux-x64_bin.deb \
|
||||
&& dpkg -i jdk-23_linux-x64_bin.deb
|
||||
|
||||
# Set the working directory inside the container
|
||||
|
||||
Binary file not shown.
@@ -2,6 +2,8 @@ import dotenv from 'dotenv';
|
||||
import { format, toZonedTime } from 'date-fns-tz';
|
||||
import { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import crypto from 'node:crypto';
|
||||
import axios from 'axios';
|
||||
dotenv.config();
|
||||
|
||||
export const cloudServerUrl = 'http://localhost:8080/app';
|
||||
@@ -148,6 +150,7 @@ export const useLocal = process.env.USE_LOCAL ? process.env.USE_LOCAL.toLowerCas
|
||||
export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '465' ? false : true;
|
||||
export const smtpenable =
|
||||
process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false;
|
||||
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
// `generateId` is used to unique Id for fileAdapter
|
||||
export function generateId(length) {
|
||||
@@ -297,3 +300,39 @@ export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
|
||||
? format(zonedDate, `${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`, { timeZone })
|
||||
: formatTimeInTimezone(date, timeZone);
|
||||
}
|
||||
|
||||
// Utility: Convert base64 to buffer
|
||||
export const base64ToBuffer = base64 => Buffer.from(base64, 'base64');
|
||||
|
||||
// Utility: Generate SHA-256 hash from PDF page metadata
|
||||
const getPdfMetadataHash = async pdfBytes => {
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
const metaString = pdfDoc
|
||||
.getPages()
|
||||
.map((page, index) => {
|
||||
const { width, height } = page.getSize();
|
||||
return `${index + 1}:${Math.round(width)}x${Math.round(height)}`;
|
||||
})
|
||||
.join('|');
|
||||
|
||||
return crypto.createHash('sha256').update(metaString).digest('hex');
|
||||
};
|
||||
// Utility: Validate if uploaded file matches original template PDF
|
||||
export const handleReplaceFileValidation = async (baseFileUrl, newFileBase64) => {
|
||||
try {
|
||||
const { data } = await axios.get(baseFileUrl, { responseType: 'arraybuffer' });
|
||||
const basePdfBytes = Buffer.from(data);
|
||||
const uploadedPdfBytes = base64ToBuffer(newFileBase64);
|
||||
|
||||
const baseHash = await getPdfMetadataHash(basePdfBytes);
|
||||
const uploadedHash = await getPdfMetadataHash(uploadedPdfBytes);
|
||||
|
||||
if (baseHash === uploadedHash) {
|
||||
return { base64: newFileBase64 };
|
||||
}
|
||||
return { error: 'PDFs do NOT match based on page number, width, and height' };
|
||||
} catch (err) {
|
||||
console.error('Validation Error:', err.message);
|
||||
return { error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ import editContact from './parsefunction/editContact.js';
|
||||
import forwardDoc from './parsefunction/ForwardDoc.js';
|
||||
import saveAsTemplate from './parsefunction/saveAsTemplate.js';
|
||||
import updateTenant from './parsefunction/updateTenant.js';
|
||||
import recreateDocument from './parsefunction/recreateDocument.js';
|
||||
|
||||
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
|
||||
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
|
||||
@@ -112,3 +113,4 @@ Parse.Cloud.define('editcontact', editContact);
|
||||
Parse.Cloud.define('forwarddoc', forwardDoc);
|
||||
Parse.Cloud.define('saveastemplate', saveAsTemplate);
|
||||
Parse.Cloud.define('updatetenant', updateTenant);
|
||||
Parse.Cloud.define('recreatedoc', recreateDocument);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
export default function DigitalSign(pdf, pfx, details) {
|
||||
const signcmd = `java -jar PDFDigitalSigner.jar "${pdf}" "${pfx.name}" "${pfx.passphrase}" "${details.name}" "${details.location}" "${details.reason}"`;
|
||||
// const signcmd = `java -jar PDFDigitalSigner.jar "${pdf}" keystore.pfx opensign "${details.name}" "${details.location}" "${details.reason}"`;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(signcmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`Error: ${error.message}`);
|
||||
}
|
||||
if (stderr) {
|
||||
reject(`stderr: ${stderr}`);
|
||||
}
|
||||
// Resolve the promise with the output
|
||||
resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function retryAsync(fn, args = [], retries = 3, delay = 2000) {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await fn(...args); // Try executing the async function
|
||||
if (response) return { response, attempt }; // Stop retrying if response is received
|
||||
} catch (error) {
|
||||
if (attempt < retries) {
|
||||
console.log(`Attempt ${attempt} failed. Retrying in ${delay / 1000} seconds...\n`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay)); // Wait before retrying
|
||||
} else {
|
||||
return { error, attempt };
|
||||
// throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,16 @@ const masterKEY = process.env.MASTER_KEY;
|
||||
const eSignName = 'OpenSign';
|
||||
const eSigncontact = 'hello@opensignlabs.com';
|
||||
|
||||
async function unlinkFile(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
try {
|
||||
fs.unlinkSync(path);
|
||||
} catch (err) {
|
||||
console.log('Err in unlink file: ', path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `updateDoc` is used to create url in from pdfFile
|
||||
async function uploadFile(pdfName, filepath) {
|
||||
try {
|
||||
@@ -33,8 +43,8 @@ async function uploadFile(pdfName, filepath) {
|
||||
return { imageUrl: fileUrl };
|
||||
} catch (err) {
|
||||
console.log('Err ', err);
|
||||
// `fs.unlinkSync` is used to remove exported signed pdf file from exports folder
|
||||
fs.unlinkSync(filepath);
|
||||
// below line of code is used to remove exported signed pdf file from exports folder
|
||||
unlinkFile(filepath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,10 +250,10 @@ async function sendCompletedMail(obj) {
|
||||
});
|
||||
// console.log('res', res.data.result);
|
||||
if (res.data?.result?.status !== 'success') {
|
||||
fs.unlinkSync(`./exports/signed_certificate_${doc.objectId}.pdf`);
|
||||
unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`);
|
||||
}
|
||||
} catch (err) {
|
||||
fs.unlinkSync(`./exports/signed_certificate_${doc.objectId}.pdf`);
|
||||
unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +298,7 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
|
||||
sendCompletedMail({ isCustomMail, doc, mailProvider, filename });
|
||||
}
|
||||
saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId);
|
||||
fs.unlinkSync(pfx.name);
|
||||
unlinkFile(pfx.name);
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -438,10 +448,10 @@ async function PDF(req) {
|
||||
const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl };
|
||||
sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, `signed_${name}`);
|
||||
} else {
|
||||
fs.unlinkSync(pfxname);
|
||||
unlinkFile(pfxname);
|
||||
}
|
||||
// `fs.unlinkSync` is used to remove exported signed pdf file from exports folder
|
||||
fs.unlinkSync(signedFilePath);
|
||||
// below code is used to remove exported signed pdf file from exports folder
|
||||
unlinkFile(signedFilePath);
|
||||
// console.log(`New Signed PDF created called: ${filePath}`);
|
||||
if (updatedDoc.message === 'success') {
|
||||
return { status: 'success', data: data.imageUrl };
|
||||
@@ -466,7 +476,7 @@ async function PDF(req) {
|
||||
} catch (err) {
|
||||
console.log('err in saving debugginglog', err);
|
||||
}
|
||||
fs.unlinkSync(pfxname);
|
||||
unlinkFile(pfxname);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
export default async function recreateDocument(request) {
|
||||
const { docId } = request.params;
|
||||
if (!docId) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing docId parameter');
|
||||
}
|
||||
if (!request.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User not aunthenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
const docQuery = new Parse.Query('contracts_Document');
|
||||
docQuery.equalTo('objectId', docId);
|
||||
const doc = await docQuery.first({ useMasterKey: true });
|
||||
if (!doc) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found');
|
||||
}
|
||||
const _docRes = doc?.toJSON();
|
||||
const { objectId, SignedUrl, AuditTrail, ACL, DeclineBy, DeclineReason, ...docRes } = _docRes;
|
||||
const createDoc = new Parse.Object('contracts_Document');
|
||||
Object.entries(docRes).forEach(([key, value]) => {
|
||||
if (key === 'IsDeclined') {
|
||||
createDoc.set(key, false);
|
||||
} else {
|
||||
createDoc.set(key, value);
|
||||
}
|
||||
// console.log(`${key}: ${value}`);
|
||||
});
|
||||
const createDocRes = await createDoc.save(null, { useMasterKey: true });
|
||||
// console.log('createDocRes', createDocRes);
|
||||
const newDoc = JSON.parse(JSON.stringify(createDocRes));
|
||||
return { objectId: newDoc.objectId, createdAt: newDoc.createdAt, updatedAt: newDoc.updatedAt };
|
||||
} catch (err) {
|
||||
console.log('err in recreate document', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user