mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-17 21:25:54 +02:00
Generated
+295
-259
File diff suppressed because it is too large
Load Diff
+21
-21
@@ -5,15 +5,15 @@
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@imgly/background-removal": "^1.6.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.14.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.14.2",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"axios": "^1.9.0",
|
||||
"axios": "^1.10.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next": "^25.3.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
@@ -36,24 +36,24 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-quill-new": "^3.4.6",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.6.1",
|
||||
"react-router": "^7.6.3",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-select": "^5.10.1",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
"react-timezone-select": "^3.2.8",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"react-tooltip": "^5.29.1",
|
||||
"reactour": "^1.19.4",
|
||||
"redux": "^5.0.1",
|
||||
"regex-parser": "^2.3.1",
|
||||
"serve": "^14.2.4",
|
||||
"styled-components": "^5.3.11",
|
||||
"web-vitals": "^5.0.2",
|
||||
"web-vitals": "^5.0.3",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -94,38 +94,38 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.4",
|
||||
"@babel/core": "^7.27.7",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/runtime-corejs2": "^7.27.4",
|
||||
"@babel/runtime-corejs2": "^7.27.6",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^18.3.23",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||
"@vitest/ui": "^3.2.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"daisyui": "^4.12.24",
|
||||
"dotenv": "^16.5.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"dotenv": "^16.6.1",
|
||||
"eslint": "^9.30.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.0",
|
||||
"postcss": "^8.5.4",
|
||||
"prettier": "^3.5.3",
|
||||
"lint-staged": "^16.1.2",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.0"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || 22"
|
||||
|
||||
@@ -761,6 +761,8 @@
|
||||
"Rotation-alert": "Seite drehen",
|
||||
"bulk-import": "Massenimport",
|
||||
"contacts-file": "Kontaktdatei (xlsx, csv)",
|
||||
"import-guideline": "Laden Sie eine CSV- oder Excel-Datei mit den Spalten Name, Email und optional Phone hoch. Es werden nur die ersten 100 Kontakte importiert.",
|
||||
"download-sample": "Beispieldatei herunterladen",
|
||||
"100-records-only": "Derzeit können Sie nur bis zu 100 Datensätze importieren.",
|
||||
"csv-excel-support-only": "Laden Sie eine Datei in einem der folgenden Formate hoch: CSV, XLSX oder XLS.",
|
||||
"contact-imported": "{{imported}} Kontakte wurden importiert. {{failed}} Kontakte konnten nicht importiert werden.",
|
||||
@@ -1033,5 +1035,11 @@
|
||||
"choose-one":"Wählen Sie eine aus",
|
||||
"search-templates": "Vorlagen durchsuchen…",
|
||||
"search-documents": "Dokumente suchen…",
|
||||
"search-contacts": "Kontakte durchsuchen…"
|
||||
"search-contacts": "Kontakte durchsuchen…",
|
||||
"invalid-email-found": "Ungültige E-Mail gefunden: {{email}}",
|
||||
"duplicate-email-found": "Doppelte E-Mail gefunden: {{email}}",
|
||||
"vertical": "Vertikal",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Abrechnung",
|
||||
"console": "Konsole"
|
||||
}
|
||||
|
||||
@@ -761,6 +761,8 @@
|
||||
"Rotation-alert": "Rotate page",
|
||||
"bulk-import": "Bulk import",
|
||||
"contacts-file": "Contacts file (xlsx, csv)",
|
||||
"import-guideline": "Upload a CSV or Excel file with columns Name, Email and optional Phone. Only the first 100 records will be imported.",
|
||||
"download-sample": "Download sample file",
|
||||
"100-records-only": "Currently you can only import up to 100 records.",
|
||||
"csv-excel-support-only": "Upload a file in one of the following formats: CSV, XLSX or XLS.",
|
||||
"contact-imported": "{{imported}} contacts were imported. {{failed}} contacts failed to import.",
|
||||
@@ -1033,5 +1035,11 @@
|
||||
"choose-one":"Choose One",
|
||||
"search-templates": "Search templates…",
|
||||
"search-documents": "Search documents…",
|
||||
"search-contacts": "Search contacts…"
|
||||
"search-contacts": "Search contacts…",
|
||||
"invalid-email-found": "Invalid email found: {{email}}",
|
||||
"duplicate-email-found": "Duplicate email found: {{email}}",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Billing",
|
||||
"console": "Console"
|
||||
}
|
||||
@@ -761,6 +761,8 @@
|
||||
"Rotation-alert": "Girar página",
|
||||
"bulk-import": "Importación masiva",
|
||||
"contacts-file": "Archivo de contactos (xlsx, csv)",
|
||||
"import-guideline": "Sube un archivo CSV o Excel con las columnas Name, Email y opcionalmente Phone. Solo se importarán los primeros 100 contactos.",
|
||||
"download-sample": "Descargar archivo de ejemplo",
|
||||
"100-records-only": "Currently you can only import up to 100 records.",
|
||||
"csv-excel-support-only": "Cargue un archivo en uno de los siguientes formatos: CSV, XLSX o XLS.",
|
||||
"contact-imported": "Se importaron {{imported}} contactos. {{failed}} contactos no pudieron importarse.",
|
||||
@@ -1033,5 +1035,11 @@
|
||||
"choose-one":"Elige uno",
|
||||
"search-templates": "Buscar plantillas…",
|
||||
"search-documents": "Buscar documentos…",
|
||||
"search-contacts": "Buscar contactos…"
|
||||
"search-contacts": "Buscar contactos…",
|
||||
"invalid-email-found": "Correo electrónico no válido encontrado: {{email}}",
|
||||
"duplicate-email-found": "Correo electrónico duplicado encontrado: {{email}}",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Facturación",
|
||||
"console": "Consola"
|
||||
}
|
||||
|
||||
@@ -761,6 +761,8 @@
|
||||
"Rotation-alert": "Faire pivoter la page",
|
||||
"bulk-import": "Importation en masse",
|
||||
"contacts-file": "Fichier de contacts (xlsx, csv)",
|
||||
"import-guideline": "Téléchargez un fichier CSV ou Excel avec les colonnes Name, Email et éventuellement Phone. Seuls les 100 premiers contacts seront importés.",
|
||||
"download-sample": "Télécharger un fichier d'exemple",
|
||||
"100-records-only": "Actuellement, vous ne pouvez importer que 100 enregistrements.",
|
||||
"csv-excel-support-only": "Veuillez télécharger un fichier dans l'un des formats suivants : CSV, XLSX ou XLS.",
|
||||
"contact-imported": "{{imported}} contacts ont été importés. {{failed}} contacts n'ont pas pu être importés.",
|
||||
@@ -1033,5 +1035,11 @@
|
||||
"choose-one":"Choisissez-en un",
|
||||
"search-templates": "Rechercher des modèles…",
|
||||
"search-documents": "Rechercher des documents…",
|
||||
"search-contacts": "Rechercher des contacts…"
|
||||
"search-contacts": "Rechercher des contacts…",
|
||||
"invalid-email-found": "Adresse e-mail invalide trouvée : {{email}}",
|
||||
"duplicate-email-found": "Adresse e-mail en double trouvée : {{email}}",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Facturation",
|
||||
"console": "Console"
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@
|
||||
"widgets-name": {
|
||||
"signature": "हस्ताक्षर",
|
||||
"stamp": "मोहर",
|
||||
"initials": "हस्ताक्षर",
|
||||
"initials": "प्रारंभिक अक्षर",
|
||||
"name": "नाम",
|
||||
"job title": "पद",
|
||||
"company": "कंपनी",
|
||||
@@ -389,7 +389,7 @@
|
||||
"Email-verified-alert-2": "ईमेल पहले से ही सत्यापित है।",
|
||||
"upload-stamp-image": "मोहर छवि अपलोड करें",
|
||||
"draw": "आरेखित करें",
|
||||
"type": "प्रकार",
|
||||
"type": "टाइप",
|
||||
"color-type": {
|
||||
"red": "लाल",
|
||||
"blue": "नीला",
|
||||
@@ -761,6 +761,8 @@
|
||||
"Rotation-alert": "पृष्ठ घुमाएँ",
|
||||
"bulk-import": "थोक आयात",
|
||||
"contacts-file": "संपर्क फ़ाइल (xlsx, csv)",
|
||||
"import-guideline": "नाम, ईमेल और वैकल्पिक फोन कॉलम वाली CSV या Excel फ़ाइल अपलोड करें। केवल पहले 100 संपर्क आयात किए जाएंगे.",
|
||||
"download-sample": "उदाहरण फ़ाइल डाउनलोड करें",
|
||||
"100-records-only": "वर्तमान में आप केवल 100 रिकॉर्ड तक आयात कर सकते हैं।",
|
||||
"csv-excel-support-only": "निम्नलिखित प्रारूपों में से किसी एक में फ़ाइल अपलोड करें: CSV, XLSX या XLS।",
|
||||
"contact-imported": "{{imported}} संपर्क आयात किए गए। {{failed}} संपर्क आयात करने में विफल रहे।",
|
||||
@@ -1033,5 +1035,11 @@
|
||||
"choose-one":"एक का चयन",
|
||||
"search-templates": "टेम्पलेट खोजें…",
|
||||
"search-documents": "दस्तावेज़ खोजें…",
|
||||
"search-contacts": "संपर्क खोजें…"
|
||||
"search-contacts": "संपर्क खोजें…",
|
||||
"invalid-email-found": "अमान्य ईमेल पाया गया: {{email}}",
|
||||
"duplicate-email-found": "डुप्लिकेट ईमेल पाया गया: {{email}}",
|
||||
"vertical": "वर्टिकल",
|
||||
"horizontal": "हॉरिज़ॉन्टल",
|
||||
"billing": "बिलिंग",
|
||||
"console": "कंसोल"
|
||||
}
|
||||
|
||||
@@ -761,6 +761,8 @@
|
||||
"Rotation-alert": "Ruota pagina",
|
||||
"bulk-import": "Importazione massiva",
|
||||
"contacts-file": "File contatti (xlsx, csv)",
|
||||
"import-guideline": "Carica un file CSV o Excel con le colonne Name, Email e opzionale Phone. Verranno importati solo i primi 100 contatti.",
|
||||
"download-sample": "Scarica file di esempio",
|
||||
"100-records-only": "Attualmente puoi importare solo fino a 100 record.",
|
||||
"csv-excel-support-only": "Carica un file nei seguenti formati: CSV, XLSX o XLS.",
|
||||
"contact-imported": "{{imported}} contatti importati. {{failed}} contatti non sono stati importati.",
|
||||
@@ -1033,5 +1035,11 @@
|
||||
"choose-one":"Scegline uno",
|
||||
"search-templates": "Cerca modelli…",
|
||||
"search-documents": "Cerca documenti…",
|
||||
"search-contacts": "Cerca contatti…"
|
||||
"search-contacts": "Cerca contatti…",
|
||||
"invalid-email-found": "Email non valida trovata: {{email}}",
|
||||
"duplicate-email-found": "Email duplicata trovata: {{email}}",
|
||||
"vertical": "Verticale",
|
||||
"horizontal": "Orizzontale",
|
||||
"billing": "Fatturazione",
|
||||
"console": "Console"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Name,Email,Phone
|
||||
John Doe,john@example.com,1234567890
|
||||
Jane Smith,jane@example.com,9876543210
|
||||
Foo Bar,foo@example.com,5555555555
|
||||
|
@@ -15,6 +15,7 @@ const BulkSendUi = (props) => {
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -47,7 +48,14 @@ const BulkSendUi = (props) => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
let emails = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
const signerEmail = element?.email || element?.signerPtr?.Email;
|
||||
|
||||
// only add when there's a non-empty signerEmail
|
||||
if (signerEmail) {
|
||||
emails = [...emails, signerEmail];
|
||||
}
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
@@ -60,7 +68,10 @@ const BulkSendUi = (props) => {
|
||||
];
|
||||
}
|
||||
});
|
||||
setEmails(emails);
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
@@ -82,10 +93,16 @@ const BulkSendUi = (props) => {
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const item of data) {
|
||||
let email = "";
|
||||
for (const field of item.fields) {
|
||||
if (!emailRegex.test(field.email)) {
|
||||
alert(`Invalid email found: ${field.email}`);
|
||||
alert(t("invalid-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,15 +160,14 @@ const BulkSendUi = (props) => {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
Signers: props.item.Signers
|
||||
? [...props.item.Signers, ...existSigner]
|
||||
: [...existSigner]
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
SignatureType: props.signatureType
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="op-navbar bg-base-100 shadow">
|
||||
<div className="op-navbar bg-base-100 shadow touch-none">
|
||||
<div className="flex-none">
|
||||
<button
|
||||
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
|
||||
|
||||
@@ -148,7 +148,7 @@ const DashboardCard = (props) => {
|
||||
);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers.some(
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser.id
|
||||
);
|
||||
if (isSigner) {
|
||||
|
||||
@@ -134,7 +134,7 @@ function DashboardReport(props) {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers.some(
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
|
||||
@@ -405,7 +405,7 @@ function DriveBody(props) {
|
||||
</tr>
|
||||
)
|
||||
) : listType === "list" && data.Type === "Folder" ? (
|
||||
<div key={ind} className="relative w-[100px] h-[100px] mx-2 my-3">
|
||||
<div className="relative w-[100px] h-auto mx-auto">
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
|
||||
{/* folder */}
|
||||
@@ -447,7 +447,6 @@ function DriveBody(props) {
|
||||
)}
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content
|
||||
className="ContextMenuContent"
|
||||
@@ -481,7 +480,7 @@ function DriveBody(props) {
|
||||
<HoverCard.Trigger asChild>
|
||||
<div>
|
||||
<ContextMenu.Root>
|
||||
<div className="relative w-[100px] h-[100px] mx-2 my-3">
|
||||
<div className="relative w-[100px] h-auto mx-auto">
|
||||
<ContextMenu.Trigger
|
||||
asChild
|
||||
className="flex flex-col justify-center items-center select-none-cls"
|
||||
@@ -639,9 +638,9 @@ function DriveBody(props) {
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(100px,1fr))] mx-4 mt-3 pb-[20px] items-center gap-3 justify-center">
|
||||
{props.pdfData.map((data, ind) => {
|
||||
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>;
|
||||
return <React.Fragment key={ind}>{handleFolderData(data, ind, "list")}</React.Fragment>;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -389,8 +389,8 @@ function DropdownWidgetOption(props) {
|
||||
checked={layout.toLowerCase() === data.toLowerCase()}
|
||||
onChange={() => setLayout(data.toLowerCase())}
|
||||
/>
|
||||
<label className="text-[13px] font-500 mb-0 capitalize">
|
||||
{data}
|
||||
<label className="text-[13px] font-500 mb-0">
|
||||
{t(data)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -167,7 +167,7 @@ function Header(props) {
|
||||
{isMobile && props?.isShowHeader ? (
|
||||
<div
|
||||
id="navbar"
|
||||
className="stickyHead"
|
||||
className="stickyHead touch-none"
|
||||
style={{
|
||||
width: window.innerWidth + "px"
|
||||
}}
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
radioButtonWidget,
|
||||
textInputWidget,
|
||||
cellsWidget,
|
||||
textWidget
|
||||
textWidget,
|
||||
selectFormat
|
||||
} from "../../constant/Utils";
|
||||
import PlaceholderType from "./PlaceholderType";
|
||||
import moment from "moment";
|
||||
@@ -25,37 +26,6 @@ import { useDispatch } from "react-redux";
|
||||
import { setIsShowModal } from "../../redux/reducers/widgetSlice";
|
||||
import { themeColor } from "../../constant/const";
|
||||
|
||||
const selectFormat = (data) => {
|
||||
switch (data) {
|
||||
case "L":
|
||||
return "MM/dd/yyyy";
|
||||
case "MM/DD/YYYY":
|
||||
return "MM/dd/yyyy";
|
||||
case "DD-MM-YYYY":
|
||||
return "dd-MM-yyyy";
|
||||
case "DD/MM/YYYY":
|
||||
return "dd/MM/yyyy";
|
||||
case "LL":
|
||||
return "MMMM dd, yyyy";
|
||||
case "DD MMM, YYYY":
|
||||
return "dd MMM, yyyy";
|
||||
case "YYYY-MM-DD":
|
||||
return "yyyy-MM-dd";
|
||||
case "MM-DD-YYYY":
|
||||
return "MM-dd-yyyy";
|
||||
case "MM.DD.YYYY":
|
||||
return "MM.dd.yyyy";
|
||||
case "MMM DD, YYYY":
|
||||
return "MMM dd, yyyy";
|
||||
case "MMMM DD, YYYY":
|
||||
return "MMMM dd, yyyy";
|
||||
case "DD MMMM, YYYY":
|
||||
return "dd MMMM, yyyy";
|
||||
default:
|
||||
return "MM/dd/yyyy";
|
||||
}
|
||||
};
|
||||
|
||||
//function to get default format
|
||||
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
|
||||
|
||||
@@ -110,7 +80,9 @@ function Placeholder(props) {
|
||||
"MMM DD, YYYY",
|
||||
"LL",
|
||||
"DD MMM, YYYY",
|
||||
"DD MMMM, YYYY"
|
||||
"DD MMMM, YYYY",
|
||||
"DD.MM.YYYY",
|
||||
"DD/MM/YYYY"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,15 +104,18 @@ function Placeholder(props) {
|
||||
if (selectDate && selectDate.format === "dd-MM-yyyy") {
|
||||
const [day, month, year] = selectDate.date.split("-");
|
||||
date = new Date(`${year}-${month}-${day}`);
|
||||
} else if (selectDate && selectDate.format === "dd.MM.yyyy") {
|
||||
const [day, month, year] = selectDate.date.split(".");
|
||||
date = new Date(`${year}.${month}.${day}`);
|
||||
} else if (selectDate && selectDate.format === "dd/MM/yyyy") {
|
||||
const [day, month, year] = selectDate.date.split("/");
|
||||
date = new Date(`${year}/${month}/${day}`);
|
||||
} else {
|
||||
date = new Date(selectDate?.date);
|
||||
}
|
||||
const milliseconds = date.getTime();
|
||||
const newDate = moment(milliseconds).format(data);
|
||||
const dateObj = {
|
||||
date: newDate,
|
||||
format: selectFormat(data)
|
||||
};
|
||||
const dateObj = { date: newDate, format: selectFormat(data) };
|
||||
updateDate.push(dateObj);
|
||||
});
|
||||
setDateFormat(updateDate);
|
||||
@@ -391,9 +366,12 @@ function Placeholder(props) {
|
||||
|
||||
//function to save date and format on local array onchange date and onclick format
|
||||
const handleSaveDate = (data, isDateChange) => {
|
||||
const isSpecialDateFormat =
|
||||
data?.format &&
|
||||
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
|
||||
let updateDate = data.date;
|
||||
let date;
|
||||
if (data?.format === "dd-MM-yyyy") {
|
||||
if (isSpecialDateFormat) {
|
||||
date = isDateChange
|
||||
? moment(updateDate).format(changeDateToMomentFormat(data.format))
|
||||
: updateDate;
|
||||
@@ -859,8 +837,16 @@ function Placeholder(props) {
|
||||
props.isNeedSign && props.data?.Id !== props?.uniqueId && "0.4",
|
||||
background: handleBackground()
|
||||
}}
|
||||
onDrag={() => {
|
||||
onDrag={(_, d) => {
|
||||
props.handleTabDrag && props.handleTabDrag(props.pos.key);
|
||||
props.showGuidelines &&
|
||||
props.showGuidelines(
|
||||
true,
|
||||
d.x,
|
||||
d.y,
|
||||
d.node.offsetWidth,
|
||||
d.node.offsetHeight
|
||||
);
|
||||
}}
|
||||
size={{
|
||||
width:
|
||||
@@ -881,8 +867,26 @@ function Placeholder(props) {
|
||||
calculateFont(props.pos.options?.fontSize, true)
|
||||
}
|
||||
maxHeight="auto"
|
||||
onResizeStart={() => {
|
||||
onResizeStart={(e, dir, ref) => {
|
||||
props.setIsResize && props.setIsResize(true);
|
||||
props.showGuidelines &&
|
||||
props.showGuidelines(
|
||||
true,
|
||||
xPos(props.pos, props.isSignYourself),
|
||||
yPos(props.pos, props.isSignYourself),
|
||||
ref.offsetWidth,
|
||||
ref.offsetHeight
|
||||
);
|
||||
}}
|
||||
onResize={(e, dir, ref, delta, position) => {
|
||||
props.showGuidelines &&
|
||||
props.showGuidelines(
|
||||
true,
|
||||
position.x,
|
||||
position.y,
|
||||
ref.offsetWidth,
|
||||
ref.offsetHeight
|
||||
);
|
||||
}}
|
||||
onResizeStop={(e, direction, ref) => {
|
||||
setTimeout(() => {
|
||||
@@ -900,6 +904,7 @@ function Placeholder(props) {
|
||||
props.data && props.data.Id,
|
||||
props.isResize
|
||||
);
|
||||
props.showGuidelines && props.showGuidelines(false);
|
||||
}}
|
||||
onDragStop={(event, dragElement) => {
|
||||
props.handleStop &&
|
||||
@@ -909,6 +914,9 @@ function Placeholder(props) {
|
||||
props.data?.Id,
|
||||
props.pos?.key
|
||||
);
|
||||
props.isDragging &&
|
||||
props.showGuidelines &&
|
||||
props.showGuidelines(false);
|
||||
}}
|
||||
position={{
|
||||
x: xPos(props.pos, props.isSignYourself),
|
||||
@@ -945,7 +953,6 @@ function Placeholder(props) {
|
||||
![radioButtonWidget, "checkbox"].includes(props.pos.type) &&
|
||||
props.pos.key === props?.currWidgetsDetails?.key && <BorderResize />
|
||||
)}
|
||||
|
||||
{/* 1- Show a border if props.pos.key === props?.currWidgetsDetails?.key, indicating the current user's selected widget.
|
||||
2- If props.isShowBorder is true, display border for all widgets.
|
||||
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from "react";
|
||||
import React, { useEffect, useState, forwardRef } from "react";
|
||||
import {
|
||||
getMonth,
|
||||
getYear,
|
||||
@@ -36,7 +36,6 @@ function PlaceholderType(props) {
|
||||
const widgetData =
|
||||
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
|
||||
const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
|
||||
const inputRef = useRef(null);
|
||||
const [widgetValue, setwidgetValue] = useState();
|
||||
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
|
||||
const [hint, setHint] = useState("");
|
||||
@@ -158,7 +157,7 @@ function PlaceholderType(props) {
|
||||
const checkBoxWrapperClass = `flex items-start ${
|
||||
checkBoxLayout === "horizontal"
|
||||
? `flex-row flex-wrap ${isMultipleCheckbox ? "gap-x-2" : ""}`
|
||||
: `flex-col ${isMultipleCheckbox ? "gap-y-[5px]" : ""}`
|
||||
: `flex-col ${isMultipleCheckbox ? "gap-y-[3px]" : ""}`
|
||||
}`; // Using gap-y-1 for consistency, adjust if needed
|
||||
|
||||
return (
|
||||
@@ -193,7 +192,6 @@ function PlaceholderType(props) {
|
||||
case textInputWidget:
|
||||
return props.isSignYourself || iswidgetEnable ? (
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
placeholder={hint || t("widgets-name.text")}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -277,7 +275,6 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -300,7 +297,6 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -323,7 +319,6 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -429,7 +424,6 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -453,7 +447,7 @@ function PlaceholderType(props) {
|
||||
const isOnlyOneBtn = props.pos.options?.values?.length > 0 ? true : false;
|
||||
const radioWrapperClass = `flex items-start ${
|
||||
radioLayout === "horizontal"
|
||||
? `flex-row flex-wrap ${isOnlyOneBtn ? "gap-x-2" : ""}`
|
||||
? `flex-row flex-wrap ${isOnlyOneBtn ? "gap-x-[10px]" : ""}`
|
||||
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
|
||||
}`; // Using gap-y-1 for consistency, adjust if needed
|
||||
return (
|
||||
|
||||
@@ -82,7 +82,7 @@ const RecipientList = (props) => {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{props.signersdata.length > 0 &&
|
||||
{props.signersdata?.length > 0 &&
|
||||
props.signersdata.map((obj, ind) => {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -209,8 +209,8 @@ function RenderAllPdfPage(props) {
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<i className="fa-light fa-plus text-gray-500"></i>
|
||||
<span className="text-xs lg:text-sm text-base-content ">
|
||||
Add pages
|
||||
<span className="text-xs lg:text-sm text-base-content">
|
||||
{t("add-pages")}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import RSC from "react-scrollbars-custom";
|
||||
import { Document, Page } from "react-pdf";
|
||||
import {
|
||||
@@ -11,13 +11,45 @@ import {
|
||||
import Placeholder from "./Placeholder";
|
||||
import Alert from "../../primitives/Alert";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import usePdfPinchZoom from "../../hook/usePdfPinchZoom";
|
||||
|
||||
function RenderPdf(props) {
|
||||
const { t } = useTranslation();
|
||||
const [scaledHeight, setScaledHeight] = useState();
|
||||
const [guideline, setGuideline] = useState({
|
||||
show: false,
|
||||
x1: 0,
|
||||
x2: 0,
|
||||
y1: 0,
|
||||
y2: 0
|
||||
});
|
||||
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
|
||||
const isGuestSigner = localStorage.getItem("isGuestSigner");
|
||||
|
||||
const pdfContainerRef = useRef(null);
|
||||
|
||||
// enable pinch to zoom only on actual pdf wrapper
|
||||
usePdfPinchZoom(
|
||||
pdfContainerRef,
|
||||
props.scale,
|
||||
props.setScale,
|
||||
props.setZoomPercent
|
||||
);
|
||||
|
||||
const handleGuideline = (isShow, x = 0, y = 0, width = 0, height = 0) => {
|
||||
if (isShow) {
|
||||
setGuideline({
|
||||
show: true,
|
||||
x1: x,
|
||||
x2: x + width,
|
||||
y1: y,
|
||||
y2: y + height
|
||||
});
|
||||
} else {
|
||||
setGuideline({ show: false, x1: 0, x2: 0, y1: 0, y2: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
// handle signature block width and height according to screen
|
||||
const posWidth = (pos, signYourself) => {
|
||||
const containerScale = getContainerScale(
|
||||
@@ -115,6 +147,7 @@ function RenderPdf(props) {
|
||||
isSignYourself={false}
|
||||
posWidth={posWidth}
|
||||
posHeight={posHeight}
|
||||
showGuidelines={handleGuideline}
|
||||
isDragging={props.isDragging}
|
||||
pdfDetails={props.pdfDetails}
|
||||
unSignedWidgetId={props.unSignedWidgetId}
|
||||
@@ -204,14 +237,17 @@ function RenderPdf(props) {
|
||||
data-tut={isMobile ? "reactourForth" : undefined}
|
||||
className={
|
||||
isMobile
|
||||
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto`
|
||||
: ""
|
||||
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto relative`
|
||||
: "relative"
|
||||
}
|
||||
style={{
|
||||
width:
|
||||
props.containerWH?.width && props.containerWH?.width * props.scale
|
||||
}}
|
||||
ref={props.drop}
|
||||
ref={(node) => {
|
||||
pdfContainerRef.current = node;
|
||||
props.drop && props.drop(node);
|
||||
}}
|
||||
id="container"
|
||||
>
|
||||
{props.pdfLoad !== false &&
|
||||
@@ -258,6 +294,7 @@ function RenderPdf(props) {
|
||||
isSignYourself={false}
|
||||
posWidth={posWidth}
|
||||
posHeight={posHeight}
|
||||
showGuidelines={handleGuideline}
|
||||
isDragging={props.isDragging}
|
||||
setIsValidate={props.setIsValidate}
|
||||
setIsRadio={props.setIsRadio}
|
||||
@@ -325,6 +362,7 @@ function RenderPdf(props) {
|
||||
isSignYourself={true}
|
||||
posWidth={posWidth}
|
||||
posHeight={posHeight}
|
||||
showGuidelines={handleGuideline}
|
||||
pdfDetails={props.pdfDetails[0]}
|
||||
isDragging={props.isDragging}
|
||||
setIsCheckbox={props.setIsCheckbox}
|
||||
@@ -389,6 +427,30 @@ function RenderPdf(props) {
|
||||
}}
|
||||
/>
|
||||
</Document>
|
||||
{guideline.show && (
|
||||
<>
|
||||
{/* top guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ top: guideline.y1 }}
|
||||
/>
|
||||
{/* bottom guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ top: guideline.y2 }}
|
||||
/>
|
||||
{/* left guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ left: guideline.x1 }}
|
||||
/>
|
||||
{/* right guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ left: guideline.x2 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</RSC>
|
||||
</>
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
textInputWidget,
|
||||
cellsWidget,
|
||||
textWidget,
|
||||
years
|
||||
years,
|
||||
convertTextToImg
|
||||
} from "../../constant/Utils";
|
||||
import CellsWidget from "./CellsWidget";
|
||||
import DatePicker from "react-datepicker";
|
||||
@@ -67,6 +68,11 @@ const fontOptions = [
|
||||
{ value: "Delicious Handrawn" }
|
||||
// Add more font options as needed
|
||||
];
|
||||
const allColor = ["blue", "red", "black"];
|
||||
const textInputcls =
|
||||
"op-input op-input-bordered op-input-sm focus:outline-none text-base-content hover:border-base-content w-full text-xs";
|
||||
const isTabCls = "bg-[#002864] text-white rounded-[15px] px-[10px] py-[4px]";
|
||||
|
||||
function WidgetsValueModal(props) {
|
||||
const dispatch = useDispatch();
|
||||
const saveSignCheckbox = useSelector(
|
||||
@@ -167,10 +173,7 @@ function WidgetsValueModal(props) {
|
||||
)
|
||||
: new Date()
|
||||
);
|
||||
const allColor = ["blue", "red", "black"];
|
||||
const textInputcls =
|
||||
"op-input op-input-bordered op-input-sm focus:outline-none text-base-content hover:border-base-content w-full text-xs";
|
||||
const isTabCls = "bg-[#002864] text-white rounded-[15px] px-[10px] py-[4px]";
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
["name", "email", "job title", "company", textInputWidget].includes(
|
||||
@@ -187,6 +190,8 @@ function WidgetsValueModal(props) {
|
||||
} else {
|
||||
setHint(currWidgetsDetails?.type);
|
||||
}
|
||||
} else if (["signature", "initials"].includes(currWidgetsDetails?.type)) {
|
||||
setIsTab(currWidgetsDetails?.signatureType);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currWidgetsDetails]); // Added currWidgetsDetails to dependency array for reset logic
|
||||
@@ -345,7 +350,6 @@ function WidgetsValueModal(props) {
|
||||
const placeholderIndex = signer.placeHolder.findIndex(
|
||||
(x) => x.pageNumber === pageNumber
|
||||
);
|
||||
|
||||
const updatedPlaceholders = onSaveSign(
|
||||
type,
|
||||
signer.placeHolder,
|
||||
@@ -358,7 +362,9 @@ function WidgetsValueModal(props) {
|
||||
typedSignature,
|
||||
isAutoSign,
|
||||
widgetsType,
|
||||
isApplyAll
|
||||
isApplyAll,
|
||||
fontSelect,
|
||||
penColor
|
||||
);
|
||||
return {
|
||||
...signer,
|
||||
@@ -379,7 +385,9 @@ function WidgetsValueModal(props) {
|
||||
imgWH,
|
||||
isDefaultSign,
|
||||
isTypeText,
|
||||
typedSignature
|
||||
typedSignature,
|
||||
false,
|
||||
widgetsType
|
||||
);
|
||||
setXyPosition(getUpdatePosition);
|
||||
}
|
||||
@@ -405,7 +413,9 @@ function WidgetsValueModal(props) {
|
||||
if (getIndex !== -1) {
|
||||
setIsSignTypes(true);
|
||||
const tab = signatureTypes?.[getIndex].name;
|
||||
if (tab === "draw") {
|
||||
if (currWidgetsDetails?.signatureType === "type") {
|
||||
setIsTab("type");
|
||||
} else if (tab === "draw") {
|
||||
setIsTab("draw");
|
||||
} else if (tab === "upload") {
|
||||
setIsImageSelect(true);
|
||||
@@ -619,7 +629,7 @@ function WidgetsValueModal(props) {
|
||||
if (isTab === "type") {
|
||||
setSignature("");
|
||||
handleSaveSignature(
|
||||
null,
|
||||
"type",
|
||||
false,
|
||||
!currWidgetsDetails?.type === "initials" && textWidth > 150
|
||||
? 150
|
||||
@@ -694,94 +704,43 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
}
|
||||
if (isTab === "type") {
|
||||
//trim user name or typed name value to show in initial signature
|
||||
//trim user name or typed name value to show in initial signature
|
||||
const trimmedName = typedSignature
|
||||
? typedSignature?.trim()
|
||||
: currentUserName?.trim();
|
||||
//get full name of user
|
||||
const fullUserName = typedSignature || currentUserName
|
||||
//get full name of user
|
||||
const fullUserName = typedSignature || currentUserName;
|
||||
const firstCharacter = trimmedName?.charAt(0);
|
||||
const userName =
|
||||
currWidgetsDetails?.type === "initials"
|
||||
? firstCharacter
|
||||
: fullUserName;
|
||||
currWidgetsDetails?.type === "initials" ? firstCharacter : fullUserName;
|
||||
const signatureValue = currWidgetsDetails?.typeSignature;
|
||||
setTypedSignature(signatureValue || userName || "");
|
||||
convertToImg(fontSelect, signatureValue || userName || "");
|
||||
convertToImg(fontSelect, signatureValue || userName || "");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isTab]);
|
||||
// function for convert input text value in image
|
||||
const convertToImg = async (fontStyle, text, color) => {
|
||||
// 1) Read the max widget dimensions:
|
||||
const maxWidth = currWidgetsDetails.Width; // e.g. 150 px
|
||||
const maxHeight = currWidgetsDetails.Height; // e.g. 40 px
|
||||
|
||||
// 2) Pick a “baseline” font size for measurement:
|
||||
const baselineFontSizePx = 40;
|
||||
const chosenFontFamily = fontStyle || fontSelect || "Fasthand";
|
||||
const maxWidth = currWidgetsDetails?.Width;
|
||||
const maxHeight = currWidgetsDetails?.Height;
|
||||
const widgetDims = { Width: maxWidth, Height: maxHeight };
|
||||
const fontFamily = fontStyle || fontSelect || "Fasthand";
|
||||
const fillColor = color || penColor;
|
||||
|
||||
// 3) Create a temporary <span> (hidden) to measure the text at 40px:
|
||||
const span = document.createElement("span");
|
||||
span.textContent = text;
|
||||
span.style.font = `${baselineFontSizePx}px ${chosenFontFamily}`;
|
||||
span.style.visibility = "hidden"; // keep it in the DOM so offsetWidth/Height works
|
||||
span.style.whiteSpace = "nowrap"; // so we measure a single line
|
||||
document.body.appendChild(span);
|
||||
|
||||
// Measured size at 40px:
|
||||
const measuredWidth = span.offsetWidth;
|
||||
const measuredHeight = span.offsetHeight;
|
||||
document.body.removeChild(span);
|
||||
|
||||
// 4) Compute uniform scale so that 40px‐sized text fits inside (maxWidth × maxHeight):
|
||||
const scaleX = maxWidth / measuredWidth;
|
||||
const scaleY = maxHeight / measuredHeight;
|
||||
const scale = Math.min(scaleX, scaleY, 1); // never scale up beyond 1
|
||||
|
||||
// 5) Final text size in **CSS px**:
|
||||
const finalFontSizePx = baselineFontSizePx * scale;
|
||||
|
||||
// 6) Create a <canvas> that is ALWAYS maxWidth × maxHeight in **CSS px**,
|
||||
// but use devicePixelRatio for sharpness.
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// ★ Instead of using `finalTextWidth/Height`, force it to be the max box:
|
||||
canvas.width = Math.ceil(maxWidth * pixelRatio);
|
||||
canvas.height = Math.ceil(maxHeight * pixelRatio);
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(pixelRatio, pixelRatio);
|
||||
|
||||
// 7) Draw the text **centered** inside the full maxWidth×maxHeight box:
|
||||
ctx.font = `${finalFontSizePx}px ${chosenFontFamily}`;
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
// ★ Center = (maxWidth/2, maxHeight/2):
|
||||
const centerX = maxWidth / 2;
|
||||
const centerY = maxHeight / 2;
|
||||
|
||||
ctx.fillText(text, centerX, centerY);
|
||||
|
||||
// 8) Export to a PNG data-URL:
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims);
|
||||
setSignature(dataUrl);
|
||||
setTextWidth(maxWidth);
|
||||
setTextHeight(maxHeight);
|
||||
};
|
||||
const PenColorComponent = (props) => {
|
||||
const PenColorComponent = () => {
|
||||
return (
|
||||
<div className="flex flex-row items-center m-[5px] gap-3">
|
||||
<span className="text-base-content">Options</span>
|
||||
<span className="text-base-content">{t("options")}</span>
|
||||
{allColor.map((data, key) => {
|
||||
return (
|
||||
<i
|
||||
key={key}
|
||||
onClick={() => {
|
||||
props?.convertToImg &&
|
||||
props?.convertToImg(fontSelect, typedSignature, data);
|
||||
convertToImg(fontSelect, typedSignature, data);
|
||||
setPenColor(allColor[key]);
|
||||
}}
|
||||
className={`border-b-[2px] cursor-pointer ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
|
||||
@@ -792,17 +751,6 @@ function WidgetsValueModal(props) {
|
||||
);
|
||||
};
|
||||
|
||||
// `handleCancelBtn` function trigger when user click on cross button
|
||||
const handleCancelBtn = () => {
|
||||
setPenColor("blue");
|
||||
setIsImageSelect(false);
|
||||
setIsDefaultSign(false);
|
||||
setImage(null);
|
||||
setOriginalImage(null); // Add this
|
||||
setRemoveBgEnabled(false); // Add this
|
||||
handleTab();
|
||||
};
|
||||
|
||||
// Effect to store original image and apply initial BG removal if enabled
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -905,7 +853,7 @@ function WidgetsValueModal(props) {
|
||||
checked={isSavedSign}
|
||||
onChange={(e) => setIsSavedSign(e.target.checked)}
|
||||
/>
|
||||
{t("save")} {currWidgetsDetails?.type}
|
||||
{t("save")} {t(`widgets-name.${currWidgetsDetails?.type}`)}
|
||||
</label>
|
||||
);
|
||||
//function for set checked and unchecked value of checkbox
|
||||
@@ -1051,10 +999,28 @@ function WidgetsValueModal(props) {
|
||||
currWidgetsDetails?.type !== "image" && (
|
||||
<div className="text-base-content rounded-[4px] tabWidth">
|
||||
<div className="ml-3 flex justify-start gap-4 text-[11px] md:text-base my-[3px]">
|
||||
<>
|
||||
{currWidgetsDetails?.type !== "initials" &&
|
||||
defaultSignImg &&
|
||||
isTabEnabled("default") ? (
|
||||
{currWidgetsDetails?.type !== "initials" &&
|
||||
defaultSignImg &&
|
||||
isTabEnabled("default") ? (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(true);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("mysignature");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "mysignature" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("my-signature")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
currWidgetsDetails?.type === "initials" &&
|
||||
myInitial &&
|
||||
isTabEnabled("default") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
@@ -1065,84 +1031,64 @@ function WidgetsValueModal(props) {
|
||||
}}
|
||||
className={`${
|
||||
isTab === "mysignature" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("my-signature")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
currWidgetsDetails?.type === "initials" &&
|
||||
myInitial &&
|
||||
isTabEnabled("default") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(true);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("mysignature");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "mysignature" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("my-initials")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{isTabEnabled("draw") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("draw");
|
||||
setImage();
|
||||
setSignature("");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "draw" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("draw")}
|
||||
{t("my-initials")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("upload") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("uploadImage");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "uploadImage" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("upload-image")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("typed") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("type");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "type" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("type")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
{isTabEnabled("draw") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("draw");
|
||||
setImage();
|
||||
setSignature("");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "draw" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("draw")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("upload") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("uploadImage");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "uploadImage" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("upload-image")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("typed") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("type");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "type" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("type")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1177,7 +1123,7 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
</div>
|
||||
{/* Standalone autoSignAll for "My Signature/Initials" (isDefaultSign) if conditions met */}
|
||||
{setIsAutoSign && uniqueId && (
|
||||
{uniqueId && (
|
||||
<div className="flex justify-center my-2">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1206,7 +1152,7 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
</div>
|
||||
{/* Standalone autoSignAll for "My Signature/Initials" (isDefaultSign) if conditions met */}
|
||||
{setIsAutoSign && uniqueId && (
|
||||
{uniqueId && (
|
||||
<div className="flex justify-center my-2">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1231,8 +1177,8 @@ function WidgetsValueModal(props) {
|
||||
ref={imageRef}
|
||||
hidden
|
||||
/>
|
||||
<i className="fa-light fa-cloud-upload-alt uploadImgLogo"></i>
|
||||
<div className="text-[10px]">{t("upload")}</div>
|
||||
<i className="fa-light fa-cloud-upload-alt uploadImgLogo text-base-content"></i>
|
||||
<div className="text-[10px] text-base-content">{t("upload")}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1251,14 +1197,14 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
</div>
|
||||
{/* Shared container for autoSignAll and removeBackground when image is displayed for upload/stamp/image */}
|
||||
{((setIsAutoSign && uniqueId) ||
|
||||
{(uniqueId ||
|
||||
(image &&
|
||||
(isImageSelect ||
|
||||
["image", "stamp"].includes(
|
||||
currWidgetsDetails?.type
|
||||
)))) && (
|
||||
<div className="flex justify-center items-center gap-x-2 my-2">
|
||||
{setIsAutoSign && uniqueId && autoSignAll}
|
||||
{uniqueId && autoSignAll}
|
||||
{image &&
|
||||
(isImageSelect ||
|
||||
["image", "stamp"].includes(
|
||||
@@ -1348,7 +1294,7 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
<div className="flex flex-row ml-1 mt-2 gap-x-3 text-base-content">
|
||||
{/* Standalone autoSignAll for "Type" tab if conditions met */}
|
||||
{setIsAutoSign && uniqueId && (
|
||||
{uniqueId && (
|
||||
<div className="flex justify-start my-1">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1384,7 +1330,7 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
<div className="flex flex-row ml-1 mt-1 gap-x-3 text-base-content">
|
||||
{/* Standalone autoSignAll for "Draw" tab if conditions met */}
|
||||
{setIsAutoSign && uniqueId && (
|
||||
{uniqueId && (
|
||||
<div className="flex justify-start my-1">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1781,6 +1727,7 @@ function WidgetsValueModal(props) {
|
||||
break;
|
||||
}
|
||||
};
|
||||
// console.log("currwidgetdetails ", currWidgetsDetails )
|
||||
//function too use on click on next/finish button then update modal UI according to current widgets
|
||||
const handleClickOnNext = (isFinishDoc) => {
|
||||
if (
|
||||
|
||||
@@ -18,16 +18,16 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
|
||||
className={({ isActive }) =>
|
||||
`${
|
||||
isActive ? " bg-base-300 text-base-content" : ""
|
||||
} flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
}
|
||||
onClick={closeSidebar}
|
||||
tabIndex={isOpen ? 0 : -1}
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="w-[20px] h-[20px] flex justify-center">
|
||||
<i className={`${item.icon} text-[18px]`} aria-hidden="true"></i>
|
||||
<i className={`${item.icon} text-[20px]`} aria-hidden="true"></i>
|
||||
</span>
|
||||
<span className="ml-3 lg:ml-4">
|
||||
<span className="flex items-center mb-0.5">
|
||||
{t(`sidebar.${item.title}`, { appName: drivename })}
|
||||
</span>
|
||||
</NavLink>
|
||||
|
||||
@@ -12,16 +12,16 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
|
||||
<li role="none" className="my-0.5">
|
||||
<button
|
||||
onClick={() => toggleSubmenu(item.title)}
|
||||
className="flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none "
|
||||
className="flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none"
|
||||
aria-expanded={submenuOpen}
|
||||
aria-haspopup="true"
|
||||
aria-controls={`submenu-${title}`}
|
||||
>
|
||||
<span className="w-[20px] h-[20px] flex justify-center">
|
||||
<i className={`${icon} text-[18px]`}></i>
|
||||
<i className={`${icon} text-[20px]`}></i>
|
||||
</span>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<span className="ml-3 lg:ml-4 text-start">
|
||||
<span className="flex items-center mb-0.5">
|
||||
{t(`sidebar.${item.title}`, { appName })}
|
||||
</span>
|
||||
<i
|
||||
@@ -46,20 +46,20 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
|
||||
}
|
||||
className={({ isActive }) =>
|
||||
`${
|
||||
isActive ? "bg-base-300 text-base-content" : ""
|
||||
} flex items-center justify-start text-left pl-6 md:pl-8 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
isActive ? "bg-base-300 text-base-content " : ""
|
||||
} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
}
|
||||
onClick={closeSidebar}
|
||||
role="menuitem"
|
||||
tabIndex={submenuOpen ? 0 : -1}
|
||||
>
|
||||
<span className="w-[15px] h-[15px] flex justify-center">
|
||||
<span className="w-[18px] h-[18px] flex justify-center">
|
||||
<i
|
||||
className={`${childItem.icon} text-[18px]`}
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
<span className="ml-3 lg:ml-4">
|
||||
<span className="mb-0.5">
|
||||
{t(`sidebar.${item.title}-Children.${childItem.title}`, {
|
||||
appName: drivename
|
||||
})}
|
||||
|
||||
@@ -241,6 +241,13 @@ export const handleImageResize = (
|
||||
signerId,
|
||||
showResize
|
||||
) => {
|
||||
// Compute widget dimensions only once
|
||||
const { offsetWidth, offsetHeight } = ref;
|
||||
const factor = scale * containerScale || 1;
|
||||
const widgetWidth = offsetWidth / factor;
|
||||
const widgetHeight = offsetHeight / factor;
|
||||
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
|
||||
|
||||
const filterSignerPos = signerPos.filter((data) => data.Id === signerId);
|
||||
if (filterSignerPos.length > 0) {
|
||||
const getPlaceHolder = filterSignerPos[0].placeHolder;
|
||||
@@ -252,12 +259,28 @@ export const handleImageResize = (
|
||||
const getPosData = getXYdata;
|
||||
const addSignPos = getPosData.map((url) => {
|
||||
if (url.key === key) {
|
||||
return {
|
||||
// Base fields for every resized signature
|
||||
const base = {
|
||||
...url,
|
||||
Width: ref.offsetWidth / (containerScale * scale),
|
||||
Height: ref.offsetHeight / (containerScale * scale),
|
||||
Width: widgetWidth,
|
||||
Height: widgetHeight,
|
||||
IsResize: showResize ? true : false
|
||||
};
|
||||
// If it's a “type” signature, regenerate the image and options
|
||||
if (url.typeSignature && url.signatureType === "type") {
|
||||
const signImg = convertTextToImg(
|
||||
url.typeFont ?? "Fasthand",
|
||||
url.typeSignature,
|
||||
url.fontColor ?? "blue",
|
||||
widgetDims
|
||||
);
|
||||
return {
|
||||
...base,
|
||||
SignUrl: signImg,
|
||||
options: { ...url.options, response: signImg }
|
||||
};
|
||||
}
|
||||
return base;
|
||||
}
|
||||
return url;
|
||||
});
|
||||
@@ -280,6 +303,7 @@ export const handleImageResize = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const widgets = [
|
||||
{ type: "signature", icon: "fa-light fa-pen-nib", iconSize: "20px" },
|
||||
{ type: "stamp", icon: "fa-light fa-stamp", iconSize: "19px" },
|
||||
@@ -336,6 +360,8 @@ export const selectFormat = (data) => {
|
||||
return "MMMM dd, yyyy";
|
||||
case "DD MMMM, YYYY":
|
||||
return "dd MMMM, yyyy";
|
||||
case "DD.MM.YYYY":
|
||||
return "dd.MM.yyyy";
|
||||
default:
|
||||
return "MM/dd/yyyy";
|
||||
}
|
||||
@@ -363,6 +389,8 @@ export const changeDateToMomentFormat = (format) => {
|
||||
return "MMM DD, YYYY";
|
||||
case "dd MMMM, yyyy":
|
||||
return "DD MMMM, YYYY";
|
||||
case "dd.MM.yyyy":
|
||||
return"DD.MM.YYYY";
|
||||
default:
|
||||
return "L";
|
||||
}
|
||||
@@ -590,7 +618,6 @@ export async function getBase64FromIMG(url) {
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = function () {
|
||||
const pdfBase = this.result;
|
||||
|
||||
resolve(pdfBase);
|
||||
};
|
||||
});
|
||||
@@ -632,27 +659,57 @@ export const handleSignYourselfImageResize = (
|
||||
containerScale,
|
||||
scale
|
||||
) => {
|
||||
const getXYdata = xyPosition[index].pos;
|
||||
const getPosData = getXYdata;
|
||||
const addSign = getPosData.map((url) => {
|
||||
if (url.key === key) {
|
||||
return {
|
||||
...url,
|
||||
Width: ref.offsetWidth / (scale * containerScale),
|
||||
Height: ref.offsetHeight / (scale * containerScale),
|
||||
IsResize: true
|
||||
};
|
||||
}
|
||||
return url;
|
||||
});
|
||||
// Guard against bad index
|
||||
if (!xyPosition[index]) {
|
||||
console.log(`ImageResize invalid index ${index}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newUpdateUrl = xyPosition.map((obj, ind) => {
|
||||
if (ind === index) {
|
||||
return { ...obj, pos: addSign };
|
||||
}
|
||||
return obj;
|
||||
// Compute widget dimensions only once
|
||||
const { offsetWidth, offsetHeight } = ref;
|
||||
const factor = scale * containerScale || 1;
|
||||
const widgetWidth = offsetWidth / factor;
|
||||
const widgetHeight = offsetHeight / factor;
|
||||
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
|
||||
|
||||
// Single pass to update only the targeted index/key
|
||||
const updated = xyPosition.map((item, idx) => {
|
||||
if (idx !== index) return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
pos: item.pos.map((url) => {
|
||||
if (url.key !== key) return url;
|
||||
|
||||
// Base fields for every resized signature
|
||||
const base = {
|
||||
...url,
|
||||
Width: widgetWidth,
|
||||
Height: widgetHeight,
|
||||
IsResize: true
|
||||
};
|
||||
|
||||
// If it's a “type” signature, regenerate the image and options
|
||||
if (url.typeSignature && url.signatureType === "type") {
|
||||
const signImg = convertTextToImg(
|
||||
url.typeFont ?? "Fasthand",
|
||||
url.typeSignature,
|
||||
url.fontColor ?? "blue",
|
||||
widgetDims
|
||||
);
|
||||
|
||||
return {
|
||||
...base,
|
||||
SignUrl: signImg,
|
||||
options: { ...url.options, response: signImg }
|
||||
};
|
||||
}
|
||||
|
||||
return base;
|
||||
})
|
||||
};
|
||||
});
|
||||
setXyPosition(newUpdateUrl);
|
||||
setXyPosition(updated);
|
||||
};
|
||||
|
||||
//function for call cloud function signPdf and generate digital signature
|
||||
@@ -1205,6 +1262,62 @@ export const embedDocId = async (pdfOriginalWH, pdfDoc, documentId) => {
|
||||
}
|
||||
};
|
||||
|
||||
// function for convert input text value in image
|
||||
export function convertTextToImg(fontStyle, text, color, widgetDims) {
|
||||
// read your widget dimensions:
|
||||
const maxWidth = widgetDims.Width;
|
||||
const maxHeight = widgetDims.Height;
|
||||
const baselineFontSizePx = maxHeight;
|
||||
const fontFamily = fontStyle || "Fasthand";
|
||||
const fillColor = color || "blue";
|
||||
|
||||
// 1. Use a temporary canvas to measure actual text size
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
tempCtx.font = `${baselineFontSizePx}px ${fontFamily}`;
|
||||
const metrics = tempCtx.measureText(text);
|
||||
|
||||
const actualHeight =
|
||||
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
||||
const actualWidth = metrics.width;
|
||||
|
||||
// const renderscale = 3;
|
||||
// 2. Scale font size so text height fills canvas
|
||||
const scaleX = maxWidth / actualWidth;
|
||||
const scaleY = maxHeight / actualHeight;
|
||||
const scale = Math.min(scaleX, scaleY, 1);
|
||||
// final font-size
|
||||
// const finalFontSizePx = baselineFontSizePx * scale * renderscale;
|
||||
const finalFontSizePx = baselineFontSizePx * scale;
|
||||
// 3. Create the final canvas
|
||||
const pxRatio = window.devicePixelRatio * 2 || 3;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.ceil(maxWidth * pxRatio);
|
||||
canvas.height = Math.ceil(maxHeight * pxRatio);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(pxRatio, pxRatio);
|
||||
|
||||
// 4. Setup text with final font
|
||||
ctx.font = `${finalFontSizePx}px ${fontFamily}`;
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
|
||||
const newMetrics = ctx.measureText(text);
|
||||
const ascent = newMetrics.actualBoundingBoxAscent;
|
||||
const descent = newMetrics.actualBoundingBoxDescent;
|
||||
|
||||
// 5. Align text vertically so it touches top and bottom
|
||||
const y = maxHeight / 2 + (ascent - descent) / 2;
|
||||
|
||||
ctx.clearRect(0, 0, maxWidth, maxHeight);
|
||||
ctx.fillText(text, maxWidth / 2, y);
|
||||
|
||||
// 6. Output image
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
//function for save button to save signature or image url
|
||||
export function onSaveSign(
|
||||
type,
|
||||
@@ -1218,10 +1331,14 @@ export function onSaveSign(
|
||||
typedSignature,
|
||||
isAutoSign,
|
||||
widgetsType,
|
||||
isApplyAll
|
||||
isApplyAll,
|
||||
typeFont,
|
||||
fontColor
|
||||
) {
|
||||
let getIMGWH, posWidth, posHeight;
|
||||
let getXYdata = xyPosition[index].pos;
|
||||
const isSignOrInitials =
|
||||
widgetsType && ["signature", "initials"].includes(widgetsType);
|
||||
const updateXYData = getXYdata.map((position) => {
|
||||
if (position.key === signKey) {
|
||||
if (isDefaultSign === "initials") {
|
||||
@@ -1248,14 +1365,22 @@ export function onSaveSign(
|
||||
}
|
||||
posWidth = getIMGWH ? getIMGWH.newWidth : 150;
|
||||
posHeight = getIMGWH ? getIMGWH.newHeight : 60;
|
||||
const widgetDims = { Width: posWidth, Height: posHeight };
|
||||
const signImg = typedSignature
|
||||
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
|
||||
: signatureImg;
|
||||
return {
|
||||
...position,
|
||||
Width: posWidth,
|
||||
Height: posHeight,
|
||||
SignUrl: signatureImg,
|
||||
...(type && { signatureType: type }),
|
||||
options: { ...position.options, response: signatureImg },
|
||||
...(typedSignature && { typeSignature: typedSignature })
|
||||
SignUrl: signImg,
|
||||
...(isSignOrInitials && { signatureType: type || "" }),
|
||||
options: { ...position.options, response: signImg },
|
||||
...(typedSignature && {
|
||||
typeSignature: typedSignature,
|
||||
typeFont: typeFont ?? "Fasthand",
|
||||
fontColor: fontColor ?? "blue"
|
||||
})
|
||||
};
|
||||
}
|
||||
return position;
|
||||
@@ -1271,20 +1396,29 @@ export function onSaveSign(
|
||||
if (isApplyAll || isAutoSign) {
|
||||
const updatedArray = updateXYposition.map((page) => ({
|
||||
...page,
|
||||
pos: page.pos.map(
|
||||
(item) =>
|
||||
item.type === widgetsType
|
||||
? {
|
||||
...item,
|
||||
Width: posWidth,
|
||||
Height: posHeight,
|
||||
SignUrl: signatureImg,
|
||||
...(type && { signatureType: type }),
|
||||
options: { ...item.options, response: signatureImg },
|
||||
...(typedSignature && { typeSignature: typedSignature })
|
||||
}
|
||||
: item // Otherwise, keep it unchanged
|
||||
)
|
||||
pos: page.pos.map((item) => {
|
||||
if (item.type === widgetsType) {
|
||||
const widgetDims = { Width: item.Width, Height: item.Height };
|
||||
const signImg = typedSignature
|
||||
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
|
||||
: signatureImg;
|
||||
return {
|
||||
...item,
|
||||
// Only copy signature content, keep existing dimensions
|
||||
// Width: posWidth,
|
||||
// Height: posHeight,
|
||||
SignUrl: signImg,
|
||||
...(isSignOrInitials && { signatureType: type || "" }),
|
||||
options: { ...item.options, response: signImg },
|
||||
...(typedSignature && {
|
||||
typeSignature: typedSignature,
|
||||
typeFont: typeFont || "Fasthand",
|
||||
fontColor: fontColor || "blue"
|
||||
})
|
||||
};
|
||||
}
|
||||
return item; // Otherwise, keep it unchanged
|
||||
})
|
||||
}));
|
||||
return updatedArray;
|
||||
} else {
|
||||
@@ -1318,8 +1452,8 @@ export const handleCopySignUrl = (
|
||||
if (x.key === currentPos.key) {
|
||||
return {
|
||||
...x,
|
||||
Width: existSignPosition.Width,
|
||||
Height: existSignPosition.Height,
|
||||
// Width: existSignPosition.Width,
|
||||
// Height: existSignPosition.Height,
|
||||
SignUrl: existSignPosition.SignUrl,
|
||||
signatureType: existSignPosition.signatureType,
|
||||
options: { ...x.options, response: existSignPosition.SignUrl },
|
||||
@@ -1351,7 +1485,6 @@ export const calculateImgAspectRatio = (imgWH, pos) => {
|
||||
const aspectRatio = imgWH.width / imgWH.height;
|
||||
newWidth = aspectRatio * placeholderHeight;
|
||||
newHeight = placeholderHeight;
|
||||
|
||||
return { newHeight, newWidth };
|
||||
};
|
||||
|
||||
@@ -1404,8 +1537,8 @@ export function onSaveImage(
|
||||
item.type === widgetsType && item.type !== "image"
|
||||
? {
|
||||
...item,
|
||||
Width: getIMGWH.newWidth,
|
||||
Height: getIMGWH.newHeight,
|
||||
// Width: getIMGWH.newWidth,
|
||||
// Height: getIMGWH.newHeight,
|
||||
SignUrl: image.src,
|
||||
ImageType: image.imgType,
|
||||
options: {
|
||||
@@ -1552,13 +1685,7 @@ const getWidgetsFontColor = (type) => {
|
||||
}
|
||||
};
|
||||
//function for embed multiple signature using pdf-lib
|
||||
export const multiSignEmbed = async (
|
||||
pdfOriginalWH,
|
||||
widgets,
|
||||
pdfDoc,
|
||||
signyourself,
|
||||
scale
|
||||
) => {
|
||||
export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
// `fontBytes` is used to embed custom font in pdf
|
||||
const fontBytes = await fileasbytes(
|
||||
"https://cdn.opensignlabs.com/webfonts/times.ttf"
|
||||
@@ -1567,15 +1694,9 @@ export const multiSignEmbed = async (
|
||||
const font = await pdfDoc.embedFont(fontBytes, { subset: true });
|
||||
let hasError = false;
|
||||
for (let item of widgets) {
|
||||
//pdfOriginalWH contained all pdf's pages width and height
|
||||
//'getSize' is used to get particular pdf's page width and height
|
||||
const getSize = pdfOriginalWH.find(
|
||||
(page) => page?.pageNumber === item?.pageNumber
|
||||
);
|
||||
if (hasError) break; // Stop the outer loop if an error occurred
|
||||
const typeExist = item.pos.some((data) => data?.type);
|
||||
let updateItem;
|
||||
|
||||
if (typeExist) {
|
||||
if (signyourself) {
|
||||
updateItem = item.pos;
|
||||
@@ -1601,6 +1722,21 @@ export const multiSignEmbed = async (
|
||||
const pages = pdfDoc.getPages();
|
||||
const form = pdfDoc.getForm();
|
||||
const page = pages[pageNo - 1];
|
||||
// `page.getCropBox()` returns the visible area of the PDF page.
|
||||
// It provides an object with the following properties:
|
||||
// - x: the x-coordinate of the lower-left corner of the visible area
|
||||
// - y: the y-coordinate of the lower-left corner of the visible area
|
||||
// - width: the width of the visible area (x1 - x0)
|
||||
// - height: the height of the visible area (y1 - y0)
|
||||
// If the page does not explicitly define a CropBox, this method returns
|
||||
// the MediaBox instead, which represents the full physical size of the page.
|
||||
//
|
||||
// Example CropBox: [0, 7.92, 612, 799.92]
|
||||
// => x = 0, y = 7.92, width = 612 - 0 = 612, height = 799.92 - 7.92 = 792
|
||||
// This is equivalent to `react-pdf`'s `page.view` array: [x0, y0, x1, y1]
|
||||
const { _, y, width, height } = page.getCropBox();
|
||||
const originalHeight = height + y;
|
||||
const getSize = { height: originalHeight, width: width };
|
||||
const images = await Promise.all(
|
||||
widgetsPositionArr.map(async (widget) => {
|
||||
// `SignUrl` this is wrong nomenclature and maintain for older code in this options we save base64 of signature image from sign pad
|
||||
@@ -1701,8 +1837,8 @@ export const multiSignEmbed = async (
|
||||
let currentY = yPos(position) + 2;
|
||||
// Size and spacing settings
|
||||
const checkboxSize = fontSize - 1; // checkbox diameter
|
||||
const checkboxTextGapFromLeft = fontSize + 5; // gap between box and its label
|
||||
const verticalGap = fontSize + 3.2; // gap between two rows (vertical layout)
|
||||
const checkboxTextGapFromLeft = fontSize + 3.4; // gap between box and its label
|
||||
const verticalGap = fontSize + 3.4; // gap between two rows (vertical layout)
|
||||
let horizontalGap = 0; // will compute after drawing each label
|
||||
if (position?.options?.values.length > 0) {
|
||||
position.options.values.forEach((item, ind) => {
|
||||
@@ -1766,8 +1902,10 @@ export const multiSignEmbed = async (
|
||||
// Measure the width of this label text at `fontSize`
|
||||
const textWidth = font.widthOfTextAtSize(item, fontSize);
|
||||
// Next checkbox should come after: [box] + gap + [label text] + extra 10pt padding
|
||||
horizontalGap =
|
||||
checkboxSize + checkboxTextGapFromLeft + textWidth;
|
||||
const gap = position.options?.isHideLabel
|
||||
? checkboxTextGapFromLeft - 5
|
||||
: checkboxTextGapFromLeft + textWidth;
|
||||
horizontalGap = checkboxSize + gap;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1912,13 +2050,13 @@ export const multiSignEmbed = async (
|
||||
const radioGroup = form.createRadioGroup(radioRandomId);
|
||||
//getting radio buttons options text font size
|
||||
const optionsFontSize = fontSize; // font size for option text
|
||||
const radioTextGapFromLeft = fontSize + 6; // gap between circle and its label
|
||||
const radioTextGapFromLeft = fontSize + 3; // gap between circle and its label
|
||||
const radioSize = fontSize; // circle diameter (square of width×height)
|
||||
// Initial “cursor” positions (from your existing helpers)
|
||||
let currentX = xPos(position) + 2;
|
||||
let currentY = yPos(position);
|
||||
let currentY = yPos(position) + 3;
|
||||
// Vertical gap between two radio‐rows
|
||||
const verticalGap = fontSize + 6;
|
||||
const verticalGap = fontSize + 3;
|
||||
// We’ll compute horizontalGap on the fly—after drawing each label
|
||||
// Initialize to zero (will be set after first option is placed)
|
||||
let horizontalGap = 0;
|
||||
@@ -1974,7 +2112,10 @@ export const multiSignEmbed = async (
|
||||
const textWidth = font.widthOfTextAtSize(item, optionsFontSize);
|
||||
// radioSize = the circle. radioTextGapFromLeft = gap between circle and label.
|
||||
// Add a small extra padding (e.g. 10pt) before placing next circle.
|
||||
horizontalGap = radioSize + radioTextGapFromLeft + textWidth;
|
||||
const gap = position?.options?.isHideLabel
|
||||
? radioTextGapFromLeft - 6
|
||||
: radioTextGapFromLeft + textWidth;
|
||||
horizontalGap = radioSize + gap;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2725,9 +2866,9 @@ function getWidgetPosition(page, image, sizeRatio, getSize) {
|
||||
let pageWidth;
|
||||
// pageHeight;
|
||||
if ([90, 270].includes(page.getRotation().angle)) {
|
||||
pageWidth = page.getHeight();
|
||||
pageWidth = getSize?.height || page.getHeight();
|
||||
} else {
|
||||
pageWidth = page.getWidth();
|
||||
pageWidth = getSize?.width || page.getWidth();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
if (!image?.hasOwnProperty("vpWidth")) {
|
||||
@@ -2864,8 +3005,9 @@ export const convertBase64ToFile = async (
|
||||
}
|
||||
};
|
||||
export const onClickZoomIn = (scale, zoomPercent, setScale, setZoomPercent) => {
|
||||
setScale(scale + 0.1 * scale);
|
||||
setZoomPercent(zoomPercent + 10);
|
||||
const newPercent = zoomPercent + 10;
|
||||
setZoomPercent(newPercent);
|
||||
setScale(1 + newPercent / 100);
|
||||
};
|
||||
export const onClickZoomOut = (
|
||||
zoomPercent,
|
||||
@@ -2874,12 +3016,9 @@ export const onClickZoomOut = (
|
||||
setScale
|
||||
) => {
|
||||
if (zoomPercent > 0) {
|
||||
if (zoomPercent === 10) {
|
||||
setScale(1);
|
||||
} else {
|
||||
setScale(scale - 0.1 * scale);
|
||||
}
|
||||
setZoomPercent(zoomPercent - 10);
|
||||
const newPercent = Math.max(0, zoomPercent - 10);
|
||||
setZoomPercent(newPercent);
|
||||
setScale(1 + newPercent / 100);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3325,8 +3464,13 @@ export const getOriginalWH = async (pdf) => {
|
||||
for (let index = 0; index < totalPages; index++) {
|
||||
try {
|
||||
const getPage = await pdf.getPage(index + 1);
|
||||
const width = getPage?.view[2];
|
||||
const height = getPage?.view[3];
|
||||
const scale = 1;
|
||||
//getting extra height which is removed from viewPort height (letter type pdf have originial h-799.92 but using getPage.getViewport getting height-792 something diffrence-7.92px)
|
||||
//getPage?.view[1] store diffrence of height (Page starts 7.92 from bottom)
|
||||
const y0 = getPage?.view[1] || 0;
|
||||
let { width, height } = getPage.getViewport({ scale });
|
||||
//adding it on viewPort height then get original height ex- 792+7.92 = 799.92 orignial height
|
||||
height = height + y0;
|
||||
pdfWHObj.push({ pageNumber: index + 1, width, height });
|
||||
} catch (e) {
|
||||
console.log(`Error getting page ${index + 1} of PDF: ${e.message}`);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function usePdfPinchZoom(ref, scale, setScale, setZoomPercent) {
|
||||
const initialDistanceRef = useRef(null);
|
||||
const startScaleRef = useRef(scale);
|
||||
const currentScaleRef = useRef(scale);
|
||||
const pinchScaleRef = useRef(null); // stores relative scale during a gesture
|
||||
const frameRef = useRef(null);
|
||||
|
||||
// keep a reference to the latest scale value
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
currentScaleRef.current = scale;
|
||||
// once React commits the new scale after a pinch we clear the transform
|
||||
if (element && !pinchScaleRef.current) {
|
||||
element.style.transform = "";
|
||||
}
|
||||
}, [scale, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const getDistance = (touches) => {
|
||||
const [touch1, touch2] = touches;
|
||||
const dx = touch1.clientX - touch2.clientX;
|
||||
const dy = touch1.clientY - touch2.clientY;
|
||||
return Math.hypot(dx, dy);
|
||||
};
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
if (e.touches.length === 2) {
|
||||
initialDistanceRef.current = getDistance(e.touches);
|
||||
startScaleRef.current = currentScaleRef.current;
|
||||
element.style.transformOrigin = "0 0";
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
if (e.touches.length === 2 && initialDistanceRef.current) {
|
||||
e.preventDefault();
|
||||
const newDistance = getDistance(e.touches);
|
||||
const pinchScale = newDistance / initialDistanceRef.current;
|
||||
const newScale = Math.max(1, startScaleRef.current * pinchScale);
|
||||
// store relative scale so it can be applied on top of current width
|
||||
pinchScaleRef.current = newScale / startScaleRef.current;
|
||||
|
||||
if (!frameRef.current) {
|
||||
frameRef.current = requestAnimationFrame(() => {
|
||||
element.style.transform = `scale(${pinchScaleRef.current})`;
|
||||
frameRef.current = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (pinchScaleRef.current) {
|
||||
let relativeScale = pinchScaleRef.current;
|
||||
const inlineTransform = element.style.transform;
|
||||
if (inlineTransform && inlineTransform.startsWith("scale(")) {
|
||||
const value = parseFloat(inlineTransform.slice(6, -1));
|
||||
if (!isNaN(value)) {
|
||||
relativeScale = value;
|
||||
}
|
||||
}
|
||||
const finalScale = Math.max(1, startScaleRef.current * relativeScale);
|
||||
const percent = parseFloat(((finalScale - 1) * 100).toFixed(2));
|
||||
const unchanged =
|
||||
Math.abs(finalScale - currentScaleRef.current) < 0.001;
|
||||
|
||||
pinchScaleRef.current = null;
|
||||
if (frameRef.current) {
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
frameRef.current = null;
|
||||
}
|
||||
|
||||
if (unchanged) {
|
||||
// if scale didn't actually change, reset transform right away
|
||||
element.style.transform = "";
|
||||
} else {
|
||||
// keep transform applied until state update reflects the new scale
|
||||
element.style.transform = `scale(${relativeScale})`;
|
||||
}
|
||||
|
||||
setScale(finalScale);
|
||||
setZoomPercent(percent);
|
||||
|
||||
// ensure refs reflect the latest value before next gesture starts
|
||||
currentScaleRef.current = finalScale;
|
||||
startScaleRef.current = finalScale;
|
||||
}
|
||||
initialDistanceRef.current = null;
|
||||
};
|
||||
|
||||
element.addEventListener("touchstart", handleTouchStart, {
|
||||
passive: false
|
||||
});
|
||||
element.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
element.addEventListener("touchend", handleTouchEnd);
|
||||
element.addEventListener("touchcancel", handleTouchEnd);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("touchstart", handleTouchStart);
|
||||
element.removeEventListener("touchmove", handleTouchMove);
|
||||
element.removeEventListener("touchend", handleTouchEnd);
|
||||
element.removeEventListener("touchcancel", handleTouchEnd);
|
||||
};
|
||||
}, [ref, setScale, setZoomPercent]);
|
||||
}
|
||||
@@ -693,7 +693,6 @@ function PdfRequestFiles(
|
||||
}
|
||||
//embed all widgets in document
|
||||
const pdfBytes = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
widgets,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
|
||||
@@ -863,7 +863,6 @@ function PlaceHolderSign() {
|
||||
try {
|
||||
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
|
||||
const pdfBase64 = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
placeholder,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
@@ -1745,14 +1744,14 @@ function PlaceHolderSign() {
|
||||
objectId: data.objectId
|
||||
};
|
||||
const updatePlaceHolder = signerPos.map((x) => {
|
||||
if (x.Id === id) {
|
||||
if (x.Id === id || x.signerObjId === id) {
|
||||
return { ...x, signerPtr: signerPtr, signerObjId: data.objectId };
|
||||
}
|
||||
return { ...x };
|
||||
});
|
||||
setSignerPos(updatePlaceHolder);
|
||||
const updateSigner = signersdata.map((x) => {
|
||||
if (x.Id === id) {
|
||||
if (x.Id === id || x.objectId === id) {
|
||||
return { ...x, ...data, className: "contracts_Contactbook" };
|
||||
}
|
||||
return { ...x };
|
||||
@@ -1767,7 +1766,9 @@ function PlaceHolderSign() {
|
||||
}
|
||||
}
|
||||
setSignersData(updateSigner);
|
||||
const index = signersdata.findIndex((x) => x.Id === id);
|
||||
const index = signersdata.findIndex(
|
||||
(x) => x.Id === id || x.objectId === id
|
||||
);
|
||||
setIsSelectId(index);
|
||||
}
|
||||
if (isNewContact.status) {
|
||||
@@ -1931,7 +1932,9 @@ function PlaceHolderSign() {
|
||||
};
|
||||
//`handleInputChange` function to get signers list from dropdown
|
||||
const handleInputChange = (item, id) => {
|
||||
const signerExist = forms.some((x) => x.value === item.value);
|
||||
const signerExist = signersdata?.some(
|
||||
(x) => x.objectId && x.objectId === item.value
|
||||
);
|
||||
if (signerExist) {
|
||||
alert(t("already-exist-signer"));
|
||||
} else {
|
||||
|
||||
@@ -56,6 +56,10 @@ const Preferences = () => {
|
||||
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY");
|
||||
const [is12HourTime, setIs12HourTime] = useState(false);
|
||||
const [isLTVEnabled, setIsLTVEnabled] = useState(false);
|
||||
const [isDefaultMail, setIsDefaultMail] = useState({
|
||||
requestMail: false,
|
||||
completionMail: false
|
||||
});
|
||||
useEffect(() => {
|
||||
fetchSignType();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -227,6 +231,7 @@ const Preferences = () => {
|
||||
setRequestSubject(
|
||||
`{{sender_name}} has requested you to sign {{document_title}}`
|
||||
);
|
||||
setIsDefaultMail((prev) => ({ ...prev, requestMail: true }));
|
||||
}
|
||||
setDefaultReqHtml({
|
||||
body: defaultRequestBody,
|
||||
@@ -241,6 +246,7 @@ const Preferences = () => {
|
||||
setCompletionSubject(
|
||||
`Document {{document_title}} has been signed by all parties`
|
||||
);
|
||||
setIsDefaultMail((prev) => ({ ...prev, completionMail: true }));
|
||||
}
|
||||
setDefaultCompHtml({
|
||||
body: defaultCompletionBody,
|
||||
@@ -318,13 +324,45 @@ const Preferences = () => {
|
||||
};
|
||||
|
||||
//function to use reset form
|
||||
const handleReset = (request, completion) => {
|
||||
if (request) {
|
||||
const handleReset = async (request, completion) => {
|
||||
let extUser =
|
||||
localStorage.getItem("Extand_Class") &&
|
||||
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
handleModifyMail(request);
|
||||
if (request && !isDefaultMail?.requestMail) {
|
||||
setRequestBody(defaultReqHtml?.body);
|
||||
setRequestSubject(defaultReqHtml?.subject);
|
||||
} else if (completion) {
|
||||
try {
|
||||
await Parse.Cloud.run("updatetenant", {
|
||||
tenantId: tenantId,
|
||||
details: { RequestBody: "", RequestSubject: "" }
|
||||
});
|
||||
if (extUser && extUser?.objectId) {
|
||||
extUser.TenantId.RequestBody = "";
|
||||
extUser.TenantId.RequestSubject = "";
|
||||
const _extUser = JSON.parse(JSON.stringify(extUser));
|
||||
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err in reseting request mail", err);
|
||||
}
|
||||
} else if (completion && !isDefaultMail?.completionMail) {
|
||||
setCompletionSubject(defaultCompHtml?.subject);
|
||||
SetCompletionBody(defaultCompHtml?.body);
|
||||
try {
|
||||
await Parse.Cloud.run("updatetenant", {
|
||||
tenantId: tenantId,
|
||||
details: { CompletionBody: "", CompletionSubject: "" }
|
||||
});
|
||||
if (extUser && extUser?.objectId) {
|
||||
extUser.TenantId.CompletionBody = "";
|
||||
extUser.TenantId.CompletionSubject = "";
|
||||
const _extUser = JSON.parse(JSON.stringify(extUser));
|
||||
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err in reseting completion mail", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
//function for handle ontext change and save again text in delta
|
||||
@@ -344,6 +382,11 @@ const Preferences = () => {
|
||||
const handleTourInput = () => setIsTourEnabled(!isTourEnabled);
|
||||
const handleSendinOrderInput = () => setSendinOrder(!sendinOrder);
|
||||
const tabName = (ind) => tab.find((t, i) => i === ind)?.name;
|
||||
const handleModifyMail = (mode) => {
|
||||
mode === "request"
|
||||
? setIsDefaultMail((p) => ({ ...p, requestMail: !p?.requestMail }))
|
||||
: setIsDefaultMail((p) => ({ ...p, completionMail: !p?.completionMail }));
|
||||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Title title={t("Preferences")} />
|
||||
@@ -737,6 +780,18 @@ const Preferences = () => {
|
||||
{t("request-email")}
|
||||
</h1>
|
||||
<div className="relative mt-2 mb-4">
|
||||
{
|
||||
isDefaultMail?.requestMail && (
|
||||
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
|
||||
<button
|
||||
onClick={() => handleModifyMail("request")}
|
||||
className="op-btn op-btn-primary shadow-lg"
|
||||
>
|
||||
Modify
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<form
|
||||
onSubmit={handleSaveRequestEmail}
|
||||
className="p-3 border-[1px] border-base-content rounded-box"
|
||||
@@ -801,6 +856,18 @@ const Preferences = () => {
|
||||
{t("completion-email")}
|
||||
</h1>
|
||||
<div className="relative my-2">
|
||||
{
|
||||
isDefaultMail?.completionMail && (
|
||||
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
|
||||
<button
|
||||
onClick={() => handleModifyMail("completion")}
|
||||
className="op-btn op-btn-primary shadow-lg"
|
||||
>
|
||||
Modify
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<form
|
||||
onSubmit={handleSaveCompletionEmail}
|
||||
className="p-3 border-[1px] border-base-content rounded-box"
|
||||
|
||||
@@ -180,7 +180,7 @@ const Report = () => {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers.some(
|
||||
const isSigner = obj?.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
|
||||
@@ -648,7 +648,6 @@ function SignYourSelf() {
|
||||
}
|
||||
//embed all widgets in document
|
||||
const pdfBytes = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
xyPosition,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
|
||||
@@ -833,7 +833,6 @@ const TemplatePlaceholder = () => {
|
||||
try {
|
||||
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
|
||||
const pdfBase64 = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
placeholder,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
|
||||
@@ -91,6 +91,7 @@ const ReportTable = (props) => {
|
||||
const [contact, setContact] = useState({ Name: "", Email: "", Phone: "" });
|
||||
const [isSuccess, setIsSuccess] = useState({});
|
||||
const [templateId, setTemplateId] = useState("");
|
||||
const [sortOrder, setSortOrder] = useState("asc");
|
||||
const isTemplateReport = props.ReportName === "Templates";
|
||||
const recordsPerPage = 5;
|
||||
const startIndex = (currentPage - 1) * props.docPerPage;
|
||||
@@ -437,7 +438,22 @@ const ReportTable = (props) => {
|
||||
// Get current list
|
||||
const indexOfLastDoc = currentPage * props.docPerPage;
|
||||
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
|
||||
const currentList = props.List?.slice(indexOfFirstDoc, indexOfLastDoc);
|
||||
const sortedList = React.useMemo(() => {
|
||||
if (props.ReportName === "Contactbook") {
|
||||
const contacts = [...props.List];
|
||||
contacts.sort((a, b) => {
|
||||
const nameA = a?.Name?.toLowerCase() || "";
|
||||
const nameB = b?.Name?.toLowerCase() || "";
|
||||
if (nameA < nameB) return sortOrder === "asc" ? -1 : 1;
|
||||
if (nameA > nameB) return sortOrder === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
return contacts;
|
||||
}
|
||||
return props.List;
|
||||
}, [props.List, sortOrder, props.ReportName]);
|
||||
|
||||
const currentList = sortedList?.slice(indexOfFirstDoc, indexOfLastDoc);
|
||||
|
||||
// Change page
|
||||
const paginateFront = () => {
|
||||
@@ -457,6 +473,10 @@ const ReportTable = (props) => {
|
||||
setIsContactform(!isContactform);
|
||||
};
|
||||
|
||||
const toggleSortOrder = () => {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
};
|
||||
|
||||
const handleUserData = (data) => {
|
||||
props.setList((prevData) => [data, ...prevData]);
|
||||
};
|
||||
@@ -1536,6 +1556,17 @@ const ReportTable = (props) => {
|
||||
required
|
||||
className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
<p className="mt-1 ml-2 text-[11px] text-gray-600">
|
||||
{t("import-guideline")}{" "}
|
||||
<a
|
||||
href="/sample_contacts.csv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
{t("download-sample")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-md m-2">
|
||||
<div className="flex flex-col md:flex-row gap-1">
|
||||
@@ -1624,7 +1655,24 @@ const ReportTable = (props) => {
|
||||
<tr className="border-y-[1px]">
|
||||
{props.heading?.map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<th className="p-2">{t(`report-heading.${item}`)}</th>
|
||||
<th className="p-2">
|
||||
{t(`report-heading.${item}`)}
|
||||
{props.ReportName === "Contactbook" && item === "Name" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSortOrder}
|
||||
className="ml-1"
|
||||
>
|
||||
<i
|
||||
className={
|
||||
sortOrder === "asc"
|
||||
? "fa-light fa-arrow-down-a-z"
|
||||
: "fa-light fa-arrow-up-a-z"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
)}
|
||||
</th>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{props.actions?.length > 0 && (
|
||||
@@ -1817,7 +1865,7 @@ const ReportTable = (props) => {
|
||||
{/* template report */}
|
||||
{isOption[item.objectId] &&
|
||||
act.action === "option" && (
|
||||
<ul className="absolute -right-1 top-auto z-[70] w-52 op-dropdown-content op-menu shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
|
||||
<ul className="absolute -right-1 top-auto z-[70] w-52 op-dropdown-content op-menu op-menu-sm shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
|
||||
{act.subaction?.map((subact) => (
|
||||
<li
|
||||
key={subact.btnId}
|
||||
@@ -1908,7 +1956,7 @@ const ReportTable = (props) => {
|
||||
{/* doc report */}
|
||||
{isOption[item.objectId] &&
|
||||
act.action === "option" && (
|
||||
<ul className="absolute -right-1 top-auto z-[70] w-max op-dropdown-content op-menu shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
|
||||
<ul className="absolute -right-1 top-auto z-[70] w-max op-dropdown-content op-menu op-menu-sm shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
|
||||
{act.subaction?.map(
|
||||
(subact) =>
|
||||
!restrictBtn(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { defineConfig, splitVendorChunkPlugin } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { resolve } from "path";
|
||||
@@ -12,7 +12,8 @@ export default defineConfig(({ command, mode }) => {
|
||||
return {
|
||||
plugins: [
|
||||
react(),
|
||||
svgr() // Transform SVGs into React components
|
||||
svgr(), // Transform SVGs into React components
|
||||
splitVendorChunkPlugin()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -289,6 +289,8 @@ export const selectFormat = data => {
|
||||
return 'MMMM dd, yyyy';
|
||||
case 'DD MMMM, YYYY':
|
||||
return 'dd MMMM, yyyy';
|
||||
case 'DD.MM.YYYY':
|
||||
return 'dd.MM.yyyy';
|
||||
default:
|
||||
return 'MM/dd/yyyy';
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ async function AuthLoginAsMail(request) {
|
||||
} catch (err) {
|
||||
console.log('err in Auth');
|
||||
console.log(err);
|
||||
return 'Result not found', err;
|
||||
return 'Result not found';
|
||||
}
|
||||
}
|
||||
export default AuthLoginAsMail;
|
||||
|
||||
@@ -84,7 +84,7 @@ async function sendMail(document, publicUrl) {
|
||||
};
|
||||
let params = {
|
||||
extUserId: document.ExtUserPtr.objectId,
|
||||
recipient: objectId ? existSigner?.Email : signerMail[i].email,
|
||||
recipient: existSigner?.Email || signerMail[i].email,
|
||||
subject: replaceVar?.subject ? replaceVar?.subject : mailTemplate(mailparam).subject,
|
||||
from: document.ExtUserPtr.Email,
|
||||
replyto: senderEmail || '',
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
// `saveRoleContact` is used to save user in contracts_Guest role and create contact
|
||||
const saveRoleContact = async contact => {
|
||||
try {
|
||||
const Role = new Parse.Query(Parse.Role);
|
||||
const guestRole = await Role.equalTo('name', 'contracts_Guest').first();
|
||||
if (guestRole) {
|
||||
// Check if the user is already in the role
|
||||
const relation = guestRole.relation('users');
|
||||
const usersInRoleQuery = relation.query();
|
||||
usersInRoleQuery.equalTo('objectId', contact.UserId.objectId);
|
||||
const usersInRole = await usersInRoleQuery.find();
|
||||
if (usersInRole.length > 0) {
|
||||
console.log('User already added to Guest role.');
|
||||
} else {
|
||||
relation.add({ __type: 'Pointer', className: '_User', id: contact.UserId.objectId });
|
||||
await guestRole.save(null, { useMasterKey: true });
|
||||
// console.log('User added to Guest role successfully.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err in role save', err);
|
||||
}
|
||||
const contactQuery = new Parse.Object('contracts_Contactbook');
|
||||
contactQuery.set('Name', contact.Name);
|
||||
contactQuery.set('Email', contact.Email);
|
||||
|
||||
@@ -59,6 +59,7 @@ export default async function saveAsTemplate(request) {
|
||||
options: {
|
||||
...p.options,
|
||||
status: 'required',
|
||||
...(p?.options?.defaultValue ? { defaultValue: '' } : {}),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
@@ -73,10 +74,28 @@ export default async function saveAsTemplate(request) {
|
||||
};
|
||||
templateCls.set('Placeholders', [placeHolders]);
|
||||
} else {
|
||||
const placeHolders = _docRes?.Placeholders?.map((x, i) => {
|
||||
const email = x.email ? { email: '' } : {};
|
||||
return { ...x, signerObjId: '', signerPtr: {}, Role: 'Role ' + (i + 1), ...email };
|
||||
});
|
||||
const placeHolders = _docRes?.Placeholders.map((signer, signerIndex) => ({
|
||||
// copy everything else, then overwrite these fields:
|
||||
...signer,
|
||||
signerObjId: '',
|
||||
signerPtr: {},
|
||||
Role: `Role ${signerIndex + 1}`,
|
||||
email: '',
|
||||
|
||||
// rebuild placeHolder/pages
|
||||
placeHolder: (signer.placeHolder || []).map(page => ({
|
||||
...page,
|
||||
pos: (page.pos || []).map(widget => {
|
||||
// if there is a defaultValue in options, zero it out
|
||||
if (widget.options && widget.options.defaultValue !== undefined) {
|
||||
return { ...widget, options: { ...widget.options, defaultValue: '' } }; // reset only the value
|
||||
}
|
||||
// otherwise, return the widget unchanged
|
||||
return widget;
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
templateCls.set('Placeholders', placeHolders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ const makeEmail = async (
|
||||
replyto,
|
||||
testPdf
|
||||
) => {
|
||||
const publicUrl = new URL(process.env.SERVER_URL);
|
||||
const htmlContent = html;
|
||||
const boundary = 'boundary_' + Date.now().toString(16);
|
||||
const bccHeader = bcc && bcc.length > 0 ? `BCC: ${bcc.join(',')}\n` : ''; // Construct BCC header if provided
|
||||
|
||||
@@ -8,12 +8,15 @@ export default async function updateTenant(request) {
|
||||
if (!request.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'unauthorized');
|
||||
}
|
||||
const validKeys = ['CompletionBody', 'CompletionSubject', 'RequestBody', 'RequestSubject'];
|
||||
try {
|
||||
const tenant = new Parse.Object('partners_Tenant');
|
||||
tenant.id = tenantId;
|
||||
// Update tenant details
|
||||
Object.keys(details).forEach(key => {
|
||||
tenant.set(key, details?.[key]);
|
||||
if (validKeys.includes(key)) {
|
||||
tenant.set(key, details?.[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const tenantRes = await tenant.save(null, { useMasterKey: true });
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.up = async Parse => {
|
||||
const schema = new Parse.Schema('contracts_Subscriptions');
|
||||
schema.addNumber('CreditAlertLevel');
|
||||
return schema.update();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.down = async Parse => {
|
||||
const schema = new Parse.Schema('contracts_Subscriptions');
|
||||
schema.deleteField('CreditAlertLevel');
|
||||
return schema.update();
|
||||
};
|
||||
@@ -232,7 +232,6 @@ if (!process.env.TESTING) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
// console.log('isWindows', isWindows);
|
||||
createContactIndex();
|
||||
|
||||
const migrate = isWindows
|
||||
? `set APPLICATION_ID=${serverAppId}&& set SERVER_URL=${cloudServerUrl}&& set MASTER_KEY=${process.env.MASTER_KEY}&& npx parse-dbtool migrate`
|
||||
: `APPLICATION_ID=${serverAppId} SERVER_URL=${cloudServerUrl} MASTER_KEY=${process.env.MASTER_KEY} npx parse-dbtool migrate`;
|
||||
|
||||
Generated
+962
-386
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,8 @@
|
||||
"watch": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.828.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.828.0",
|
||||
"@aws-sdk/client-s3": "^3.840.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.840.0",
|
||||
"@parse/fs-files-adapter": "^3.0.0",
|
||||
"@parse/s3-files-adapter": "^4.2.0",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
@@ -31,38 +31,38 @@
|
||||
"coherentpdf": "^2.5.5",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^5.1.0",
|
||||
"form-data": "^4.0.3",
|
||||
"generate-api-key": "^1.0.2",
|
||||
"googleapis": "^150.0.1",
|
||||
"libreoffice-convert": "^1.6.1",
|
||||
"mailgun.js": "^12.0.2",
|
||||
"mailgun.js": "^12.0.3",
|
||||
"mongodb": "^6.17.0",
|
||||
"multer": "^2.0.1",
|
||||
"multer-s3": "^3.0.1",
|
||||
"node-forge": "^1.3.1",
|
||||
"nodemailer": "^7.0.3",
|
||||
"nodemailer": "^7.0.4",
|
||||
"parse": "^6.1.1",
|
||||
"parse-dbtool": "^1.2.0",
|
||||
"parse-server": "^8.2.1",
|
||||
"parse-server-api-mail-adapter": "^4.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"posthog-node": "^5.1.0",
|
||||
"posthog-node": "^5.1.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"rate-limiter-flexible": "^7.1.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"ws": "^8.18.2"
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.27.5",
|
||||
"eslint": "^9.29.0",
|
||||
"jasmine": "^5.8.0",
|
||||
"mongodb-runner": "^5.9.0",
|
||||
"mongodb-runner": "^5.9.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "^3.5.3"
|
||||
"prettier": "^3.6.2"
|
||||
},
|
||||
"overrides": {
|
||||
"ws": "$ws",
|
||||
|
||||
Reference in New Issue
Block a user