Compare commits

...
18 Commits
Author SHA1 Message Date
prafull-opensignlabsandGitHub ca1b87309f Merge pull request #2011 from OpenSignLabs/updates-18996309218
feat: handle undefine username/password for SMTP
2025-11-01 17:24:58 +05:30
prafull-opensignlabs 0b4548130d feat: handle undefine username/password for SMTP 2025-11-01 11:53:04 +00:00
prafull-opensignlabsandGitHub e8b3c7fed8 Merge pull request #1994 from OpenSignLabs/updates-18555669223
v2.30.0
2025-10-16 14:19:28 +05:30
prafull-opensignlabs 2ce43ec2c8 v2.30.0 2025-10-16 08:48:28 +00:00
prafull-opensignlabsandGitHub d5af1e8a17 Merge pull request #1973 from OpenSignLabs/updates-18164868546
v2.29.2
2025-10-01 19:38:24 +05:30
prafull-opensignlabs fab7998e22 refactor: handle session expired modal 2025-10-01 14:07:45 +00:00
prafull-opensignlabsandGitHub 4a4db6851e Merge pull request #1969 from OpenSignLabs/updates-18129802734
v2.29.1
2025-09-30 17:57:55 +05:30
prafull-opensignlabs 870973e7f3 Merge pull request 2025-09-30 12:26:28 +00:00
prafull-opensignlabsandGitHub 4146680c1e Merge pull request #1966 from OpenSignLabs/updates-18086083096
v2.29.0
2025-09-29 10:24:16 +05:30
prafull-opensignlabs d9fcc69ca4 v2.29.0 2025-09-29 04:47:23 +00:00
prafull-opensignlabsandGitHub d1c405912b Merge pull request #1945 from OpenSignLabs/updates-17798985943
refactor: remove unnecessary code
2025-09-17 18:51:19 +05:30
prafull-opensignlabs 745aca1754 Merge pull request #1298 from nxglabs/sync-to-public_repo-17798472199
Merge pull request #1297 from nxglabs/staging
2025-09-17 13:19:46 +00:00
prafull-opensignlabsandGitHub 672eeea851 Merge pull request #1943 from OpenSignLabs/updates-17788011031
build(deps): build dependencies and dark mode ui
2025-09-17 11:21:02 +05:30
prafull-opensignlabs 98efff65c9 Merge pull request #1295 from nxglabs/sync-to-public_repo-17787708673
fix: show signer name in typed signature instead in selfsign flow
2025-09-17 05:48:01 +00:00
prafull-opensignlabsandGitHub fe21978538 Merge pull request #1941 from OpenSignLabs/updates-17737216200
v2.28.0
2025-09-15 20:23:17 +05:30
prafull-opensignlabs ca4ccfd889 Merge pull request #1292 from nxglabs/sync-to-public_repo-17730712175
Merge pull request #1291 from nxglabs/staging
2025-09-15 14:51:16 +00:00
prafull-opensignlabsandGitHub 6cb2d69566 Merge pull request #1931 from OpenSignLabs/updates-17548337132
fix: getting error 'user not found' while deleting user
2025-09-08 16:22:46 +05:30
prafull-opensignlabs 98b9ebb228 fix: getting error 'user not found' while deleting user 2025-09-08 10:51:52 +00:00
88 changed files with 5751 additions and 4475 deletions
+10 -10
View File
@@ -1,15 +1,15 @@
{ {
"name": "open_sign", "name": "open_sign",
"version": "2.26.0", "version": "2.30.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.8.4", "@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0", "@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.15.2", "@lottiefiles/dotlottie-react": "^0.16.2",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"axios": "^1.11.0", "axios": "^1.12.2",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^25.4.0", "i18next": "^25.4.0",
@@ -36,13 +36,13 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-gtm-module": "^2.0.11", "react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-i18next": "^15.7.3", "react-i18next": "^15.7.4",
"react-konva": "^18.2.12", "react-konva": "^18.2.12",
"react-pdf": "^9.2.1", "react-pdf": "^9.2.1",
"react-quill-new": "^3.6.0", "react-quill-new": "^3.6.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-rnd": "^10.5.2", "react-rnd": "^10.5.2",
"react-router": "^7.8.2", "react-router": "^7.9.3",
"react-scrollbars-custom": "^4.1.1", "react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2", "react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2", "react-signature-canvas": "^1.1.0-alpha.2",
@@ -51,7 +51,7 @@
"reactour": "^1.19.4", "reactour": "^1.19.4",
"redux": "^5.0.1", "redux": "^5.0.1",
"regex-parser": "^2.3.1", "regex-parser": "^2.3.1",
"serve": "^14.2.4", "serve": "^14.2.5",
"styled-components": "^5.3.11", "styled-components": "^5.3.11",
"web-vitals": "^5.1.0", "web-vitals": "^5.1.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
@@ -101,7 +101,7 @@
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/react": "^19.1.12", "@types/react": "^19.1.13",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
@@ -110,10 +110,10 @@
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"daisyui": "^4.12.24", "daisyui": "^4.12.24",
"eslint": "^9.34.0", "eslint": "^9.37.0",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"jsdom": "^26.1.0", "jsdom": "^27.0.0",
"lint-staged": "^16.1.6", "lint-staged": "^16.1.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
@@ -8,7 +8,7 @@
"dark-mode": "Dunkelmodus", "dark-mode": "Dunkelmodus",
"name": "Name", "name": "Name",
"phone": "Telefon", "phone": "Telefon",
"phone-optional": "optional", "phone-optional": "Optional",
"email": "E-Mail", "email": "E-Mail",
"company": "Unternehmen", "company": "Unternehmen",
"job-title": "Berufsbezeichnung", "job-title": "Berufsbezeichnung",
@@ -165,7 +165,8 @@
"daily-mail-quota": "Tägliches E-Mail-Kontingent", "daily-mail-quota": "Tägliches E-Mail-Kontingent",
"Save as template": "Als Vorlage speichern", "Save as template": "Als Vorlage speichern",
"Fix & resend": "Korrigieren und erneut senden", "Fix & resend": "Korrigieren und erneut senden",
"Kiosk Mode": "Kiosk Modus" "Kiosk Mode": "Kiosk Modus",
"Reset password": "Passwort zurücksetzen"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Nr.", "Sr.No": "Nr.",
@@ -385,7 +386,8 @@
"dropdown": "Dropdown", "dropdown": "Dropdown",
"radio button": "Radiobutton", "radio button": "Radiobutton",
"image": "Bild", "image": "Bild",
"email": "E-Mail" "email": "E-Mail",
"number": "Zahl"
}, },
"fields": "Felder", "fields": "Felder",
"recipients": "Empfänger", "recipients": "Empfänger",
@@ -442,6 +444,19 @@
"maximum-check": "Maximale Anzahl", "maximum-check": "Maximale Anzahl",
"cell-count": "Zellzahl", "cell-count": "Zellzahl",
"default-value": "Standardwert", "default-value": "Standardwert",
"formula": "Formel",
"formula-placeholder": "Erstellen Sie eine Formel mit Zahlen-Widgets",
"formula-no-widgets": "Für diesen Unterzeichner sind noch keine weiteren Zahlen-Widgets vorhanden.",
"supported-operators": "Unterstützte Operatoren:",
"formula-helper": "Klicken Sie auf einen Zahlen-Widget-Chip, um ihn einzufügen (z. B. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Widget-Namen müssen in doppelte geschweifte Klammern gesetzt werden (z. B. {{number-1}}).",
"formula-error-characters": "Außerhalb der Klammern sind nur Zahlen sowie +, -, *, /, (, ) erlaubt.",
"formula-error-braces": "Jede öffnende {{ muss eine passende }} haben.",
"formula-error-operators": "Verwenden Sie bei Operatoren oder Klammern mindestens zwei Werte.",
"formula-error-leading-operator": "Formeln dürfen nicht mit Operatoren beginnen.",
"decimal-places": "Dezimalstellen",
"number-of-decimal-places": "Anzahl der Dezimalstellen",
"decimal-places-helper": "Auf 0 setzen für ganze Zahlen",
"select": "Auswählen", "select": "Auswählen",
"read-only": "Ist schreibgeschützt", "read-only": "Ist schreibgeschützt",
"hide-labels": "Labels ausblenden", "hide-labels": "Labels ausblenden",
@@ -901,6 +916,10 @@
"thanks-for-feedback": "Danke für Ihr Feedback 🙏", "thanks-for-feedback": "Danke für Ihr Feedback 🙏",
"share-your-feedback": "Teilen Sie Ihr Feedback", "share-your-feedback": "Teilen Sie Ihr Feedback",
"share-your-review": "Teilen Sie Ihre Bewertung", "share-your-review": "Teilen Sie Ihre Bewertung",
"please-select-rating": "Bitte wählen Sie eine Bewertung aus",
"feedback-optional": "Feedback (optional)",
"feedback-saved": "Feedback wurde gespeichert.",
"feedback-save-error": "Feedback konnte nicht gespeichert werden, bitte versuchen Sie es erneut.",
"date-format": "Datumsformat", "date-format": "Datumsformat",
"document-deleted": "Das Dokument wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.", "document-deleted": "Das Dokument wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
"save-as-template-?": "Sind Sie sicher, dass Sie dieses Dokument als Vorlage speichern möchten?", "save-as-template-?": "Sind Sie sicher, dass Sie dieses Dokument als Vorlage speichern möchten?",
@@ -1210,5 +1229,32 @@
"otp-email": "Wir haben einen Bestätigungscode gesendet", "otp-email": "Wir haben einen Bestätigungscode gesendet",
"save-as-temp-warn": "Hinweis: Dieses Dokument enthält vorausgefüllte Widgets, die automatisch entfernt werden, da sie bereits im Basisdokument integriert sind.", "save-as-temp-warn": "Hinweis: Dieses Dokument enthält vorausgefüllte Widgets, die automatisch entfernt werden, da sie bereits im Basisdokument integriert sind.",
"edit-document": "Dokument bearbeiten", "edit-document": "Dokument bearbeiten",
"otp-email": "Wir haben einen Bestätigungscode gesendet" "modify": "Ändern",
} "merge-certificate-to-pdf": "Zertifikat mit PDF zusammenführen",
"merge-cc-to-pdf-help": {
"p1": "Dies stellt sicher, dass das Abschlusszertifikat im endgültigen PDF-Dokument enthalten ist. Bitte beachten Sie jedoch, dass das Zertifikat nach dem Zusammenführen nicht mehr vom Hauptdokument getrennt werden kann.",
"p2": "Wenn Sie sich entscheiden, nicht zusammenzuführen, wird das Abschlusszertifikat als separate PDF-Datei zusammen mit dem unterzeichneten Dokument bereitgestellt."
},
"read-only-date-error": "Das schreibgeschützte Datums-Widget muss ein Standarddatum haben.",
"set-date": "Datum festlegen",
"set-today": "Unterzeichnungsdatum",
"enter-name": "Name eingeben",
"enter-email": "E-Mail eingeben",
"subscribe-to-opensign-msg": "Abonnieren Sie {{appName}} und genießen Sie unbegrenzt kostenlose digitale Signaturen.",
"duplicate-subscribe-msg": "Mit Duplizieren können Sie eine exakte Kopie der ausgewählten Vorlage erstellen, die Sie wiederverwenden oder ändern können, ohne das Original zu beeinflussen.",
"save-as-template-msg": "Speichern Sie dieses Dokument als wiederverwendbare Vorlage, die Sie für zukünftige Dokumente erneut nutzen können.",
"public-credit-alert": "Das Signieren über diesen Link verbraucht Ihre kostenlosen E-Mail-Credits. Als kostenloser Nutzer erhalten Sie 15 E-Mail-Credits pro Monat, um zu verhindern, dass Spammer unsere Systeme missbrauchen. Um höhere Limits und ununterbrochenen Zugriff zu genießen, abonnieren Sie die <1>OpenSign™-Bezahlpläne.</1>",
"know-more-about": "Mehr erfahren über",
"free-unlimited-signatures": "Kostenlose unbegrenzte Signaturen",
"email-quota-updated": "Tägliches E-Mail-Kontingent erfolgreich aktualisiert.",
"password-has-been-reset": "Passwort wurde zurückgesetzt.",
"Unauthorized to reset your own password.": "Nicht berechtigt, Ihr eigenes Passwort zurückzusetzen.",
"User not found or not allowed.": "Benutzer nicht gefunden oder nicht erlaubt.",
"User not found.": "Benutzer nicht gefunden.",
"Admin user tenant not found.": "Admin-Benutzer-Tenant nicht gefunden.",
"User is not authenticated.": "Benutzer ist nicht authentifiziert.",
"Please provide required parameters.": "Bitte geben Sie die erforderlichen Parameter an.",
"reset-password": "Benutzerpasswort zurücksetzen",
"enter-strong-password": "Geben Sie ein sicheres Passwort ein oder generieren Sie eines automatisch.",
"autogenerate": "Automatisch generieren"
}
@@ -8,7 +8,7 @@
"dark-mode": "Dark mode", "dark-mode": "Dark mode",
"name": "Name", "name": "Name",
"phone": "Phone", "phone": "Phone",
"phone-optional": "optional", "phone-optional": "Optional",
"email": "Email", "email": "Email",
"company": "Company", "company": "Company",
"job-title": "Job title", "job-title": "Job title",
@@ -165,7 +165,8 @@
"daily-mail-quota": "Daily Email Quota", "daily-mail-quota": "Daily Email Quota",
"Save as template": "Save as template", "Save as template": "Save as template",
"Fix & resend": "Fix & Resend", "Fix & resend": "Fix & Resend",
"Kiosk Mode": "Kiosk Mode" "Kiosk Mode": "Kiosk Mode",
"Reset password": "Reset password"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Sr.No", "Sr.No": "Sr.No",
@@ -385,7 +386,8 @@
"dropdown": "dropdown", "dropdown": "dropdown",
"radio button": "radio button", "radio button": "radio button",
"image": "image", "image": "image",
"email": "email" "email": "email",
"number": "number"
}, },
"fields": "Fields", "fields": "Fields",
"recipients": "Recipients", "recipients": "Recipients",
@@ -443,6 +445,19 @@
"maximum-check": "Maximum check", "maximum-check": "Maximum check",
"cell-count": "Cell count", "cell-count": "Cell count",
"default-value": "Default value", "default-value": "Default value",
"formula": "Formula",
"formula-placeholder": "Build a formula using number widgets",
"formula-no-widgets": "No other number widgets are assigned to this signer yet.",
"supported-operators": "Supported operators:",
"formula-helper": "Click a number widget chip to insert it (e.g. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Wrap widget names in double curly braces (e.g. {{number-1}}).",
"formula-error-characters": "Only numbers and +, -, *, /, (, ) are allowed outside of braces.",
"formula-error-braces": "Each opening {{ must have a matching }}.",
"formula-error-operators": "Add at least two values when using operators or parentheses.",
"formula-error-leading-operator": "Formulas cannot start with operators.",
"decimal-places": "Decimal Places",
"number-of-decimal-places": "Number of decimal places",
"decimal-places-helper": "Set to 0 for whole numbers",
"select": "Select", "select": "Select",
"read-only": "read only", "read-only": "read only",
"hide-labels": "Hide labels", "hide-labels": "Hide labels",
@@ -902,6 +917,10 @@
"thanks-for-feedback": "Thanks for your feedback 🙏", "thanks-for-feedback": "Thanks for your feedback 🙏",
"share-your-feedback": "Share your feedback", "share-your-feedback": "Share your feedback",
"share-your-review": "Share your review", "share-your-review": "Share your review",
"please-select-rating": "Please select a rating",
"feedback-optional": "Feedback (Optional)",
"feedback-saved": "Feedback has been saved.",
"feedback-save-error": "Unable to save feedback, please try again.",
"date-format": "Date format", "date-format": "Date format",
"document-deleted": "The document has been deleted or you don't have access. Please contact the sender.", "document-deleted": "The document has been deleted or you don't have access. Please contact the sender.",
"save-as-template-?": "Are you sure you want to save this document as template?", "save-as-template-?": "Are you sure you want to save this document as template?",
@@ -1208,8 +1227,35 @@
"enter-region-of-space": "Enter region of space", "enter-region-of-space": "Enter region of space",
"enter-access-key": "Enter access key", "enter-access-key": "Enter access key",
"enter-secret-access-key": "Enter secret access key", "enter-secret-access-key": "Enter secret access key",
"otp-email":"Weve sent a verification code to", "otp-email": "We've sent a verification code to",
"save-as-temp-warn":"Note: This document includes prefilled widgets, which will be automatically removed as they are already incorporated into the base document.", "save-as-temp-warn": "Note: This document includes prefilled widgets, which will be automatically removed as they are already incorporated into the base document.",
"edit-document": "Edit document", "edit-document": "Edit document",
"otp-email":"We've sent a verification code to" "modify": "Modify",
} "merge-certificate-to-pdf": "Merge Certificate to PDF",
"merge-cc-to-pdf-help": {
"p1": "This will ensure that the completion certificate is included in the final PDF document. However, please note that once merged, the certificate cannot be separated from the main document.",
"p2": "If you choose not to merge, the completion certificate will be provided as a separate PDF file along with the signed document."
},
"read-only-date-error": "Read-only date widget must have a default date.",
"set-date": "Set Date",
"set-today": "Signing Date",
"enter-name": "Enter name",
"enter-email": "Enter email",
"subscribe-to-opensign-msg": "Subscribe to {{appName}} and enjoy unlimited free digital signatures.",
"duplicate-subscribe-msg": "Duplicate lets you create an exact copy of the selected template, allowing you to reuse or modify it without affecting the original.",
"save-as-template-msg": "Save this document as a reusable template that you can use again for future documents.",
"public-credit-alert": "Signing through this link consumes your free email credits. As a Free user, you are allotted 15 email credits per month to prevent spammers from abusing our systems. To enjoy higher limits and uninterrupted access, Subscribe to <1>OpenSign™ Paid plans.</1>",
"know-more-about": "Know more about",
"free-unlimited-signatures": "Free Unlimited Signatures",
"email-quota-updated": "Daily email quota updated successfully.",
"password-has-been-reset": "Password has been reset.",
"Unauthorized to reset your own password.": "Unauthorized to reset your own password.",
"User not found or not allowed.": "User not found or not allowed.",
"User not found.": "User not found.",
"Admin user tenant not found.": "Admin user tenant not found.",
"User is not authenticated.": "User is not authenticated.",
"Please provide required parameters.": "Please provide required parameters.",
"reset-password": "Reset User Password",
"enter-strong-password": "Enter a strong password or auto-generate one.",
"autogenerate": "Autogenerate"
}
@@ -8,7 +8,7 @@
"dark-mode": "Modo oscuro", "dark-mode": "Modo oscuro",
"name": "Nombre", "name": "Nombre",
"phone": "Teléfono", "phone": "Teléfono",
"phone-optional": "opcional", "phone-optional": "Opcional",
"email": "Correo", "email": "Correo",
"company": "Compañía", "company": "Compañía",
"job-title": "Título del puesto", "job-title": "Título del puesto",
@@ -165,7 +165,8 @@
"daily-mail-quota": "Cuota diaria de correos electrónicos", "daily-mail-quota": "Cuota diaria de correos electrónicos",
"Save as template": "Guardar como plantilla", "Save as template": "Guardar como plantilla",
"Fix & resend": "Corregir y reenviar", "Fix & resend": "Corregir y reenviar",
"Kiosk Mode": "Modo Kiosco" "Kiosk Mode": "Modo Kiosco",
"Reset password": "Restablecer contraseña"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Nº", "Sr.No": "Nº",
@@ -385,7 +386,8 @@
"dropdown": "desplegable", "dropdown": "desplegable",
"radio button": "botón de radio", "radio button": "botón de radio",
"image": "imagen", "image": "imagen",
"email": "correo" "email": "correo",
"number": "número"
}, },
"fields": "Campos", "fields": "Campos",
"recipients": "Destinatarios", "recipients": "Destinatarios",
@@ -443,6 +445,19 @@
"maximum-check": "Chequeo máximo", "maximum-check": "Chequeo máximo",
"cell-count": "recuento de células", "cell-count": "recuento de células",
"default-value": "Valor por defecto", "default-value": "Valor por defecto",
"formula": "Fórmula",
"formula-placeholder": "Crea una fórmula utilizando widgets numéricos",
"formula-no-widgets": "Todavía no hay otros widgets numéricos asignados a este firmante.",
"supported-operators": "Operadores compatibles:",
"formula-helper": "Haz clic en una ficha de widget numérico para insertarla (p. ej. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Envuelve los nombres de los widgets entre dobles llaves (p. ej. {{number-1}}).",
"formula-error-characters": "Fuera de las llaves solo se permiten números y +, -, *, /, (, ).",
"formula-error-braces": "Cada {{ de apertura debe tener una }} correspondiente.",
"formula-error-operators": "Cuando uses operadores o paréntesis, agrega al menos dos valores.",
"formula-error-leading-operator": "Las fórmulas no pueden comenzar con operadores.",
"decimal-places": "Decimales",
"number-of-decimal-places": "Número de decimales",
"decimal-places-helper": "Establecer en 0 para números enteros",
"select": "Seleccionar", "select": "Seleccionar",
"read-only": "Es de solo lectura", "read-only": "Es de solo lectura",
"hide-labels": "Esconder etiquetas", "hide-labels": "Esconder etiquetas",
@@ -902,6 +917,10 @@
"thanks-for-feedback": "Gracias por su comentario 🙏", "thanks-for-feedback": "Gracias por su comentario 🙏",
"share-your-feedback": "Comparta sus comentarios", "share-your-feedback": "Comparta sus comentarios",
"share-your-review": "Comparta su reseña", "share-your-review": "Comparta su reseña",
"please-select-rating": "Por favor seleccione una calificación",
"feedback-optional": "Comentarios (opcional)",
"feedback-saved": "Los comentarios han sido guardados.",
"feedback-save-error": "No se pudieron guardar los comentarios, inténtelo de nuevo.",
"date-format": "Formato de fecha", "date-format": "Formato de fecha",
"document-deleted": "El documento ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.", "document-deleted": "El documento ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
"save-as-template-?": "¿Está seguro de que desea guardar este documento como plantilla?", "save-as-template-?": "¿Está seguro de que desea guardar este documento como plantilla?",
@@ -1211,5 +1230,32 @@
"otp-email": "Hemos enviado un código de verificación", "otp-email": "Hemos enviado un código de verificación",
"save-as-temp-warn": "Nota: Este documento incluye widgets prellenados, que se eliminarán automáticamente ya que ya están incorporados en el documento base.", "save-as-temp-warn": "Nota: Este documento incluye widgets prellenados, que se eliminarán automáticamente ya que ya están incorporados en el documento base.",
"edit-document": "Editar documento", "edit-document": "Editar documento",
"otp-email": "Hemos enviado un código de verificación" "modify": "Modificar",
} "merge-certificate-to-pdf": "Combinar certificado con PDF",
"merge-cc-to-pdf-help": {
"p1": "Esto garantizará que el certificado de finalización esté incluido en el documento PDF final. Sin embargo, tenga en cuenta que una vez combinado, el certificado no se podrá separar del documento principal.",
"p2": "Si elige no combinar, el certificado de finalización se proporcionará como un archivo PDF separado junto con el documento firmado."
},
"read-only-date-error": "El widget de fecha de solo lectura debe tener una fecha predeterminada.",
"set-date": "Establecer fecha",
"set-today": "Fecha de firma",
"enter-name": "Ingrese nombre",
"enter-email": "Ingrese correo electrónico",
"subscribe-to-opensign-msg": "Suscríbase a {{appName}} y disfrute de firmas digitales gratuitas ilimitadas.",
"duplicate-subscribe-msg": "Duplicar le permite crear una copia exacta de la plantilla seleccionada, lo que le permite reutilizarla o modificarla sin afectar al original.",
"save-as-template-msg": "Guarde este documento como una plantilla reutilizable que podrá usar nuevamente para futuros documentos.",
"public-credit-alert": "Firmar a través de este enlace consumirá sus créditos de correo electrónico gratuitos. Como usuario gratuito, se le asignan 15 créditos de correo electrónico por mes para evitar que los spammers abusen de nuestros sistemas. Para disfrutar de límites más altos y acceso ininterrumpido, suscríbase a los planes de pago de <1>OpenSign™.</1>",
"know-more-about": "Saber más sobre",
"free-unlimited-signatures": "¿Firmas ilimitadas gratis",
"email-quota-updated": "La cuota diaria de correos se actualizó correctamente.",
"password-has-been-reset": "La contraseña ha sido restablecida.",
"Unauthorized to reset your own password.": "No está autorizado para restablecer su propia contraseña.",
"User not found or not allowed.": "Usuario no encontrado o no autorizado.",
"User not found.": "Usuario no encontrado.",
"Admin user tenant not found.": "Inquilino de usuario administrador no encontrado.",
"User is not authenticated.": "El usuario no está autenticado.",
"Please provide required parameters.": "Por favor, proporcione los parámetros requeridos.",
"reset-password": "Restablecer contraseña de usuario",
"enter-strong-password": "Ingrese una contraseña segura o genere una automáticamente.",
"autogenerate": "Generar automáticamente"
}
@@ -8,7 +8,7 @@
"dark-mode": "Mode sombre", "dark-mode": "Mode sombre",
"name": "Nom et Prénom", "name": "Nom et Prénom",
"phone": "Téléphone", "phone": "Téléphone",
"phone-optional": "(facultatif)", "phone-optional": "Optionnel",
"email": "E-mail", "email": "E-mail",
"company": "Organisation", "company": "Organisation",
"job-title": "Votre Fonction", "job-title": "Votre Fonction",
@@ -200,7 +200,8 @@
"daily-mail-quota": "Quota d'e-mails quotidien", "daily-mail-quota": "Quota d'e-mails quotidien",
"Save as template": "Enregistrer comme modèle", "Save as template": "Enregistrer comme modèle",
"Fix & resend": "Corriger et renvoyer", "Fix & resend": "Corriger et renvoyer",
"Kiosk Mode": "Mode Kiosque" "Kiosk Mode": "Mode Kiosque",
"Reset password": "Réinitialiser le mot de passe"
}, },
"report-help": { "report-help": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.", "Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -385,7 +386,8 @@
"dropdown": "dérouler", "dropdown": "dérouler",
"radio button": "bouton radio", "radio button": "bouton radio",
"image": "image", "image": "image",
"email": "e-mail" "email": "e-mail",
"number": "nombre"
}, },
"fields": "Des champs", "fields": "Des champs",
"recipients": "Destinataires", "recipients": "Destinataires",
@@ -426,7 +428,7 @@
"initial-alert": "Mon initiale est introuvable", "initial-alert": "Mon initiale est introuvable",
"copy-title": "Copier le widget vers", "copy-title": "Copier le widget vers",
"contact-delete-alert": "Êtes-vous sûr de vouloir supprimer ce contact ?", "contact-delete-alert": "Êtes-vous sûr de vouloir supprimer ce contact ?",
"reset-password": "Le lien de réinitialisation du mot de passe a été envoyé à votre identifiant de messagerie", "reset-password-alert-1": "Le lien de réinitialisation du mot de passe a été envoyé à votre identifiant de messagerie",
"reset-password-alert-2": "Veuillez configurer l'adaptateur de messagerie", "reset-password-alert-2": "Veuillez configurer l'adaptateur de messagerie",
"reset-password-alert-3": "Réinitialisez votre mot de passe", "reset-password-alert-3": "Réinitialisez votre mot de passe",
"faild-animation": "Échec du chargement de l'animation", "faild-animation": "Échec du chargement de l'animation",
@@ -442,6 +444,19 @@
"maximum-check": "Contrôle maximum", "maximum-check": "Contrôle maximum",
"cell-count": "numération cellulaire", "cell-count": "numération cellulaire",
"default-value": "Valeur par défaut", "default-value": "Valeur par défaut",
"formula": "Formule",
"formula-placeholder": "Créez une formule à l'aide des widgets numériques",
"formula-no-widgets": "Aucun autre widget numérique n'est encore attribué à ce signataire.",
"supported-operators": "Opérateurs pris en charge :",
"formula-helper": "Cliquez sur une puce de widget numérique pour l'insérer (ex. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Encadrez les noms de widget avec des doubles accolades (ex. {{number-1}}).",
"formula-error-characters": "En dehors des accolades, seuls les chiffres et +, -, *, /, (, ) sont autorisés.",
"formula-error-braces": "Chaque {{ ouvrante doit avoir une }} correspondante.",
"formula-error-operators": "Lorsque vous utilisez des opérateurs ou des parenthèses, ajoutez au moins deux valeurs.",
"formula-error-leading-operator": "Les formules ne peuvent pas commencer par des opérateurs.",
"decimal-places": "Décimales",
"number-of-decimal-places": "Nombre de décimales",
"decimal-places-helper": "Définir sur 0 pour les nombres entiers",
"select": "Sélectionner", "select": "Sélectionner",
"read-only": "Est en lecture seule", "read-only": "Est en lecture seule",
"hide-labels": "Masquer les étiquettes", "hide-labels": "Masquer les étiquettes",
@@ -901,6 +916,10 @@
"thanks-for-feedback": "Merci pour votre retour 🙏", "thanks-for-feedback": "Merci pour votre retour 🙏",
"share-your-feedback": "Partagez votre avis", "share-your-feedback": "Partagez votre avis",
"share-your-review": "Partagez votre avis", "share-your-review": "Partagez votre avis",
"please-select-rating": "Veuillez sélectionner une note",
"feedback-optional": "Retour (optionnel)",
"feedback-saved": "Le retour a été enregistré.",
"feedback-save-error": "Impossible d'enregistrer le retour, veuillez réessayer.",
"date-format": "Format de date", "date-format": "Format de date",
"document-deleted": "Le document a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.", "document-deleted": "Le document a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.",
"save-as-template-?": "Êtes-vous sûr de vouloir enregistrer ce document comme modèle ?", "save-as-template-?": "Êtes-vous sûr de vouloir enregistrer ce document comme modèle ?",
@@ -1210,5 +1229,32 @@
"otp-email": "Nous avons envoyé un code de vérification", "otp-email": "Nous avons envoyé un code de vérification",
"save-as-temp-warn": "Remarque : Ce document contient des widgets préremplis, qui seront automatiquement supprimés car ils sont déjà intégrés dans le document de base.", "save-as-temp-warn": "Remarque : Ce document contient des widgets préremplis, qui seront automatiquement supprimés car ils sont déjà intégrés dans le document de base.",
"edit-document": "Modifier le document", "edit-document": "Modifier le document",
"otp-email": "Nous avons envoyé un code de vérification" "modify": "Modifier",
} "merge-certificate-to-pdf": "Fusionner le certificat avec le PDF",
"merge-cc-to-pdf-help": {
"p1": "Cela garantira que le certificat de finalisation est inclus dans le document PDF final. Cependant, veuillez noter qu'une fois fusionné, le certificat ne peut plus être séparé du document principal.",
"p2": "Si vous choisissez de ne pas fusionner, le certificat de finalisation sera fourni sous forme de fichier PDF distinct accompagné du document signé."
},
"read-only-date-error": "Le widget de date en lecture seule doit avoir une date par défaut.",
"set-date": "Définir la date",
"set-today": "Date de signature",
"enter-name": "Saisir le nom",
"enter-email": "Saisir l'e-mail",
"subscribe-to-opensign-msg": "Abonnez-vous à {{appName}} et profitez de signatures numériques gratuites illimitées.",
"duplicate-subscribe-msg": "Dupliquer vous permet de créer une copie exacte du modèle sélectionné, que vous pouvez réutiliser ou modifier sans affecter l'original.",
"save-as-template-msg": "Enregistrez ce document comme modèle réutilisable que vous pourrez utiliser à nouveau pour de futurs documents.",
"public-credit-alert": "Signer via ce lien consomme vos crédits e-mail gratuits. En tant qu'utilisateur gratuit, vous disposez de 15 crédits e-mail par mois afin dempêcher les spammeurs dabuser de nos systèmes. Pour profiter de limites plus élevées et d'un accès ininterrompu, abonnez-vous aux forfaits payants de <1>OpenSign™.</1>",
"know-more-about": "En savoir plus sur",
"free-unlimited-signatures": "Signatures illimitées gratuites",
"email-quota-updated": "Quota quotidien d'e-mails mis à jour avec succès.",
"password-has-been-reset": "Le mot de passe a été réinitialisé.",
"Unauthorized to reset your own password.": "Non autorisé à réinitialiser votre propre mot de passe.",
"User not found or not allowed.": "Utilisateur introuvable ou non autorisé.",
"User not found.": "Utilisateur introuvable.",
"Admin user tenant not found.": "Locataire administrateur introuvable.",
"User is not authenticated.": "L'utilisateur n'est pas authentifié.",
"Please provide required parameters.": "Veuillez fournir les paramètres requis.",
"reset-password": "Réinitialiser le mot de passe utilisateur",
"enter-strong-password": "Entrez un mot de passe sécurisé ou générez-en un automatiquement.",
"autogenerate": "Générer automatiquement"
}
@@ -165,7 +165,8 @@
"daily-mail-quota": "दैनिक ईमेल कोटा", "daily-mail-quota": "दैनिक ईमेल कोटा",
"Save as template": "टेम्पलेट के रूप में सहेजें", "Save as template": "टेम्पलेट के रूप में सहेजें",
"Fix & resend": "ठीक करें और पुनः भेजें", "Fix & resend": "ठीक करें और पुनः भेजें",
"Kiosk Mode": "कियोस्क मोड" "Kiosk Mode": "कियोस्क मोड",
"Reset password": "पासवर्ड रीसेट करें"
}, },
"report-heading": { "report-heading": {
"Sr.No": "क्रमांक", "Sr.No": "क्रमांक",
@@ -385,7 +386,8 @@
"dropdown": "ड्रॉपडाउन", "dropdown": "ड्रॉपडाउन",
"radio button": "रेडियो बटन", "radio button": "रेडियो बटन",
"image": "छवि", "image": "छवि",
"email": "ईमेल" "email": "ईमेल",
"number": "संख्या"
}, },
"fields": "क्षेत्र", "fields": "क्षेत्र",
"recipients": "प्राप्तकर्ता", "recipients": "प्राप्तकर्ता",
@@ -442,6 +444,19 @@
"maximum-check": "अधिकतम जाँच", "maximum-check": "अधिकतम जाँच",
"cell-count": "सेल्स संख्या", "cell-count": "सेल्स संख्या",
"default-value": "डिफ़ॉल्ट मान", "default-value": "डिफ़ॉल्ट मान",
"formula": "सूत्र",
"formula-placeholder": "संख्या विजेट्स का उपयोग करके एक सूत्र बनाएं",
"formula-no-widgets": "इस हस्ताक्षरकर्ता को अभी तक कोई अन्य संख्या विजेट असाइन नहीं किया गया है।",
"supported-operators": "समर्थित ऑपरेटर:",
"formula-helper": "सूची में से संख्या विजेट चिप पर क्लिक करके उसे जोड़ें (उदा. {{number-1}} + {{number-2}})।",
"formula-error-letter": "विजेट नामों को दोहरी घुंघराली ब्रैकेट में रखें (उदा. {{number-1}})।",
"formula-error-characters": "ब्रैकेट के बाहर केवल संख्याएँ और +, -, *, /, (, ) की अनुमति है।",
"formula-error-braces": "हर खुलने वाली {{ के लिए एक मिलान करने वाली }} होना चाहिए।",
"formula-error-operators": "ऑपरेटर या कोष्ठक का उपयोग करते समय कम से कम दो मान जोड़ें।",
"formula-error-leading-operator": "सूत्र ऑपरेटर से शुरू नहीं हो सकते।",
"decimal-places": "दशमलव स्थान",
"number-of-decimal-places": "दशमलव स्थानों की संख्या",
"decimal-places-helper": "पूर्णांक के लिए 0 पर सेट करें",
"select": "चुनें", "select": "चुनें",
"read-only": "सिर्फ पढ़ने के लिए है", "read-only": "सिर्फ पढ़ने के लिए है",
"hide-labels": "लेबल छिपाएँ", "hide-labels": "लेबल छिपाएँ",
@@ -901,6 +916,10 @@
"thanks-for-feedback": "आपकी प्रतिक्रिया के लिए धन्यवाद 🙏", "thanks-for-feedback": "आपकी प्रतिक्रिया के लिए धन्यवाद 🙏",
"share-your-feedback": "अपनी प्रतिक्रिया साझा करें", "share-your-feedback": "अपनी प्रतिक्रिया साझा करें",
"share-your-review": "अपनी समीक्षा साझा करें", "share-your-review": "अपनी समीक्षा साझा करें",
"please-select-rating": "कृपया एक रेटिंग चुनें",
"feedback-optional": "प्रतिक्रिया (वैकल्पिक)",
"feedback-saved": "प्रतिक्रिया सहेज ली गई है।",
"feedback-save-error": "प्रतिक्रिया सहेजी नहीं जा सकी, कृपया पुनः प्रयास करें।",
"date-format": "दिनांक प्रारूप", "date-format": "दिनांक प्रारूप",
"document-deleted": "दस्तावेज़ हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।", "document-deleted": "दस्तावेज़ हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
"save-as-template-?": "क्या आप वाकई इस दस्तावेज़ को टेम्पलेट के रूप में सहेजना चाहते हैं?", "save-as-template-?": "क्या आप वाकई इस दस्तावेज़ को टेम्पलेट के रूप में सहेजना चाहते हैं?",
@@ -1210,5 +1229,32 @@
"otp-email": "हमने एक सत्यापन कोड भेजा है", "otp-email": "हमने एक सत्यापन कोड भेजा है",
"save-as-temp-warn": "नोट: इस दस्तावेज़ में पहले से भरे हुए विजेट शामिल हैं, जिन्हें स्वचालित रूप से हटा दिया जाएगा क्योंकि वे पहले से ही मूल दस्तावेज़ में सम्मिलित हैं।", "save-as-temp-warn": "नोट: इस दस्तावेज़ में पहले से भरे हुए विजेट शामिल हैं, जिन्हें स्वचालित रूप से हटा दिया जाएगा क्योंकि वे पहले से ही मूल दस्तावेज़ में सम्मिलित हैं।",
"edit-document": "दस्तावेज़ संपादित करें", "edit-document": "दस्तावेज़ संपादित करें",
"otp-email": "हमने एक सत्यापन कोड भेजा है" "modify": "संशोधित करें",
} "merge-certificate-to-pdf": "प्रमाणपत्र को PDF में मिलाएँ",
"merge-cc-to-pdf-help": {
"p1": "यह सुनिश्चित करेगा कि पूर्णता प्रमाणपत्र अंतिम PDF दस्तावेज़ में शामिल हो। हालाँकि, कृपया ध्यान दें कि एक बार मिलाने के बाद, प्रमाणपत्र को मुख्य दस्तावेज़ से अलग नहीं किया जा सकता।",
"p2": "यदि आप मिलाना नहीं चुनते हैं, तो पूर्णता प्रमाणपत्र हस्ताक्षरित दस्तावेज़ के साथ एक अलग PDF फ़ाइल के रूप में प्रदान किया जाएगा।"
},
"read-only-date-error": "रीड-ओनली दिनांक विजेट में एक डिफ़ॉल्ट दिनांक होना आवश्यक है।",
"set-date": "दिनांक सेट करें",
"set-today": "हस्ताक्षर की तिथि",
"enter-name": "नाम दर्ज करें",
"enter-email": "ईमेल दर्ज करें",
"subscribe-to-opensign-msg": "{{appName}} की सदस्यता लें और असीमित निःशुल्क डिजिटल हस्ताक्षरों का आनंद लें।",
"duplicate-subscribe-msg": "डुप्लिकेट करने से आप चयनित टेम्पलेट की एक सटीक प्रति बना सकते हैं, जिसे आप पुनः उपयोग या संशोधित कर सकते हैं बिना मूल को प्रभावित किए।",
"save-as-template-msg": "इस दस्तावेज़ को एक पुन: प्रयोज्य टेम्पलेट के रूप में सहेजें जिसे आप भविष्य के दस्तावेज़ों के लिए फिर से उपयोग कर सकते हैं।",
"public-credit-alert": "इस लिंक के माध्यम से हस्ताक्षर करने पर आपके निःशुल्क ईमेल क्रेडिट का उपभोग होगा। एक निःशुल्क उपयोगकर्ता के रूप में, आपको स्पैमर्स को हमारी प्रणाली का दुरुपयोग करने से रोकने के लिए प्रति माह 15 ईमेल क्रेडिट आवंटित किए जाते हैं। उच्च सीमा और निर्बाध पहुँच का आनंद लेने के लिए, <1>OpenSign™ पेड प्लान</1> की सदस्यता लें।",
"know-more-about": "इसके बारे में और जानें",
"free-unlimited-signatures": "मुफ़्त असीमित हस्ताक्षर",
"email-quota-updated": "दैनिक ईमेल कोटा सफलतापूर्वक अपडेट किया गया।",
"password-has-been-reset": "पासवर्ड रीसेट कर दिया गया है।",
"Unauthorized to reset your own password.": "अपना पासवर्ड रीसेट करने के लिए अधिकृत नहीं है।",
"User not found or not allowed.": "उपयोगकर्ता नहीं मिला या अनुमति नहीं है।",
"User not found.": "उपयोगकर्ता नहीं मिला।",
"Admin user tenant not found.": "एडमिन उपयोगकर्ता टेनेंट नहीं मिला।",
"User is not authenticated.": "उपयोगकर्ता प्रमाणीकृत नहीं है।",
"Please provide required parameters.": "कृपया आवश्यक पैरामीटर प्रदान करें।",
"reset-password": "उपयोगकर्ता पासवर्ड रीसेट करें",
"enter-strong-password": "एक मज़बूत पासवर्ड दर्ज करें या स्वचालित रूप से उत्पन्न करें।",
"autogenerate": "स्वतः उत्पन्न करें"
}
@@ -8,7 +8,7 @@
"dark-mode": "Modalità scura", "dark-mode": "Modalità scura",
"name": "Nome", "name": "Nome",
"phone": "Telefono", "phone": "Telefono",
"phone-optional": "facoltativo", "phone-optional": "Opzionale",
"email": "Email", "email": "Email",
"company": "Azienda", "company": "Azienda",
"job-title": "Titolo professionale", "job-title": "Titolo professionale",
@@ -165,7 +165,8 @@
"daily-mail-quota": "Quota e-mail giornaliera", "daily-mail-quota": "Quota e-mail giornaliera",
"Save as template": "Salva come modello", "Save as template": "Salva come modello",
"Fix & resend": "Correggi e reinvia", "Fix & resend": "Correggi e reinvia",
"Kiosk Mode": "Modalità Kiosk" "Kiosk Mode": "Modalità Kiosk",
"Reset password": "Reimposta password"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Nr.", "Sr.No": "Nr.",
@@ -385,7 +386,8 @@
"dropdown": "menu a tendina", "dropdown": "menu a tendina",
"radio button": "pulsante di opzione", "radio button": "pulsante di opzione",
"image": "immagine", "image": "immagine",
"email": "email" "email": "email",
"number": "numero"
}, },
"fields": "Campi", "fields": "Campi",
"recipients": "Destinatari", "recipients": "Destinatari",
@@ -442,6 +444,19 @@
"maximum-check": "Controllo massimo", "maximum-check": "Controllo massimo",
"cell-count": "conteggio delle cellule", "cell-count": "conteggio delle cellule",
"default-value": "Valore predefinita", "default-value": "Valore predefinita",
"formula": "Formula",
"formula-placeholder": "Crea una formula utilizzando i widget numerici",
"formula-no-widgets": "Non ci sono ancora altri widget numerici assegnati a questo firmatario.",
"supported-operators": "Operatori supportati:",
"formula-helper": "Fai clic su una chip di widget numerico per inserirla (es. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Racchiudi i nomi dei widget tra doppie parentesi graffe (es. {{number-1}}).",
"formula-error-characters": "Al di fuori delle parentesi sono consentiti solo numeri e +, -, *, /, (, ).",
"formula-error-braces": "Ogni {{ di apertura deve avere una }} corrispondente.",
"formula-error-operators": "Quando usi operatori o parentesi, aggiungi almeno due valori.",
"formula-error-leading-operator": "Le formule non possono iniziare con operatori.",
"decimal-places": "Cifre decimali",
"number-of-decimal-places": "Numero di cifre decimali",
"decimal-places-helper": "Imposta su 0 per numeri interi",
"select": "Seleziona", "select": "Seleziona",
"read-only": "È di sola lettura", "read-only": "È di sola lettura",
"hide-labels": "Nascondi etichette", "hide-labels": "Nascondi etichette",
@@ -901,6 +916,10 @@
"thanks-for-feedback": "Grazie per il tuo feedback 🙏", "thanks-for-feedback": "Grazie per il tuo feedback 🙏",
"share-your-feedback": "Condividi il tuo feedback", "share-your-feedback": "Condividi il tuo feedback",
"share-your-review": "Condividi la tua recensione", "share-your-review": "Condividi la tua recensione",
"please-select-rating": "Si prega di selezionare una valutazione",
"feedback-optional": "Feedback (opzionale)",
"feedback-saved": "Il feedback è stato salvato.",
"feedback-save-error": "Impossibile salvare il feedback, riprova.",
"date-format": "Formato data", "date-format": "Formato data",
"document-deleted": "Il documento è stato eliminato o non hai accesso. Si prega di contattare il mittente.", "document-deleted": "Il documento è stato eliminato o non hai accesso. Si prega di contattare il mittente.",
"save-as-template-?": "Sei sicuro di voler salvare questo documento come modello?", "save-as-template-?": "Sei sicuro di voler salvare questo documento come modello?",
@@ -1210,5 +1229,32 @@
"otp-email": "Abbiamo inviato un codice di verifica", "otp-email": "Abbiamo inviato un codice di verifica",
"save-as-temp-warn": "Nota: Questo documento include widget precompilati, che verranno automaticamente rimossi poiché sono già incorporati nel documento di base.", "save-as-temp-warn": "Nota: Questo documento include widget precompilati, che verranno automaticamente rimossi poiché sono già incorporati nel documento di base.",
"edit-document": "Modifica documento", "edit-document": "Modifica documento",
"otp-email": "Abbiamo inviato un codice di verifica" "modify": "Modifica",
} "merge-certificate-to-pdf": "Unisci certificato al PDF",
"merge-cc-to-pdf-help": {
"p1": "Questo garantirà che il certificato di completamento sia incluso nel documento PDF finale. Tuttavia, si noti che una volta unito, il certificato non può essere separato dal documento principale.",
"p2": "Se scegli di non unire, il certificato di completamento sarà fornito come file PDF separato insieme al documento firmato."
},
"read-only-date-error": "Il widget data di sola lettura deve avere una data predefinita.",
"set-date": "Imposta data",
"set-today": "Data di firma",
"enter-name": "Inserisci nome",
"enter-email": "Inserisci e-mail",
"subscribe-to-opensign-msg": "Abbonati a {{appName}} e goditi firme digitali gratuite illimitate.",
"duplicate-subscribe-msg": "Duplicare consente di creare una copia esatta del modello selezionato, permettendoti di riutilizzarlo o modificarlo senza influire sull'originale.",
"save-as-template-msg": "Salva questo documento come modello riutilizzabile che potrai usare di nuovo per documenti futuri.",
"public-credit-alert": "Firmare tramite questo link consumerà i tuoi crediti e-mail gratuiti. Come utente gratuito, ti vengono assegnati 15 crediti e-mail al mese per impedire agli spammer di abusare dei nostri sistemi. Per usufruire di limiti più elevati e accesso ininterrotto, abbonati ai piani a pagamento di <1>OpenSign™.</1>",
"know-more-about": "Scopri di più su",
"free-unlimited-signatures": "Firme illimitate gratuite",
"email-quota-updated": "Quota e-mail giornaliera aggiornata con successo.",
"password-has-been-reset": "La password è stata reimpostata.",
"Unauthorized to reset your own password.": "Non sei autorizzato a reimpostare la tua password.",
"User not found or not allowed.": "Utente non trovato o non autorizzato.",
"User not found.": "Utente non trovato.",
"Admin user tenant not found.": "Tenant utente amministratore non trovato.",
"User is not authenticated.": "Utente non autenticato.",
"Please provide required parameters.": "Fornisci i parametri richiesti.",
"reset-password": "Reimposta password utente",
"enter-strong-password": "Inserisci una password sicura o generane una automaticamente.",
"autogenerate": "Genera automaticamente"
}
+1 -8
View File
@@ -19,7 +19,6 @@ import Loader from "./primitives/Loader";
import UserList from "./pages/UserList"; import UserList from "./pages/UserList";
import { serverUrl_fn } from "./constant/appinfo"; import { serverUrl_fn } from "./constant/appinfo";
import DocSuccessPage from "./pages/DocSuccessPage"; import DocSuccessPage from "./pages/DocSuccessPage";
import ValidateSession from "./primitives/ValidateSession";
import DragProvider from "./components/DragProivder"; import DragProvider from "./components/DragProivder";
import Title from "./components/Title"; import Title from "./components/Title";
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf")); const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
@@ -88,13 +87,7 @@ function App() {
path="/forgetpassword" path="/forgetpassword"
element={<Lazy Page={ForgetPassword} />} element={<Lazy Page={ForgetPassword} />}
/> />
<Route <Route element={<HomeLayout />}>
element={
<ValidateSession>
<HomeLayout />
</ValidateSession>
}
>
<Route path="/users" element={<UserList />} /> <Route path="/users" element={<UserList />} />
<Route <Route
path="/changepassword" path="/changepassword"
@@ -244,6 +244,7 @@ const AddSigner = (props) => {
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))} onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
@@ -263,6 +264,7 @@ const AddSigner = (props) => {
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
disabled={addYourself} disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
+2
View File
@@ -179,6 +179,7 @@ const AddUser = (props) => {
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
required required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
@@ -200,6 +201,7 @@ const AddUser = (props) => {
} }
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
+3
View File
@@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next";
import { appInfo } from "../constant/appinfo"; import { appInfo } from "../constant/appinfo";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { toggleSidebar } from "../redux/reducers/sidebarReducer.js"; import { toggleSidebar } from "../redux/reducers/sidebarReducer.js";
import { sessionStatus } from "../redux/reducers/userReducer.js";
const Header = ({ isConsole, setIsLoggingOut }) => { const Header = ({ isConsole, setIsLoggingOut }) => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@@ -67,6 +68,8 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
await Parse.User.logOut(); await Parse.User.logOut();
} catch (err) { } catch (err) {
console.log("Err while logging out", err); console.log("Err while logging out", err);
} finally {
dispatch(sessionStatus(true));
} }
let appdata = localStorage.getItem("userSettings"); let appdata = localStorage.getItem("userSettings");
let applogo = localStorage.getItem("appLogo"); let applogo = localStorage.getItem("appLogo");
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import axios from "axios"; import axios from "axios";
import Parse from "parse"; import Parse from "parse";
import getReplacedHashQuery from "../../constant/getReplacedHashQuery"; import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
@@ -168,25 +168,21 @@ const DashboardCard = (props) => {
} }
} }
setresponse(arr.length); setresponse(arr.length);
setLoading(false);
}); });
} else { } else {
await axios.get(url, { headers: headers }).then((res) => { await axios.get(url, { headers: headers }).then((res) => {
if (res.data.results.length > 0) { if (res?.data?.[props.Data.key]) {
setLoading(false); setresponse(parseInt(res.data[props.Data.key]));
if (props.Data.key !== "count") { } else if (res?.data?.results?.length > 0) {
setresponse(res.data.results[0][props.Data.key]); setresponse(res.data.results.length);
} else {
setresponse(res.data[props.Data.key]);
}
} else { } else {
setresponse(0); setresponse(0);
setLoading(false);
} }
}); });
} }
} catch (e) { } catch (e) {
console.error("Problem", e); console.error("Problem", e);
} finally {
setLoading(false); setLoading(false);
} }
} }
@@ -71,11 +71,9 @@ function DriveBody(props) {
}); });
props.setPdfData(updatedData); props.setPdfData(updatedData);
props.sortingData(null, null, updatedData); props.sortingData(null, null, updatedData);
await axios try {
.put( await axios.put(
`${localStorage.getItem( `${localStorage.getItem("baseUrl")}classes/contracts_Document/${docId}`,
"baseUrl"
)}classes/contracts_Document/${docId}`,
updateName, updateName,
{ {
headers: { headers: {
@@ -84,18 +82,14 @@ function DriveBody(props) {
"X-Parse-Session-Token": localStorage.getItem("accesstoken") "X-Parse-Session-Token": localStorage.getItem("accesstoken")
} }
} }
) );
.then(() => { } catch (err) {
// const res = result.data; console.log("Err ", err);
// console.log("res", res); props.setIsAlert({
}) isShow: true,
.catch((err) => { alertMessage: t("something-went-wrong-mssg")
console.log("Err ", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
}); });
}
} }
}; };
@@ -245,7 +239,6 @@ function DriveBody(props) {
) )
.then((Listdata) => { .then((Listdata) => {
// console.log("Listdata ", Listdata);
const res = Listdata.data; const res = Listdata.data;
if (res) { if (res) {
const updatedData = props.pdfData.filter( const updatedData = props.pdfData.filter(
@@ -52,7 +52,7 @@ const EditTemplate = ({
Bcc: template?.Bcc, Bcc: template?.Bcc,
RedirectUrl: template?.RedirectUrl || "", RedirectUrl: template?.RedirectUrl || "",
AllowModifications: template?.AllowModifications || false, AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15 TimeToCompleteDays: template?.TimeToCompleteDays || 15,
}); });
const [isUpdate, setIsUpdate] = useState(false); const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
@@ -244,6 +244,7 @@ const EditTemplate = ({
setShowConfirm(false); setShowConfirm(false);
handleClose(); handleClose();
}; };
return ( return (
<ModalUi <ModalUi
isOpen isOpen
@@ -31,7 +31,6 @@ function EmailComponent({
setIsLoading(true); setIsLoading(true);
const params = { docId: pdfDetails?.[0]?.objectId, recipients: emailList }; const params = { docId: pdfDetails?.[0]?.objectId, recipients: emailList };
const sendmail = await Parse.Cloud.run("forwarddoc", params); const sendmail = await Parse.Cloud.run("forwarddoc", params);
console.log("sendmail ", sendmail);
if (sendmail?.status === "success") { if (sendmail?.status === "success") {
setSuccessEmail(true); setSuccessEmail(true);
setIsEmail(false); setIsEmail(false);
+80 -59
View File
@@ -75,9 +75,12 @@ function Header(props) {
alert(t("only-pdf-allowed")); alert(t("only-pdf-allowed"));
return; return;
} }
const mb = Math.round(file?.size / Math.pow(1024, 2)); const fileSize =
if (mb > maxFileSize) { maxFileSize;
alert(`${t("file-alert-1")} ${maxFileSize} MB`); const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e); removeFile(e);
return; return;
} }
@@ -134,6 +137,13 @@ function Header(props) {
useObjectStreams: false useObjectStreams: false
}); });
const pdfBuffer = base64ToArrayBuffer(pdfBase64); const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer); props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64); props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true); props.setIsUploadPdf && props.setIsUploadPdf(true);
@@ -167,7 +177,9 @@ function Header(props) {
); );
}; };
const handleDownloadBtn = async () => { const handleDownloadBtn = async () => {
if (props?.isCompleted) { if (
props?.isCompleted
) {
props?.setIsDownloadModal(true); props?.setIsDownloadModal(true);
} else { } else {
await handleDownloadDoc(); await handleDownloadDoc();
@@ -222,25 +234,27 @@ function Header(props) {
{t("download")} {t("download")}
</div> </div>
</DropdownMenu.Item> </DropdownMenu.Item>
{props?.isCompleted && ( {
<DropdownMenu.Item props?.isCompleted && (
className="DropdownMenuItem" <DropdownMenu.Item
onClick={() => className="DropdownMenuItem"
handleDownloadCertificate( onClick={() =>
props?.pdfDetails, handleDownloadCertificate(
setIsDownloading props?.pdfDetails,
) setIsDownloading
} )
> }
<div className="border-none bg-[#fff]"> >
<i <div className="border-none bg-[#fff]">
className="fa-light fa-award mr-[3px]" <i
aria-hidden="true" className="fa-light fa-award mr-[3px]"
></i> aria-hidden="true"
{t("certificate")} ></i>
</div> {t("certificate")}
</DropdownMenu.Item> </div>
)} </DropdownMenu.Item>
)
}
{props?.isSignYourself && ( {props?.isSignYourself && (
<DropdownMenu.Item <DropdownMenu.Item
className="DropdownMenuItem" className="DropdownMenuItem"
@@ -518,24 +532,28 @@ function Header(props) {
></i> ></i>
<span className="hidden lg:block">{t("print")}</span> <span className="hidden lg:block">{t("print")}</span>
</button> </button>
{props?.isCompleted && ( {
<button props?.isCompleted && (
type="button" <button
onClick={() => type="button"
handleDownloadCertificate( onClick={() =>
props?.pdfDetails, handleDownloadCertificate(
setIsDownloading props?.pdfDetails,
) setIsDownloading
} )
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow" }
> className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
<i >
className="fa-light fa-award py-[3px]" <i
aria-hidden="true" className="fa-light fa-award py-[3px]"
></i> aria-hidden="true"
<span className="hidden lg:block">{t("certificate")}</span> ></i>
</button> <span className="hidden lg:block">
)} {t("certificate")}
</span>
</button>
)
}
<button <button
type="button" type="button"
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow" className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
@@ -592,23 +610,25 @@ function Header(props) {
) )
) : props?.isCompleted ? ( ) : props?.isCompleted ? (
<div className="flex flex-row"> <div className="flex flex-row">
{props?.isCompleted && ( {
<button props?.isCompleted && (
type="button" <button
onClick={() => type="button"
handleDownloadCertificate( onClick={() =>
props?.pdfDetails, handleDownloadCertificate(
setIsDownloading props?.pdfDetails,
) setIsDownloading
} )
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow" }
> className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
<i className="fa-light fa-award" aria-hidden="true"></i> >
<span className="hidden lg:block ml-1"> <i className="fa-light fa-award" aria-hidden="true"></i>
{t("certificate")} <span className="hidden lg:block ml-1">
</span> {t("certificate")}
</button> </span>
)} </button>
)
}
<button <button
onClick={(e) => onClick={(e) =>
handleToPrint(e, setIsDownloading, props?.pdfDetails) handleToPrint(e, setIsDownloading, props?.pdfDetails)
@@ -622,7 +642,8 @@ function Header(props) {
<button <button
type="button" type="button"
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow" className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
onClick={() => props?.setIsDownloadModal(true)} // onClick={() => props?.setIsDownloadModal(true)}
onClick={() => handleDownloadBtn()}
> >
<i className="fa-light fa-download" aria-hidden="true"></i> <i className="fa-light fa-download" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">{t("download")}</span> <span className="hidden lg:block ml-1">{t("download")}</span>
+13 -3
View File
@@ -67,9 +67,12 @@ function PdfTools(props) {
alert(t("only-pdf-allowed")); alert(t("only-pdf-allowed"));
return; return;
} }
const mb = Math.round(file?.size / Math.pow(1024, 2)); const fileSize =
if (mb > maxFileSize) { maxFileSize;
alert(`${t("file-alert-1")} ${maxFileSize} MB`); const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e); removeFile(e);
return; return;
} }
@@ -126,6 +129,13 @@ function PdfTools(props) {
useObjectStreams: false useObjectStreams: false
}); });
const pdfBuffer = base64ToArrayBuffer(pdfBase64); const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer); props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64); props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true); props.setIsUploadPdf && props.setIsUploadPdf(true);
+304 -154
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, forwardRef } from "react";
import BorderResize from "./BorderResize"; import BorderResize from "./BorderResize";
import { Rnd } from "react-rnd"; import { Rnd } from "react-rnd";
import { import {
@@ -15,7 +15,13 @@ import {
cellsWidget, cellsWidget,
textWidget, textWidget,
selectFormat, selectFormat,
randomId randomId,
getYear,
getMonth,
months,
years,
getDefaultDate,
getDefaultFormat
} from "../../constant/Utils"; } from "../../constant/Utils";
import PlaceholderType from "./PlaceholderType"; import PlaceholderType from "./PlaceholderType";
import moment from "moment"; import moment from "moment";
@@ -29,47 +35,34 @@ import {
} from "../../redux/reducers/widgetSlice"; } from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const"; import { themeColor } from "../../constant/const";
import { useGuidelinesContext } from "../../context/GuidelinesContext"; import { useGuidelinesContext } from "../../context/GuidelinesContext";
import DatePicker from "react-datepicker";
//function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
//function to convert formated date to new Date() format
const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formated date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
if (parsedDate.isValid()) {
date = new Date(parsedDate.toISOString());
return date;
} else {
date = new Date();
return date;
}
};
function Placeholder(props) { function Placeholder(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const prefillImg = useSelector((state) => state.widget.prefillImg); const prefillImg = useSelector((state) => state.widget.prefillImg);
const { showGuidelines } = useGuidelinesContext(); const { showGuidelines } = useGuidelinesContext();
const statusArr = ["Required", "Optional"];
const widgetData = const widgetData =
props.pos?.options?.defaultValue || props.pos?.options?.response; props.pos?.options?.defaultValue || props.pos?.options?.response;
const [isDateModal, setIsDateModal] = useState(false); const [isDateModal, setIsDateModal] = useState(false);
const [containerScale, setContainerScale] = useState(); const [containerScale, setContainerScale] = useState();
const [selectDate, setSelectDate] = useState({}); const [selectDate, setSelectDate] = useState({});
const [dateFormat, setDateFormat] = useState([]); const [dateFormatList, setDateFormatList] = useState([]);
const [clickonWidget, setClickonWidget] = useState({}); const [clickonWidget, setClickonWidget] = useState({});
const [isDateReadOnly, setIsDateReadOnly] = useState( const [formdata, setFormdata] = useState({
props?.pos?.options?.isReadOnly || false status: props?.pos?.options?.status || "required",
isReadOnly: props?.pos?.options?.isReadOnly || false
});
const [isToday, setIsToday] = useState(
props.pos?.options?.response === "today" ? true : false
); );
const startDate = props?.pos?.options?.response const startDate = props?.pos?.options?.response
? getDefaultDate( ? getDefaultDate(
props?.pos?.options?.response, props?.pos?.options?.response,
props.pos?.options?.validation?.format props.pos?.options?.validation?.format
) )
: new Date(); : "";
useEffect(() => { useEffect(() => {
const getPdfPageWidth = props.pdfOriginalWH.find( const getPdfPageWidth = props.pdfOriginalWH.find(
(data) => data.pageNumber === props.pageNumber (data) => data.pageNumber === props.pageNumber
@@ -102,29 +95,87 @@ function Placeholder(props) {
} }
}, [widgetData]); }, [widgetData]);
//function is used to convert date according to selected format
const handleGetFormatDate = (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 (isSpecialDateFormat && updateDate) {
date = isDateChange
? moment(updateDate).format(changeDateToMomentFormat(data.format))
: updateDate;
} else if (updateDate) {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
const newDate = new Date(updateDate);
date = moment(newDate.getTime()).format(
changeDateToMomentFormat(data?.format)
);
}
setSelectDate({ date: date, format: data?.format });
return date;
};
const saveDateSetting = (data) => {
if (formdata?.isReadOnly && !selectDate.date && !isToday) {
alert(t("read-only-date-error"));
return;
}
handleSaveDate(data);
setIsDateModal(false);
props.setFontColor("");
props.setFontSize("");
};
//function to save date and format on local array onchange date and onclick format
const handleSaveDate = (data, isDateChange) => {
const date = isToday ? "today" : handleGetFormatDate(data, isDateChange);
//`onChangeInput` is used to save data related to date in a placeholder field
onChangeInput(
date,
props.pos,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
data?.format,
props.fontSize || props.pos?.options?.fontSize || 12,
props.fontColor || props.pos?.options?.fontColor || "black",
formdata
);
};
//Date-picker- accept date - Thu Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
//if date have in this format - "30/12/2025"
//then first need to be convert in this format - Tue Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
//Using moment - moment("30/12/2025", "DD/MM/YYYY").toDate();
//moment package where we show date according to format in list
//it support date in this type - "2025-09-11"// ISO string, "2025-09-11T14:30:00" // ISO datetime
//function change format array list with selected date and format //function change format array list with selected date and format
const changeDateFormat = () => { const changeDateFormat = () => {
const updateDate = []; const updateDate = [];
dateFormatArr.map((data) => { dateFormatArr.map((data) => {
let date; let date = selectDate?.date || new Date();
if (selectDate && selectDate.format === "dd-MM-yyyy") { const isString = typeof date === "string";
const [day, month, year] = selectDate.date.split("-"); if (selectDate.format === "dd-MM-yyyy" && isString) {
const [day, month, year] = date.split("-");
date = new Date(`${year}-${month}-${day}`); date = new Date(`${year}-${month}-${day}`);
} else if (selectDate && selectDate.format === "dd.MM.yyyy") { } else if (selectDate.format === "dd.MM.yyyy" && isString) {
const [day, month, year] = selectDate.date.split("."); const [day, month, year] = date.split(".");
date = new Date(`${year}.${month}.${day}`); date = new Date(`${year}.${month}.${day}`);
} else if (selectDate && selectDate.format === "dd/MM/yyyy") { } else if (selectDate.format === "dd/MM/yyyy" && isString) {
const [day, month, year] = selectDate.date.split("/"); const [day, month, year] = date.split("/");
date = new Date(`${year}/${month}/${day}`); date = new Date(`${year}/${month}/${day}`);
} else { } else {
date = new Date(selectDate?.date); date = selectDate?.date ? new Date(selectDate?.date) : new Date();
} }
const milliseconds = date.getTime(); const milliseconds = date.getTime();
const newDate = moment(milliseconds).format(data); const newDate = moment(milliseconds).format(data);
const dateObj = { date: newDate, format: selectFormat(data) }; const dateObj = { date: newDate, format: selectFormat(data) };
updateDate.push(dateObj); updateDate.push(dateObj);
}); });
setDateFormat(updateDate); setDateFormatList(updateDate);
}; };
useEffect(() => { useEffect(() => {
@@ -216,7 +267,8 @@ function Placeholder(props) {
(props.isSignYourself || props.isSelfSign) (props.isSignYourself || props.isSelfSign)
) { ) {
props.handleTextSettingModal(true); props.handleTextSettingModal(true);
} else { }
else {
props?.handleNameModal && props?.handleNameModal(true); props?.handleNameModal && props?.handleNameModal(true);
} }
@@ -254,13 +306,7 @@ function Placeholder(props) {
) { ) {
props.setUniqueId(props?.data?.Id); props.setUniqueId(props?.data?.Id);
} }
if (props?.data?.Role !== "prefill") {
//checking widget's type and open widget copy modal for required widgets
if (
["signature", textInputWidget, textWidget, "stamp", "initials"].includes(
props.pos.type
)
) {
props.setIsPageCopy(true); props.setIsPageCopy(true);
props.setCurrWidgetsDetails(props.pos); props.setCurrWidgetsDetails(props.pos);
} else { } else {
@@ -275,7 +321,7 @@ function Placeholder(props) {
props.data && props.data?.Id props.data && props.data?.Id
); );
//condiiton is used to store copied prefill image base64 url in redux for display image //condiiton is used to store copied prefill image base64 url in redux for display image
if (props?.data?.Role === "prefill") { if (props?.pos?.type === "image") {
const getPrefillImg = prefillImg?.find((x) => x.id === props.pos.key); const getPrefillImg = prefillImg?.find((x) => x.id === props.pos.key);
dispatch( dispatch(
setPrefillImg({ setPrefillImg({
@@ -287,41 +333,6 @@ 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 (isSpecialDateFormat) {
date = isDateChange
? moment(updateDate).format(changeDateToMomentFormat(data.format))
: updateDate;
} else {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
const newDate = new Date(updateDate);
date = moment(newDate.getTime()).format(
changeDateToMomentFormat(data?.format)
);
}
//`onChangeInput` is used to save data related to date in a placeholder field
onChangeInput(
date,
props.pos,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
data?.format,
props.fontSize || props.pos?.options?.fontSize || 12,
props.fontColor || props.pos?.options?.fontColor || "black",
isDateReadOnly || false
);
setSelectDate({ date: date, format: data?.format });
};
const setCellCount = (key, newCount) => { const setCellCount = (key, newCount) => {
props.setXyPosition((prev) => { props.setXyPosition((prev) => {
const isSignerList = prev.some((d) => d.signerPtr); const isSignerList = prev.some((d) => d.signerPtr);
@@ -650,6 +661,40 @@ function Placeholder(props) {
return false; return false;
} }
}; };
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 md:ml-2 text-xs py-2 focus:outline-none hover:border-base-content "
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleChangeFormat = (e) => {
const selectedIndex = e.target.value;
e.stopPropagation();
if (
props?.isPlaceholder &&
props?.data?.Role !== "prefill" &&
!selectDate?.date
) {
setSelectDate((prev) => ({
...prev,
format: dateFormatList[selectedIndex]?.format
}));
} else {
setSelectDate(dateFormatList[selectedIndex]);
}
};
const handleClearDate = () => {
setIsToday(false);
setSelectDate((prev) => ({
...prev,
date: ""
}));
};
return ( return (
<> <>
{/* Check if a text widget (prefill type) exists. Once the user enters a value and clicks outside or the widget becomes non-selectable, it should appear as plain text (just like embedded text in a document). When the user clicks on the text again, it should become editable. */} {/* Check if a text widget (prefill type) exists. Once the user enters a value and clicks outside or the widget becomes non-selectable, it should appear as plain text (just like embedded text in a document). When the user clicks on the text again, it should become editable. */}
@@ -889,10 +934,8 @@ function Placeholder(props) {
(props?.isAlllowModify && (props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key)) !props?.assignedWidgetId.includes(props.pos.key))
} }
setSelectDate={setSelectDate}
selectDate={selectDate} selectDate={selectDate}
startDate={startDate} startDate={startDate}
handleSaveDate={handleSaveDate}
xPos={props.xPos} xPos={props.xPos}
calculateFont={calculateFont} calculateFont={calculateFont}
setCellCount={setCellCount} setCellCount={setCellCount}
@@ -902,22 +945,18 @@ function Placeholder(props) {
)} )}
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}> <ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
<div className="text-base-content h-[100%] p-[20px]"> <div className="text-base-content h-[100%] p-[20px]">
<div className="flex flex-col md:flex-row md:items-center gap-y-3"> <div className="flex flex-col gap-3">
<div className="flex flex-row items-center gap-x-1"> <div className="flex flex-col md:items-center md:flex-row gap-y-3">
<span className="capitalize">{t("format")} :</span> <span className="capitalize">{t("format")} :</span>
<select <select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs" className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-4"
defaultValue={""} defaultValue={""}
onChange={(e) => { onChange={(e) => handleChangeFormat(e)}
const selectedIndex = e.target.value;
e.stopPropagation();
setSelectDate(dateFormat[selectedIndex]);
}}
> >
<option value="" disabled> <option value="" disabled>
{t("select-date-format")} {t("select-date-format")}
</option> </option>
{dateFormat.map((data, ind) => { {dateFormatList.map((data, ind) => {
return ( return (
<option className="text-[13px]" value={ind} key={ind}> <option className="text-[13px]" value={ind} key={ind}>
{data?.date ? data?.date : "nodata"} {data?.date ? data?.date : "nodata"}
@@ -925,79 +964,190 @@ function Placeholder(props) {
); );
})} })}
</select> </select>
<span className="text-xs text-gray-400 ml-1">
{selectDate.format}
</span>
</div> </div>
<span className="text-xs text-gray-400 ml-1"> {props?.data?.Role !== "prefill" && props?.isPlaceholder && (
{selectDate.format} <>
</span> <div className="flex flex-col md:flex-row md:items-center gap-2">
</div> <span>{t("set-date")} :</span>
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2 mt-3"> <DatePicker
<div className="flex flex-row items-center"> renderCustomHeader={({ date, changeYear, changeMonth }) => (
<span className="capitalize">{t("font-size")} :</span> <div className="flex justify-start md:ml-2 ">
<select <select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs" className="bg-transparent outline-none"
value={props.fontSize || clickonWidget.options?.fontSize || 12} value={months[getMonth(date)]}
onChange={(e) => props.setFontSize(parseInt(e.target.value))} onChange={({ target: { value } }) =>
> changeMonth(months.indexOf(value))
{fontsizeArr.map((size, ind) => ( }
<option className="text-[13px]" value={size} key={ind}> >
{size} {months.map((option) => (
</option> <option key={option} value={option}>
))} {option}
</select> </option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) =>
changeYear(value)
}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={getDefaultDate(
selectDate?.date,
selectDate?.format
)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
onChange={(date) => {
setIsToday(false);
const isDateChange = true;
const dateObj = {
date: date,
format: selectDate.format
};
handleGetFormatDate(dateObj, isDateChange);
}}
dateFormat={
selectDate
? selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
}
portalId="root-portal"
/>
<label className=" flex items-center gap-1 cursor-pointer">
<input
checked={isToday}
type="checkbox"
className="op-checkbox op-checkbox-xs"
onClick={() => {
setSelectDate((prev) => ({
...prev,
date: ""
}));
setIsToday(!isToday);
}}
/>
<span className="ml-[2px]">{t("set-today")}</span>
</label>
<span
onClick={() => handleClearDate()}
className="underline text-blue-500 cursor-pointer md:ml-2"
>
{t("clear")}
</span>
</div>
<div className="flex flex-row items-center gap-[10px]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
name="status"
onChange={() =>
setFormdata({
...formdata,
status: data.toLowerCase()
})
}
checked={
formdata.status.toLowerCase() === data.toLowerCase()
}
/>
<div className="text-[13px]">
{t(`widget-status.${data}`)}
</div>
</div>
);
})}
</div>
</>
)}
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2">
<div className="flex flex-row items-center">
<span className="capitalize">{t("font-size")} :</span>
<select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={
props.fontSize || clickonWidget.options?.fontSize || 12
}
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex flex-row gap-1 items-center">
<span className="capitalize">{t("color")} :</span>
<select
value={
props.fontColor ||
clickonWidget.options?.fontColor ||
"black"
}
onChange={(e) => props.setFontColor(e.target.value)}
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
style={{
background:
props.fontColor || props.pos.options?.fontColor || "black"
}}
className="w-5 h-[19px] ml-1"
></span>
</div>
</div> </div>
<div className="flex flex-row gap-1 items-center"> {props?.isPlaceholder && props?.data?.Role !== "prefill" && (
<span className="capitalize">{t("color")} :</span> <label className="flex items-center gap-1 cursor-pointer">
<select <input
value={ name="isReadOnly"
props.fontColor || clickonWidget.options?.fontColor || "black" type="checkbox"
} checked={formdata?.isReadOnly}
onChange={(e) => props.setFontColor(e.target.value)} className="op-checkbox op-checkbox-xs"
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs" onChange={() =>
> setFormdata({
{fontColorArr.map((color, ind) => ( ...formdata,
<option value={color} key={ind}> isReadOnly: !formdata?.isReadOnly
{t(`color-type.${color}`)} })
</option> }
))} />
</select> <span className="capitalize ml-[2px]">{t("read-only")}</span>
<span
style={{
background:
props.fontColor || props.pos.options?.fontColor || "black"
}}
className="w-5 h-[19px] ml-1"
></span>
</div>
</div>
{props?.isPlaceholder && props?.data?.Role !== "prefill" && (
<div className="flex items-center mt-3">
<input
id="isReadOnly"
name="isReadOnly"
type="checkbox"
checked={
isDateReadOnly || props.pos.options?.isReadOnly || false
}
className="op-checkbox op-checkbox-xs"
onChange={() => setIsDateReadOnly(!isDateReadOnly)}
/>
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
>
{t("read-only")}
</label> </label>
</div> )}
)} </div>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div> <div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button <button
type="button" type="button"
className="op-btn op-btn-primary" className="op-btn op-btn-primary"
onClick={() => { onClick={() => {
setIsDateModal(false); saveDateSetting(selectDate);
handleSaveDate(selectDate);
props.setFontColor("");
props.setFontSize("");
}} }}
> >
{t("save")} {t("save")}
@@ -1,4 +1,4 @@
import { useEffect, useState, forwardRef } from "react"; import { useEffect, useMemo, useState, forwardRef } from "react";
import { import {
getMonth, getMonth,
getYear, getYear,
@@ -10,7 +10,8 @@ import {
years, years,
selectCheckbox, selectCheckbox,
checkRegularExpress, checkRegularExpress,
isBase64 isBase64,
getSignerPages
} from "../../constant/Utils"; } from "../../constant/Utils";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
@@ -19,6 +20,7 @@ import { useTranslation } from "react-i18next";
import CellsWidget from "./CellsWidget"; import CellsWidget from "./CellsWidget";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import Loader from "../../primitives/Loader"; import Loader from "../../primitives/Loader";
const textWidgetCls = const textWidgetCls =
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap"; "w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap";
const widgetCls = const widgetCls =
@@ -34,8 +36,9 @@ function PlaceholderType(props) {
((props.isSelfSign || props.isNeedSign) && ((props.isSelfSign || props.isNeedSign) &&
props.data?.signerObjId === props.signerObjId); props.data?.signerObjId === props.signerObjId);
const isReadOnly = const isReadOnly =
props.pos.options?.isReadOnly || props?.data?.Role !== "prefill" &&
props.data?.signerObjId !== props.signerObjId; (props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId);
// prefer the latest response value over any default value // prefer the latest response value over any default value
const widgetData = const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? ""; props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
@@ -64,18 +67,21 @@ function PlaceholderType(props) {
props?.pos?.options?.defaultValue || props?.pos?.options?.defaultValue ||
[] []
); );
} else { }
else {
// keep displayed value in sync with the stored response // keep displayed value in sync with the stored response
setwidgetValue(widgetData); setwidgetValue(widgetData);
} }
if (props.pos?.options?.hint) { if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint); setHint(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) { } else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type, setHint); checkRegularExpress(props.pos?.options?.validation?.type, setHint);
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos]); }, [props.pos, widgetData, type]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => ( const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div <div
style={{ style={{
@@ -83,8 +89,7 @@ function PlaceholderType(props) {
color: fontColor, color: fontColor,
fontFamily: "Arial, sans-serif" fontFamily: "Arial, sans-serif"
}} }}
className={`${isReadOnly ? `select-none` : ``} ${selectWidgetCls} overflow-hidden`} className={`${isReadOnly ? `select-none opacity-25` : ``} ${selectWidgetCls} overflow-hidden`}
disabled={isReadOnly}
onClick={onClick} onClick={onClick}
ref={ref} ref={ref}
> >
@@ -399,11 +404,9 @@ function PlaceholderType(props) {
popperPlacement="top-end" popperPlacement="top-end"
customInput={<ExampleCustomInput />} customInput={<ExampleCustomInput />}
dateFormat={ dateFormat={
props.selectDate props?.selectDate?.format ||
? props.selectDate?.format props.pos?.options?.validation?.format ||
: props.pos?.options?.validation?.format "MM/dd/yyyy"
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
} }
/> />
) : ( ) : (
@@ -412,11 +415,9 @@ function PlaceholderType(props) {
className="select-none-cls overflow-hidden" className="select-none-cls overflow-hidden"
> >
<span> <span>
{props.selectDate {props?.selectDate?.format ||
? props.selectDate?.format props.pos?.options?.validation?.format ||
: props.pos?.options?.validation?.format "MM/dd/yyyy"}
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"}
</span> </span>
</div> </div>
); );
@@ -28,6 +28,8 @@ import {
} from "../../redux/reducers/widgetSlice"; } from "../../redux/reducers/widgetSlice";
import * as utils from "../../utils"; import * as utils from "../../utils";
const widgetTitle = "font-medium";
const ShowTextWidget = ({ position, handleWidgetDetails }) => { const ShowTextWidget = ({ position, handleWidgetDetails }) => {
const inputRef = useRef(null); const inputRef = useRef(null);
const [inputValue, setInputValue] = useState(position.options.response || ""); const [inputValue, setInputValue] = useState(position.options.response || "");
@@ -46,14 +48,77 @@ const ShowTextWidget = ({ position, handleWidgetDetails }) => {
); );
}; };
const ImageComponent = (props) => {
const { t } = useTranslation();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const imageRefs = useRef({});
let imgUrl = "";
const isBase64Url = isBase64(props?.position?.SignUrl);
if (isBase64Url) {
imgUrl = props?.position?.SignUrl;
} else {
const getPrefillImg = prefillImg?.find(
(x) => x.id === props?.position?.key
);
imgUrl = getPrefillImg?.base64;
}
return (
<>
<span className={widgetTitle}>{props?.position.options?.name}</span>
{imgUrl ? (
<>
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
<img
alt="print img"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamicallys
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
onLoad={() => props?.handleImageLoaded?.(props?.position.key)}
onError={() => props?.handleImageLoaded?.(props?.position.key)}
/>
</div>
<span
onClick={() => props?.handleClearImage(props?.position)}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</>
) : (
<div
className="cursor-pointer op-card border-[1px] op-border-hover flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center"
onClick={() => imageRefs.current[props?.position.key]?.click()}
>
{props?.imageLoaders[props?.position?.key] && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
<Loader />
</div>
)}
<input
type="file"
onChange={(e) => props?.onImageChange?.(e, props?.position)}
className="filetype"
accept="image/png,image/jpeg"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamically
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
)}
</>
);
};
function PrefillWidgetModal(props) { function PrefillWidgetModal(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
// Track already loaded image keys so they don't increment multiple times // Track already loaded image keys so they don't increment multiple times
const loadedSet = useRef(new Set()); const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates const initializedRef = useRef(false); // prevent rerun on state updates
const prefillImg = useSelector((state) => state.widget.prefillImg);
const [uniqueWidget, setUniqueWidget] = useState([]);
const [image, setImage] = useState(null); const [image, setImage] = useState(null);
const [imageLoaders, setImageLoaders] = useState({}); const [imageLoaders, setImageLoaders] = useState({});
const [currentWidget, setCurrentWidget] = useState(""); const [currentWidget, setCurrentWidget] = useState("");
@@ -62,13 +127,12 @@ function PrefillWidgetModal(props) {
const [loadedImages, setLoadedImages] = useState(0); const [loadedImages, setLoadedImages] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const years = range(1950, getYear(new Date()) + 16, 1); const years = range(1950, getYear(new Date()) + 16, 1);
const widgetTitle = "font-medium";
const isAnyLoaderActive = Object.values(imageLoaders).some( const isAnyLoaderActive = Object.values(imageLoaders).some(
(val) => val === true (val) => val === true
); );
// useMemo to memoize the calculation of unique widgets // useMemo to memoize the calculation of unique widgets
const memoizedUniqueWidget = useMemo(() => { const uniqueWidget = useMemo(() => {
//functions to used remove duplicate name values across all pages //functions to used remove duplicate name values across all pages
if (!props.prefillData) return []; if (!props.prefillData) return [];
//This will help us track which name values have already been encountered across all pages. //This will help us track which name values have already been encountered across all pages.
@@ -94,6 +158,7 @@ function PrefillWidgetModal(props) {
return flatArray || []; return flatArray || [];
}, [props.prefillData]); }, [props.prefillData]);
useEffect(() => { useEffect(() => {
dispatch(resetWidgetState([])); dispatch(resetWidgetState([]));
}, []); }, []);
@@ -119,10 +184,6 @@ function PrefillWidgetModal(props) {
savePrefillImg(); savePrefillImg();
}, [props.xyPosition]); }, [props.xyPosition]);
useEffect(() => {
setUniqueWidget(memoizedUniqueWidget);
}, [memoizedUniqueWidget]);
useEffect(() => { useEffect(() => {
if (totalImages > 0 && loadedImages === totalImages) { if (totalImages > 0 && loadedImages === totalImages) {
setLoading(false); setLoading(false);
@@ -168,67 +229,7 @@ function PrefillWidgetModal(props) {
} }
return date; return date;
}; };
const ImageComponent = (props) => {
const imageRefs = useRef([]);
let imgUrl = "";
const isBase64Url = isBase64(props?.position?.SignUrl);
if (isBase64Url) {
imgUrl = props?.position?.SignUrl;
} else {
const getPrefillImg = prefillImg?.find(
(x) => x.id === props?.position?.key
);
imgUrl = getPrefillImg?.base64;
}
return (
<>
<span className={widgetTitle}>{props?.position.options?.name}</span>
{imgUrl ? (
<>
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
<img
alt="print img"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamicallys
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
onLoad={() => handleImageLoaded?.(props?.position.key)}
onError={() => handleImageLoaded?.(props?.position.key)}
/>
</div>
<span
onClick={() => handleClearImage(props?.position)}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</>
) : (
<div
className="cursor-pointer op-card border-[1px] op-border-hover flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center"
onClick={() => imageRefs.current[props?.position.key]?.click()}
>
{imageLoaders[props?.position?.key] && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
<Loader />
</div>
)}
<input
type="file"
onChange={(e) => onImageChange(e, props?.position)}
className="filetype"
accept="image/png,image/jpeg"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamically
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
)}
</>
);
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => ( const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div <div
style={{ fontFamily: "Arial, sans-serif" }} style={{ fontFamily: "Arial, sans-serif" }}
@@ -278,7 +279,6 @@ function PrefillWidgetModal(props) {
image.src, image.src,
image.imgType image.imgType
); );
setImageLoaders({});
if (imageUrl) { if (imageUrl) {
return imageUrl; return imageUrl;
} }
@@ -419,6 +419,7 @@ function PrefillWidgetModal(props) {
// (tracks how many images have finished loading) // (tracks how many images have finished loading)
setLoadedImages((prev) => prev + 1); setLoadedImages((prev) => prev + 1);
} }
setImageLoaders({});
}; };
const handleWidgetType = (position) => { const handleWidgetType = (position) => {
switch (position?.type) { switch (position?.type) {
@@ -437,18 +438,16 @@ function PrefillWidgetModal(props) {
className="mt-[2px] op-checkbox op-checkbox-xs" className="mt-[2px] op-checkbox op-checkbox-xs"
type="checkbox" type="checkbox"
checked={selectCheckbox(ind, position)} checked={selectCheckbox(ind, position)}
onChange={(e) => { onChange={(e) =>
handleCheckboxValue(e.target.checked, ind, position); handleCheckboxValue(e.target.checked, ind, position)
}} }
/> />
{!position.options?.isHideLabel && ( <label
<label htmlFor={`checkbox-${position.key + ind}`}
htmlFor={`checkbox-${position.key + ind}`} className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
className="text-xs mb-0 text-center ml-[3px] cursor-pointer" >
> {data}
{data} </label>
</label>
)}
</div> </div>
))} ))}
</div> </div>
@@ -474,9 +473,7 @@ function PrefillWidgetModal(props) {
value={ value={
position?.options?.response || position?.options?.defaultValue position?.options?.response || position?.options?.defaultValue
} }
onChange={(e) => { onChange={(e) => handleWidgetDetails(position, e.target.value)}
handleWidgetDetails(position, e.target.value);
}}
> >
{/* Default/Title option */} {/* Default/Title option */}
<option value="" disabled hidden> <option value="" disabled hidden>
@@ -526,9 +523,7 @@ function PrefillWidgetModal(props) {
)} )}
closeOnScroll={true} closeOnScroll={true}
selected={handleDate(position)} selected={handleDate(position)}
onChange={(date) => { onChange={(date) => handleOnDateChange(date, position)}
handleOnDateChange(date, position);
}}
customInput={<ExampleCustomInput />} customInput={<ExampleCustomInput />}
dateFormat={position?.options?.validation?.format || "MM/dd/yyyy"} dateFormat={position?.options?.validation?.format || "MM/dd/yyyy"}
/> />
@@ -538,7 +533,10 @@ function PrefillWidgetModal(props) {
return ( return (
<ImageComponent <ImageComponent
position={position} position={position}
docId={props?.docId} imageLoaders={imageLoaders}
onImageChange={onImageChange}
handleImageLoaded={handleImageLoaded}
handleClearImage={handleClearImage}
/> />
); );
case radioButtonWidget: case radioButtonWidget:
@@ -556,18 +554,14 @@ function PrefillWidgetModal(props) {
className="mt-[2px] op-radio op-radio-xs" className="mt-[2px] op-radio op-radio-xs"
type="radio" type="radio"
checked={handleRadioCheck(data, position)} checked={handleRadioCheck(data, position)}
onChange={() => { onChange={() => handleWidgetDetails(position, data)}
handleWidgetDetails(position, data);
}}
/> />
{!position.options?.isHideLabel && ( <label
<label htmlFor={`radio-${position.key + ind}`}
htmlFor={`radio-${position.key + ind}`} className="text-xs mb-0 ml-[2px] cursor-pointer"
className="text-xs mb-0 ml-[2px] cursor-pointer" >
> {data}
{data} </label>
</label>
)}
</div> </div>
))} ))}
</div> </div>
@@ -591,8 +585,8 @@ function PrefillWidgetModal(props) {
} }
}; };
const handleEmbedPrefill = (item) => { const handleEmbedPrefill = async (item) => {
props.handleCreateDocument(); await props.handleCreateDocument();
}; };
//`loadOptions` function to use show all list of signer in dropdown //`loadOptions` function to use show all list of signer in dropdown
const loadOptions = async (inputValue) => { const loadOptions = async (inputValue) => {
@@ -654,7 +648,7 @@ function PrefillWidgetModal(props) {
handleClose={props.handleClosePrefillModal} handleClose={props.handleClosePrefillModal}
> >
<div className="relative"> <div className="relative">
{loading && ( {(props?.isSubmit || loading) && (
<div className="absolute inset-0 flex items-center justify-center bg-white/70 z-[9999]"> <div className="absolute inset-0 flex items-center justify-center bg-white/70 z-[9999]">
<Loader /> <Loader />
</div> </div>
@@ -706,9 +700,9 @@ function PrefillWidgetModal(props) {
multiValueLabel: () => "mb-[2px]", multiValueLabel: () => "mb-[2px]",
menu: () => menu: () =>
"mt-1 shadow-md rounded-lg bg-base-200 text-base-content absolute z-9999", "mt-1 shadow-md rounded-lg bg-base-200 text-base-content absolute z-9999",
menuList: () => "shadow-md rounded-lg ", menuList: () => "shadow-md rounded-lg",
option: () => option: () =>
"bg-base-200 text-base-content rounded-lg m-1 hover:bg-base-300 p-2 ", "bg-base-200 text-base-content rounded-lg m-1 hover:bg-base-300 p-2",
noOptionsMessage: () => noOptionsMessage: () =>
"p-2 bg-base-200 rounded-lg m-1 p-2" "p-2 bg-base-200 rounded-lg m-1 p-2"
}} }}
@@ -719,7 +713,7 @@ function PrefillWidgetModal(props) {
</div> </div>
<button <button
onClick={(e) => handleCreateNew(e, field.value)} onClick={(e) => handleCreateNew(e, field.value)}
className="op-btn op-btn-accent op-btn-outline op-btn-sm " className="op-btn op-btn-accent op-btn-outline op-btn-sm"
> >
<i className="fa-light fa-plus"></i> <i className="fa-light fa-plus"></i>
</button> </button>
@@ -733,7 +727,7 @@ function PrefillWidgetModal(props) {
</div> </div>
<div className="flex gap-2 mx-4 mb-3"> <div className="flex gap-2 mx-4 mb-3">
<button <button
disabled={isAnyLoaderActive} disabled={isAnyLoaderActive || props?.isSubmit}
className="op-btn op-btn-primary op-btn-sm w-[80px]" className="op-btn op-btn-primary op-btn-sm w-[80px]"
onClick={() => handleEmbedPrefill(props?.item)} onClick={() => handleEmbedPrefill(props?.item)}
> >
@@ -85,9 +85,12 @@ function RenderAllPdfPage(props) {
alert(t("only-pdf-allowed")); alert(t("only-pdf-allowed"));
return; return;
} }
const mb = Math.round(file?.size / Math.pow(1024, 2)); const fileSize =
if (mb > maxFileSize) { maxFileSize;
alert(`${t("file-alert-1")} ${maxFileSize} MB`); const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e); removeFile(e);
return; return;
} }
@@ -144,6 +147,13 @@ function RenderAllPdfPage(props) {
useObjectStreams: false useObjectStreams: false
}); });
const pdfBuffer = base64ToArrayBuffer(pdfBase64); const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer); props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64); props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true); props.setIsUploadPdf && props.setIsUploadPdf(true);
@@ -271,6 +271,7 @@ function RenderPdf(props) {
setIsReqSignTourDisabled={props.setIsReqSignTourDisabled} setIsReqSignTourDisabled={props.setIsReqSignTourDisabled}
calculateFontsize={calculateFontsize} calculateFontsize={calculateFontsize}
currWidgetsDetails={props?.currWidgetsDetails} currWidgetsDetails={props?.currWidgetsDetails}
handleNameModal={props?.handleNameModal}
/> />
</React.Fragment> </React.Fragment>
) )
@@ -484,6 +485,8 @@ function RenderPdf(props) {
setIsReqSignTourDisabled={ setIsReqSignTourDisabled={
props.setIsReqSignTourDisabled props.setIsReqSignTourDisabled
} }
unSignedWidgetId={props.unSignedWidgetId}
handleNameModal={props.handleNameModal}
/> />
) )
)} )}
@@ -91,14 +91,14 @@ function WidgetComponent(props) {
name, name,
jobTitle, jobTitle,
company, company,
email,
date, date,
text, text,
cells, cells,
checkbox, checkbox,
dropdown, dropdown,
radioButton, radioButton,
image, image
email
]; ];
const getWidgetArray = widgets; const getWidgetArray = widgets;
const newUpdateSigner = getWidgetArray.map((obj, ind) => { const newUpdateSigner = getWidgetArray.map((obj, ind) => {
@@ -109,7 +109,7 @@ function WidgetComponent(props) {
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, []);
//allow only (signature,stamp,initials,text,name, job title, company,email) widget when isAllowModification true and user have session token // allow only (signature, stamp, initials, text, name, job title, company, email, cells) widget when isAllowModification true and user have session token
const modifiedWidgets = widget.filter( const modifiedWidgets = widget.filter(
(data) => (data) =>
![ ![
@@ -121,7 +121,7 @@ function WidgetComponent(props) {
"checkbox" "checkbox"
].includes(data.type) ].includes(data.type)
); );
//allow only (signature,stamp,initials,text) widget when isAllowModification true and user does not have session token // allow only (signature, stamp, initials, text, cells) widget when isAllowModification true and user does not have session token
const unlogedInUserWidgets = widget.filter( const unlogedInUserWidgets = widget.filter(
(data) => (data) =>
![ ![
@@ -263,7 +263,12 @@ function WidgetComponent(props) {
)} )}
</span> </span>
</div> </div>
<div className="p-[15px] flex flex-col pt-4" data-tut="addWidgets"> <div
className="p-[12px] grid lg:grid-cols-2 gap-x-2 lg:gap-y-1.5 pt-3"
data-tut="addWidgets"
role="list"
aria-label="Add widgets"
>
<WidgetList <WidgetList
updateWidgets={handleWidgetType} updateWidgets={handleWidgetType}
handleDivClick={props.handleDivClick} handleDivClick={props.handleDivClick}
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import "../../styles/AddUser.css"; import "../../styles/AddUser.css";
import RegexParser from "regex-parser"; import RegexParser from "regex-parser";
@@ -21,11 +21,30 @@ const WidgetNameModal = (props) => {
hint: "", hint: "",
textvalidate: "", textvalidate: "",
isReadOnly: false, isReadOnly: false,
cellCount: 5 cellCount: 5,
}); });
const [isValid, setIsValid] = useState(true); const [isValid, setIsValid] = useState(true);
const statusArr = ["Required", "Optional"]; const statusArr = ["Required", "Optional"];
const [signatureType, setSignatureType] = useState([]); const [signatureType, setSignatureType] = useState([]);
const type = props?.defaultdata?.type;
const isCellWidget = useMemo(() => type === cellsWidget, [type]);
const isSignOrInitials = useMemo(
() => ["signature", "initials"].includes(type),
[type]
);
const showFontControls = useMemo(
() =>
[
textInputWidget,
textWidget,
cellsWidget,
"name",
"company",
"job title",
"email"
].includes(props.defaultdata?.type),
[type]
);
const handleHint = () => { const handleHint = () => {
const type = props.defaultdata?.type; const type = props.defaultdata?.type;
@@ -54,13 +73,13 @@ const WidgetNameModal = (props) => {
? props.defaultdata?.options?.validation?.pattern ? props.defaultdata?.options?.validation?.pattern
: props.defaultdata?.options?.validation?.type || "", : props.defaultdata?.options?.validation?.type || "",
isReadOnly: props.defaultdata?.options?.isReadOnly || false, isReadOnly: props.defaultdata?.options?.isReadOnly || false,
cellCount: props.defaultdata?.options?.cellCount || 5 cellCount: props.defaultdata?.options?.cellCount || 5,
}); });
} else { } else {
setFormdata({ setFormdata({
...formdata, ...formdata,
name: props.defaultdata?.options?.name || "", name: props.defaultdata?.options?.name || "",
cellCount: props.defaultdata?.options?.cellCount || 5 cellCount: props.defaultdata?.options?.cellCount || 5,
}); });
} }
@@ -68,12 +87,13 @@ const WidgetNameModal = (props) => {
const defaultSignatureType = signTypes || []; const defaultSignatureType = signTypes || [];
setSignatureType(defaultSignatureType); setSignatureType(defaultSignatureType);
} }
// eslint-disable-next-line // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.defaultdata]); }, [props.defaultdata]);
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (props.handleData) { if (props.handleData) {
if (["signature", "initials"].includes(props.defaultdata?.type)) { if (isSignOrInitials) {
const enabledSignTypes = signatureType?.filter((x) => x.enabled); const enabledSignTypes = signatureType?.filter((x) => x.enabled);
const isDefaultSignTypeOnly = const isDefaultSignTypeOnly =
enabledSignTypes?.length === 1 && enabledSignTypes?.length === 1 &&
@@ -87,9 +107,11 @@ const WidgetNameModal = (props) => {
props.handleData(data, props.defaultdata?.type); props.handleData(data, props.defaultdata?.type);
} }
} else { } else {
const isTextInput = [textInputWidget, cellsWidget].includes(
props.defaultdata?.type const isTextInput = [
); textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type);
const { isReadOnly, defaultValue, status } = formdata; const { isReadOnly, defaultValue, status } = formdata;
// If its a textinput widget, enforce that read-only fields have // If its a textinput widget, enforce that read-only fields have
// either a defaultValue or an "optional" status. // either a defaultValue or an "optional" status.
@@ -110,7 +132,7 @@ const WidgetNameModal = (props) => {
status: "required", status: "required",
hint: "", hint: "",
textvalidate: "", textvalidate: "",
cellCount: 5 cellCount: 5,
}); });
setSignatureType(signTypes); setSignatureType(signTypes);
} }
@@ -148,10 +170,11 @@ const WidgetNameModal = (props) => {
} else { } else {
setIsValid(true); setIsValid(true);
} }
const val =
props.defaultdata?.type === cellsWidget const val = isCellWidget
? e.target.value.slice(0, formdata.cellCount) ? e.target.value.slice(0, formdata.cellCount)
: e.target.value; : e.target.value;
setFormdata({ ...formdata, [e.target.name]: val }); setFormdata({ ...formdata, [e.target.name]: val });
}; };
@@ -160,7 +183,7 @@ const WidgetNameModal = (props) => {
case "email": case "email":
return "/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/"; return "/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/";
case "number": case "number":
return "/^\\d+$/"; return "^\\d+(?:\\.\\d+)?$"; // "/^\\d+$/";
case "text": case "text":
//allow space in text regex //allow space in text regex
return "/^[a-zA-Z ]+$/"; return "/^[a-zA-Z ]+$/";
@@ -184,23 +207,19 @@ const WidgetNameModal = (props) => {
<ModalUi <ModalUi
isOpen={props.isOpen} isOpen={props.isOpen}
handleClose={props.handleClose && props.handleClose} handleClose={props.handleClose && props.handleClose}
title={ title={isSignOrInitials ? t("signature-setting") : t("widget-info")}
["signature", "initials"].includes(props.defaultdata?.type)
? t("signature-setting")
: t("widget-info")
}
> >
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={`${ className={`${
[textInputWidget, cellsWidget].includes(props.defaultdata?.type) [textInputWidget, cellsWidget].includes(props.defaultdata?.type)
? "pt-0" ? "pt-0"
: ["signature", "initials"].includes(props.defaultdata?.type) : isSignOrInitials
? "pt-2" ? "pt-2"
: "" : ""
} p-[20px] text-base-content`} } p-[20px] text-base-content`}
> >
{!["signature", "initials"].includes(props.defaultdata?.type) && ( {!isSignOrInitials && (
<div className="mb-[0.75rem] text-[13px]"> <div className="mb-[0.75rem] text-[13px]">
<label htmlFor="name"> <label htmlFor="name">
{t("name")} {t("name")}
@@ -217,7 +236,7 @@ const WidgetNameModal = (props) => {
/> />
</div> </div>
)} )}
{props.defaultdata?.type === cellsWidget && ( {isCellWidget && (
<div className="mb-[0.75rem] text-[13px]"> <div className="mb-[0.75rem] text-[13px]">
<label htmlFor="cellCount">{t("cell-count")}</label> <label htmlFor="cellCount">{t("cell-count")}</label>
<input <input
@@ -231,7 +250,10 @@ const WidgetNameModal = (props) => {
/> />
</div> </div>
)} )}
{[textInputWidget, cellsWidget].includes(props.defaultdata?.type) && {[
textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type) &&
props?.roleName !== "prefill" && ( props?.roleName !== "prefill" && (
<> <>
<div className="mb-[0.75rem]"> <div className="mb-[0.75rem]">
@@ -244,11 +266,7 @@ const WidgetNameModal = (props) => {
value={formdata.defaultValue} value={formdata.defaultValue}
onChange={(e) => handledefaultChange(e)} onChange={(e) => handledefaultChange(e)}
autoComplete="off" autoComplete="off"
maxLength={ maxLength={isCellWidget ? formdata.cellCount : undefined}
props.defaultdata?.type === cellsWidget
? formdata.cellCount
: undefined
}
onBlur={() => { onBlur={() => {
if (isValid === false) { if (isValid === false) {
setFormdata({ ...formdata, defaultValue: "" }); setFormdata({ ...formdata, defaultValue: "" });
@@ -262,7 +280,7 @@ const WidgetNameModal = (props) => {
style={{ fontSize: 12 }} style={{ fontSize: 12 }}
> >
<i <i
className="fa-light fa-exclamation-circle text-[15px]" className="fa-light fa-exclamation-circle text-[15px] mr-1"
style={{ color: "#fab005" }} style={{ color: "#fab005" }}
></i> ></i>
{t("invalid-default-value")} {t("invalid-default-value")}
@@ -271,7 +289,8 @@ const WidgetNameModal = (props) => {
</div> </div>
</> </>
)} )}
{!["signature", "initials"].includes(props.defaultdata?.type) && {!props?.isSelfSign &&
!isSignOrInitials &&
props?.roleName !== "prefill" && ( props?.roleName !== "prefill" && (
<div className="mb-[0.75rem]"> <div className="mb-[0.75rem]">
<div className="flex flex-row gap-[10px] mb-[0.5rem]"> <div className="flex flex-row gap-[10px] mb-[0.5rem]">
@@ -302,9 +321,10 @@ const WidgetNameModal = (props) => {
); );
})} })}
</div> </div>
{[textInputWidget, cellsWidget].includes( {[
props.defaultdata?.type textInputWidget,
) && cellsWidget,
].includes(props.defaultdata?.type) &&
props?.roleName !== "prefill" && ( props?.roleName !== "prefill" && (
<div className="flex items-center"> <div className="flex items-center">
<input <input
@@ -330,7 +350,7 @@ const WidgetNameModal = (props) => {
)} )}
</div> </div>
)} )}
{["signature", "initials"].includes(props.defaultdata?.type) && ( {isSignOrInitials && (
<div className="mb-[0.75rem]"> <div className="mb-[0.75rem]">
<label htmlFor="signaturetype" className="text-[14px] mb-[0.7rem]"> <label htmlFor="signaturetype" className="text-[14px] mb-[0.7rem]">
{t("allowed-signature-types")} {t("allowed-signature-types")}
@@ -358,7 +378,7 @@ const WidgetNameModal = (props) => {
</div> </div>
</div> </div>
)} )}
{props?.roleName !== "prefill" && ( {!props?.isSelfSign && props?.roleName !== "prefill" && (
<div className="mb-[0.75rem]"> <div className="mb-[0.75rem]">
<label htmlFor="hint" className="text-[13px]"> <label htmlFor="hint" className="text-[13px]">
{t("hint")} {t("hint")}
@@ -372,15 +392,7 @@ const WidgetNameModal = (props) => {
/> />
</div> </div>
)} )}
{[ {showFontControls && (
textInputWidget,
textWidget,
cellsWidget,
"name",
"company",
"job title",
"email"
].includes(props.defaultdata?.type) && (
<div className="flex flex-col md:flex-row md:items-center gap-3 mb-3"> <div className="flex flex-col md:flex-row md:items-center gap-3 mb-3">
<div className="flex items-center gap-2 "> <div className="flex items-center gap-2 ">
<span className="whitespace-nowrap">{t("font-size")}: </span> <span className="whitespace-nowrap">{t("font-size")}: </span>
@@ -1,4 +1,4 @@
import { forwardRef, useEffect, useRef, useState } from "react"; import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { removeBackground } from "@imgly/background-removal"; import { removeBackground } from "@imgly/background-removal";
@@ -21,7 +21,10 @@ import {
convertTextToImg, convertTextToImg,
convertJpegToPng, convertJpegToPng,
convertBase64ToFile, convertBase64ToFile,
generatePdfName generatePdfName,
getDefaultDate,
getDefaultFormat,
getSignerPages
} from "../../constant/Utils"; } from "../../constant/Utils";
import CellsWidget from "./CellsWidget"; import CellsWidget from "./CellsWidget";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
@@ -36,30 +39,17 @@ import {
setLastIndex, setLastIndex,
setScrollTriggerId, setScrollTriggerId,
setPrefillImg, setPrefillImg,
setPrefillImgLoad setPrefillImgLoad,
setTypedSignFont
} from "../../redux/reducers/widgetSlice"; } from "../../redux/reducers/widgetSlice";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import Loader from "../../primitives/Loader"; import Loader from "../../primitives/Loader";
import { emailRegex } from "../../constant/const"; import { emailRegex } from "../../constant/const";
import RegexParser from "regex-parser"; import RegexParser from "regex-parser";
import { saveToMySign } from "../../utils/widgetUtils"; import {
saveToMySign
} from "../../utils/widgetUtils";
//function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
//function to convert formatted date to new Date() format
const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formatted date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
if (parsedDate.isValid()) {
date = new Date(parsedDate.toISOString());
return date;
} else {
date = new Date();
return date;
}
};
const fontOptions = [ const fontOptions = [
{ value: "Fasthand" }, { value: "Fasthand" },
{ value: "Dancing Script" }, { value: "Dancing Script" },
@@ -79,6 +69,7 @@ function WidgetsValueModal(props) {
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg); const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
const myInitial = useSelector((state) => state.widget.myInitial); const myInitial = useSelector((state) => state.widget.myInitial);
const lastWidget = useSelector((state) => state.widget.lastIndex); const lastWidget = useSelector((state) => state.widget.lastIndex);
const typedSignFont = useSelector((state) => state.widget.typedSignFont);
const { t } = useTranslation(); const { t } = useTranslation();
const canvasRef = useRef(null); const canvasRef = useRef(null);
const imageRef = useRef(null); const imageRef = useRef(null);
@@ -123,12 +114,28 @@ function WidgetsValueModal(props) {
props?.kiosk_signer || JSON.parse(localStorage.getItem("kiosk_signer")); props?.kiosk_signer || JSON.parse(localStorage.getItem("kiosk_signer"));
const jsonSender = senderUser && JSON.parse(senderUser); const jsonSender = senderUser && JSON.parse(senderUser);
const currentUserName = jsonSender && jsonSender?.name; const currentUserName = jsonSender && jsonSender?.name;
const type = currWidgetsDetails?.type;
const widgetTypeTranslation = t(`widgets-name.${currWidgetsDetails?.type}`); const widgetTypeTranslation = t(`widgets-name.${currWidgetsDetails?.type}`);
const [widgetValue, setWidgetValue] = useState(
currWidgetsDetails.type !== "checkbox" && const [widgetValue, setWidgetValue] = useState(() => {
(currWidgetsDetails?.options?.response || if (currWidgetsDetails.type === "checkbox") {
currWidgetsDetails?.options?.defaultValue) return undefined;
); }
return (
currWidgetsDetails?.options?.response ||
currWidgetsDetails?.options?.defaultValue
);
});
const blocked = [
"checkbox",
"dropdown",
"date",
"radio",
];
const showClearbtn =
!blocked.includes(type);
const [cellsValue, setCellsValue] = useState(() => { const [cellsValue, setCellsValue] = useState(() => {
const count = currWidgetsDetails?.options?.cellCount || 5; const count = currWidgetsDetails?.options?.cellCount || 5;
const val = const val =
@@ -173,7 +180,7 @@ function WidgetsValueModal(props) {
currWidgetsDetails?.options?.response, currWidgetsDetails?.options?.response,
currWidgetsDetails?.options?.validation?.format currWidgetsDetails?.options?.validation?.format
) )
: new Date() : ""
); );
useEffect(() => { useEffect(() => {
dispatch(setScrollTriggerId(currWidgetsDetails?.key)); dispatch(setScrollTriggerId(currWidgetsDetails?.key));
@@ -204,7 +211,8 @@ function WidgetsValueModal(props) {
} else { } else {
setHint(currWidgetsDetails?.type); setHint(currWidgetsDetails?.type);
} }
} else if (isSignOrInitials) { }
else if (isSignOrInitials) {
setIsTab(currWidgetsDetails?.signatureType); setIsTab(currWidgetsDetails?.signatureType);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -254,9 +262,7 @@ function WidgetsValueModal(props) {
setXyPosition, setXyPosition,
uniqueId, uniqueId,
false, false,
data?.format, data?.format
currWidgetsDetails?.options?.fontSize || 12,
currWidgetsDetails?.options?.fontColor || "black"
); );
setSelectDate({ date: date, format: data?.format }); setSelectDate({ date: date, format: data?.format });
}; };
@@ -278,10 +284,7 @@ function WidgetsValueModal(props) {
setIsLoader(true); setIsLoader(true);
try { try {
dispatch( dispatch(
setPrefillImg({ setPrefillImg({ id: currWidgetsDetails?.key, base64: image?.src })
id: currWidgetsDetails?.key,
base64: image?.src
})
); );
const imageName = generatePdfName(16); const imageName = generatePdfName(16);
const imageUrl = await convertBase64ToFile( const imageUrl = await convertBase64ToFile(
@@ -630,11 +633,12 @@ function WidgetsValueModal(props) {
useEffect(() => { useEffect(() => {
const loadFont = async () => { const loadFont = async () => {
try { try {
await document.fonts.load(`20px ${fontSelect}`); const selectedFont = typedSignFont || fontSelect;
const selectFontSTyle = fontOptions.find( await document.fonts.load(`20px ${selectedFont}`);
(font) => font.value === fontSelect const selectFontStyle = fontOptions.find(
(font) => font.value === selectedFont
); );
setFontSelect(selectFontSTyle?.value || fontOptions[0].value); setFontSelect(selectFontStyle?.value || fontOptions[0].value);
} catch (error) { } catch (error) {
console.error("Error loading font:", error); console.error("Error loading font:", error);
} }
@@ -671,18 +675,21 @@ function WidgetsValueModal(props) {
} }
} }
if (isTab === "type") { if (isTab === "type") {
const signerName = localStorage.getItem("signer")
? JSON.parse(localStorage.getItem("signer"))?.Name
: currentUserName;
//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 const trimmedName = typedSignature
? typedSignature?.trim() ? typedSignature?.trim()
: props?.journey === "kiosk-signing" && kiosk_signer : props?.journey === "kiosk-signing" && kiosk_signer
? kiosk_signer[0]?.Name?.trim() ? kiosk_signer[0]?.Name?.trim()
: currentUserName?.trim(); : signerName?.trim();
//get full name of user //get full name of user
const fullUserName = const fullUserName =
typedSignature || typedSignature ||
(props?.journey === "kiosk-signing" && kiosk_signer (props?.journey === "kiosk-signing" && kiosk_signer
? kiosk_signer[0]?.Name ? kiosk_signer[0]?.Name
: currentUserName); : signerName);
const firstCharacter = trimmedName?.charAt(0); const firstCharacter = trimmedName?.charAt(0);
const userName = const userName =
currWidgetsDetails?.type === "initials" ? firstCharacter : fullUserName; currWidgetsDetails?.type === "initials" ? firstCharacter : fullUserName;
@@ -697,12 +704,13 @@ function WidgetsValueModal(props) {
const maxWidth = currWidgetsDetails?.Width; const maxWidth = currWidgetsDetails?.Width;
const maxHeight = currWidgetsDetails?.Height; const maxHeight = currWidgetsDetails?.Height;
const widgetDims = { Width: maxWidth, Height: maxHeight }; const widgetDims = { Width: maxWidth, Height: maxHeight };
const fontFamily = fontStyle || fontSelect || "Fasthand"; const fontFamily = fontStyle || typedSignFont || fontSelect || "Fasthand";
const fillColor = color || penColor; const fillColor = color || penColor;
const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims); const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims);
setSignature(dataUrl); setSignature(dataUrl);
setTextWidth(maxWidth); setTextWidth(maxWidth);
setTextHeight(maxHeight); setTextHeight(maxHeight);
dispatch(setTypedSignFont(fontFamily));
}; };
const PenColorComponent = () => { const PenColorComponent = () => {
return ( return (
@@ -876,21 +884,23 @@ function WidgetsValueModal(props) {
onClick={onClick} onClick={onClick}
ref={ref} ref={ref}
> >
{value} {value ? value : "Select date"}
<i className="fa-light fa-calendar ml-[5px]"></i> <i className="fa-light fa-calendar ml-[5px]"></i>
</div> </div>
)); ));
ExampleCustomInput.displayName = "ExampleCustomInput"; ExampleCustomInput.displayName = "ExampleCustomInput";
//function to set onchange date //function to set onchange date
const handleOnDateChange = (date) => { const handleOnDateChange = (date) => {
setWidgetValue(date);
setStartDate(date); setStartDate(date);
}; };
const handleOnchangeTextBox = (e) => { const handleOnchangeTextBox = (e) => {
// hide any prior validation error while typing // hide any prior validation error while typing
setIsShowValidation(false); setIsShowValidation(false);
setWidgetValue(e.target.value); let value = e.target.value;
setWidgetValue(value);
onChangeInput( onChangeInput(
e.target.value, value,
currWidgetsDetails, currWidgetsDetails,
xyPosition, xyPosition,
props.index, props.index,
@@ -1200,8 +1210,8 @@ function WidgetsValueModal(props) {
) )
) : isTab === "type" ? ( ) : isTab === "type" ? (
<> <>
<div className="flex justify-between items-center tabWidth"> <div className="flex justify-between items-center tabWidth rounded-[4px]">
<span className="mr-[5px] text-[12px]"> <span className="ml-[5px] text-[12px] text-base-content">
{currWidgetsDetails?.type === "initials" {currWidgetsDetails?.type === "initials"
? t("initial-teb") ? t("initial-teb")
: t("signature-tab")} : t("signature-tab")}
@@ -1228,7 +1238,7 @@ function WidgetsValueModal(props) {
}} }}
/> />
</div> </div>
<div className="border-[1px] border-[#d6d3d3] mt-[10px] ml-[5px]"> <div className="border-[1px] border-[#d6d3d3] mt-[10px] rounded-[4px]">
{fontOptions.map((font, ind) => { {fontOptions.map((font, ind) => {
return ( return (
<div <div
@@ -1251,7 +1261,7 @@ function WidgetsValueModal(props) {
> >
{typedSignature {typedSignature
? typedSignature ? typedSignature
: "Your signature"} : t("Your-Signature")}
</div> </div>
</div> </div>
); );
@@ -1466,52 +1476,59 @@ function WidgetsValueModal(props) {
); );
case "date": case "date":
return ( return (
<div <div className="flex flex-col">
className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`} <div
> className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`}
<DatePicker >
renderCustomHeader={({ date, changeYear, changeMonth }) => ( <DatePicker
<div className="flex justify-start ml-2 "> renderCustomHeader={({ date, changeYear, changeMonth }) => (
<select <div className="flex justify-start ml-2 ">
className="bg-transparent outline-none" <select
value={months[getMonth(date)]} className="bg-transparent outline-none"
onChange={({ target: { value } }) => value={months[getMonth(date)]}
changeMonth(months.indexOf(value)) onChange={({ target: { value } }) =>
} changeMonth(months.indexOf(value))
> }
{months.map((option) => ( >
<option key={option} value={option}> {months.map((option) => (
{option} <option key={option} value={option}>
</option> {option}
))} </option>
</select> ))}
<select </select>
className="bg-transparent outline-none" <select
value={getYear(date)} className="bg-transparent outline-none"
onChange={({ target: { value } }) => changeYear(value)} value={getYear(date)}
> onChange={({ target: { value } }) => changeYear(value)}
{years.map((option) => ( >
<option key={option} value={option}> {years.map((option) => (
{option} <option key={option} value={option}>
</option> {option}
))} </option>
</select> ))}
</div> </select>
)} </div>
closeOnScroll={true} )}
selected={startDate} closeOnScroll={true}
onChange={(date) => handleOnDateChange(date)} selected={startDate}
popperPlacement="top-end" onChange={(date) => handleOnDateChange(date)}
customInput={<ExampleCustomInput />} popperPlacement="top-end"
dateFormat={ customInput={<ExampleCustomInput />}
selectDate dateFormat={
? selectDate?.format selectDate
: currWidgetsDetails?.options?.validation?.format ? selectDate?.format
? currWidgetsDetails?.options?.validation?.format : currWidgetsDetails?.options?.validation?.format
: "MM/dd/yyyy" ? currWidgetsDetails?.options?.validation?.format
} : "MM/dd/yyyy"
portalId="root-portal" }
/> portalId="root-portal"
/>
</div>
<div className="flex justify-center">
<span className="text-gray-300">
{currWidgetsDetails?.options?.validation?.format}
</span>
</div>
</div> </div>
); );
case "email": case "email":
@@ -1591,9 +1608,6 @@ function WidgetsValueModal(props) {
const isCheckBox = const isCheckBox =
!currWidgetsDetails.options?.isReadOnly && !currWidgetsDetails.options?.isReadOnly &&
currWidgetsDetails.type === "checkbox"; currWidgetsDetails.type === "checkbox";
const isRadio =
!currWidgetsDetails?.options?.isReadOnly &&
currWidgetsDetails?.type === radioButtonWidget;
if (isCheckBox) { if (isCheckBox) {
//get minimum required count if exist //get minimum required count if exist
const minCount = const minCount =
@@ -1655,7 +1669,7 @@ function WidgetsValueModal(props) {
} }
}, []); }, []);
return ( return (
<span className="text-center text-[12px]"> <span className="text-center text-[12px] text-base-content">
{t("required-mssg", { leftRequiredWidget, totalWidget })} {t("required-mssg", { leftRequiredWidget, totalWidget })}
</span> </span>
); );
@@ -1725,13 +1739,14 @@ function WidgetsValueModal(props) {
} else if (!isSave) { } else if (!isSave) {
const widgetsPosition = xyPosition?.find((data) => data.Id === uniqueId); const widgetsPosition = xyPosition?.find((data) => data.Id === uniqueId);
let nextWidgetDetails = null; let nextWidgetDetails = null;
const editableWidgets = widgetsPosition?.placeHolder.flatMap((page) => const editableWidgets = (widgetsPosition?.placeHolder ?? []).flatMap(
page.pos ({ pos = [], pageNumber }) =>
.filter((widget) => !widget.options?.isReadOnly) pos
.map((widget) => ({ .filter(
widget, ({ options }) =>
pageNumber: page.pageNumber !options?.isReadOnly
})) )
.map((widget) => ({ widget, pageNumber }))
); );
//get current index of widget //get current index of widget
const currentIndex = editableWidgets.findIndex( const currentIndex = editableWidgets.findIndex(
@@ -1887,15 +1902,15 @@ function WidgetsValueModal(props) {
{isFinish ? ( {isFinish ? (
<> <>
<div className="p-1 mt-3"> <div className="p-1 mt-3">
<span className="text-base">{t("finish-mssg")}</span> <span className="text-base text-base-content">
{t("finish-mssg")}
</span>
</div> </div>
<div className="flex gap-3 items-center mt-4"> <div className="flex gap-3 items-center mt-4">
<button <button
type="button" type="button"
className="op-btn op-btn-primary op-btn-sm px-4" className="op-btn op-btn-primary op-btn-sm px-4"
onClick={() => { onClick={() => handleFinish()}
handleFinish();
}}
> >
{t("finish")} {t("finish")}
</button> </button>
@@ -1933,13 +1948,11 @@ function WidgetsValueModal(props) {
</div> </div>
</div> </div>
<div className="flex justify-between items-center text-center"> <div className="flex justify-between items-center text-center">
{!["checkbox", "dropdown", "date", "radio"].includes( {showClearbtn ? (
currWidgetsDetails?.type
) ? (
<button <button
type="button" type="button"
className="op-btn op-btn-ghost op-btn-sm text-base-content mr-1" className="op-btn op-btn-ghost op-btn-sm text-base-content mr-1"
onClick={() => handleClear()} onClick={handleClear}
> >
{t("clear")} {t("clear")}
</button> </button>
@@ -1955,9 +1968,7 @@ function WidgetsValueModal(props) {
<button <button
type="button" type="button"
className="op-btn op-btn-primary op-btn-sm" className="op-btn op-btn-primary op-btn-sm"
onClick={() => { onClick={() => handleClickOnNext()}
handleClickOnNext();
}}
disabled={handleDisable()} disabled={handleDisable()}
> >
{t("save")} {t("save")}
@@ -1967,10 +1978,7 @@ function WidgetsValueModal(props) {
type="button" type="button"
className="op-btn op-btn-primary op-btn-sm" className="op-btn op-btn-primary op-btn-sm"
disabled={handleDisable()} disabled={handleDisable()}
onClick={() => { onClick={() => handleClickOnNext(true)} // isFinishDoc
const isFinishDoc = true;
handleClickOnNext(isFinishDoc);
}}
> >
{t("done")} {t("done")}
</button> </button>
@@ -1978,9 +1986,7 @@ function WidgetsValueModal(props) {
<button <button
type="button" type="button"
className="op-btn op-btn-primary op-btn-sm text-xs md:text-sm" className="op-btn op-btn-primary op-btn-sm text-xs md:text-sm"
onClick={() => { onClick={() => handleClickOnNext()}
handleClickOnNext();
}}
disabled={handleDisable()} disabled={handleDisable()}
> >
{t("next-field")} {t("next-field")}
@@ -3,15 +3,20 @@ import { isMobile } from "../../constant/Utils";
// `getWidgetType` is used to load ui of widget in side list // `getWidgetType` is used to load ui of widget in side list
const getWidgetType = (item, widgetName) => { const getWidgetType = (item, widgetName) => {
return ( return (
<div className="op-btn w-fit md:w-[100%] op-btn-primary op-btn-outline op-btn-sm focus:outline-none outline outline-[1.5px] ml-[6px] md:ml-0 p-0 overflow-hidden"> <div
title={widgetName}
className="op-btn w-fit md:w-[100%] op-btn-primary op-btn-outline op-btn-sm focus:outline-none ml-[6px] outline outline-[1px] md:ml-0 p-0 overflow-hidden"
>
<div className="w-full h-full flex md:justify-between items-center"> <div className="w-full h-full flex md:justify-between items-center">
<div className="flex justify-start items-center text-[13px] ml-1"> <div className="flex justify-start items-center ml-1 overflow-hidden">
{!isMobile && <i className="fa-light fa-grip-vertical ml-[3px]"></i>} {!isMobile && (
<span className="md:inline-block text-center text-[15px] ml-[5px] font-semibold pr-1 md:pr-0"> <i className="fa-light fa-grip-vertical ml-[3px] text-[13px]"></i>
)}
<span className="md:inline-block text-center text-[11px] mx-[3px] truncate font-medium capitalize">
{widgetName} {widgetName}
</span> </span>
</div> </div>
<div className="text-[20px] op-btn op-btn-primary rounded-none w-[40px] h-full flex justify-center items-center"> <div className="text-[13px] op-btn op-btn-primary rounded-none w-[20px] h-full flex justify-center items-center">
<i className={item.icon}></i> <i className={item.icon}></i>
</div> </div>
</div> </div>
+153 -146
View File
@@ -9,7 +9,7 @@ import fontkit from "@pdf-lib/fontkit";
import { themeColor } from "./const"; import { themeColor } from "./const";
import { format, toZonedTime } from "date-fns-tz"; import { format, toZonedTime } from "date-fns-tz";
import i18n from "../i18n"; import i18n from "../i18n";
import { buildDownloadFilename } from "../utils"; import { applyNumberFormulasToPages, buildDownloadFilename } from "../utils";
export const fontsizeArr = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]; export const fontsizeArr = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28];
export const fontColorArr = ["red", "black", "blue", "yellow"]; export const fontColorArr = ["red", "black", "blue", "yellow"];
@@ -327,6 +327,7 @@ export const widgets = [
{ type: "name", icon: "fa-light fa-user", iconSize: "21px" }, { type: "name", icon: "fa-light fa-user", iconSize: "21px" },
{ type: "job title", icon: "fa-light fa-address-card", iconSize: "17px" }, { type: "job title", icon: "fa-light fa-address-card", iconSize: "17px" },
{ type: "company", icon: "fa-light fa-building", iconSize: "25px" }, { type: "company", icon: "fa-light fa-building", iconSize: "25px" },
{ type: "email", icon: "fa-light fa-envelope", iconSize: "20px" },
{ type: "date", icon: "fa-light fa-calendar-days", iconSize: "20px" }, { type: "date", icon: "fa-light fa-calendar-days", iconSize: "20px" },
{ type: textWidget, icon: "fa-light fa-text-width", iconSize: "20px" }, { type: textWidget, icon: "fa-light fa-text-width", iconSize: "20px" },
{ type: cellsWidget, icon: "fa-light fa-table-cells", iconSize: "20px" }, { type: cellsWidget, icon: "fa-light fa-table-cells", iconSize: "20px" },
@@ -337,8 +338,7 @@ export const widgets = [
iconSize: "19px" iconSize: "19px"
}, },
{ type: radioButtonWidget, icon: "fa-light fa-circle-dot", iconSize: "20px" }, { type: radioButtonWidget, icon: "fa-light fa-circle-dot", iconSize: "20px" },
{ type: "image", icon: "fa-light fa-image", iconSize: "20px" }, { type: "image", icon: "fa-light fa-image", iconSize: "20px" }
{ type: "email", icon: "fa-light fa-envelope", iconSize: "20px" }
]; ];
export const getDate = (dateformat) => { export const getDate = (dateformat) => {
@@ -410,23 +410,51 @@ export const changeDateToMomentFormat = (format) => {
return "L"; return "L";
} }
}; };
export const addWidgetOptions = (type, signer, placeholder, widgetValue) => {
export const getSignerPages = (xyPosition = [], currentKey, signerId) => {
if (!Array.isArray(xyPosition)) {
return [];
}
const hasSigners = xyPosition.some((item) =>
Array.isArray(item?.placeHolder)
);
if (hasSigners) {
const candidates =
signerId !== undefined && signerId !== null && signerId !== ""
? xyPosition.filter(
(item) => item.Id === signerId || item.signerObjId === signerId
)
: xyPosition;
for (const signer of candidates) {
const pages = signer?.placeHolder || [];
const ownsWidget = pages.some((page) =>
(page?.pos || []).some((widget) => widget?.key === currentKey)
);
if (ownsWidget) {
return pages;
}
}
return [];
}
return xyPosition;
};
export const addWidgetOptions = (
type,
signer,
placeholder,
role,
widgetValue
) => {
let defaultOpt; let defaultOpt;
//condition to handle widgets name field
if (placeholder) { if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => { const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length; return count + page.pos.filter((item) => item.type === type).length;
}, 0); }, 0);
const count = countSameWidget + 1; const count = countSameWidget + 1;
defaultOpt = { defaultOpt = { name: `${type}-${count}`, status: "required" };
name: `${type}-${count}`,
status: "required"
};
} else { } else {
defaultOpt = { defaultOpt = { name: `${type}-1`, status: "required" };
name: `${type}-1`,
status: "required"
};
} }
switch (type) { switch (type) {
case "signature": case "signature":
@@ -434,11 +462,7 @@ export const addWidgetOptions = (type, signer, placeholder, widgetValue) => {
case "stamp": case "stamp":
return defaultOpt; return defaultOpt;
case "checkbox": case "checkbox":
return { return { ...defaultOpt, isReadOnly: false, isHideLabel: false };
...defaultOpt,
isReadOnly: false,
isHideLabel: false
};
case textInputWidget: case textInputWidget:
return { ...defaultOpt, isReadOnly: false }; return { ...defaultOpt, isReadOnly: false };
case cellsWidget: case cellsWidget:
@@ -452,27 +476,18 @@ export const addWidgetOptions = (type, signer, placeholder, widgetValue) => {
case "initials": case "initials":
return defaultOpt; return defaultOpt;
case "name": case "name":
return { return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "company": case "company":
return { return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "job title": case "job title":
return { return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "date": { case "date": {
const dateFormat = signer?.DateFormat const dateFormat = signer?.DateFormat
? selectFormat(signer?.DateFormat) ? selectFormat(signer?.DateFormat)
: "MM/dd/yyyy"; : "MM/dd/yyyy";
return { return {
...defaultOpt, ...defaultOpt,
response: getDate(signer?.DateFormat), response: role === "prefill" ? getDate(signer?.DateFormat) : "",
validation: { format: dateFormat, type: "date-format" } validation: { format: dateFormat, type: "date-format" }
}; };
} }
@@ -513,15 +528,9 @@ export const addWidgetSelfsignOptions = (
return count + page.pos.filter((item) => item.type === type).length; return count + page.pos.filter((item) => item.type === type).length;
}, 0); }, 0);
const count = countSameWidget + 1; const count = countSameWidget + 1;
defaultOpt = { defaultOpt = { name: `${type}-${count}`, status: "required" };
name: `${type}-${count}`,
status: "required"
};
} else { } else {
defaultOpt = { defaultOpt = { name: `${type}-1`, status: "required" };
name: `${type}-1`,
status: "required"
};
} }
switch (type) { switch (type) {
case "signature": case "signature":
@@ -823,6 +832,8 @@ export const createDocument = async (
) => { ) => {
if (template && template.length > 0) { if (template && template.length > 0) {
const Doc = template[0]; const Doc = template[0];
const date = new Date();
const isoDate = date.toISOString();
let extUserId = Doc.ExtUserPtr.objectId; let extUserId = Doc.ExtUserPtr.objectId;
let creatorId = Doc.CreatedBy.objectId; let creatorId = Doc.CreatedBy.objectId;
const Extand_Class = localStorage.getItem("Extand_Class"); const Extand_Class = localStorage.getItem("Extand_Class");
@@ -898,6 +909,7 @@ export const createDocument = async (
IsTourEnabled: Doc?.IsTourEnabled || false, IsTourEnabled: Doc?.IsTourEnabled || false,
AllowModifications: Doc?.AllowModifications || false, AllowModifications: Doc?.AllowModifications || false,
TimeToCompleteDays: parseInt(Doc?.TimeToCompleteDays) || 15, TimeToCompleteDays: parseInt(Doc?.TimeToCompleteDays) || 15,
DocSentAt: { __type: "Date", iso: isoDate },
...SignatureType, ...SignatureType,
...NotifyOnSignatures, ...NotifyOnSignatures,
...Bcc, ...Bcc,
@@ -999,7 +1011,7 @@ export const onChangeInput = (
dateFormat, dateFormat,
fontSize, fontSize,
fontColor, fontColor,
isDateReadOnly dateDetails
) => { ) => {
const isSigners = xyPosition.some( const isSigners = xyPosition.some(
(data) => data.signerPtr || data.Role === "prefill" (data) => data.signerPtr || data.Role === "prefill"
@@ -1031,9 +1043,18 @@ export const onChangeInput = (
options: { options: {
...position.options, ...position.options,
response: value, response: value,
fontSize: fontSize, fontSize: fontSize ? fontSize : position.options?.fontSize,
fontColor: fontColor, fontColor: fontColor
isReadOnly: isDateReadOnly || false, ? fontColor
: position.options?.fontColor,
isReadOnly:
dateDetails?.isReadOnly !== "undefined"
? dateDetails?.isReadOnly
: position.options?.isReadOnly,
status:
dateDetails?.status !== "undefined"
? dateDetails?.status
: position.options?.status,
validation: { validation: {
type: "date-format", type: "date-format",
format: dateFormat // This indicates the required date format explicitly. format: dateFormat // This indicates the required date format explicitly.
@@ -1060,9 +1081,11 @@ export const onChangeInput = (
return obj; return obj;
}); });
const recalculatedPages = applyNumberFormulasToPages(newUpdateSignPos);
const newUpdateSigner = xyPosition.map((obj) => { const newUpdateSigner = xyPosition.map((obj) => {
if (obj.Id === userId) { if (obj.Id === userId) {
return { ...obj, placeHolder: newUpdateSignPos }; return { ...obj, placeHolder: recalculatedPages };
} }
return obj; return obj;
}); });
@@ -1108,7 +1131,7 @@ export const onChangeInput = (
} }
return obj; return obj;
}); });
setXyPosition(updatePlaceholder); setXyPosition(applyNumberFormulasToPages(updatePlaceholder));
} }
}; };
//function to increase height of text area on press enter //function to increase height of text area on press enter
@@ -1203,13 +1226,13 @@ export const calculateInitialWidthHeight = (widgetData) => {
export const widgetDataValue = (type, value) => { export const widgetDataValue = (type, value) => {
switch (type) { switch (type) {
case "name": case "name":
return value?.Name; return value?.Name || value?.name;
case "company": case "company":
return value?.Company; return value?.Company || value?.company;
case "job title": case "job title":
return value?.JobTitle; return value?.JobTitle || value?.jobTitle;
case "email": case "email":
return value?.Email; return value?.Email || value?.email;
default: default:
return ""; return "";
} }
@@ -1833,6 +1856,16 @@ export const embedWidgetsToDoc = async (
console.log("error in image", e); console.log("error in image", e);
} }
for (let [id, position] of widgetsPositionArr.entries()) { for (let [id, position] of widgetsPositionArr.entries()) {
const isTextTypeWidget = [
textWidget,
textInputWidget,
cellsWidget,
"name",
"company",
"job title",
"date",
"email"
].includes(position.type);
if (hasError) break; // Stop the inner loop if an error occurred if (hasError) break; // Stop the inner loop if an error occurred
try { try {
let img; let img;
@@ -1881,33 +1914,13 @@ export const embedWidgetsToDoc = async (
return y; return y;
} }
} else { } else {
const WidgetsTypeTextExist = [ const yPosition = isTextTypeWidget ? resizePos + 6 : resizePos;
textWidget,
textInputWidget,
cellsWidget,
"name",
"company",
"job title",
"date",
"email"
].includes(position.type);
const yPosition = WidgetsTypeTextExist ? resizePos + 6 : resizePos;
return yPosition; return yPosition;
} }
}; };
const color = position?.options?.fontColor; const color = position?.options?.fontColor;
const updateColorInRgb = getWidgetsFontColor(color); const updateColorInRgb = getWidgetsFontColor(color);
const fontSize = parseInt(position?.options?.fontSize || 12); const fontSize = parseInt(position?.options?.fontSize || 12);
const isTextTypeWidget = [
textWidget,
textInputWidget,
cellsWidget,
"name",
"company",
"job title",
"date",
"email"
].includes(position.type);
if (position.type === "checkbox") { if (position.type === "checkbox") {
// Determine layout mode: 'vertical' (default) or 'horizontal' // Determine layout mode: 'vertical' (default) or 'horizontal'
// const isHorizontal = position.layout === "horizontal"; // const isHorizontal = position.layout === "horizontal";
@@ -1993,9 +2006,19 @@ export const embedWidgetsToDoc = async (
} else if (isTextTypeWidget) { } else if (isTextTypeWidget) {
let textContent = ""; let textContent = "";
if (position?.options?.response) { if (position?.options?.response) {
textContent = position.options?.response; if (
position.type === "date" &&
position.options?.response === "today"
) {
const getFormat = changeDateToMomentFormat(
position?.options?.validation?.format
);
textContent = moment().format(getFormat);
} else {
textContent = position.options?.response;
}
} else if (position?.options?.defaultValue) { } else if (position?.options?.defaultValue) {
textContent = position?.options?.defaultValue; textContent = position?.options?.defaultValue?.toString();
} }
if (position.type === cellsWidget) { if (position.type === cellsWidget) {
const cellCount = const cellCount =
@@ -2022,15 +2045,16 @@ export const embedWidgetsToDoc = async (
if (ch) page.drawText(ch, textPosition); if (ch) page.drawText(ch, textPosition);
} }
} else { } else {
const fixedWidth = widgetWidth; // Set your fixed width const fixedWidth = widgetWidth; // Set your fixed
const isNewOnEnterLineExist = textContent.includes("\n"); textContent = textContent?.toString();
const isNewOnEnterLineExist = textContent?.includes("\n");
// Function to break text into lines based on the fixed width // Function to break text into lines based on the fixed width
const NewbreakTextIntoLines = (textContent, width) => { const NewbreakTextIntoLines = (textContent, width) => {
const lines = []; const lines = [];
let currentLine = ""; let currentLine = "";
for (const word of textContent.split(" ")) { for (const word of textContent?.split(" ")) {
//get text line width //get text line width
const lineWidth = font.widthOfTextAtSize( const lineWidth = font.widthOfTextAtSize(
`${currentLine} ${word}`, `${currentLine} ${word}`,
@@ -2215,7 +2239,6 @@ export const embedWidgetsToDoc = async (
width: widgetWidth, width: widgetWidth,
height: widgetHeight height: widgetHeight
}; };
const imageOptions = getWidgetPosition(page, signature, 1, getSize); const imageOptions = getWidgetPosition(page, signature, 1, getSize);
page.drawImage(img, imageOptions); page.drawImage(img, imageOptions);
} }
@@ -2343,56 +2366,34 @@ export const contractDocument = async (documentId, include) => {
//function for add default signature or image for all requested location //function for add default signature or image for all requested location
export const addDefaultSignatureImg = (xyPosition, defaultSignImg, type) => { export const addDefaultSignatureImg = (xyPosition, defaultSignImg, type) => {
let imgWH = { width: "", height: "" };
const img = new Image();
img.src = defaultSignImg;
if (img.complete) {
imgWH = { width: img.width, height: img.height };
}
let xyDefaultPos = []; let xyDefaultPos = [];
for (let i = 0; i < xyPosition.length; i++) { for (let i = 0; i < xyPosition.length; i++) {
// let getIMGWH;
const getXYdata = xyPosition[i].pos; const getXYdata = xyPosition[i].pos;
const getPageNo = xyPosition[i].pageNumber; const getPageNo = xyPosition[i].pageNumber;
const getPosData = getXYdata; const getPosData = getXYdata;
const addSign = getPosData.map((position) => { const addSign = getPosData.map((position) => {
// getIMGWH = calculateImgAspectRatio(imgWH, position);
if (position.type) { if (position.type) {
if (position?.type === type) { if (position?.type === type) {
return { return {
...position, ...position,
SignUrl: defaultSignImg, SignUrl: defaultSignImg,
// Width: getIMGWH.newWidth,
// Height: getIMGWH.newHeight,
ImageType: "default", ImageType: "default",
options: { options: { ...position.options, response: defaultSignImg }
...position.options,
response: defaultSignImg
}
}; };
} }
} else if (position && !position.isStamp) { } else if (position && !position.isStamp) {
return { return {
...position, ...position,
SignUrl: defaultSignImg, SignUrl: defaultSignImg,
// Width: getIMGWH.newWidth,
// Height: getIMGWH.newHeight,
ImageType: "default", ImageType: "default",
options: { options: { ...position.options, response: defaultSignImg }
...position.options,
response: defaultSignImg
}
}; };
} }
return position; return position;
}); });
const newXypos = { const newXypos = { pageNumber: getPageNo, pos: addSign };
pageNumber: getPageNo,
pos: addSign
};
xyDefaultPos.push(newXypos); xyDefaultPos.push(newXypos);
} }
return xyDefaultPos; return xyDefaultPos;
@@ -2979,34 +2980,34 @@ export const saveLanguageInLocal = (i18n) => {
const detectedLanguage = i18n.language || "en"; const detectedLanguage = i18n.language || "en";
localStorage.setItem("i18nextLng", detectedLanguage); localStorage.setItem("i18nextLng", detectedLanguage);
}; };
//function to get default signatur eof current user from `contracts_Signature` class
// function to get default signature of current user from `contracts_Signature` class
export const getDefaultSignature = async (objectId) => { export const getDefaultSignature = async (objectId) => {
try { try {
const query = new Parse.Query("contracts_Signature"); if (objectId) {
query.equalTo("UserId", { const result = await Parse.Cloud.run("getdefaultsignature", {
__type: "Pointer", userId: objectId
className: "_User", });
objectId: objectId if (result) {
}); const res = JSON.parse(JSON.stringify(result));
const defaultSignature = res?.ImageURL
? await getBase64FromUrl(res?.ImageURL, true)
: "";
const defaultInitial = res?.Initials
? await getBase64FromUrl(res?.Initials, true)
: "";
const result = await query.first(); return {
if (result) { status: "success",
const res = JSON.parse(JSON.stringify(result)); res: {
const defaultSignature = res?.ImageURL id: result?.id,
? await getBase64FromUrl(res?.ImageURL, true) defaultSignature: defaultSignature,
: ""; defaultInitial: defaultInitial
const defaultInitial = res?.Initials }
? await getBase64FromUrl(res?.Initials, true) };
: ""; }
} else {
return { return { status: "error" };
status: "success",
res: {
id: result?.id,
defaultSignature: defaultSignature,
defaultInitial: defaultInitial
}
};
} }
} catch (err) { } catch (err) {
console.log( console.log(
@@ -3332,10 +3333,7 @@ export const handleHeighlightWidget = (
...data, ...data,
pos: data.pos.map((position) => { pos: data.pos.map((position) => {
if (position.key === key) { if (position.key === key) {
return { return { ...position, zIndex: highestZIndex + 1 };
...position,
zIndex: highestZIndex + 1
};
} }
return position; return position;
}) })
@@ -3429,10 +3427,10 @@ export const updateDateWidgetsRes = (
journey, journey,
isRemovePrefill isRemovePrefill
) => { ) => {
const extUser = localStorage.getItem("Extand_Class"); const contactUser = documentData?.Signers?.find(
const contactUser = documentData?.Signers.find(
(data) => data.objectId === signerId (data) => data.objectId === signerId
); );
const extUser = JSON.stringify([contactUser]);
let placeHolders = documentData?.Placeholders; let placeHolders = documentData?.Placeholders;
if (isRemovePrefill) { if (isRemovePrefill) {
placeHolders = placeHolders?.filter((x) => x.Role !== "prefill"); placeHolders = placeHolders?.filter((x) => x.Role !== "prefill");
@@ -3445,18 +3443,7 @@ export const updateDateWidgetsRes = (
placeHolder: item?.placeHolder?.map((ph) => ({ placeHolder: item?.placeHolder?.map((ph) => ({
...ph, ...ph,
pos: ph?.pos?.map((widget) => { pos: ph?.pos?.map((widget) => {
// only for date widgets *and* missing response if (
if (widget.type === "date" && !widget.options.response) {
return {
...widget,
options: {
...widget.options,
response: getDate(
changeDateToMomentFormat(widget.options.validation.format)
)
}
};
} else if (
["name", "email", "job title", "company"].includes(widget.type) && ["name", "email", "job title", "company"].includes(widget.type) &&
!widget.options.defaultValue && !widget.options.defaultValue &&
!widget.options.response !widget.options.response
@@ -3590,6 +3577,7 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
//in `defaultValue` variable is used to get how many checkbox checked by default //in `defaultValue` variable is used to get how many checkbox checked by default
const defaultValue = const defaultValue =
requiredCheckbox[i].options?.defaultValue?.length; requiredCheckbox[i].options?.defaultValue?.length;
const checkboxValue = response ? response : defaultValue;
//condition to check parseMin and parseMax greater than 0 then consider it as a required check box //condition to check parseMin and parseMax greater than 0 then consider it as a required check box
if ( if (
parseMin > 0 && parseMin > 0 &&
@@ -3604,7 +3592,10 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
setminRequiredCount(parseMin); setminRequiredCount(parseMin);
} }
//else condition to validate minimum required checkbox //else condition to validate minimum required checkbox
else if (parseMin > 0 && (parseMin > response || !response)) { else if (
parseMin > 0 &&
(parseMin > checkboxValue || !checkboxValue)
) {
if (!showAlert) { if (!showAlert) {
showAlert = true; showAlert = true;
widgetKey = requiredCheckbox[i].key; widgetKey = requiredCheckbox[i].key;
@@ -3803,7 +3794,7 @@ export const sendEmailToSigners = async (
htmlReqBody, htmlReqBody,
variables variables
); );
} else if (defaultMail) { } else if (defaultMail?.body && defaultMail?.subject) {
const mailBody = defaultMail?.body; const mailBody = defaultMail?.body;
const mailSubject = defaultMail.subject; const mailSubject = defaultMail.subject;
const replacedRequestBody = mailBody.replace(/"/g, "'"); const replacedRequestBody = mailBody.replace(/"/g, "'");
@@ -4031,3 +4022,19 @@ export const handleDeleteWidget = (key, Id, pageNumber, signerPos) => {
} }
} }
}; };
//function to convert formatted date to new Date() format
export const getDefaultDate = (dateStr, format) => {
//get valid date format for moment to convert formatted date to new Date() format
const formats = changeDateToMomentFormat(format);
const parsedDate = moment(dateStr, formats);
let date;
if (parsedDate.isValid()) {
date = new Date(parsedDate.toISOString());
return date;
} else if (dateStr === "today") {
date = new Date();
return date;
}
};
//function to get default format
export const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
+1 -1
View File
@@ -9,7 +9,7 @@ export const getThemeIconColor = () => {
return theme === "opensigndark" ? "#CCCCCC" : "#686968"; return theme === "opensigndark" ? "#CCCCCC" : "#686968";
}; };
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export const maxFileSize = 10; // 10MB
export const maxTitleLength = 250; // 250 characters export const maxTitleLength = 250; // 250 characters
export const maxNoteLength = 200; // 200 characters export const maxNoteLength = 200; // 200 characters
export const maxDescriptionLength = 500; // 500 characters export const maxDescriptionLength = 500; // 500 characters
export const maxFileSize = 80; // for cloud 10MB / 80MB for self-hosted
+7 -17
View File
@@ -4,6 +4,10 @@ const parseAppId = process.env.REACT_APP_APPID
? process.env.REACT_APP_APPID ? process.env.REACT_APP_APPID
: "opensign"; : "opensign";
const serverUrl = serverUrl_fn(); const serverUrl = serverUrl_fn();
const commonheader = {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
};
export const SaveFileSize = async (size, imageUrl, tenantId, userId) => { export const SaveFileSize = async (size, imageUrl, tenantId, userId) => {
//checking server url and save file's size //checking server url and save file's size
const tenantPtr = { const tenantPtr = {
@@ -29,7 +33,6 @@ export const SaveFileSize = async (size, imageUrl, tenantId, userId) => {
); );
const response = res.data.results; const response = res.data.results;
let data; let data;
// console.log("response", response);
if (response && response.length > 0) { if (response && response.length > 0) {
data = { data = {
usedStorage: response[0].usedStorage usedStorage: response[0].usedStorage
@@ -39,20 +42,12 @@ export const SaveFileSize = async (size, imageUrl, tenantId, userId) => {
await axios.put( await axios.put(
`${serverUrl}/classes/partners_TenantCredits/${response[0].objectId}`, `${serverUrl}/classes/partners_TenantCredits/${response[0].objectId}`,
data, data,
{ { headers: commonheader }
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
}
}
); );
} else { } else {
data = { usedStorage: size, PartnersTenant: tenantPtr }; data = { usedStorage: size, PartnersTenant: tenantPtr };
await axios.post(`${serverUrl}/classes/partners_TenantCredits`, data, { await axios.post(`${serverUrl}/classes/partners_TenantCredits`, data, {
headers: { headers: commonheader
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
}
}); });
} }
} catch (err) { } catch (err) {
@@ -69,14 +64,9 @@ const saveDataFile = async (size, imageUrl, tenantPtr, UserId) => {
TenantPtr: tenantPtr, TenantPtr: tenantPtr,
...(UserId ? { UserId: UserId } : {}) ...(UserId ? { UserId: UserId } : {})
}; };
// console.log("data save",file, data)
try { try {
await axios.post(`${serverUrl}/classes/partners_DataFiles`, data, { await axios.post(`${serverUrl}/classes/partners_DataFiles`, data, {
headers: { headers: commonheader
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
}
}); });
} catch (err) { } catch (err) {
console.log("err in save usage ", err); console.log("err in save usage ", err);
+2 -1
View File
@@ -2,10 +2,11 @@ import { useEffect, useMemo } from "react";
export function useManifestUrl(appName, logo) { export function useManifestUrl(appName, logo) {
const url = useMemo(() => { const url = useMemo(() => {
const start_url = window.location.origin || ".";
const manifest = { const manifest = {
short_name: appName, short_name: appName,
name: appName, name: appName,
start_url: ".", start_url: start_url,
display: "standalone", display: "standalone",
theme_color: "#000000", theme_color: "#000000",
background_color: "#ffffff", background_color: "#ffffff",
+1 -39
View File
@@ -11,7 +11,7 @@ export const extraCols = [
export default function reportJson(id) { export default function reportJson(id) {
const head = ["Title", "Note", "Folder", "File", "Owner", "Signers"]; const head = ["Title", "Note", "Folder", "File", "Owner", "Signers"];
const declineHead = ["Title", "Reason", "Folder", "File", "Owner", "Signers"]; const declineHead = ["Title", "Reason", "Folder", "File", "Owner", "Signers"];
const iphead = ["Title", "Note", "Folder", "File", "Signers"]; const iphead = ["Title", "Note", "Folder", "File", "Signers", "Sent Date"];
const contactbook = ["Name", "Email", "Phone", "Company", "JobTitle"]; const contactbook = ["Name", "Email", "Phone", "Company", "JobTitle"];
const dashboardReportHead = ["Title", "File", "Owner", "Signers"]; const dashboardReportHead = ["Title", "File", "Owner", "Signers"];
const templateReport = ["Title", "File", "Owner", "Signers"]; const templateReport = ["Title", "File", "Owner", "Signers"];
@@ -38,25 +38,6 @@ export default function reportJson(id) {
redirectUrl: "", redirectUrl: "",
action: "delete" action: "delete"
} }
// {
// btnId: "22534",
// hoverLabel: "option",
// btnColor: "",
// restrictBtn: true,
// textColor: "black",
// btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
// action: "option",
// subaction: [
// {
// btnId: "1630",
// btnLabel: "Save as template",
// hoverLabel: "Save as template",
// btnIcon: "fa-light fa-envelope",
// redirectUrl: "",
// action: "saveastemplate"
// }
// ]
// }
], ],
helpMsg: helpMsg:
"These are documents you have started but have not finalized for sending." "These are documents you have started but have not finalized for sending."
@@ -429,25 +410,6 @@ export default function reportJson(id) {
redirectUrl: "", redirectUrl: "",
action: "delete" action: "delete"
} }
// {
// btnId: "55534",
// hoverLabel: "option",
// btnColor: "",
// restrictBtn: true,
// textColor: "black",
// btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
// action: "option",
// subaction: [
// {
// btnId: "6630",
// btnLabel: "Save as template",
// hoverLabel: "Save as template",
// btnIcon: "fa-light fa-envelope",
// redirectUrl: "",
// action: "saveastemplate"
// }
// ]
// }
] ]
}; };
// contactbook report // contactbook report
+1 -1
View File
@@ -36,7 +36,7 @@ const dashboardJson = [
queryType: "", queryType: "",
class: "contracts_Document", class: "contracts_Document",
query: query:
'where={"Type":null,"Signers":{"#*exists":true},"Placeholders":{"#*exists":true},"SignedUrl":{"#*exists":true},"IsCompleted":false,"IsDeclined":false,"IsArchive":null,"CreatedBy":{"__type":"Pointer","className":"_User","objectId":"#UserId.objectId#"},"ExpiryDate":{"#*gt":{"__type":"#Date#","iso":"#today#"}}}&keys=Name,ExpiryDate,SignedUrl,Signers&count=1', 'where={"Type":{"#*ne":"Folder"},"Signers":{"#*exists":true},"Placeholders":{"#*exists":true},"SignedUrl":{"#*exists":true},"IsCompleted":{"#*ne":true},"IsDeclined":{"#*ne":true},"IsArchive":{"#*ne":true},"CreatedBy":{"__type":"Pointer","className":"_User","objectId":"#UserId.objectId#"},"ExpiryDate":{"#*gt":{"__type":"#Date#","iso":"#today#"}}}&count=1&limit=0',
key: "count", key: "count",
Redirect_type: "Report", Redirect_type: "Report",
Redirect_id: "1MwEuxLEkF", Redirect_id: "1MwEuxLEkF",
+68 -85
View File
@@ -4,21 +4,22 @@ import Footer from "../components/Footer";
import Sidebar from "../components/sidebar/Sidebar"; import Sidebar from "../components/sidebar/Sidebar";
import Tour from "../primitives/Tour"; import Tour from "../primitives/Tour";
import axios from "axios"; import axios from "axios";
import { useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import Parse from "parse"; import Parse from "parse";
import ModalUi from "../primitives/ModalUi"; import { useNavigate, Outlet } from "react-router";
import { useNavigate, useLocation, Outlet } from "react-router";
import Loader from "../primitives/Loader"; import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { sessionStatus } from "../redux/reducers/userReducer";
import SessionExpiredModal from "../primitives/SessionExpiredModal";
const HomeLayout = () => { const HomeLayout = () => {
const appName = const appName =
"OpenSign™"; "OpenSign™";
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const dispatch = useDispatch();
const arr = useSelector((state) => state.TourSteps); const arr = useSelector((state) => state.TourSteps);
const [isUserValid, setIsUserValid] = useState(true); const isValidSession = useSelector((state) => state.user.isValidSession);
const [isLoader, setIsLoader] = useState(true); const [isLoader, setIsLoader] = useState(true);
const [isCloseBtn, setIsCloseBtn] = useState(true); const [isCloseBtn, setIsCloseBtn] = useState(true);
const [isTour, setIsTour] = useState(false); const [isTour, setIsTour] = useState(false);
@@ -35,28 +36,31 @@ const HomeLayout = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!tenantId) { if (localStorage.getItem("accesstoken")) {
setIsUserValid(false); if (!tenantId) {
} else { dispatch(sessionStatus(false));
(async () => { } else {
try { (async () => {
// Use the session token to validate the user try {
const userQuery = new Parse.Query(Parse.User); // Use the session token to validate the user
const user = await userQuery.get(Parse?.User?.current()?.id, { const userQuery = new Parse.Query(Parse.User);
sessionToken: localStorage.getItem("accesstoken") const user = await userQuery.get(Parse?.User?.current()?.id, {
}); sessionToken: localStorage.getItem("accesstoken")
if (user) { });
localStorage.setItem("profileImg", user.get("ProfilePic") || ""); if (user) {
setIsUserValid(true); localStorage.setItem("profileImg", user.get("ProfilePic") || "");
setIsLoader(false); dispatch(sessionStatus(true));
} else { setIsLoader(false);
setIsUserValid(false); } else {
dispatch(sessionStatus(true));
}
} catch (error) {
console.log("catch");
// Session token is invalid or there was an error
dispatch(sessionStatus(false));
} }
} catch (error) { })();
// Session token is invalid or there was an error }
setIsUserValid(false);
}
})();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -169,77 +173,56 @@ const HomeLayout = () => {
} }
} }
const handleLoginBtn = async () => { return isValidSession && localStorage.getItem("accesstoken") ? (
try {
await Parse?.User?.logOut();
} catch (err) {
console.log("err ", err);
} finally {
localStorage.removeItem("accesstoken");
navigate("/", { replace: true, state: { from: location } });
}
};
return (
<div className="flex flex-col h-screen overflow-hidden"> <div className="flex flex-col h-screen overflow-hidden">
{/* HEADER */} {/* HEADER */}
<header className="z-[501]"> <header className="z-[501]">
{!isLoader && <Header setIsLoggingOut={setIsLoggingOut} />} {!isLoader && <Header setIsLoggingOut={setIsLoggingOut} />}
</header> </header>
{isUserValid ? ( {isLoader ? (
<div className="flex h-[100vh] justify-center items-center">
<Loader />
</div>
) : (
<> <>
{isLoader ? ( {isLoggingOut && (
<div className="flex h-[100vh] justify-center items-center"> <div className="inset-0 bg-black/30 z-[1000] fixed flex justify-center items-center">
<Loader /> <Loader />
</div> </div>
) : (
<>
{isLoggingOut && (
<div className="inset-0 bg-black/30 z-[1000] fixed flex justify-center items-center">
<Loader />
</div>
)}
{/* BODY */}
<div className="flex flex-1 overflow-hidden">
{/* SIDEBAR with width animation */}
<Sidebar />
{/* MAIN (includes both content + footer in one scrollable column) */}
<main
id="renderList"
className="flex-1 overflow-auto transition-all duration-300 ease-in-out"
>
<div className="flex flex-col min-h-full">
{/* your page content */}
<div className="p-3">{<Outlet />}</div>
{/* sticky-but-scrollable footer */}
<div className="mt-auto z-30">
<Footer />
</div>
</div>
</main>
</div>
<Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
disableKeyboardNavigation={["esc"]}
// disableInteraction={true}
scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
</>
)} )}
</> {/* BODY */}
) : ( <div className="flex flex-1 overflow-hidden">
<ModalUi showHeader={false} isOpen={true} showClose={false}> {/* SIDEBAR with width animation */}
<div className="flex flex-col justify-center items-center py-4 md:py-5 gap-5"> <Sidebar />
<p className="text-xl font-medium">{t("session-expired")}</p> {/* MAIN (includes both content + footer in one scrollable column) */}
<button onClick={handleLoginBtn} className="op-btn op-btn-neutral"> <main
{t("login")} id="renderList"
</button> className="flex-1 overflow-auto transition-all duration-300 ease-in-out"
>
<div className="flex flex-col min-h-full">
{/* your page content */}
<div className="p-3">{<Outlet />}</div>
{/* sticky-but-scrollable footer */}
<div className="mt-auto z-30">
<Footer />
</div>
</div>
</main>
</div> </div>
</ModalUi> <Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
disableKeyboardNavigation={["esc"]}
// disableInteraction={true}
scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
</>
)} )}
</div> </div>
) : (
<SessionExpiredModal />
); );
}; };
+1 -1
View File
@@ -27,7 +27,7 @@ function processDimensions(x, y, width, height) {
}; };
} }
const DebugPdf = () => { const DebugPdf = () => {
const {t} = useTranslation() const { t } = useTranslation();
const { width } = useWindowSize(); const { width } = useWindowSize();
const [pdf, setPdf] = useState(""); const [pdf, setPdf] = useState("");
const [isModal, setIsModal] = useState(true); const [isModal, setIsModal] = useState(true);
+24 -17
View File
@@ -41,7 +41,7 @@ const DocSuccessPage = () => {
objectId: docId, objectId: docId,
SignedUrl: docUrl, SignedUrl: docUrl,
CertificateUrl: certificate, CertificateUrl: certificate,
IsCompleted: completed IsCompleted: completed,
}; };
setPdfDetails([details]); setPdfDetails([details]);
const base64Pdf = await getBase64FromUrl(docUrl); const base64Pdf = await getBase64FromUrl(docUrl);
@@ -52,7 +52,9 @@ const DocSuccessPage = () => {
}; };
const handleDownload = () => { const handleDownload = () => {
if (pdfDetails?.[0]?.IsCompleted) { if (
pdfDetails?.[0]?.IsCompleted
) {
setIsDownloadModal(true); setIsDownloadModal(true);
} else { } else {
handleDownloadPdf(pdfDetails, setIsDownloading, pdfBase64Url); handleDownloadPdf(pdfDetails, setIsDownloading, pdfBase64Url);
@@ -99,21 +101,26 @@ const DocSuccessPage = () => {
<span>{t("download")}</span> <span>{t("download")}</span>
</button> </button>
{pdfDetails?.[0]?.IsCompleted && ( {
<button pdfDetails?.[0]?.IsCompleted && (
type="button" <button
onClick={() => type="button"
handleDownloadCertificate(pdfDetails, setIsDownloading) onClick={() =>
} handleDownloadCertificate(
className="font-medium text-sm md:text-[13px] md:px-4 py-2 op-btn op-btn-secondary" pdfDetails,
> setIsDownloading
<i )
className="fa-light fa-award mx-[3px] md:mx-0" }
aria-hidden="true" className="font-medium text-sm md:text-[13px] md:px-4 py-2 op-btn op-btn-secondary"
></i> >
<span>{t("certificate")}</span> <i
</button> className="fa-light fa-award mx-[3px] md:mx-0"
)} aria-hidden="true"
></i>
<span>{t("certificate")}</span>
</button>
)
}
<button <button
onClick={(e) => onClick={(e) =>
handleToPrint(e, setIsDownloading, pdfDetails) handleToPrint(e, setIsDownloading, pdfDetails)
+34 -15
View File
@@ -30,6 +30,8 @@ import ModalUi from "../primitives/ModalUi";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import Loader from "../primitives/Loader"; import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../redux/reducers/userReducer";
// `Form` render all type of Form on this basis of their provided in path // `Form` render all type of Form on this basis of their provided in path
function Form() { function Form() {
@@ -50,6 +52,7 @@ const Forms = (props) => {
const abortController = new AbortController(); const abortController = new AbortController();
const inputFileRef = useRef(null); const inputFileRef = useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch();
const [signers, setSigners] = useState([]); const [signers, setSigners] = useState([]);
const [folder, setFolder] = useState({ ObjectId: "", Name: "" }); const [folder, setFolder] = useState({ ObjectId: "", Name: "" });
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -67,7 +70,7 @@ const Forms = (props) => {
NotifyOnSignatures: "", NotifyOnSignatures: "",
Bcc: [], Bcc: [],
RedirectUrl: "", RedirectUrl: "",
AllowModifications: false AllowModifications: false,
}); });
const [fileupload, setFileUpload] = useState(""); const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]);
@@ -98,6 +101,8 @@ const Forms = (props) => {
extUserData?.IsTourEnabled === false extUserData?.IsTourEnabled === false
? "false" ? "false"
: "true"; : "true";
const fileSize =
maxFileSize;
useEffect(() => { useEffect(() => {
handleReset(); handleReset();
return () => abortController.abort(); return () => abortController.abort();
@@ -136,11 +141,10 @@ const Forms = (props) => {
return; return;
} }
// setFormData((prev) => ({ ...prev, file: files[0] })); // setFormData((prev) => ({ ...prev, file: files[0] }));
const totalMb = Math.round( const totalBytes = Math.round(files.reduce((sum, f) => sum + f.size, 0)); // in bytes
files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2) const fileSizeBytes = fileSize * 1024 * 1024;
); if (totalBytes > fileSizeBytes) {
if (totalMb > maxFileSize) { alert(`${t("file-alert-1")} ${fileSize} MB`);
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
setFileUpload(""); setFileUpload("");
setSelectedFiles([]); setSelectedFiles([]);
removeFile(e); removeFile(e);
@@ -276,7 +280,11 @@ const Forms = (props) => {
console.log("err in docx to pdf ", err); console.log("err in docx to pdf ", err);
const error = const error =
t("docx-error"); t("docx-error");
alert(error); if (err?.code === 209) {
dispatch(sessionStatus(false));
} else {
alert(error);
}
return; return;
} }
} }
@@ -334,8 +342,13 @@ const Forms = (props) => {
setSelectedFiles([]); setSelectedFiles([]);
} }
} catch (error) { } catch (error) {
alert(error.message); if (error?.code === 209) {
dispatch(sessionStatus(false));
} else {
alert(error.message);
}
setSelectedFiles([]); setSelectedFiles([]);
removeFile(e);
} }
}; };
// `isValidURL` is used to check valid webhook url // `isValidURL` is used to check valid webhook url
@@ -461,7 +474,7 @@ const Forms = (props) => {
IsEnableOTP: "false", IsEnableOTP: "false",
IsTourEnabled: istourenabled, IsTourEnabled: istourenabled,
RedirectUrl: "", RedirectUrl: "",
AllowModifications: false AllowModifications: false,
}); });
setFileUpload(""); setFileUpload("");
setSelectedFiles([]); setSelectedFiles([]);
@@ -470,7 +483,9 @@ const Forms = (props) => {
} }
} catch (err) { } catch (err) {
console.log("err ", err); console.log("err ", err);
if (err.message === "only 15 reminder allowed") { if (err?.code === 209) {
dispatch(sessionStatus(false));
} else if (err.message === "only 15 reminder allowed") {
setIsAlert({ setIsAlert({
type: "danger", type: "danger",
message: t("only-15-reminder-allowed") message: t("only-15-reminder-allowed")
@@ -541,7 +556,7 @@ const Forms = (props) => {
IsTourEnabled: istourenabled, IsTourEnabled: istourenabled,
NotifyOnSignatures: notifySign, NotifyOnSignatures: notifySign,
RedirectUrl: "", RedirectUrl: "",
AllowModifications: false AllowModifications: false,
}; };
setFormData(obj); setFormData(obj);
removeFile(); removeFile();
@@ -602,7 +617,9 @@ const Forms = (props) => {
} }
} catch (err) { } catch (err) {
removeFile(); removeFile();
if (err?.response?.status === 401) { if (err?.code === 209) {
dispatch(sessionStatus(false));
} else if (err?.response?.status === 401) {
setIsPassword(true); setIsPassword(true);
setIsCorrectPass(false); setIsCorrectPass(false);
} else { } else {
@@ -631,10 +648,12 @@ const Forms = (props) => {
}; };
return ( return (
<div className="shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"> <div
className={`${isSubmit ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
>
{isAlert?.message && <Alert type={isAlert.type}>{isAlert.message}</Alert>} {isAlert?.message && <Alert type={isAlert.type}>{isAlert.message}</Alert>}
{isSubmit ? ( {isSubmit ? (
<div className="h-[300px] flex justify-center items-center"> <div className="flex flex-col justify-center items-center h-[100vh]">
<Loader /> <Loader />
</div> </div>
) : ( ) : (
@@ -1047,7 +1066,7 @@ const Forms = (props) => {
></div> ></div>
)} )}
{isAdvanceOpt && ( {isAdvanceOpt && (
<div className="card bg-base-100 rounded-box p-3 flex-grow "> <div className="card bg-base-100 rounded-box p-3 flex-grow ">
{formData?.autoreminder === true && {formData?.autoreminder === true &&
props.title !== "New Template" && ( props.title !== "New Template" && (
<div className="text-xs mt-2"> <div className="text-xs mt-2">
+2
View File
@@ -378,6 +378,7 @@ function GuestLogin() {
e.target.setCustomValidity(t("input-required")) e.target.setCustomValidity(t("input-required"))
} }
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
placeholder={t("enter-name")}
required required
/> />
</div> </div>
@@ -395,6 +396,7 @@ function GuestLogin() {
value={contact.email} value={contact.email}
onChange={handleInputChange} onChange={handleInputChange}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
required required
disabled disabled
/> />
+20 -42
View File
@@ -46,10 +46,9 @@ const ManageSign = () => {
objectId: User.id objectId: User.id
}; };
try { try {
const signCls = "contracts_Signature"; const signRes = await Parse.Cloud.run("getdefaultsignature", {
const signQuery = new Parse.Query(signCls); userId: User.id
signQuery.equalTo("UserId", userId); });
const signRes = await signQuery.first();
if (signRes) { if (signRes) {
const res = signRes.toJSON(); const res = signRes.toJSON();
setId(res.objectId); setId(res.objectId);
@@ -226,44 +225,23 @@ const ManageSign = () => {
}; };
const saveEntry = async (obj) => { const saveEntry = async (obj) => {
const signCls = "contracts_Signature"; try {
const User = Parse?.User?.current()?.id; const User = Parse?.User?.current()?.id;
const userId = { __type: "Pointer", className: "_User", objectId: User }; const res = await Parse.Cloud.run("managesign", {
if (id) { signature: obj.url,
try { userId: User,
const updateSign = new Parse.Object(signCls); initials: obj.initialsUrl,
updateSign.id = id; id: id,
updateSign.set("Initials", obj.initialsUrl ? obj.initialsUrl : ""); title: obj.name
updateSign.set("ImageURL", obj.url ? obj.url : ""); });
updateSign.set("SignatureName", obj.name); setIsAlert({ type: "success", message: t("signature-saved-alert") });
updateSign.set("UserId", userId); return res;
const res = await updateSign.save(); } catch (err) {
setIsAlert({ type: "success", message: t("signature-saved-alert") }); console.log(err);
return res; setIsAlert({ type: "danger", message: `${err.message}` });
} catch (err) { } finally {
console.log(err); setIsLoader(false);
setIsAlert({ type: "danger", message: `${err.message}` }); setTimeout(() => setIsAlert({}), 2000);
} finally {
setIsLoader(false);
setTimeout(() => setIsAlert({}), 2000);
}
} else {
try {
const updateSign = new Parse.Object(signCls);
updateSign.set("Initials", obj.initialsUrl ? obj.initialsUrl : "");
updateSign.set("ImageURL", obj.url);
updateSign.set("SignatureName", obj.name);
updateSign.set("UserId", userId);
const res = await updateSign.save();
setIsAlert({ type: "success", message: t("signature-saved-alert") });
return res;
} catch (err) {
console.log(err);
setIsAlert({ type: "success", message: `${err.message}` });
} finally {
setIsLoader(false);
setTimeout(() => setIsAlert({}), 2000);
}
} }
}; };
+32 -29
View File
@@ -14,8 +14,9 @@ import {
setMyInitial, setMyInitial,
setDefaultSignImg, setDefaultSignImg,
resetWidgetState, resetWidgetState,
setPrefillImg setPrefillImg,
} from "../redux/reducers/widgetSlice.js"; setTypedSignFont
} from "../redux/reducers/widgetSlice";
import { import {
contractDocument, contractDocument,
embedWidgetsToDoc, embedWidgetsToDoc,
@@ -48,7 +49,7 @@ import {
widgetDataValue, widgetDataValue,
getOriginalWH, getOriginalWH,
handleCheckResponse, handleCheckResponse,
convertJpegToPng convertJpegToPng,
} from "../constant/Utils"; } from "../constant/Utils";
import Header from "../components/pdf/PdfHeader"; import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf"; import RenderPdf from "../components/pdf/RenderPdf";
@@ -736,6 +737,7 @@ function PdfRequestFiles(
widgets widgets
); );
if (resSign && resSign.status === "success") { if (resSign && resSign.status === "success") {
dispatch(setTypedSignFont("Fasthand"));
setPdfUrl(resSign.data); setPdfUrl(resSign.data);
setIsSigned(true); setIsSigned(true);
setSignedSigners([]); setSignedSigners([]);
@@ -884,7 +886,8 @@ function PdfRequestFiles(
?.IsCompleted ?.IsCompleted
? `&completed=true` ? `&completed=true`
: ""; : "";
const params = `docid=${updatedDoc.updatedPdfDetails[0].objectId}&docurl=${encodeURIComponent(url)}${isCompleted}${fileAdapter}`; const params =
`docid=${updatedDoc.updatedPdfDetails[0].objectId}&docurl=${encodeURIComponent(url)}${isCompleted}${fileAdapter}`;
window.location.href = `/success?${params}`; window.location.href = `/success?${params}`;
} }
} else { } else {
@@ -1509,6 +1512,7 @@ function PdfRequestFiles(
dragTypeValue, dragTypeValue,
owner, owner,
filterSignerPos?.placeHolder, filterSignerPos?.placeHolder,
null,
widgetValue widgetValue
), ),
Width: widgetWidth / (containerScale * scale), Width: widgetWidth / (containerScale * scale),
@@ -1545,6 +1549,7 @@ function PdfRequestFiles(
dragTypeValue, dragTypeValue,
owner, owner,
filterSignerPos?.placeHolder, filterSignerPos?.placeHolder,
null,
widgetValue widgetValue
), ),
Width: widgetWidth / (containerScale * scale), Width: widgetWidth / (containerScale * scale),
@@ -1714,6 +1719,10 @@ function PdfRequestFiles(
} }
return email; return email;
}; };
const handleDownload = async () => {
setIsCompleted((prev) => ({ ...prev, isModal: false }));
setIsDownloadModal(true);
};
return ( return (
<> <>
{isLoading.isLoad ? ( {isLoading.isLoad ? (
@@ -1925,34 +1934,28 @@ function PdfRequestFiles(
{t("print")} {t("print")}
</span> </span>
</button> </button>
<button <button
type="button" type="button"
onClick={() => onClick={() =>
handleDownloadCertificate( handleDownloadCertificate(
pdfDetails, pdfDetails,
setIsDownloading setIsDownloading
) )
} }
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-secondary" className="font-[500] text-[13px] mr-[5px] op-btn op-btn-secondary"
> >
<i <i
className="fa-light fa-award mx-[3px] lg:mx-0" className="fa-light fa-award mx-[3px] lg:mx-0"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span className="hidden lg:block"> <span className="hidden lg:block">
{t("certificate")} {t("certificate")}
</span> </span>
</button> </button>
<button <button
type="button" type="button"
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-primary" className="font-[500] text-[13px] mr-[5px] op-btn op-btn-primary"
onClick={() => { onClick={handleDownload}
setIsCompleted((prev) => ({
...prev,
isModal: false
}));
setIsDownloadModal(true);
}}
> >
<i <i
className="fa-light fa-download" className="fa-light fa-download"
+32 -50
View File
@@ -553,7 +553,8 @@ function PlaceHolderSign() {
options: addWidgetOptions( options: addWidgetOptions(
dragTypeValue, dragTypeValue,
owner, owner,
filterSignerPos?.placeHolder filterSignerPos?.placeHolder,
roleName
), ),
Width: widgetWidth / (containerScale * scale), Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale) Height: widgetHeight / (containerScale * scale)
@@ -588,7 +589,8 @@ function PlaceHolderSign() {
options: addWidgetOptions( options: addWidgetOptions(
dragTypeValue, dragTypeValue,
owner, owner,
filterSignerPos?.placeHolder filterSignerPos?.placeHolder,
roleName
), ),
Width: widgetWidth / (containerScale * scale), Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale) Height: widgetHeight / (containerScale * scale)
@@ -720,9 +722,7 @@ function PlaceHolderSign() {
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const addSignPos = getPageNumer?.[0]?.pos?.map((url) => {
const getPosData = getXYdata;
const addSignPos = getPosData.map((url) => {
if (url.key === keyValue) { if (url.key === keyValue) {
return { return {
...url, ...url,
@@ -1453,9 +1453,9 @@ function PlaceHolderSign() {
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const getPosData = getXYdata; const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const addSignPos = getPosData.map((position) => { const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
if (currWidgetsDetails?.type === radioButtonWidget) { if (currWidgetsDetails?.type === radioButtonWidget) {
if (addOption) { if (addOption) {
@@ -1484,12 +1484,8 @@ function PlaceHolderSign() {
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
defaultValue: defaultValue, defaultValue: defaultValue,
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} }
@@ -1523,12 +1519,8 @@ function PlaceHolderSign() {
layout: layout, layout: layout,
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} }
@@ -1541,12 +1533,8 @@ function PlaceHolderSign() {
status: status, status: status,
values: dropdownOptions, values: dropdownOptions,
defaultValue: defaultValue, defaultValue: defaultValue,
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black",
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
...(isReadOnly ? { isReadOnly: isReadOnly || false } : {}) ...(isReadOnly ? { isReadOnly: isReadOnly || false } : {})
} }
}; };
@@ -1561,9 +1549,11 @@ function PlaceHolderSign() {
} }
return obj; return obj;
}); });
const recalculatedPlaceholders =
utils.applyNumberFormulasToPages(newUpdateSignPos);
const newUpdateSigner = signerPos.map((obj) => { const newUpdateSigner = signerPos.map((obj) => {
if (obj.Id === uniqueId) { if (obj.Id === uniqueId) {
return { ...obj, placeHolder: newUpdateSignPos }; return { ...obj, placeHolder: recalculatedPlaceholders };
} }
return obj; return obj;
}); });
@@ -1595,9 +1585,9 @@ function PlaceHolderSign() {
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const getPosData = getXYdata; const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const addSignPos = getPosData.map((position) => { const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
if (position.type === textInputWidget) { if (position.type === textInputWidget) {
return { return {
@@ -1610,12 +1600,8 @@ function PlaceHolderSign() {
defaultValue: defaultdata?.defaultValue || "", defaultValue: defaultdata?.defaultValue || "",
validation: validation:
{}, {},
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black",
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
isReadOnly: defaultdata?.isReadOnly || false isReadOnly: defaultdata?.isReadOnly || false
} }
}; };
@@ -1634,16 +1620,13 @@ function PlaceHolderSign() {
), ),
validation: validation:
{}, {},
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black",
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
isReadOnly: defaultdata?.isReadOnly || false isReadOnly: defaultdata?.isReadOnly || false
} }
}; };
} else if (["signature"].includes(position.type)) { }
else if (["signature"].includes(position.type)) {
return { return {
...position, ...position,
options: { options: {
@@ -1661,12 +1644,8 @@ function PlaceHolderSign() {
status: defaultdata.status, status: defaultdata.status,
defaultValue: defaultdata.defaultValue, defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "", hint: defaultdata?.hint || "",
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} }
@@ -2045,12 +2024,12 @@ function PlaceHolderSign() {
setSignBtnPosition={setSignBtnPosition} setSignBtnPosition={setSignBtnPosition}
pageNumber={pageNumber} pageNumber={pageNumber}
pdfBase64Url={pdfBase64Url} pdfBase64Url={pdfBase64Url}
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
setPdfArrayBuffer={setPdfArrayBuffer} setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url} setPdfBase64Url={setPdfBase64Url}
setIsUploadPdf={setIsUploadPdf} setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer} pdfArrayBuffer={pdfArrayBuffer}
isMergePdfBtn={true} isMergePdfBtn={true}
pdfDetails={pdfDetails}
/> />
{/* pdf render view */} {/* pdf render view */}
<div className="w-full md:w-[57%] flex mr-4"> <div className="w-full md:w-[57%] flex mr-4">
@@ -2070,6 +2049,7 @@ function PlaceHolderSign() {
setAllPages={setAllPages} setAllPages={setAllPages}
setPageNumber={setPageNumber} setPageNumber={setPageNumber}
setIsTour={setPlaceholderTour} setIsTour={setPlaceholderTour}
pdfDetails={pdfDetails}
/> />
<div className=" w-full md:w-[95%] "> <div className=" w-full md:w-[95%] ">
{/* this modal is used show alert set placeholder for all signers before send mail */} {/* this modal is used show alert set placeholder for all signers before send mail */}
@@ -2532,6 +2512,8 @@ function PlaceHolderSign() {
fontColor={fontColor} fontColor={fontColor}
setFontColor={setFontColor} setFontColor={setFontColor}
roleName={roleName} roleName={roleName}
widgetsSource={signerPos}
activeSignerId={uniqueId}
/> />
<RotateAlert <RotateAlert
showRotateAlert={showRotateAlert.status} showRotateAlert={showRotateAlert.status}
+4 -3
View File
@@ -61,6 +61,7 @@ const Preferences = () => {
completionMail: false completionMail: false
}); });
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME"); const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
useEffect(() => { useEffect(() => {
fetchSignType(); fetchSignType();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -194,7 +195,7 @@ const Preferences = () => {
DateFormat: dateFormat, DateFormat: dateFormat,
Is12HourTime: is12HourTime, Is12HourTime: is12HourTime,
IsLTVEnabled: isLTVEnabled, IsLTVEnabled: isLTVEnabled,
DownloadFilenameFormat: fileNameFormat DownloadFilenameFormat: fileNameFormat,
}; };
const updateRes = await Parse.Cloud.run("updatepreferences", params); const updateRes = await Parse.Cloud.run("updatepreferences", params);
if (updateRes) { if (updateRes) {
@@ -804,7 +805,7 @@ const Preferences = () => {
onClick={() => handleModifyMail("request")} onClick={() => handleModifyMail("request")}
className="op-btn op-btn-primary shadow-lg" className="op-btn op-btn-primary shadow-lg"
> >
Modify {t("modify")}
</button> </button>
</div> </div>
) )
@@ -880,7 +881,7 @@ const Preferences = () => {
onClick={() => handleModifyMail("completion")} onClick={() => handleModifyMail("completion")}
className="op-btn op-btn-primary shadow-lg" className="op-btn op-btn-primary shadow-lg"
> >
Modify {t("modify")}
</button> </button>
</div> </div>
) )
+7 -3
View File
@@ -32,7 +32,11 @@ const Report = () => {
const [defaultColumns, setDefaultColumns] = useState([]); const [defaultColumns, setDefaultColumns] = useState([]);
const [isColumnModal, setIsColumnModal] = useState(false); const [isColumnModal, setIsColumnModal] = useState(false);
const debounceTimer = useRef(null); const debounceTimer = useRef(null);
const docPerPage = 20; // Number of documents to display per page (should always be half of docLimit for proper pagination)
const docPerPage = 10;
// Number of documents to fetch per API call
const docLimit = 20;
// below useEffect is call when id param change // below useEffect is call when id param change
useEffect(() => { useEffect(() => {
@@ -48,7 +52,7 @@ const Report = () => {
setVisibleColumns([]); setVisibleColumns([]);
setColumnLabels({}); setColumnLabels({});
} }
getReportData(0, docPerPage, ""); getReportData(0, docLimit, "");
// Function returned from useEffect is called on unmount // Function returned from useEffect is called on unmount
return () => { return () => {
@@ -64,7 +68,7 @@ const Report = () => {
// below useEffect call when isNextRecord state is true and fetch next record // below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => { useEffect(() => {
if (isNextRecord) { if (isNextRecord) {
getReportData(list.length, 20, searchTerm); getReportData(list?.length, docLimit, searchTerm);
} }
// eslint-disable-next-line // eslint-disable-next-line
}, [isNextRecord]); }, [isNextRecord]);
+63 -39
View File
@@ -63,11 +63,13 @@ import {
setMyInitial, setMyInitial,
setDefaultSignImg, setDefaultSignImg,
setIsShowModal, setIsShowModal,
resetWidgetState resetWidgetState,
} from "../redux/reducers/widgetSlice.js"; setTypedSignFont
} from "../redux/reducers/widgetSlice";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal"; import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal"; import WidgetNameModal from "../components/pdf/WidgetNameModal";
import CellsSettingModal from "../components/pdf/CellsSettingModal"; import CellsSettingModal from "../components/pdf/CellsSettingModal";
import { applyNumberFormulasToPages } from "../utils";
//For signYourself inProgress section signer can add sign and complete doc sign. //For signYourself inProgress section signer can add sign and complete doc sign.
function SignYourSelf() { function SignYourSelf() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -139,6 +141,8 @@ function SignYourSelf() {
const [isDownloadModal, setIsDownloadModal] = useState(false); const [isDownloadModal, setIsDownloadModal] = useState(false);
const [isResize, setIsResize] = useState(false); const [isResize, setIsResize] = useState(false);
const [isUploadPdf, setIsUploadPdf] = useState(false); const [isUploadPdf, setIsUploadPdf] = useState(false);
const [unSignedWidgetId, setUnSignedWidgetId] = useState("");
const [widgetsTour, setWidgetsTour] = useState(false);
const [owner, setOwner] = useState({}); const [owner, setOwner] = useState({});
const [, drop] = useDrop({ const [, drop] = useDrop({
@@ -585,9 +589,23 @@ function SignYourSelf() {
} }
if (!isEnableOTP || isEmailVerified) { if (!isEnableOTP || isEmailVerified) {
let showAlert = false, let showAlert = false,
isSignatureExist = false; widgetKey,
tourPageNumber;
try { try {
const isSignatureExist = xyPosition?.some((p) =>
p?.pos?.some((x) => x?.type === "signature")
);
if (!isSignatureExist) {
setIsAlert({
header: t("fields-required"),
isShow: true,
alertMessage: t("signature-widget-alert-1")
});
return;
}
for (let i = 0; i < xyPosition?.length; i++) { for (let i = 0; i < xyPosition?.length; i++) {
tourPageNumber = xyPosition[i]?.pageNumber;
const requiredWidgets = xyPosition[i].pos.filter( const requiredWidgets = xyPosition[i].pos.filter(
(position) => position.type !== "checkbox" (position) => position.type !== "checkbox"
); );
@@ -596,34 +614,24 @@ function SignYourSelf() {
for (let i = 0; i < requiredWidgets?.length; i++) { for (let i = 0; i < requiredWidgets?.length; i++) {
checkSigned = requiredWidgets[i]?.options?.response; checkSigned = requiredWidgets[i]?.options?.response;
if (!checkSigned) { if (!checkSigned) {
const checkSignUrl = requiredWidgets[i]?.pos?.SignUrl; const checkSignUrl = requiredWidgets[i]?.SignUrl;
let checkDefaultSigned = let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue; requiredWidgets[i]?.options?.defaultValue;
if (!checkSignUrl && !checkDefaultSigned && !showAlert) { if (!checkSignUrl && !checkDefaultSigned && !showAlert) {
showAlert = true; showAlert = true;
widgetKey = requiredWidgets[i]?.key;
} }
} }
} }
} }
//condition to check exist signature widget or not if (showAlert) {
if (!isSignatureExist) { break;
isSignatureExist = xyPosition[i].pos.some(
(data) => data?.type === "signature"
);
} }
} }
if (xyPosition.length === 0 || !isSignatureExist) { if (showAlert) {
setIsAlert({ setUnSignedWidgetId(widgetKey);
header: t("fields-required"), setPageNumber(tourPageNumber);
isShow: true, setWidgetsTour(true);
alertMessage: t("signature-widget-alert-1")
});
return;
} else if (showAlert) {
setIsAlert({
isShow: true,
alertMessage: t("signature-widget-alert-2")
});
return; return;
} else { } else {
setIsUiLoading(true); setIsUiLoading(true);
@@ -745,6 +753,7 @@ function SignYourSelf() {
if (resSignPdf) { if (resSignPdf) {
const signedpdf = JSON.parse(JSON.stringify(resSignPdf)); const signedpdf = JSON.parse(JSON.stringify(resSignPdf));
setPdfUrl(signedpdf); setPdfUrl(signedpdf);
dispatch(setTypedSignFont("Fasthand"));
getDocumentDetails(false); getDocumentDetails(false);
} }
}; };
@@ -769,9 +778,7 @@ function SignYourSelf() {
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
if (filterDropPos?.length > 0) { if (filterDropPos?.length > 0) {
const getXYdata = xyPosition[index].pos; const addSign = xyPosition[index]?.pos?.map((url) => {
const getPosData = getXYdata;
const addSign = getPosData.map((url) => {
if (url.key === dragKey) { if (url.key === dragKey) {
return { return {
...url, ...url,
@@ -956,11 +963,9 @@ function SignYourSelf() {
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const widgetLayout = const widgetLayout =
currWidgetsDetails?.type === "checkbox" ? { layout: layout } : {}; currWidgetsDetails?.type === "checkbox" ? { layout: layout } : {};
const addSignPos = getPosData.map((position) => { const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
if (addOption) { if (addOption) {
return { return {
@@ -1022,9 +1027,7 @@ function SignYourSelf() {
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
return { return {
...position, ...position,
@@ -1081,6 +1084,8 @@ function SignYourSelf() {
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((position) => { const updatePos = getPageNumer[0].pos.map((position) => {
const textSize = newFontSize || position?.options?.fontSize;
const textColor = newFontColor || position?.options?.fontColor;
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
if (position.type === cellsWidget) { if (position.type === cellsWidget) {
const count = parseInt( const count = parseInt(
@@ -1094,20 +1099,19 @@ function SignYourSelf() {
name: defaultdata?.name || position.options?.name || "Cells", name: defaultdata?.name || position.options?.name || "Cells",
cellCount: count, cellCount: count,
defaultValue: (defaultdata?.defaultValue || "").slice(0, count), defaultValue: (defaultdata?.defaultValue || "").slice(0, count),
fontSize: newFontSize || position.options?.fontSize || 12, fontSize: textSize || 12,
fontColor: fontColor: textColor || "black"
newFontColor || position.options?.fontColor || "black"
} }
}; };
} else { }
else {
return { return {
...position, ...position,
options: { options: {
...position.options, ...position.options,
name: defaultdata?.name || position.options?.name, name: defaultdata?.name || position.options?.name,
fontSize: newFontSize || position.options?.fontSize || 12, fontSize: textSize || 12,
fontColor: fontColor: textColor || "black"
newFontColor || position.options?.fontColor || "black"
} }
}; };
} }
@@ -1117,7 +1121,7 @@ function SignYourSelf() {
const updateXYposition = xyPosition.map((obj, ind) => const updateXYposition = xyPosition.map((obj, ind) =>
ind === index ? { ...obj, pos: updatePos } : obj ind === index ? { ...obj, pos: updatePos } : obj
); );
setXyPosition(updateXYposition); setXyPosition(applyNumberFormulasToPages(updateXYposition));
} }
setFontSize(); setFontSize();
setFontColor(); setFontColor();
@@ -1194,6 +1198,14 @@ function SignYourSelf() {
setPdfBase64Url(urlDetails.base64); setPdfBase64Url(urlDetails.base64);
setShowRotateAlert({ status: false, degree: 0 }); setShowRotateAlert({ status: false, degree: 0 });
}; };
const WidgetTourConfig = [
{
selector: '[data-tut="IsSigned"]',
content: t("signature-validate-alert-2"),
position: "top",
style: { fontSize: "13px" }
}
];
return ( return (
<> <>
{isLoading.isLoad ? ( {isLoading.isLoad ? (
@@ -1239,6 +1251,14 @@ function SignYourSelf() {
isOpen={signTour} isOpen={signTour}
/> />
)} )}
<Tour
showNumber={false}
showNavigation={false}
showNavigationNumber={false}
onRequestClose={() => setWidgetsTour(false)}
steps={WidgetTourConfig}
isOpen={widgetsTour}
/>
{/* this component used to render all pdf pages in left side */} {/* this component used to render all pdf pages in left side */}
<RenderAllPdfPage <RenderAllPdfPage
allPages={allPages} allPages={allPages}
@@ -1248,12 +1268,12 @@ function SignYourSelf() {
pageNumber={pageNumber} pageNumber={pageNumber}
containerWH={containerWH} containerWH={containerWH}
pdfBase64Url={pdfBase64Url} pdfBase64Url={pdfBase64Url}
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
setPdfArrayBuffer={setPdfArrayBuffer} setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url} setPdfBase64Url={setPdfBase64Url}
setIsUploadPdf={setIsUploadPdf} setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer} pdfArrayBuffer={pdfArrayBuffer}
isMergePdfBtn={!pdfDetails?.[0]?.IsCompleted} isMergePdfBtn={!pdfDetails?.[0]?.IsCompleted}
pdfDetails={pdfDetails}
/> />
<div className="w-full md:w-[57%] flex mr-4"> <div className="w-full md:w-[57%] flex mr-4">
<PdfTools <PdfTools
@@ -1271,6 +1291,7 @@ function SignYourSelf() {
setAllPages={setAllPages} setAllPages={setAllPages}
setPageNumber={setPageNumber} setPageNumber={setPageNumber}
isDisableEditTools={isCompleted} isDisableEditTools={isCompleted}
pdfDetails={pdfDetails}
/> />
<div className="w-full md:w-[95%]"> <div className="w-full md:w-[95%]">
<ModalUi <ModalUi
@@ -1405,6 +1426,7 @@ function SignYourSelf() {
divRef={divRef} divRef={divRef}
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal} isShowModal={isShowModal}
unSignedWidgetId={unSignedWidgetId}
/> />
)} )}
</div> </div>
@@ -1456,6 +1478,8 @@ function SignYourSelf() {
setFontSize={setFontSize} setFontSize={setFontSize}
fontColor={fontColor} fontColor={fontColor}
setFontColor={setFontColor} setFontColor={setFontColor}
widgetsSource={xyPosition}
isSelfSign={true}
/> />
<CellsSettingModal <CellsSettingModal
isOpen={isCellsSetting} isOpen={isCellsSetting}
+41 -59
View File
@@ -165,6 +165,7 @@ const TemplatePlaceholder = () => {
const [currUserId, setCurrUserId] = useState(false); const [currUserId, setCurrUserId] = useState(false);
const [documentDetails, setDocumentDetails] = useState(); const [documentDetails, setDocumentDetails] = useState();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [isUseTemplate, setIsUseTemplate] = useState(false);
const currentUser = localStorage.getItem( const currentUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser` `Parse/${localStorage.getItem("parseAppId")}/currentUser`
); );
@@ -347,8 +348,8 @@ const TemplatePlaceholder = () => {
setPrefillSigner([utils?.prefillObj()]); setPrefillSigner([utils?.prefillObj()]);
} }
setSignersData(updatedSigners); setSignersData(updatedSigners);
setUniqueId(updatedSigners[0].Id); setUniqueId(updatedSigners[0]?.Id);
setBlockColor(updatedSigners[0].blockColor); setBlockColor(updatedSigners[0]?.blockColor);
setIsSelectId(0); setIsSelectId(0);
} else { } else {
const updatedSigners = documentData[0].Signers.map((x, index) => ({ const updatedSigners = documentData[0].Signers.map((x, index) => ({
@@ -357,8 +358,8 @@ const TemplatePlaceholder = () => {
Role: "Role " + (index + 1) Role: "Role " + (index + 1)
})); }));
setSignersData(updatedSigners); setSignersData(updatedSigners);
setUniqueId(updatedSigners[0].Id); setUniqueId(updatedSigners[0]?.Id);
setBlockColor(updatedSigners[0].blockColor); setBlockColor(updatedSigners[0]?.blockColor);
setPrefillSigner([utils?.prefillObj()]); setPrefillSigner([utils?.prefillObj()]);
} }
} else { } else {
@@ -436,7 +437,6 @@ const TemplatePlaceholder = () => {
const addPositionOfSignature = (item, monitor) => { const addPositionOfSignature = (item, monitor) => {
getSignerPos(item, monitor); getSignerPos(item, monitor);
}; };
// `getSignerPos` is used to get placeholder position when user place it and save it in array // `getSignerPos` is used to get placeholder position when user place it and save it in array
const getSignerPos = (item, monitor) => { const getSignerPos = (item, monitor) => {
if (uniqueId) { if (uniqueId) {
@@ -482,7 +482,8 @@ const TemplatePlaceholder = () => {
options: addWidgetOptions( options: addWidgetOptions(
dragTypeValue, dragTypeValue,
owner, owner,
filterSignerPos?.placeHolder filterSignerPos?.placeHolder,
roleName
), ),
Width: widgetWidth / (containerScale * scale), Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale) Height: widgetHeight / (containerScale * scale)
@@ -516,7 +517,8 @@ const TemplatePlaceholder = () => {
options: addWidgetOptions( options: addWidgetOptions(
dragTypeValue, dragTypeValue,
owner, owner,
filterSignerPos?.placeHolder filterSignerPos?.placeHolder,
roleName
), ),
Width: widgetWidth / (containerScale * scale), Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale) Height: widgetHeight / (containerScale * scale)
@@ -613,7 +615,6 @@ const TemplatePlaceholder = () => {
setIsAddRole(true); setIsAddRole(true);
} }
}; };
const tourAddRole = [ const tourAddRole = [
{ {
selector: '[data-tut="reactourAddbtn"]', selector: '[data-tut="reactourAddbtn"]',
@@ -661,9 +662,7 @@ const TemplatePlaceholder = () => {
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const addSignPos = getPageNumer?.[0]?.pos?.map((url) => {
const getPosData = getXYdata;
const addSignPos = getPosData.map((url) => {
if (url.key === keyValue) { if (url.key === keyValue) {
return { return {
...url, ...url,
@@ -755,7 +754,7 @@ const TemplatePlaceholder = () => {
} }
}; };
useEffect(() => { useEffect(() => {
if (pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId) return; if (pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId || isPrefillModal) return;
const timer = setTimeout(() => { const timer = setTimeout(() => {
autosavedetails(); autosavedetails();
}, 2000); }, 2000);
@@ -931,6 +930,7 @@ const TemplatePlaceholder = () => {
} }
}; };
const handleCreateDocument = async () => { const handleCreateDocument = async () => {
setIsUseTemplate(true);
try { try {
setIsUiLoading(true); setIsUiLoading(true);
const res = await utils?.handleCheckPrefillCreateDoc( const res = await utils?.handleCheckPrefillCreateDoc(
@@ -974,6 +974,7 @@ const TemplatePlaceholder = () => {
} catch (e) { } catch (e) {
console.log("error in create document function", e); console.log("error in create document function", e);
} }
setIsUseTemplate(false);
}; };
const navigatePageToDoc = async () => { const navigatePageToDoc = async () => {
try { try {
@@ -1316,10 +1317,9 @@ const TemplatePlaceholder = () => {
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const getPosData = getXYdata; const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
const addSignPos = getPosData.map((position) => {
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
if (currWidgetsDetails?.type === radioButtonWidget) { if (currWidgetsDetails?.type === radioButtonWidget) {
if (addOption) { if (addOption) {
@@ -1348,12 +1348,8 @@ const TemplatePlaceholder = () => {
defaultValue: defaultValue, defaultValue: defaultValue,
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} }
@@ -1387,12 +1383,8 @@ const TemplatePlaceholder = () => {
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
defaultValue: defaultValue, defaultValue: defaultValue,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} }
@@ -1405,12 +1397,8 @@ const TemplatePlaceholder = () => {
status: status, status: status,
values: dropdownOptions, values: dropdownOptions,
defaultValue: defaultValue, defaultValue: defaultValue,
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black",
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
...(isReadOnly ? { isReadOnly: isReadOnly || false } : {}) ...(isReadOnly ? { isReadOnly: isReadOnly || false } : {})
} }
}; };
@@ -1425,9 +1413,11 @@ const TemplatePlaceholder = () => {
} }
return obj; return obj;
}); });
const recalculatedPlaceholders =
utils.applyNumberFormulasToPages(newUpdateSignPos);
const newUpdateSigner = signerPos.map((obj) => { const newUpdateSigner = signerPos.map((obj) => {
if (obj.Id === uniqueId) { if (obj.Id === uniqueId) {
return { ...obj, placeHolder: newUpdateSignPos }; return { ...obj, placeHolder: recalculatedPlaceholders };
} }
return obj; return obj;
}); });
@@ -1460,9 +1450,9 @@ const TemplatePlaceholder = () => {
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
const getPosData = getXYdata; const textColor = fontColor || currWidgetsDetails?.options?.fontColor;
const addSignPos = getPosData.map((position) => { const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
if (position.type === textInputWidget) { if (position.type === textInputWidget) {
return { return {
@@ -1476,12 +1466,8 @@ const TemplatePlaceholder = () => {
isReadOnly: defaultdata?.isReadOnly || false, isReadOnly: defaultdata?.isReadOnly || false,
validation: validation:
{}, {},
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} else if (position.type === cellsWidget) { } else if (position.type === cellsWidget) {
@@ -1500,15 +1486,12 @@ const TemplatePlaceholder = () => {
validation: validation:
{}, {},
isReadOnly: defaultdata?.isReadOnly || false, isReadOnly: defaultdata?.isReadOnly || false,
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} else if (["signature"].includes(position.type)) { }
else if (["signature"].includes(position.type)) {
return { return {
...position, ...position,
options: { options: {
@@ -1526,12 +1509,8 @@ const TemplatePlaceholder = () => {
status: defaultdata.status, status: defaultdata.status,
defaultValue: defaultdata.defaultValue, defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "", hint: defaultdata?.hint || "",
fontSize: fontSize: textSize || 12,
fontSize || currWidgetsDetails?.options?.fontSize || 12, fontColor: textColor || "black"
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
} }
}; };
} }
@@ -1771,12 +1750,12 @@ const TemplatePlaceholder = () => {
setSignBtnPosition={setSignBtnPosition} setSignBtnPosition={setSignBtnPosition}
pageNumber={pageNumber} pageNumber={pageNumber}
pdfBase64Url={pdfBase64Url} pdfBase64Url={pdfBase64Url}
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
setPdfArrayBuffer={setPdfArrayBuffer} setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url} setPdfBase64Url={setPdfBase64Url}
setIsUploadPdf={setIsUploadPdf} setIsUploadPdf={setIsUploadPdf}
pdfArrayBuffer={pdfArrayBuffer} pdfArrayBuffer={pdfArrayBuffer}
isMergePdfBtn={true} isMergePdfBtn={true}
pdfDetails={pdfDetails}
/> />
{/* pdf render view */} {/* pdf render view */}
@@ -1796,6 +1775,7 @@ const TemplatePlaceholder = () => {
allPages={allPages} allPages={allPages}
setAllPages={setAllPages} setAllPages={setAllPages}
setPageNumber={setPageNumber} setPageNumber={setPageNumber}
pdfDetails={pdfDetails}
/> />
<div className="w-full md:w-[95%]"> <div className="w-full md:w-[95%]">
<ModalUi <ModalUi
@@ -2121,6 +2101,8 @@ const TemplatePlaceholder = () => {
fontColor={fontColor} fontColor={fontColor}
setFontColor={setFontColor} setFontColor={setFontColor}
roleName={roleName} roleName={roleName}
widgetsSource={signerPos}
activeSignerId={uniqueId}
/> />
{isPrefillModal && ( {isPrefillModal && (
<PrefillWidgetModal <PrefillWidgetModal
@@ -2136,8 +2118,8 @@ const TemplatePlaceholder = () => {
navigatePageToDoc={navigatePageToDoc} navigatePageToDoc={navigatePageToDoc}
setIsNewContact={setIsNewContact} setIsNewContact={setIsNewContact}
isNewContact={isNewContact} isNewContact={isNewContact}
documentFlow={"template"}
docId={pdfDetails[0]?.objectId} docId={pdfDetails[0]?.objectId}
isSubmit={isUseTemplate}
/> />
)} )}
{isShowModal[currWidgetsDetails?.key] && ( {isShowModal[currWidgetsDetails?.key] && (
+51 -7
View File
@@ -12,6 +12,7 @@ import {
} from "react-i18next"; } from "react-i18next";
import DeleteUserModal from "../primitives/DeleteUserModal"; import DeleteUserModal from "../primitives/DeleteUserModal";
import axios from "axios"; import axios from "axios";
import PasswordResetModal from "../primitives/PasswordResetModal";
const actions = [ const actions = [
{ {
@@ -23,6 +24,14 @@ const actions = [
action: "delete", action: "delete",
restrictAdmin: true restrictAdmin: true
}, },
{
btnId: "1910",
hoverLabel: "Reset password",
btnIcon: "fa-light fa-key",
redirectUrl: "",
action: "resetpassword",
restrictAdmin: true
},
]; ];
const heading = ["Sr.No", "Name", "Email", "Phone", "Role", "Team", "Active"]; const heading = ["Sr.No", "Name", "Email", "Phone", "Role", "Team", "Active"];
const UserList = () => { const UserList = () => {
@@ -208,9 +217,9 @@ const UserList = () => {
}; };
// `showAlert` handle show/hide alert // `showAlert` handle show/hide alert
const showAlert = (type, msg) => { const showAlert = (type, msg, timer = 1500) => {
setIsAlert({ type, msg }); setIsAlert({ type, msg });
setTimeout(() => setIsAlert({ type: "success", msg: "" }), 1500); setTimeout(() => setIsAlert({ type: "success", msg: "" }), timer);
}; };
const handleDeleteAccount = async (item) => { const handleDeleteAccount = async (item) => {
@@ -247,13 +256,15 @@ const UserList = () => {
}; };
const handleActionBtn = async (act, item) => { const handleActionBtn = async (act, item) => {
if (act.action === "delete") { setIsActModal({ [`${act.action}_${item.objectId}`]: true });
setIsActModal({ [`delete_${item.objectId}`]: true });
}
}; };
const handleBtnVisibility = (act, item) => { const handleBtnVisibility = (act, item) => {
if (act.restrictAdmin) { if (act.restrictAdmin) {
return item?.objectId !== extClass?.[0]?.objectId; if (item?.UserRole === "contracts_Admin") {
return false;
} else {
return item?.objectId !== extClass?.[0]?.objectId;
}
} else if ( } else if (
act.restrictBtn === true && act.restrictBtn === true &&
item?.objectId === extClass?.[0]?.objectId item?.objectId === extClass?.[0]?.objectId
@@ -263,6 +274,28 @@ const UserList = () => {
return true; return true;
} }
}; };
const handleActiveToggleVisibility = (item) => {
if (item?.UserRole === "contracts_Admin") {
return false;
} else {
return item?.objectId !== extClass?.[0]?.objectId;
}
};
async function submitPassword(userId, password) {
setIsLoader(true);
setIsActModal({});
try {
const params = { userId, password };
await Parse.Cloud.run("resetpassword", params);
showAlert("success", t("password-has-been-reset"));
} catch (err) {
console.log("err while reset password", err);
showAlert("danger", t(err.message), 2000);
} finally {
setIsLoader(false);
}
}
return ( return (
<div className="relative"> <div className="relative">
{isLoader && ( {isLoader && (
@@ -340,7 +373,7 @@ const UserList = () => {
<td className="px-4 py-2"> <td className="px-4 py-2">
{formatRow(item.TeamIds)} {formatRow(item.TeamIds)}
</td> </td>
{item.UserRole !== "contracts_Admin" ? ( {handleActiveToggleVisibility(item) ? (
<td className="px-4 py-2 font-semibold"> <td className="px-4 py-2 font-semibold">
<label className="cursor-pointer relative block items-center mb-0"> <label className="cursor-pointer relative block items-center mb-0">
<input <input
@@ -436,6 +469,17 @@ const UserList = () => {
deleteRes={deleteUserRes} deleteRes={deleteUserRes}
handleClose={handleCloseModal} handleClose={handleCloseModal}
/> />
<PasswordResetModal
isOpen={
isActModal[
"resetpassword_" + item.objectId
]
}
userId={item?.UserId?.objectId}
onClose={handleCloseModal}
onSubmit={submitPassword}
showAlert={showAlert}
/>
</React.Fragment> </React.Fragment>
))} ))}
</div> </div>
+1 -1
View File
@@ -487,7 +487,7 @@ function UserProfile() {
) : ( ) : (
<> <>
{deleteUserRes ? ( {deleteUserRes ? (
<div className="h-[100px] p-[20px] flex justify-center items-center text-sm md:text-base"> <div className="h-[100px] p-[20px] flex justify-center items-center text-base-content text-sm md:text-base">
{deleteUserRes} {deleteUserRes}
</div> </div>
) : ( ) : (
@@ -174,6 +174,7 @@ const AddContact = (props) => {
required required
disabled={addYourself} disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/> />
</div> </div>
<div className="mb-[0.75rem]"> <div className="mb-[0.75rem]">
@@ -193,6 +194,7 @@ const AddContact = (props) => {
required required
disabled={addYourself} disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase"
placeholder={t("enter-email")}
/> />
</div> </div>
{isOptionalDetails && ( {isOptionalDetails && (
@@ -96,8 +96,8 @@ function DownloadPdfZip(props) {
handleClose={() => props.setIsDownloadModal(false)} handleClose={() => props.setIsDownloadModal(false)}
> >
<div className="p-[20px] h-full text-base-content"> <div className="p-[20px] h-full text-base-content">
{downloadType.map((data, ind) => { {downloadType
return ( .map((data, ind) => (
<label <label
key={ind} key={ind}
className="flex items-center gap-1 mb-2 cursor-pointer" className="flex items-center gap-1 mb-2 cursor-pointer"
@@ -111,8 +111,7 @@ function DownloadPdfZip(props) {
/> />
{data.label} {data.label}
</label> </label>
); ))}
})}
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div> <div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button <button
onClick={() => handleDownload()} onClick={() => handleDownload()}
+4 -2
View File
@@ -9,10 +9,12 @@ const ModalUi = ({
showHeader = true, showHeader = true,
showClose = true, showClose = true,
reduceWidth, reduceWidth,
position position,
crossColor
}) => { }) => {
const width = reduceWidth; const width = reduceWidth;
const isBottom = position === "bottom" ? "items-end pb-2 !bg-black/10" : ""; const isBottom = position === "bottom" ? "items-end pb-2 !bg-black/10" : "";
const crossBtnColor = crossColor ?? "text-base-content";
return ( return (
<> <>
{isOpen && ( {isOpen && (
@@ -38,7 +40,7 @@ const ModalUi = ({
)} )}
{showClose && ( {showClose && (
<button <button
className="op-btn op-btn-sm op-btn-circle op-btn-ghost text-base-content absolute right-2 top-2" className={`${crossBtnColor} op-btn op-btn-sm op-btn-circle op-btn-ghost absolute right-2 top-2 z-40`}
onClick={() => handleClose && handleClose()} onClick={() => handleClose && handleClose()}
> >
@@ -0,0 +1,205 @@
import { useMemo, useState } from "react";
import ModalUi from "./ModalUi";
import { useTranslation } from "react-i18next";
/**
* PasswordResetModal
* - Validates password rules (length >= 8, upper, lower, digit, special)
* - Buttons: Submit, Autogenerate (12 chars), Copy (Font Awesome icon only)
* - `Autogenerate` fills a valid 12-char password but remains editable
* - `Copy` copies the current password to clipboard
*
* Props:
* - isOpen: boolean
* - onClose: () => void
* - onSubmit: (password: string) => Promise<void> | void
*/
export default function PasswordResetModal({
userId,
isOpen,
onClose,
onSubmit,
showAlert
}) {
const { t } = useTranslation();
const [password, setPassword] = useState("");
const [submitting, setSubmitting] = useState(false);
const [copied, setCopied] = useState(false);
// Validation helpers
const rules = useMemo(
() => ({
hasUpper: /[A-Z]/,
hasLower: /[a-z]/,
hasDigit: /\d/,
// Richer special set including common keyboard symbols
hasSpecial: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/,
minLen: 8
}),
[]
);
const checks = useMemo(() => {
return {
lenOK: password.length >= rules.minLen,
upperLowerDigitOK:
rules.hasUpper.test(password) &&
rules.hasLower.test(password) &&
rules.hasDigit.test(password),
specialOK: rules.hasSpecial.test(password)
};
}, [password, rules]);
// Always-present condition list with per-condition status
const conditions = useMemo(
() => [
{
key: "len",
ok: checks.lenOK,
text: "password-length"
},
{
key: "uld",
ok: checks.upperLowerDigitOK,
text: "password-case"
},
{
key: "spec",
ok: checks.specialOK,
text: "password-special-case"
}
],
[checks]
);
const allValid = conditions.every((c) => c.ok) && password.length > 0;
// Autogenerate a valid random password (12 chars)
const handleAutogen = () => {
const length = 12;
const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // omit ambiguous I/O
const lower = "abcdefghijkmnopqrstuvwxyz"; // omit ambiguous l
const digits = "23456789"; // omit 0/1
const special = "!@#$%^&*()-_=+[]{};:,.?";
const all = upper + lower + digits + special;
function pick(str) {
return str[Math.floor(Math.random() * str.length)];
}
function shuffle(arr) {
return arr.sort(() => Math.random() - 0.5);
}
let pwd = [pick(upper), pick(lower), pick(digits), pick(special)];
while (pwd.length < length) pwd.push(pick(all));
pwd = shuffle(pwd).join("");
setPassword(pwd);
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(password);
showAlert("success", t("copied"), 1200);
setCopied(true);
setTimeout(() => setCopied(false, 1200));
} catch (e) {
console.error("Clipboard error", e);
}
};
const handleSubmit = async (e) => {
e?.preventDefault?.();
if (!allValid) return; // guard
try {
setSubmitting(true);
await onSubmit?.(userId, password);
setSubmitting(false);
onClose?.();
setPassword("");
} catch (err) {
console.error(err);
setSubmitting(false);
}
};
return (
<>
<ModalUi
isOpen={isOpen}
title={t("reset-password")}
handleClose={() => {
setPassword("");
onClose?.();
}}
>
<form onSubmit={handleSubmit} className="space-y-3 pt-[15px] p-[20px]">
<p className="text-sm text-base-content/60">
{t("enter-strong-password")}
</p>
<label className="op-form-control w-full">
<div className="op-label">
<span className="op-label-text">{t("new-password")}</span>
</div>
<div className="flex gap-2">
<input
type="text"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder="Enter password or click Autogenerate"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoFocus
/>
<button
type="button"
className="op-btn op-btn-square op-btn-sm"
onClick={handleCopy}
title={copied ? "Copied" : "Copy"}
aria-label="Copy password"
disabled={!password}
>
<i
className={`fa-regular fa-copy ${copied ? "opacity-70" : ""}`}
></i>
</button>
</div>
</label>
{/* When everything is valid, show just a right tick */}
<ul className="mt-1 ml-2">
{conditions.map((c) => (
<li
key={c.key}
className={`${c.ok ? "text-success" : "text-error"} text-[12px] leading-snug`}
>
{c.ok ? "✓" : "✗"} {t(c.text)}
</li>
))}
</ul>
<div className="pt-2 flex flex-row gap-x-2">
<button
type="submit"
className="op-btn op-btn-primary"
disabled={!allValid || submitting}
>
{t("submit")}
</button>
<button type="button" className="op-btn" onClick={handleAutogen}>
{t("autogenerate")}
</button>
<button
type="button"
className="op-btn op-btn-ghost"
onClick={() => {
setPassword("");
onClose?.();
}}
>
{t("cancel")}
</button>
</div>
</form>
</ModalUi>
</>
);
}
@@ -63,7 +63,7 @@ function CustomModal(props) {
className="op-btn op-btn-secondary ml-[10px] mb-3 mt-1" className="op-btn op-btn-secondary ml-[10px] mb-3 mt-1"
onClick={() => props.handleDownloadBtn()} onClick={() => props.handleDownloadBtn()}
> >
Download {t("download")}
</button> </button>
)} )}
</div> </div>
@@ -138,6 +138,12 @@ export const RenderReportCell = ({
{rowData?.ExpiryDate ? formatDate(rowData?.ExpiryDate?.iso) : "-"} {rowData?.ExpiryDate ? formatDate(rowData?.ExpiryDate?.iso) : "-"}
</td> </td>
); );
case "Sent Date":
return (
<td key={col} className="p-2 text-center">
{rowData?.DocSentAt ? formatDate(rowData?.DocSentAt?.iso) : "-"}
</td>
);
case "Signers": case "Signers":
return ( return (
<SignerCell <SignerCell
@@ -1,26 +1,29 @@
import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ModalUi from "./ModalUi"; import { useLocation, useNavigate } from "react-router";
import { useNavigate } from "react-router"; import { useDispatch } from "react-redux";
import { sessionStatus } from "../redux/reducers/userReducer";
import Parse from "parse"; import Parse from "parse";
import ModalUi from "./ModalUi";
const ValidateSession = ({ children }) => { const SessionExpiredModal = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch();
const handleLoginBtn = async () => { const handleLoginBtn = async () => {
try { try {
await Parse?.User?.logOut(); await Parse?.User?.logOut();
} catch (err) { } catch (err) {
console.log("err ", err); console.log(`err: ${err}`);
} finally { } finally {
localStorage.removeItem("accesstoken"); localStorage.removeItem("accesstoken");
navigate("/", { replace: true }); dispatch(sessionStatus(true));
navigate("/", { replace: true, state: { from: location } });
} }
}; };
return localStorage.getItem("accesstoken") ? (
children return (
) : (
<ModalUi showHeader={false} isOpen={true} showClose={false}> <ModalUi showHeader={false} isOpen={true} showClose={false}>
<div className="flex flex-col justify-center items-center py-4 md:py-5 gap-5"> <div className="flex flex-col justify-center items-center py-4 md:py-5 gap-5">
<p className="text-xl font-medium">{t("session-expired")}</p> <p className="text-xl font-medium">{t("session-expired")}</p>
@@ -32,4 +35,4 @@ const ValidateSession = ({ children }) => {
); );
}; };
export default ValidateSession; export default SessionExpiredModal;
+1 -1
View File
@@ -14,7 +14,7 @@ const Tooltip = ({
onClick={() => onClick={() =>
handleOnlickHelp ? handleOnlickHelp() : openInNewTab(url) handleOnlickHelp ? handleOnlickHelp() : openInNewTab(url)
} }
className={"text-center cursor-pointer"} className="text-center cursor-pointer focus:outline-none"
> >
<sup> <sup>
<i <i
+5 -31
View File
@@ -1,12 +1,9 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Parse from "parse"; import Parse from "parse";
import { Outlet, useNavigate, useLocation } from "react-router"; import { Outlet } from "react-router";
import ModalUi from "./ModalUi"; import SessionExpiredModal from "./SessionExpiredModal";
import { useTranslation } from "react-i18next";
const Validate = () => { const Validate = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const location = useLocation();
const [isUserValid, setIsUserValid] = useState(true); const [isUserValid, setIsUserValid] = useState(true);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -37,30 +34,7 @@ const Validate = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const handleLoginBtn = async () => { return isUserValid ? <Outlet /> : <SessionExpiredModal />;
try {
await Parse.User.logOut();
} catch (err) {
console.log("err ", err);
} finally {
localStorage.removeItem("accesstoken");
navigate("/", { replace: true, state: { from: location } });
}
};
return isUserValid ? (
<div>
<Outlet />
</div>
) : (
<ModalUi showHeader={false} isOpen={true} showClose={false}>
<div className="flex flex-col justify-center items-center py-4 md:py-5 gap-5">
<p className="text-xl font-medium">{t("session-expired")}</p>
<button onClick={handleLoginBtn} className="op-btn op-btn-neutral">
{t("login")}
</button>
</div>
</ModalUi>
);
}; };
export default Validate; export default Validate;
@@ -0,0 +1,15 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = { isValidSession: true };
const userReducer = createSlice({
name: "user",
initialState: initialState,
reducers: {
sessionStatus: (state, action) => {
state.isValidSession = action.payload;
}
}
});
export const { sessionStatus } = userReducer.actions;
export default userReducer.reducer;
@@ -13,7 +13,8 @@ const initialState = {
lastIndex: "", lastIndex: "",
scrollTriggerId: "", scrollTriggerId: "",
prefillImg: [], prefillImg: [],
prefillImgLoad: {} prefillImgLoad: {},
typedSignFont: "Fasthand"
}; };
const widgetSlice = createSlice({ const widgetSlice = createSlice({
@@ -41,6 +42,9 @@ const widgetSlice = createSlice({
setScrollTriggerId: (state, action) => { setScrollTriggerId: (state, action) => {
state.scrollTriggerId = action.payload; state.scrollTriggerId = action.payload;
}, },
setTypedSignFont: (state, action) => {
state.typedSignFont = action.payload;
},
setPrefillImg: (state, action) => { setPrefillImg: (state, action) => {
const existingIndex = state.prefillImg.findIndex( const existingIndex = state.prefillImg.findIndex(
(img) => img.id === action.payload.id (img) => img.id === action.payload.id
@@ -73,7 +77,8 @@ export const {
setLastIndex, setLastIndex,
setScrollTriggerId, setScrollTriggerId,
setPrefillImg, setPrefillImg,
setPrefillImgLoad setPrefillImgLoad,
setTypedSignFont
} = widgetSlice.actions; } = widgetSlice.actions;
export default widgetSlice.reducer; export default widgetSlice.reducer;
+3 -1
View File
@@ -4,6 +4,7 @@ import ShowTenant from "./reducers/ShowTenant";
import TourStepsReducer from "./reducers/TourStepsReducer"; import TourStepsReducer from "./reducers/TourStepsReducer";
import widgetReducer from "./reducers/widgetSlice"; import widgetReducer from "./reducers/widgetSlice";
import sidebarReducer from "./reducers/sidebarReducer"; import sidebarReducer from "./reducers/sidebarReducer";
import userReducer from "./reducers/userReducer";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@@ -11,6 +12,7 @@ export const store = configureStore({
TourSteps: TourStepsReducer, TourSteps: TourStepsReducer,
ShowTenant, ShowTenant,
widget: widgetReducer, widget: widgetReducer,
sidebar: sidebarReducer sidebar: sidebarReducer,
user: userReducer
} }
}); });
@@ -88,6 +88,7 @@ const EditContactForm = (props) => {
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))} onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
required required
/> />
</div> </div>
@@ -105,6 +106,7 @@ const EditContactForm = (props) => {
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))} onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
required required
/> />
</div> </div>
@@ -90,6 +90,7 @@ const DocumentsReport = (props) => {
const [documentId, setDocumentId] = useState(""); const [documentId, setDocumentId] = useState("");
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" }); const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [isPrefillModal, setIsPrefillModal] = useState({}); const [isPrefillModal, setIsPrefillModal] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isMailModal, setIsMailModal] = useState(false); const [isMailModal, setIsMailModal] = useState(false);
const [customizeMail, setCustomizeMail] = useState({ body: "", subject: "" }); const [customizeMail, setCustomizeMail] = useState({ body: "", subject: "" });
@@ -105,6 +106,15 @@ const DocumentsReport = (props) => {
} }
}, [props.isSearchResult]); }, [props.isSearchResult]);
// Close dropdown when clicking outside or on button again
useEffect(() => {
const onDocClick = (e) => {
if (!e.target.closest('[data-dropdown-root="1"]')) setIsOption({});
};
document.addEventListener("click", onDocClick);
return () => document.removeEventListener("click", onDocClick);
}, []);
const getPaginationRange = () => { const getPaginationRange = () => {
const totalPageNumbers = 7; // Adjust this value to show more/less page numbers const totalPageNumbers = 7; // Adjust this value to show more/less page numbers
const pages = []; const pages = [];
@@ -329,6 +339,8 @@ const DocumentsReport = (props) => {
setIsModal({ [`extendexpiry_${item.objectId}`]: true }); setIsModal({ [`extendexpiry_${item.objectId}`]: true });
} }
}; };
// Get current list // Get current list
const indexOfLastDoc = currentPage * props.docPerPage; const indexOfLastDoc = currentPage * props.docPerPage;
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage; const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
@@ -495,7 +507,9 @@ const DocumentsReport = (props) => {
const docId = props?.ReportName !== "Templates" && item.objectId; const docId = props?.ReportName !== "Templates" && item.objectId;
if (url) { if (url) {
try { try {
if (isCompleted) { if (
isCompleted
) {
setIsDownloadModal({ [item.objectId]: true }); setIsDownloadModal({ [item.objectId]: true });
} else { } else {
const signedUrl = await getSignedUrl( const signedUrl = await getSignedUrl(
@@ -789,6 +803,7 @@ const DocumentsReport = (props) => {
}; };
const handleCloseModal = () => { const handleCloseModal = () => {
setError("");
setIsModal({}); setIsModal({});
}; };
const handleSaveAsTemplate = async (doc) => { const handleSaveAsTemplate = async (doc) => {
@@ -920,6 +935,7 @@ const DocumentsReport = (props) => {
}; };
//`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document //`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document
const handlePrefillWidgetCreateDoc = async () => { const handlePrefillWidgetCreateDoc = async () => {
setIsSubmit(true);
const scale = 1; const scale = 1;
const key = Object.keys(isPrefillModal)[0]; const key = Object.keys(isPrefillModal)[0];
setActLoader({ [key]: true }); setActLoader({ [key]: true });
@@ -957,6 +973,7 @@ const DocumentsReport = (props) => {
alert(t("user-not-exist")); alert(t("user-not-exist"));
} }
} }
setIsSubmit(false);
setActLoader({}); setActLoader({});
}; };
const handleRecipientSign = (documentId, currentId) => { const handleRecipientSign = (documentId, currentId) => {
@@ -1027,10 +1044,10 @@ const DocumentsReport = (props) => {
if (isPrefill) { if (isPrefill) {
return ( return (
<span className="flex text-sm mt-3 text-red-500"> <span className="flex text-sm mt-3 text-red-500">
{t("save-as-temp-warn")} {t("save-as-temp-warn")}
</span> </span>
); );
} }
}; };
return ( return (
<div className="relative"> <div className="relative">
@@ -1166,6 +1183,7 @@ const DocumentsReport = (props) => {
<React.Fragment key={index}> <React.Fragment key={index}>
{handleBtnVisibility(act, item) && ( {handleBtnVisibility(act, item) && (
<div <div
data-dropdown-root="1"
role="button" role="button"
data-tut={act?.selector} data-tut={act?.selector}
onClick={() => handleActionBtn(act, item)} onClick={() => handleActionBtn(act, item)}
@@ -1367,6 +1385,7 @@ const DocumentsReport = (props) => {
setIsNewContact={setIsNewContact} setIsNewContact={setIsNewContact}
isNewContact={isNewContact} isNewContact={isNewContact}
docId={item.objectId} docId={item.objectId}
isSubmit={isSubmit}
/> />
)} )}
{isModal["extendexpiry_" + item.objectId] && ( {isModal["extendexpiry_" + item.objectId] && (
@@ -30,7 +30,7 @@ import "../../styles/quill.css";
import BulkSendUi from "../../components/BulkSendUi"; import BulkSendUi from "../../components/BulkSendUi";
import Loader from "../../primitives/Loader"; import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo"; import { serverUrl_fn } from "../../constant/appinfo";
import { useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useElSize } from "../../hook/useElSize"; import { useElSize } from "../../hook/useElSize";
import LottieWithLoader from "../../primitives/DotLottieReact"; import LottieWithLoader from "../../primitives/DotLottieReact";
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal"; import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
@@ -67,7 +67,7 @@ const TemplatesReport = (props) => {
const [userDetails, setUserDetails] = useState({}); const [userDetails, setUserDetails] = useState({});
const [isNextStep, setIsNextStep] = useState({}); const [isNextStep, setIsNextStep] = useState({});
const [isBulkSend, setIsBulkSend] = useState({}); const [isBulkSend, setIsBulkSend] = useState({});
const [templateDeatils, setTemplateDetails] = useState({}); const [templateDetails, setTemplateDetails] = useState({});
const [placeholders, setPlaceholders] = useState([]); const [placeholders, setPlaceholders] = useState([]);
const [isLoader, setIsLoader] = useState({}); const [isLoader, setIsLoader] = useState({});
const [isShareWith, setIsShareWith] = useState({}); const [isShareWith, setIsShareWith] = useState({});
@@ -85,6 +85,7 @@ const TemplatesReport = (props) => {
const [documentId, setDocumentId] = useState(""); const [documentId, setDocumentId] = useState("");
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" }); const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [isPrefillModal, setIsPrefillModal] = useState({}); const [isPrefillModal, setIsPrefillModal] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const startIndex = (currentPage - 1) * props.docPerPage; const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props; const { isMoreDocs, setIsNextRecord } = props;
const [isMailModal, setIsMailModal] = useState(false); const [isMailModal, setIsMailModal] = useState(false);
@@ -164,6 +165,15 @@ const TemplatesReport = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// Close dropdown when clicking outside or on button again
useEffect(() => {
const onDocClick = (e) => {
if (!e.target.closest('[data-dropdown-root="1"]')) setIsOption({});
};
document.addEventListener("click", onDocClick);
return () => document.removeEventListener("click", onDocClick);
}, []);
// `fetchTeamList` is used to fetch team list for share with functionality // `fetchTeamList` is used to fetch team list for share with functionality
const fetchTeamList = async () => { const fetchTeamList = async () => {
try { try {
@@ -238,9 +248,8 @@ const TemplatesReport = (props) => {
); );
if (checkIsSignatureExist) { if (checkIsSignatureExist) {
setActLoader({ [`${item.objectId}_${act.btnId}`]: true }); setActLoader({ [`${item.objectId}_${act.btnId}`]: true });
const templateDeatils = await fetchTemplate(item.objectId); const template = await fetchTemplate(item.objectId);
const templateData = const templateData = template.data && template.data.result;
templateDeatils.data && templateDeatils.data.result;
if (!templateData.error) { if (!templateData.error) {
setXyPosition(templateData?.Placeholders); setXyPosition(templateData?.Placeholders);
const signer = utils.handleSignersList(templateData); const signer = utils.handleSignersList(templateData);
@@ -283,7 +292,7 @@ const TemplatesReport = (props) => {
} }
} }
); );
if (axiosRes) { if (axiosRes?.data) {
return axiosRes; return axiosRes;
} }
} catch (e) { } catch (e) {
@@ -296,10 +305,10 @@ const TemplatesReport = (props) => {
const navigatePageToDoc = async (templateRes, placeholder, signer) => { const navigatePageToDoc = async (templateRes, placeholder, signer) => {
setIsPrefillModal({}); setIsPrefillModal({});
const res = await createDocument( const res = await createDocument(
[templateRes || templateDeatils], [templateRes || templateDetails],
placeholder || xyPosition, placeholder || xyPosition,
signer || signerList, signer || signerList,
templateRes?.URL || templateDeatils?.URL templateRes?.URL || templateDetails?.URL
); );
if (res.status === "success") { if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, { navigate(`/placeHolderSign/${res.id}`, {
@@ -871,14 +880,15 @@ const TemplatesReport = (props) => {
}; };
//`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document //`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document
const handlePrefillWidgetCreateDoc = async () => { const handlePrefillWidgetCreateDoc = async () => {
setIsSubmit(true);
const scale = 1; const scale = 1;
const res = await utils?.handleCheckPrefillCreateDoc( const res = await utils?.handleCheckPrefillCreateDoc(
xyPosition, xyPosition,
signerList, signerList,
setIsPrefillModal, setIsPrefillModal,
scale, scale,
templateDeatils?.URL, templateDetails?.URL,
[templateDeatils], [templateDetails],
prefillImg, prefillImg,
extClass?.[0]?.UserId?.objectId, extClass?.[0]?.UserId?.objectId,
); );
@@ -940,6 +950,7 @@ const TemplatesReport = (props) => {
alert(t("user-not-exist")); alert(t("user-not-exist"));
} }
} }
setIsSubmit(false);
}; };
//function show signer list and share link to share signUrl //function show signer list and share link to share signUrl
const handleShareList = () => { const handleShareList = () => {
@@ -1150,6 +1161,7 @@ const TemplatesReport = (props) => {
role="button" role="button"
data-tut={act?.selector} data-tut={act?.selector}
key={index} key={index}
data-dropdown-root="1"
onClick={() => handleActionBtn(act, item)} onClick={() => handleActionBtn(act, item)}
title={t(`btnLabel.${act.hoverLabel}`)} title={t(`btnLabel.${act.hoverLabel}`)}
className={ className={
@@ -1257,6 +1269,7 @@ const TemplatesReport = (props) => {
setIsNewContact={setIsNewContact} setIsNewContact={setIsNewContact}
isNewContact={isNewContact} isNewContact={isNewContact}
docId={item.objectId} docId={item.objectId}
isSubmit={isSubmit}
/> />
)} )}
{isShareWith[item.objectId] && ( {isShareWith[item.objectId] && (
@@ -1328,7 +1341,7 @@ const TemplatesReport = (props) => {
) : ( ) : (
<BulkSendUi <BulkSendUi
Placeholders={placeholders} Placeholders={placeholders}
item={templateDeatils} item={templateDetails}
handleClose={handleQuickSendClose} handleClose={handleQuickSendClose}
signatureType={signatureType} signatureType={signatureType}
/> />
+46 -11
View File
@@ -1,4 +1,4 @@
import { generateTitleFromFilename, getBase64FromUrl } from "../constant/Utils"; import { generateTitleFromFilename } from "../constant/Utils";
import { base64StringtoFile, uploadFile } from "./fileUtils"; import { base64StringtoFile, uploadFile } from "./fileUtils";
import { sanitizeFileName } from "./sanitizeFileName"; import { sanitizeFileName } from "./sanitizeFileName";
import Parse from "parse"; import Parse from "parse";
@@ -16,22 +16,20 @@ export const saveToMySign = async (widget) => {
: `${replaceSpace}__initials`; : `${replaceSpace}__initials`;
const file = base64StringtoFile(base64, fileName); const file = base64StringtoFile(base64, fileName);
const fileUrl = await uploadFile(file, User?.id); const fileUrl = await uploadFile(file, User?.id);
// below code is used to save or update default signature, initials, stamp const params = {
const signCls = new Parse.Object("contracts_Signature"); id: widget?.defaultSignId, // pass id to update existing signature/initials
if (widget?.defaultSignId) { userId: User.id
signCls.id = widget.defaultSignId; };
}
if (widget?.type === "initials") { if (widget?.type === "initials") {
signCls.set("Initials", fileUrl); params.initials = fileUrl; // save initials image url
} else if (widget?.type === "signature") { } else if (widget?.type === "signature") {
signCls.set("ImageURL", fileUrl); params.signature = fileUrl; // save signature image url
} }
signCls.set("UserId", Parse.User.createWithoutData(User.id)); const signRes = await Parse.Cloud.run("savesignature", params);
const signRes = await signCls.save();
return { base64File: base64, id: signRes?.id }; return { base64File: base64, id: signRes?.id };
} catch (err) { } catch (err) {
console.log("Err while saving signature", err); console.log("Err while saving signature", err);
return url; return err;
} }
} }
}; };
@@ -41,3 +39,40 @@ export const hasSignatureWidget = (s) =>
(s.placeHolder ?? []).some((ph) => (s.placeHolder ?? []).some((ph) =>
(ph?.pos ?? []).some((p) => p?.type === "signature") (ph?.pos ?? []).some((p) => p?.type === "signature")
); );
/**
*
* @param {Array<object>} pages
* Array of page objects. Each page is expected to have:
* - pos: Array<Widget> where Widget has shape:
* {
* key: string,
* type: string,
* options?: {
* response?: number|string,
* defaultValue?: number|string,
* formula?: string
* }
* }
*
* @returns {Array<object>}
*/
export const applyNumberFormulasToPages = (pages = []) => {
// Guard: if not an array or empty, just return as-is
if (!Array.isArray(pages) || pages.length === 0) {
return pages;
}
// Clone pages and each widget.options so we do not mutate caller's data.
// (Shallow clone is sufficient for our current usage since we only touch `options`.)
const clonedPages = pages.map((page) => ({
...page,
pos: (page?.pos || []).map((widget) => ({
...widget,
options: { ...widget.options }
}))
}));
// Return the cloned, updated pages
return clonedPages;
};
+1
View File
@@ -3,6 +3,7 @@ import { format, toZonedTime } from 'date-fns-tz';
import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js'; import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { PDFDocument, rgb } from 'pdf-lib'; import { PDFDocument, rgb } from 'pdf-lib';
import { parseUploadFile } from './utils/fileUtils.js';
dotenv.config({ quiet: true }); dotenv.config({ quiet: true });
@@ -2,20 +2,24 @@ import express from 'express';
import cors from 'cors'; import cors from 'cors';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import uploadFile from './uploadFile.js'; import uploadFile from './uploadFile.js';
import docxtopdf, { upload as docxUpload } from './docxtopdf.js'; import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js'; import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
import { deleteUserByAdmin, deleteUserGet, deleteUserPost } from './deleteUser.js'; import { deleteUserByAdmin, deleteUserPost } from './deleteAccount/deleteUser.js';
import { deleteUserGet } from './deleteAccount/deleteUserGet.js';
import { deleteUserOtp } from './deleteAccount/deleteUserOtp.js';
export const app = express(); export const app = express();
dotenv.config({ quiet: true }); dotenv.config({ quiet: true });
app.use(cors()); app.use(cors());
app.use(express.json({ limit: '50mb' })); app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(express.urlencoded({ limit: '100mb', extended: true }));
app.post('/file_upload', uploadFile); app.post('/file_upload', uploadFile);
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf); app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf); app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
app.get('/delete-account/:userId', deleteUserGet); app.get('/delete-account/:userId', deleteUserGet);
app.post('/delete-account/:userId/otp', deleteUserOtp);
app.post('/delete-account/:userId', deleteUserPost); app.post('/delete-account/:userId', deleteUserPost);
app.post('/deleteuser/:userId', deleteUserByAdmin); app.post('/deleteuser/:userId', deleteUserByAdmin);
@@ -1,7 +1,7 @@
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import { serverAppId } from '../../Utils.js'; import { serverAppId } from '../../../Utils.js';
// === Configuration === // === Configuration ===
const serverHost = new URL(process.env.SERVER_URL).hostname; const serverHost = new URL(process.env.SERVER_URL).hostname;
@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { cloudServerUrl, generateId, serverAppId } from '../../Utils.js'; import { cloudServerUrl, generateId, serverAppId } from '../../../Utils.js';
import sendmailtoSupport from './sendMailToSupport.js';
import { deleteContactsInBatch, deleteDataFiles, deleteInBatches } from './deleteFileUrl.js'; import { deleteContactsInBatch, deleteDataFiles, deleteInBatches } from './deleteFileUrl.js';
import { MAX_ATTEMPTS } from './deleteUtils.js';
const serverUrl = cloudServerUrl; const serverUrl = cloudServerUrl;
const appId = serverAppId; const appId = serverAppId;
@@ -32,7 +32,7 @@ const resetPasswordAndDeleteSession = async userId => {
await Parse.Object.destroyAll(sessions, { useMasterKey: true }); await Parse.Object.destroyAll(sessions, { useMasterKey: true });
} }
}; };
export async function deleteUser(userId, adminId) { export async function deleteUser(userId, adminId, adminTenantId, isOrgAdmin, orgPtr) {
const userPointer = { __type: 'Pointer', className: '_User', objectId: userId }; const userPointer = { __type: 'Pointer', className: '_User', objectId: userId };
let userDetails = { let userDetails = {
UserRole: 'not found', UserRole: 'not found',
@@ -47,14 +47,18 @@ export async function deleteUser(userId, adminId) {
const Users = Parse.Object.extend('contracts_Users'); const Users = Parse.Object.extend('contracts_Users');
const userQuery = new Parse.Query(Users); const userQuery = new Parse.Query(Users);
userQuery.equalTo('UserId', userPointer); userQuery.equalTo('UserId', userPointer);
if (adminId) { if (adminTenantId) {
userQuery.equalTo('TenantId', adminTenantId);
if (isOrgAdmin && orgPtr) {
userQuery.equalTo('OrganizationId', orgPtr);
}
} else if (adminId) {
userQuery.equalTo('CreatedBy', { __type: 'Pointer', className: '_User', objectId: adminId }); userQuery.equalTo('CreatedBy', { __type: 'Pointer', className: '_User', objectId: adminId });
} }
const userResult = await userQuery.first({ useMasterKey: true }); const userResult = await userQuery.first({ useMasterKey: true });
userDetails = { ...userDetails, UserId: userId }; userDetails = { ...userDetails, UserId: userId };
if (!userResult) { if (!userResult) {
const errorMessage = 'User not found.'; const errorMessage = isOrgAdmin ? 'Unauthorized.' : 'User not found.';
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
const contractsUserId = userResult.id; const contractsUserId = userResult.id;
@@ -62,6 +66,10 @@ export async function deleteUser(userId, adminId) {
const teamIds = userResult.get('TeamIds') || []; const teamIds = userResult.get('TeamIds') || [];
const organizationId = userResult.get('OrganizationId')?.id; const organizationId = userResult.get('OrganizationId')?.id;
const isAdmin = userResult?.get('UserRole') === 'contracts_Admin' ? true : false; const isAdmin = userResult?.get('UserRole') === 'contracts_Admin' ? true : false;
if (isOrgAdmin && isAdmin) {
const errorMessage = 'Unauthorized.';
return { code: 400, message: errorMessage };
}
userDetails = { userDetails = {
...userDetails, ...userDetails,
UserRole: userResult?.get('UserRole'), UserRole: userResult?.get('UserRole'),
@@ -73,7 +81,6 @@ export async function deleteUser(userId, adminId) {
}; };
if (adminId && isAdmin) { if (adminId && isAdmin) {
const errorMessage = 'An error occurred while deleting your account.'; const errorMessage = 'An error occurred while deleting your account.';
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -85,7 +92,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed during contracts_Template cleanup:', err); console.error('Failed during contracts_Template cleanup:', err);
const errorMessage = 'Failed during contracts_Template cleanup:' + err?.message; const errorMessage = 'Failed during contracts_Template cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -95,7 +101,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed during contactbook cleanup:', err); console.error('Failed during contactbook cleanup:', err);
const errorMessage = 'Failed during contactbook cleanup:' + err?.message; const errorMessage = 'Failed during contactbook cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -116,7 +121,6 @@ export async function deleteUser(userId, adminId) {
console.error('Failed during contactbook current user cleanup:', err); console.error('Failed during contactbook current user cleanup:', err);
const errorMessage = const errorMessage =
'Failed during contactbook current user cleanup: ' + (err?.message || err); 'Failed during contactbook current user cleanup: ' + (err?.message || err);
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -130,7 +134,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed to delete appToken entries:', err); console.error('Failed to delete appToken entries:', err);
const errorMessage = 'Failed to delete appToken entries:' + err?.message; const errorMessage = 'Failed to delete appToken entries:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -140,7 +143,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed during partners_DataFiles cleanup:', err); console.error('Failed during partners_DataFiles cleanup:', err);
const errorMessage = 'Failed during partners_DataFiles cleanup:' + err?.message; const errorMessage = 'Failed during partners_DataFiles cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -156,7 +158,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed to delete contracts_Organizations entry:', err); console.error('Failed to delete contracts_Organizations entry:', err);
const errorMessage = 'Failed to delete contracts_Organizations entry:' + err?.message; const errorMessage = 'Failed to delete contracts_Organizations entry:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
// STEP 7: Delete each entry in contracts_Teams by objectId from teamIds // STEP 7: Delete each entry in contracts_Teams by objectId from teamIds
@@ -170,7 +171,6 @@ export async function deleteUser(userId, adminId) {
} catch (teamErr) { } catch (teamErr) {
console.error(`Failed to delete team with ID ${team.id}:`, teamErr); console.error(`Failed to delete team with ID ${team.id}:`, teamErr);
const errorMessage = `Failed to delete team with ID ${team.id}` + teamErr?.message; const errorMessage = `Failed to delete team with ID ${team.id}` + teamErr?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
} }
@@ -178,7 +178,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed during contracts_Teams deletion loop:', err); console.error('Failed during contracts_Teams deletion loop:', err);
const errorMessage = 'Failed during contracts_Teams deletion loop:' + err?.message; const errorMessage = 'Failed during contracts_Teams deletion loop:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -194,7 +193,6 @@ export async function deleteUser(userId, adminId) {
const msg = `Failed during partners_Tenant ${'cleanup:'} `; const msg = `Failed during partners_Tenant ${'cleanup:'} `;
console.error(msg, err); console.error(msg, err);
const errorMessage = msg + err?.message; const errorMessage = msg + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -214,7 +212,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed during partners_TenantCredits cleanup:', err); console.error('Failed during partners_TenantCredits cleanup:', err);
const errorMessage = 'Failed during partners_TenantCredits cleanup:' + err?.message; const errorMessage = 'Failed during partners_TenantCredits cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
} }
@@ -228,7 +225,6 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed during contracts_Signature cleanup:', err); console.error('Failed during contracts_Signature cleanup:', err);
const errorMessage = 'Failed during contracts_Signature cleanup:' + err?.message; const errorMessage = 'Failed during contracts_Signature cleanup:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
@@ -238,120 +234,20 @@ export async function deleteUser(userId, adminId) {
} catch (err) { } catch (err) {
console.error('Failed to delete contracts_Users entry:', err); console.error('Failed to delete contracts_Users entry:', err);
const errorMessage = 'Failed to delete contracts_Users entry:' + err?.message; const errorMessage = 'Failed to delete contracts_Users entry:' + err?.message;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
return { code: 200, message: 'User and all associated data deleted successfully.' }; return { code: 200, message: 'User and all associated data deleted successfully.' };
} catch (error) { } catch (error) {
console.error('User deletion process failed:', error); console.error('User deletion process failed:', error);
const errorMessage = `User deletion failed: ${error.message || error}`; const errorMessage = `User deletion failed: ${error.message || error}`;
sendmailtoSupport(userDetails, errorMessage);
return { code: 400, message: errorMessage }; return { code: 400, message: errorMessage };
} }
} }
// 1. HTML Password Prompt Page
export const deleteUserGet = async (req, res) => {
const { userId } = req.params;
const extUserQuery = new Parse.Query('Contracts_Users');
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) {
const errorMessage = 'User not found.';
return res.send(errorMessage);
}
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
const htmlForm = `
<html>
<head>
<title>Delete Account</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background-color: #ffffff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
text-align: center;
}
h2 {
color: #dc3545;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 10px;
font-weight: 600;
}
input[type="password"] {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
button {
background-color: #d9534f;
color: #ffffff;
border: none;
padding: 12px 20px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #c9302c;
}
.warning {
color: #6c757d;
font-size: 14px;
margin-top: -10px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2>Confirm Account Deletion</h2>
<p class="warning">This action is irreversible. Please confirm by entering your password.</p>
<form method="POST" action="${routePath}/delete-account/${userId}">
<label for="password">Password</label>
<input type="password" name="password" id="password" placeholder="Please provide your password" required />
<button type="submit">Delete My Account</button>
</form>
</div>
</body>
</html>
`;
res.send(htmlForm);
};
// 2. Handle Password Verification and Deletion // 2. Handle Password Verification and Deletion
export const deleteUserPost = async (req, res) => { export const deleteUserPost = async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : ''; const { otp } = req.body;
const { password } = req.body;
let userDetails = { let userDetails = {
UserRole: 'not found', UserRole: 'not found',
Name: 'not found', Name: 'not found',
@@ -369,33 +265,68 @@ export const deleteUserPost = async (req, res) => {
const user = await userQuery.first({ useMasterKey: true }); const user = await userQuery.first({ useMasterKey: true });
if (!user) { if (!user) {
const errorMessage = 'User not found.'; const errorMessage = 'User not found.';
// sendmailtoSupport(userDetails, errorMessage);
// return res.status(404).send(errorMessage);
return res.send(errorMessage); return res.send(errorMessage);
} }
const extUserQuery = new Parse.Query('Contracts_Users'); const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId }); extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true }); const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) { if (!extUser) {
const errorMessage = 'User not found.'; const errorMessage = 'User not found.';
return res.send(errorMessage); return res.send(errorMessage);
} }
// 2. Attempt login to verify password
const username = user.get('username'); // assuming 'username' is used for login if (extUser?.get('UserRole') !== 'contracts_Admin') {
const errorMessage =
'This action is not permitted. Kindly contact your administrator to request account deletion.';
return res.send(errorMessage);
}
const extUsers = new Parse.Query('contracts_Users');
extUsers.equalTo('TenantId', extUser?.get('TenantId'));
extUsers.notEqualTo('UserRole', 'contracts_Admin');
const isTeamUsers = await extUsers.first({ useMasterKey: true });
if (isTeamUsers) {
const errorMessage = `To delete this account, start by removing all team users associated with it. Once all users are removed, you'll be able to permanently delete the account.`;
return res.send(errorMessage);
}
// Get stored OTP info
const savedOtp = extUser.get('DeleteOTP') || '';
const expiry = extUser.get('DeleteOTPExpiry');
const tries = Number(extUser.get('DeleteOTPTries') || 0);
if (tries >= MAX_ATTEMPTS) {
return res.status(429).send('Too many invalid attempts. Please resend OTP and try again.');
}
if (!otp || typeof otp !== 'string') {
// Count attempt
extUser.set('DeleteOTPTries', tries + 1);
await extUser.save(null, { useMasterKey: true });
return res.status(400).send('OTP is required.');
}
if (!savedOtp) {
return res.status(400).send('No OTP found. Please request a new OTP.');
}
if (expiry && Date.now() > expiry.getTime()) {
return res.status(400).send('OTP has expired. Please request a new OTP.');
}
if (otp !== savedOtp) {
// Increment tries on mismatch
extUser.set('DeleteOTPTries', tries + 1);
await extUser.save(null, { useMasterKey: true });
return res.status(400).send('Invalid OTP.');
}
// 2. Remove OTP related data
try { try {
// await Parse.User.logIn(username, password); // Will throw if password invalid extUser.unset('DeleteOTP');
// Use REST login to avoid mutating the global Parse current user extUser.unset('DeleteOTPExpiry');
// Will throw if password invalid extUser.unset('DeleteOTPSentAt');
const res = await axios.get(serverUrl + '/login', { extUser.unset('DeleteOTPTries');
params: { username, password }, await extUser.save(null, { useMasterKey: true });
headers: { 'X-Parse-Application-Id': appId },
});
console.log('Res ', res?.data);
} catch (err) { } catch (err) {
console.log('err while validating password: ', err?.response?.data || err); console.log('err while validating password: ', err?.response?.data || err);
const errorMessage = `Invalid password. <a href="${routePath}/delete-account/${userId}">Try again</a>`;
sendmailtoSupport(userDetails, errorMessage);
return res.status(401).send(errorMessage);
} }
const response = await deleteUser(userId); const response = await deleteUser(userId);
@@ -405,7 +336,6 @@ export const deleteUserPost = async (req, res) => {
} catch (error) { } catch (error) {
console.error('Account deletion error:', error); console.error('Account deletion error:', error);
const errorMessage = error?.message || 'An error occurred while deleting your account.'; const errorMessage = error?.message || 'An error occurred while deleting your account.';
sendmailtoSupport(userDetails, errorMessage);
return res.status(500).send(errorMessage); return res.status(500).send(errorMessage);
} }
}; };
@@ -444,10 +374,32 @@ export const deleteUserByAdmin = async (req, res) => {
const user = await userQuery.first({ useMasterKey: true }); const user = await userQuery.first({ useMasterKey: true });
if (!user) { if (!user) {
const errorMessage = 'User not found.'; const errorMessage = 'User not found.';
sendmailtoSupport(userDetails, errorMessage);
return res.status(400).json({ message: errorMessage }); return res.status(400).json({ message: errorMessage });
} }
const response = await deleteUser(userId, adminId);
if (adminId === userId) {
return res.status(400).json({ message: 'You cannot delete your own account.' });
}
// 2. ext user details
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: adminId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) {
const errorMessage = 'User not found.';
return res.status(400).json({ message: errorMessage });
}
const isAdmin =
extUser?.get('UserRole') === 'contracts_Admin' ||
extUser?.get('UserRole') === 'contracts_OrgAdmin'
? true
: false;
const isOrgAdmin = extUser?.get('UserRole') === 'contracts_OrgAdmin';
const tenantId = extUser?.get('TenantId');
const orgPtr = isOrgAdmin && extUser?.get('OrganizationId');
if (!isAdmin) {
return res.status(400).json({ message: 'Unauthorized.' });
}
const response = await deleteUser(userId, adminId, tenantId, isOrgAdmin, orgPtr);
const code = response?.code || 400; const code = response?.code || 400;
const message = response?.message || 'An error occurred while deleting your account.'; const message = response?.message || 'An error occurred while deleting your account.';
return res.status(code).json({ message: message }); return res.status(code).json({ message: message });
@@ -458,7 +410,6 @@ export const deleteUserByAdmin = async (req, res) => {
error?.message || error?.message ||
'An error occurred while deleting your account.'; 'An error occurred while deleting your account.';
console.error(`Account deletion error:`, errorMessage); console.error(`Account deletion error:`, errorMessage);
sendmailtoSupport(userDetails, errorMessage);
return res.status(code).json({ message: errorMessage }); return res.status(code).json({ message: errorMessage });
} }
}; };
@@ -0,0 +1,143 @@
import { OTP_LENGTH, RESEND_COOLDOWN_SEC } from './deleteUtils.js';
// 1. HTML Password Prompt Page
export const deleteUserGet = async (req, res) => {
const { userId } = req.params;
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) return res.status(404).send('User not found.');
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
const htmlForm = `
<html>
<head>
<title>Delete Account</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; }
.container { background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); width: 100%; max-width: 420px; text-align: center; }
h2 { color: #dc3545; margin-bottom: 14px; }
p.warning { color: #6c757d; font-size: 14px; margin-top: 0; margin-bottom: 18px; }
label { display: block; margin-bottom: 10px; font-weight: 600; text-align: left; }
input[type="text"] { width: 100%; padding: 12px; margin-bottom: 16px; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; letter-spacing: 0.2em; }
button { background-color: #d9534f; color: #ffffff; border: none; padding: 12px 16px; font-size: 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; }
button:hover { background-color: #c9302c; }
.secondary { background-color: #6c757d; }
.secondary:hover { background-color: #5a6268; }
.btn-block { width: 100%; }
.link-btn { background: transparent; border: none; color: #0d6efd; text-decoration: underline; padding: 0; font-size: 14px; cursor: pointer; }
.link-btn[disabled] { color: #6c757d; text-decoration: none; cursor: not-allowed; }
.muted { color: #6c757d; font-size: 13px; margin-top: 8px; min-height: 18px; }
.error { color: #c9302c; font-size: 13px; margin-top: 8px; min-height: 18px; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<h2>Confirm Account Deletion</h2>
<!-- Initial view: only Send OTP -->
<div id="preOtp">
<p class="warning">This action is irreversible. A verification mail will be sent to your account to your registered email.</p>
<button type="button" id="sendOtpBtn" class="secondary btn-block">Send OTP</button>
<div id="preMsg" class="muted"></div>
<div id="preErr" class="error"></div>
</div>
<!-- Shown after OTP is sent -->
<form id="otpForm" class="hidden" method="POST" action="${routePath}/delete-account/${userId}">
<p class="warning">This action is irreversible. Verify with an OTP sent to your registered email.</p>
<label for="otp">One-Time Password (OTP)</label>
<input type="text" name="otp" id="otp" placeholder="Enter ${OTP_LENGTH}-digit OTP" required maxlength="${OTP_LENGTH}" inputmode="numeric" />
<div style="text-align:center; margin-top: 10px;">
<button type="submit" id="deleteBtn">Delete My Account</button>
</div>
<div style="text-align:center; margin-top: 10px;">
<button type="button" id="resendOtpLink" class="link-btn">Resend OTP</button>
</div>
<div id="timer" class="muted"></div>
<div id="msg" class="muted"></div>
<div id="err" class="error"></div>
</form>
</div>
<script>
(function(){
const preOtp = document.getElementById('preOtp');
const sendBtn = document.getElementById('sendOtpBtn');
const preMsg = document.getElementById('preMsg');
const preErr = document.getElementById('preErr');
const form = document.getElementById('otpForm');
const otpInput = document.getElementById('otp');
const resendLink = document.getElementById('resendOtpLink');
const timerEl = document.getElementById('timer');
const msgEl = document.getElementById('msg');
const errEl = document.getElementById('err');
const RESEND_WAIT = ${RESEND_COOLDOWN_SEC};
let countdown = 0;
let iv = null;
function setText(el, t){ el.textContent = t || ''; }
function updateResendState(){
resendLink.disabled = countdown>0;
if(countdown>0){
resendLink.setAttribute('disabled','true');
resendLink.style.pointerEvents='none';
}else{
resendLink.removeAttribute('disabled');
resendLink.style.pointerEvents='auto';
}
}
function tick(){
if(countdown<=0){ clearInterval(iv); iv=null; setText(timerEl,'You can resend the OTP now.'); updateResendState(); return; }
setText(timerEl,'Resend available in '+countdown+'s'); countdown--; updateResendState();
}
function startTimer(sec){ countdown=sec||RESEND_WAIT; if(iv) clearInterval(iv); tick(); iv=setInterval(tick,1000); }
async function sendOtp(showForm){
setText(preErr,''); setText(preMsg,'Sending OTP...');
try {
const resp = await fetch('${routePath}/delete-account/${userId}/otp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
const data = await resp.json().catch(()=>({}));
if(!resp.ok){
if(resp.status===429 && data?.retryAfterSec){
startTimer(data.retryAfterSec); setText(preErr,'Please wait '+data.retryAfterSec+'s before resending.'); setText(preMsg,'');
return;
}
throw new Error(data?.error || 'Failed to send OTP.');
}
startTimer(data?.cooldownSec || RESEND_WAIT);
// Success: hide pre, show form
preOtp.classList.add('hidden');
form.classList.remove('hidden');
setText(msgEl,'OTP sent to your registered email.');
otpInput.focus();
} catch(e){
setText(preErr,e.message||'Error sending OTP.');
setText(preMsg,'');
}
}
sendBtn.addEventListener('click',()=>sendOtp(true));
resendLink.addEventListener('click',()=>{ if(countdown<=0) sendOtp(false); });
})();
</script>
</body>
</html>
`;
res.send(htmlForm);
};
@@ -0,0 +1,43 @@
import {
generateOtp,
msUntil,
sendDeleteOtpEmail,
OTP_EXPIRES_MIN,
RESEND_COOLDOWN_SEC,
} from './deleteUtils.js';
export const deleteUserOtp = async (req, res) => {
const { userId } = req.params;
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) return res.status(404).json({ error: 'User not found' });
const now = Date.now();
const lastSentAt = extUser.get('DeleteOTPSentAt')?.getTime?.() || 0;
const cooldownEndsAt = lastSentAt + RESEND_COOLDOWN_SEC * 1000;
const remainingMs = msUntil(now, cooldownEndsAt);
if (remainingMs > 0) {
return res
.status(429)
.json({ error: 'Cooldown not finished', retryAfterSec: Math.ceil(remainingMs / 1000) });
}
const otp = generateOtp();
const expiresAt = new Date(now + OTP_EXPIRES_MIN * 60 * 1000);
try {
const resp = await sendDeleteOtpEmail(extUser, otp);
extUser.set('DeleteOTP', otp);
extUser.set('DeleteOTPExpiry', expiresAt);
extUser.set('DeleteOTPSentAt', new Date(now));
extUser.set('DeleteOTPTries', 0); // reset tries on resend
await extUser.save(null, { useMasterKey: true });
return res.json({ ok: true, cooldownSec: RESEND_COOLDOWN_SEC, expiresInMin: OTP_EXPIRES_MIN });
} catch (err) {
console.log('Error sending delete OTP (POST /otp):', err?.response?.data || err);
return res.status(500).json({ error: 'Failed to send OTP' });
}
};
@@ -0,0 +1,72 @@
import axios from 'axios';
import { appName, cloudServerUrl, serverAppId } from '../../../Utils.js';
const serverUrl = cloudServerUrl;
const appId = serverAppId;
const masterKey = process.env.MASTER_KEY;
// Constants (adjust to your preference)
export const OTP_LENGTH = 6;
export const OTP_EXPIRES_MIN = 10; // OTP validity in minutes
export const RESEND_COOLDOWN_SEC = 30; // Cooldown between OTP sends
export const MAX_ATTEMPTS = 5; // Max allowed wrong attempts
export function generateOtp(len = OTP_LENGTH) {
// 6-digit numeric OTP (000000999999, padded)
const n = Math.floor(Math.random() * Math.pow(10, len));
return String(n).padStart(len, '0');
}
export async function sendDeleteOtpEmail(extUser, otp) {
const params = {
extUserId: extUser.id,
from: appName,
recipient: extUser?.get('Email'),
subject: 'OTP for Deletion account request',
html: `
<html lang="en">
<body style="margin:0;padding:0;background:#f6f7fb;font-family:Arial,Helvetica,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background:#ffffff;border:1px solid #e9ecf1;border-radius:8px;padding:20px;">
<tr>
<td align="left" style="font-size:16px;color:#0f172a;">
<div style="font-weight:bold;margin-bottom:8px;">${appName}</div>
<div style="font-size:18px;margin:0 0 12px 0;">Your verification code</div>
<div style="display:inline-block;border:1px solid #e9ecf1;border-radius:6px;background:#f8fafc;padding:10px 14px;margin-bottom:10px;">
<span style="font-family:Consolas,'Courier New',monospace;font-size:24px;letter-spacing:6px;color:#0f172a;">${otp}</span>
</div>
<p style="margin:8px 0 0 0;font-size:13px;color:#475569;">
This code expires in <strong>${OTP_EXPIRES_MIN}</strong> minutes.
</p>
<hr style="border:none;border-top:1px solid #e9ecf1;margin:18px 0;">
<p style="margin:0;font-size:12px;color:#64748b;">
If you didnt request this code, you can ignore this email.
</p>
</td>
</tr>
</table>
<div style="font-size:11px;color:#94a3b8;margin-top:12px;">
&copy; ${new Date().getFullYear()} ${appName}. All rights reserved.
</div>
</td>
</tr>
</table>
</body>
</html>
`,
};
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
};
return axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
}
export function msUntil(nowMs, futureMs) {
return Math.max(0, (futureMs || 0) - nowMs);
}
+118 -138
View File
@@ -1,169 +1,149 @@
import fs from 'node:fs';
import axios from 'axios'; import axios from 'axios';
import multer from 'multer'; import multer from 'multer';
import libre from 'libreoffice-convert'; import libre from 'libreoffice-convert';
import util from 'node:util';
import { cloudServerUrl, getSecureUrl, serverAppId } from '../../Utils.js'; import { cloudServerUrl, getSecureUrl, serverAppId } from '../../Utils.js';
libre.convertAsync = util.promisify(libre.convert); /** @returns {Promise<Buffer>} */
async function convertLibre(input, ext, opts) {
return await new Promise((resolve, reject) => {
try {
libre.convert(input, ext, opts, (err, out) => (err ? reject(err) : resolve(out)));
} catch (e) {
reject(e);
}
});
}
// -------------------- Concurrency limiter (no dependency) --------------------
const MAX_CONCURRENCY = Number(process.env.DOCX2PDF_CONCURRENCY || 2); // 1 for tiny droplets
let active = 0;
const queue = [];
function runWithLimit(task) {
return new Promise((resolve, reject) => {
const run = () => {
active++;
Promise.resolve()
.then(task)
.then(resolve, reject)
.finally(() => {
active--;
if (queue.length) queue.shift()();
});
};
if (active < MAX_CONCURRENCY) run();
else queue.push(run);
});
}
const storage = multer.diskStorage({ // -------------------- Timeout helper (async version = no TS hint) --------------------
destination(req, file, cb) { /**
cb(null, 'exports'); * @template T
}, * @param {Promise<T>} promise
filename(req, file, cb) { * @param {number} ms
cb(null, file.originalname); * @param {string} [label='operation']
* @returns {Promise<T>}
*/
export async function withTimeout(promise, ms, label = 'operation') {
let timer;
try {
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
});
return await Promise.race([promise, timeout]);
} finally {
if (timer) clearTimeout(timer);
}
}
// -------------------- Multer: use memory storage --------------------
const storage = multer.memoryStorage();
export const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const okExt = /\.docx$/i.test(file.originalname || '');
const okMime =
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.mimetype === 'application/octet-stream';
if (okExt && okMime) return cb(null, true);
cb(new Error('Only .docx files are supported'));
}, },
}); });
export const upload = multer({ storage });
function generatePdfName(length) { function generatePdfName(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = ''; let result = '';
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result; return result;
} }
export default async function docxtopdf(req, res) { export default async function docxtopdf(req, res) {
const serverUrl = cloudServerUrl; const serverUrl = cloudServerUrl;
const appId = serverAppId; const parseAppKey = { 'X-Parse-Application-Id': serverAppId };
const masterKey = process.env.MASTER_KEY; const masterKey = process.env.MASTER_KEY;
const inputPath = req.file.path;
const name = generatePdfName(16); const masterHeader = { ...parseAppKey, 'X-Parse-Master-Key': masterKey };
const fileName = `${name}.pdf`; const sessionHeader = { ...parseAppKey, 'X-Parse-Session-Token': req.headers['sessiontoken'] };
const outputPath = './exports/output.pdf';
if (!req.file || !req.file.buffer) {
return res.status(400).json({ error: 'No file uploaded.' });
}
try { try {
const userRes = await axios.get(serverUrl + '/users/me', { // ---- Auth: current user ----
headers: { const userRes = await axios.get(`${serverUrl}/users/me`, { headers: sessionHeader });
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'], // ---- contracts_Users ----
}, const whereUser = JSON.stringify({
}); UserId: { __type: 'Pointer', className: '_User', objectId: userRes.data.objectId },
const userId = JSON.stringify({
UserId: {
__type: 'Pointer',
className: '_User',
objectId: userRes.data.objectId,
},
}); });
const resUser = await axios.get( const resUser = await axios.get(
serverUrl + `/classes/contracts_Users?where=${userId}&limit=1&include=TenantId`, `${serverUrl}/classes/contracts_Users?where=${whereUser}&limit=1&include=TenantId`,
{ { headers: masterHeader }
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
); );
if (resUser?.data?.results?.length > 0) { if (!resUser?.data?.results?.length) {
const tenantId = resUser.data.results[0].TenantId?.objectId; return res.status(403).json({ error: 'User not linked to tenant.' });
const ext = '.pdf';
const outPath = `./exports/output${ext}`;
const docxBuf = fs.readFileSync(inputPath);
const pdfBuffer = await libre.convertAsync(docxBuf, ext, undefined);
fs.writeFileSync(outPath, pdfBuffer);
const file = fs.readFileSync(outPath);
const size = fs.statSync(outPath).size;
const PartnersTenant = JSON.stringify({
PartnersTenant: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
});
const resTenantCredit = await axios.get(
serverUrl + `/classes/partners_TenantCredits?where=${PartnersTenant}&limit=1`,
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
if (resTenantCredit.data?.results?.length > 0) {
const tenantCreditsId = resTenantCredit.data.results[0].objectId;
const activeFileAdapter = resUser.data.results[0].TenantId?.ActiveFileAdapter;
let fileUrl;
if (activeFileAdapter) {
const params = {
fileBase64: file.toString('base64'),
fileName,
id: activeFileAdapter,
};
const url = serverUrl + '/functions/savetofileadapter';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'],
};
try {
const savetos3 = await axios.post(url, params, { headers });
fileUrl = savetos3?.data?.result?.url;
} catch (err) {
console.log('err in save to customfile', err);
}
} else {
const parsefile = await axios.post(serverUrl + `/files/${fileName}`, file, {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
'Content-Type': 'application/pdf',
},
});
const fileRes = getSecureUrl(parsefile.data.url);
fileUrl = fileRes.url;
}
const usedStorage = resTenantCredit.data.results[0].usedStorage
? resTenantCredit.data.results[0].usedStorage + size
: size;
await axios.put(
serverUrl + `/classes/partners_TenantCredits/${tenantCreditsId}`,
{ usedStorage },
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
await axios.post(
serverUrl + '/classes/partners_DataFiles',
{
FileSize: size,
FileUrl: fileUrl,
TenantPtr: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
UserId: {
__type: 'Pointer',
className: '_User',
objectId: userRes.data.objectId,
},
},
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
[inputPath, outPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
return res.status(200).json({ message: 'success.', url: fileUrl });
}
} }
const tenantId = resUser.data.results[0]?.TenantId?.objectId;
if (!tenantId) {
return res.status(403).json({ error: 'Tenant not found for user.' });
}
// ---- DOCX -> PDF (buffer -> buffer), with concurrency + timeout ----
const fileName = `${generatePdfName(16)}.pdf`;
const pdfBuffer = await runWithLimit(() =>
withTimeout(convertLibre(req.file.buffer, '.pdf', undefined), 60_000, 'DOCX->PDF')
);
// ---- Upload PDF (no disk IO) ----
const activeFileAdapter = resUser.data.results[0]?.TenantId?.ActiveFileAdapter;
let fileUrl;
if (activeFileAdapter) {
const params = {
fileBase64: Buffer.from(pdfBuffer).toString('base64'),
fileName,
id: activeFileAdapter,
};
const url = serverUrl + '/functions/savetofileadapter';
const headers = { 'Content-Type': 'application/json', ...sessionHeader };
const savetos3 = await axios.post(url, params, { headers });
fileUrl = savetos3?.data?.result?.url;
if (!fileUrl) throw new Error('No URL returned from file adapter');
} else {
const parsefile = await axios.post(`${serverUrl}/files/${fileName}`, pdfBuffer, {
headers: { ...masterHeader, 'Content-Type': 'application/pdf' },
});
const fileRes = getSecureUrl(parsefile.data.url);
fileUrl = fileRes.url;
}
return res.status(200).json({ message: 'success.', url: fileUrl });
} catch (err) { } catch (err) {
[inputPath, outputPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
const msg = const msg =
err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.'; err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.';
console.log(`Error converting file: ${msg}`); console.log(`Error in docxtopdf: ${msg}`);
// Friendly message to the client
const message = const message =
'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com'; 'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com';
return res.status(400).json({ error: message }); return res.status(400).json({ error: message });
@@ -1,83 +0,0 @@
import { appName, smtpenable } from '../../Utils.js';
export default async function sendmailtoSupport(userDetails, errorMessage) {
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
// Render a simple HTML form. In production, consider using a templating engine.
try {
await Parse.Cloud.sendEmail({
sender: appName + ' <' + mailsender + '>',
recipient: 'support@opensignlabs.com',
subject: `Error while deleting account ${appName}`,
text: `Error while deleting account ${appName}`,
html: `<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 40px;
color: #333;
}
.container {
max-width: 600px;
margin: auto;
background: #ffffff;
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
padding: 30px;
}
h1 {
color: #dc3545;
font-size: 24px;
margin-bottom: 20px;
text-align: center;
}
h2 {
font-size: 20px;
color: #495057;
margin-top: 30px;
border-bottom: 1px solid #dee2e6;
padding-bottom: 10px;
}
.details p {
margin: 10px 0;
line-height: 1.6;
}
.label {
font-weight: bold;
color: #212529;
}
.value {
color: #495057;
}
</style>
</head>
<body>
<div class="container">
<h1>Error: ${errorMessage}</h1>
<h2>User Details</h2>
<div class="details">
<p><span class="label">UserRole:</span> <span class="value">${userDetails?.UserRole}</span></p>
<p><span class="label">Name:</span> <span class="value">${userDetails?.Name}</span></p>
<p><span class="label">Email:</span> <span class="value">${userDetails?.Email}</span></p>
<p><span class="label">UserId:</span> <span class="value">${userDetails?.UserId}</span></p>
<p><span class="label">ExtUserId:</span> <span class="value">${userDetails?.objectId}</span></p>
<p><span class="label">TenantId:</span> <span class="value">${userDetails?.TenantId}</span></p>
</div>
</div>
</body>
</html>`,
});
} catch (err) {
console.log('err while sending mail to support', err);
}
}
+8
View File
@@ -56,6 +56,10 @@ import loginUser from './parsefunction/loginUser.js';
import addUser from './parsefunction/addUser.js'; import addUser from './parsefunction/addUser.js';
import filterDocs from './parsefunction/filterDocs.js'; import filterDocs from './parsefunction/filterDocs.js';
import sendDeleteUserMail from './parsefunction/sendDeleteUserMail.js'; import sendDeleteUserMail from './parsefunction/sendDeleteUserMail.js';
import resetPassword from './parsefunction/resetPassword.js';
import saveSignature from './parsefunction/saveSignature.js';
import manageSign from './parsefunction/manageSign.js';
import getSignature from './parsefunction/getSignature.js';
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic. // This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave); Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
@@ -122,3 +126,7 @@ Parse.Cloud.define('loginuser', loginUser);
Parse.Cloud.define('adduser', addUser); Parse.Cloud.define('adduser', addUser);
Parse.Cloud.define('filterdocs', filterDocs); Parse.Cloud.define('filterdocs', filterDocs);
Parse.Cloud.define('senddeleterequest', sendDeleteUserMail); Parse.Cloud.define('senddeleterequest', sendDeleteUserMail);
Parse.Cloud.define('resetpassword', resetPassword);
Parse.Cloud.define('savesignature', saveSignature);
Parse.Cloud.define('managesign', manageSign);
Parse.Cloud.define('getdefaultsignature', getSignature);
@@ -7,6 +7,7 @@ import fs from 'node:fs';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import GenerateCertificate from './pdf/GenerateCertificate.js'; import GenerateCertificate from './pdf/GenerateCertificate.js';
import { getSecureUrl } from '../../Utils.js'; import { getSecureUrl } from '../../Utils.js';
import { parseUploadFile } from '../../utils/fileUtils.js';
dotenv.config({ quiet: true }); dotenv.config({ quiet: true });
const eSignName = 'OpenSign'; const eSignName = 'OpenSign';
const eSigncontact = 'hello@opensignlabs.com'; const eSigncontact = 'hello@opensignlabs.com';
@@ -16,10 +17,14 @@ async function uploadFile(pdfName, filepath) {
try { try {
const filedata = fs.readFileSync(filepath); const filedata = fs.readFileSync(filepath);
let fileUrl; let fileUrl;
const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
await file.save({ useMasterKey: true }); // const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
const fileRes = getSecureUrl(file.url()); // await file.save({ useMasterKey: true });
fileUrl = fileRes.url; // const fileRes = getSecureUrl(file.url());
// fileUrl = fileRes.url;
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
fileUrl = getSecureUrl(fileRes?.url)?.url;
return { imageUrl: fileUrl }; return { imageUrl: fileUrl };
} catch (err) { } catch (err) {
console.log('Err ', err); console.log('Err ', err);
@@ -52,7 +57,9 @@ export default async function generateCertificatebydocId(req) {
const certificatePath = `./exports/certificate_${docId}.pdf`; const certificatePath = `./exports/certificate_${docId}.pdf`;
try { try {
const getDocument = new Parse.Query('contracts_Document'); const getDocument = new Parse.Query('contracts_Document');
getDocument.include('ExtUserPtr,Signers,AuditTrail.UserPtr,Placeholders,ExtUserPtr.TenantId'); getDocument.include(
'ExtUserPtr,Signers,AuditTrail.UserPtr,Placeholders,ExtUserPtr.TenantId,ExtUserPtr.UserId'
);
const docRes = await getDocument.get(docId, { useMasterKey: true }); const docRes = await getDocument.get(docId, { useMasterKey: true });
if (docRes && docRes?.get('IsCompleted') && !docRes?.get('CertificateUrl')) { if (docRes && docRes?.get('IsCompleted') && !docRes?.get('CertificateUrl')) {
@@ -46,7 +46,7 @@ export default async function getDrive(request) {
return { error: 'Please provide required parameter!' }; return { error: 'Please provide required parameter!' };
} }
} catch (err) { } catch (err) {
console.log('err', err); console.log('err', err?.response?.data || err);
if (err.code == 209) { if (err.code == 209) {
return { error: 'Invalid session token' }; return { error: 'Invalid session token' };
} else { } else {
@@ -0,0 +1,18 @@
export default async function getSignature(request) {
const { userId } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
try {
const query = new Parse.Query('contracts_Signature');
query.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const result = await query.first({ useMasterKey: true });
return result;
} catch (err) {
console.error('Error fetching signature:', err);
throw err;
}
}
@@ -20,22 +20,23 @@ async function getTenantByUserId(userId, contactId) {
} }
} else { } else {
const query = new Parse.Query('contracts_Users'); const query = new Parse.Query('contracts_Users');
query.equalTo('UserId', { query.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
__type: 'Pointer',
className: '_User',
objectId: userId,
});
const extuser = await query.first({ useMasterKey: true }); const extuser = await query.first({ useMasterKey: true });
if (extuser) { if (extuser) {
const tenantId = extuser?.get('TenantId')?.id || '';
const user = extuser?.get('CreatedBy')?.id || userId; const user = extuser?.get('CreatedBy')?.id || userId;
const tenantCreditsQuery = new Parse.Query('partners_Tenant'); const tenantQuery = new Parse.Query('partners_Tenant');
tenantCreditsQuery.equalTo('UserId', { if (tenantId) {
__type: 'Pointer', tenantQuery.equalTo('objectId', tenantId);
className: '_User', } else {
objectId: user, tenantQuery.equalTo('UserId', {
}); __type: 'Pointer',
tenantCreditsQuery.exclude('FileAdapters,PfxFile'); className: '_User',
const res = await tenantCreditsQuery.first({ useMasterKey: true }); objectId: user,
});
}
tenantQuery.exclude('FileAdapters,PfxFile');
const res = await tenantQuery.first({ useMasterKey: true });
return res; return res;
} else { } else {
return {}; return {};
@@ -0,0 +1,28 @@
export default async function manageSign(request) {
const { signature, userId, initials, id, title } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
try {
const signatureCls = new Parse.Object('contracts_Signature');
if (id) {
signatureCls.id = id;
}
signatureCls.set('Initials', initials ? initials : '');
signatureCls.set('ImageURL', signature ? signature : '');
signatureCls.set('SignatureName', title ? title : '');
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
const signRes = await signatureCls.save(null, { useMasterKey: true });
return signRes;
} catch (err) {
console.error('Error saving signature:', err);
throw err;
}
}
@@ -14,13 +14,19 @@ import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
import { Placeholder } from './Placeholder.js'; import { Placeholder } from './Placeholder.js';
import { SignPdf } from '@signpdf/signpdf'; import { SignPdf } from '@signpdf/signpdf';
import { P12Signer } from '@signpdf/signer-p12'; import { P12Signer } from '@signpdf/signer-p12';
import { buildDownloadFilename } from '../../../utils/fileUtils.js'; import { buildDownloadFilename, parseUploadFile } from '../../../utils/fileUtils.js';
const serverUrl = cloudServerUrl; // process.env.SERVER_URL; const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
const APPID = serverAppId; const APPID = serverAppId;
const masterKEY = process.env.MASTER_KEY; const masterKEY = process.env.MASTER_KEY;
const eSignName = 'OpenSign'; const eSignName = 'OpenSign';
const eSigncontact = 'hello@opensignlabs.com'; const eSigncontact = 'hello@opensignlabs.com';
const docUrl = `${serverUrl}/classes/contracts_Document`;
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
};
async function unlinkFile(path) { async function unlinkFile(path) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
@@ -37,10 +43,14 @@ async function uploadFile(pdfName, filepath) {
try { try {
const filedata = fs.readFileSync(filepath); const filedata = fs.readFileSync(filepath);
let fileUrl; let fileUrl;
const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
await file.save({ useMasterKey: true }); // const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
const fileRes = getSecureUrl(file.url()); // await file.save({ useMasterKey: true });
fileUrl = fileRes.url; // const fileRes = getSecureUrl(file.url());
// fileUrl = fileRes.url;
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
fileUrl = getSecureUrl(fileRes?.url)?.url;
return { imageUrl: fileUrl }; return { imageUrl: fileUrl };
} catch (err) { } catch (err) {
@@ -90,13 +100,7 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
isCompleted = true; isCompleted = true;
} }
const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted }; const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted };
const signedRes = await axios.put(serverUrl + '/classes/contracts_Document/' + docId, body, { const signedRes = await axios.put(`${docUrl}/${docId}`, body, { headers });
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
return { isCompleted: isCompleted, message: 'success', AuditTrail: updateAuditTrail }; return { isCompleted: isCompleted, message: 'success', AuditTrail: updateAuditTrail };
} catch (err) { } catch (err) {
console.log('update doc err ', err); console.log('update doc err ', err);
@@ -141,13 +145,7 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
html: body, html: body,
mailProvider: mailProvider, mailProvider: mailProvider,
}; };
await axios.post(serverUrl + '/functions/sendmailv3', params, { await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
} }
} catch (err) { } catch (err) {
console.log('err in sendnotifymail', err); console.log('err in sendnotifymail', err);
@@ -255,13 +253,7 @@ async function sendCompletedMail(obj) {
filename: docName, filename: docName,
}; };
try { try {
const res = await axios.post(serverUrl + '/functions/sendmailv3', params, { const res = await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
// console.log('res', res.data.result); // console.log('res', res.data.result);
if (res.data?.result?.status !== 'success') { if (res.data?.result?.status !== 'success') {
unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`); unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`);
@@ -298,13 +290,7 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
fs.writeFileSync(certificatePath, signedCertificate); fs.writeFileSync(certificatePath, signedCertificate);
const file = await uploadFile('certificate.pdf', certificatePath); const file = await uploadFile('certificate.pdf', certificatePath);
const body = { CertificateUrl: file.imageUrl }; const body = { CertificateUrl: file.imageUrl };
await axios.put(serverUrl + '/classes/contracts_Document/' + doc.objectId, body, { await axios.put(`${docUrl}/${doc.objectId}`, body, { headers });
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
},
});
// used in API only // used in API only
if (doc.IsSendMail === false) { if (doc.IsSendMail === false) {
console.log("don't send mail"); console.log("don't send mail");
@@ -313,6 +299,41 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
} }
saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId); saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId);
unlinkFile(pfx.name); unlinkFile(pfx.name);
return file.imageUrl;
}
/**
* Process a PDF for signing:
* - updates audit trail, generates certificate.
* - Optionally inserts a signature placeholder (Placeholder()).
* - Otherwise (no merge + no placeholder), it flattens forms for finalization.
*
* @param {Object} _resDoc - Document details (expects AuditTrail, etc.)
* @param {Buffer|Uint8Array} pdfBytes - Original PDF bytes
* @param {string} [options.reason] - Reason text used in placeholder
* @param {string} [options.UserPtr] - user pointer (for audit trail)
* @param {string} [options.ipAddress] - IP (for audit trail)
* @param {string} [options.Signature] - Signature (for audit trail)
* @returns {Promise<Buffer>} merged PDF Buffer
*/
async function processPdf(_resDoc, PdfBuffer, reason) {
// No CC merge; operate directly on the original PDF
const pdfDoc = await PDFDocument.load(PdfBuffer);
const form = pdfDoc.getForm();
// Updates the field appearances to ensure visual changes are reflected.
form.updateFieldAppearances();
// Flattens the form, converting all form fields into non-editable, static content
form.flatten();
Placeholder({
pdfDoc: pdfDoc,
reason: `Digitally signed by ${eSignName} for ${reason}`,
location: 'n/a',
name: eSignName,
contactInfo: eSigncontact,
signatureLength: 16000,
});
const pdfWithPlaceholderBytes = await pdfDoc.save();
return Buffer.from(pdfWithPlaceholderBytes);
} }
/** /**
* *
@@ -333,7 +354,7 @@ async function PDF(req) {
const publicUrl = req.headers.public_url; const publicUrl = req.headers.public_url;
// below bode is used to get info of docId // below bode is used to get info of docId
const docQuery = new Parse.Query('contracts_Document'); const docQuery = new Parse.Query('contracts_Document');
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc'); docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,CreatedBy');
docQuery.equalTo('objectId', docId); docQuery.equalTo('objectId', docId);
const resDoc = await docQuery.first({ useMasterKey: true }); const resDoc = await docQuery.first({ useMasterKey: true });
if (!resDoc) { if (!resDoc) {
@@ -389,7 +410,11 @@ async function PDF(req) {
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed'); const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
let isCompleted = false; let isCompleted = false;
if (_resDoc.Signers && _resDoc.Signers.length > 0) { if (_resDoc.Signers && _resDoc.Signers.length > 0) {
if (auditTrail.length === _resDoc.Signers.length) { const removePrefill =
_resDoc?.Placeholders?.length > 0 &&
_resDoc?.Placeholders?.filter(x => x?.Role !== 'prefill');
if (auditTrail.length === removePrefill?.length) {
// if (auditTrail.length === _resDoc.Signers.length) {
isCompleted = true; isCompleted = true;
} }
} else { } else {
@@ -408,24 +433,9 @@ async function PDF(req) {
signersName && signersName.length > 0 signersName && signersName.length > 0
? signersName?.join(', ') ? signersName?.join(', ')
: username + ' <' + userEmail + '>'; : username + ' <' + userEmail + '>';
const pdfDoc = await PDFDocument.load(PdfBuffer);
const form = pdfDoc.getForm();
// Updates the field appearances to ensure visual changes are reflected.
form.updateFieldAppearances();
// Flattens the form, converting all form fields into non-editable, static content
form.flatten();
const p12Cert = new P12Signer(P12Buffer, { passphrase: passphrase || null }); const p12Cert = new P12Signer(P12Buffer, { passphrase: passphrase || null });
signedFilePath = `./exports/signed_${name}`; signedFilePath = `./exports/signed_${name}`;
Placeholder({ PdfBuffer = await processPdf(_resDoc, PdfBuffer, reason, UserPtr, userIP, sign);
pdfDoc: pdfDoc,
reason: `Digitally signed by ${eSignName} for ${reason}`,
location: 'n/a',
name: eSignName,
contactInfo: eSigncontact,
signatureLength: 16000,
});
const pdfWithPlaceholderBytes = await pdfDoc.save();
PdfBuffer = Buffer.from(pdfWithPlaceholderBytes);
//`new signPDF` create new instance of pdfBuffer and p12Buffer //`new signPDF` create new instance of pdfBuffer and p12Buffer
const OBJ = new SignPdf(); const OBJ = new SignPdf();
// `signedDocs` is used to signpdf digitally // `signedDocs` is used to signpdf digitally
@@ -484,9 +494,7 @@ async function PDF(req) {
console.log('Err in signpdf', err); console.log('Err in signpdf', err);
const body = { DebugginLog: err?.message }; const body = { DebugginLog: err?.message };
try { try {
await axios.put(serverUrl + '/classes/contracts_Document/' + docId, body, { await axios.put(`${docUrl}/${docId}`, body, { headers });
headers: { 'X-Parse-Application-Id': APPID, 'X-Parse-Master-Key': masterKEY },
});
} catch (err) { } catch (err) {
console.log('err in saving debugginglog', err); console.log('err in saving debugginglog', err);
} }
@@ -25,6 +25,7 @@ export default function reportJson(id, currentUserId) {
'RequestSubject', 'RequestSubject',
'ExtUserPtr.TenantId.RequestBody', 'ExtUserPtr.TenantId.RequestBody',
'ExtUserPtr.TenantId.RequestSubject', 'ExtUserPtr.TenantId.RequestSubject',
'DocSentAt',
]; ];
const filterKeys = [ const filterKeys = [
'TimeToCompleteDays', 'TimeToCompleteDays',
@@ -0,0 +1,62 @@
export default async function resetPassword(request) {
const userId = request.params.userId;
const newPassword = request.params.password;
if (!userId || !newPassword) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide required parameters.');
}
if (!request.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
}
if (request.user.id === userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Unauthorized to reset your own password.');
}
try {
// 1. Get Admin User TenantId
const adminUserQuery = new Parse.Query('contracts_Users');
adminUserQuery.equalTo('UserId', request.user);
const adminUser = await adminUserQuery.first({ useMasterKey: true });
const tenantId = adminUser?.get('TenantId');
if (!tenantId) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Admin user tenant not found.');
}
const isAdmin =
adminUser.get('UserRole') === 'contracts_Admin' ||
adminUser.get('UserRole') === 'contracts_OrgAdmin';
if (!isAdmin) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Unauthorized.');
}
// 2. Verify the user belongs to the same tenant and is not an admin
const targetUserQuery = new Parse.Query('contracts_Users');
targetUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
targetUserQuery.equalTo('TenantId', tenantId);
targetUserQuery.notEqualTo('UserRole', 'contracts_Admin');
const targetUser = await targetUserQuery.first({ useMasterKey: true });
if (!targetUser) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found or not allowed.');
}
// 3. Update user's password
const userQuery = new Parse.Query(Parse.User);
userQuery.equalTo('objectId', userId);
const user = await userQuery.first({ useMasterKey: true });
if (!user) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.');
}
user.set('password', newPassword);
await user.save(null, { useMasterKey: true });
return { status: 'success', message: 'Password has been reset.' };
} catch (error) {
console.error('Error while resetting password:', error);
throw error;
}
}
@@ -59,6 +59,7 @@ export default async function saveAsTemplate(request) {
options: { options: {
...p.options, ...p.options,
status: 'required', status: 'required',
response: '',
...(p?.options?.defaultValue ? { defaultValue: '' } : {}), ...(p?.options?.defaultValue ? { defaultValue: '' } : {}),
}, },
})), })),
@@ -1,4 +1,6 @@
import { flattenPdf, getSecureUrl } from '../../Utils.js'; import { flattenPdf, getSecureUrl } from '../../Utils.js';
import { parseUploadFile } from '../../utils/fileUtils.js';
export default async function saveFile(request) { export default async function saveFile(request) {
if (!request.params.fileBase64) { if (!request.params.fileBase64) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide file.'); throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide file.');
@@ -10,6 +12,7 @@ export default async function saveFile(request) {
const extCls = new Parse.Query('contracts_Users'); const extCls = new Parse.Query('contracts_Users');
extCls.equalTo('UserId', request.user); extCls.equalTo('UserId', request.user);
extCls.include('TenantId'); extCls.include('TenantId');
extCls.include('UserId');
const resExt = await extCls.first({ useMasterKey: true }); const resExt = await extCls.first({ useMasterKey: true });
if (resExt) { if (resExt) {
const _resExt = JSON.parse(JSON.stringify(resExt)); const _resExt = JSON.parse(JSON.stringify(resExt));
@@ -20,17 +23,26 @@ export default async function saveFile(request) {
if (ext === 'pdf') { if (ext === 'pdf') {
mimeType = 'application/pdf'; mimeType = 'application/pdf';
const flatPdf = await flattenPdf(fileBase64); const flatPdf = await flattenPdf(fileBase64);
file = [...flatPdf]; // file = [...flatPdf];
file = flatPdf;
} else if (ext === 'png' || ext === 'jpeg' || ext === 'jpg') { } else if (ext === 'png' || ext === 'jpeg' || ext === 'jpg') {
mimeType = `image/${ext}`; mimeType = `image/${ext}`;
file = { base64: fileBase64 }; // file = { base64: fileBase64 };
file = Buffer.from(fileBase64, 'base64');
}
// const pdfFile = new Parse.File(fileName, file, mimeType);
// // Save the Parse File if needed
// const pdfData = await pdfFile.save({ useMasterKey: true });
// const presignedUrl = pdfData.url();
// const fileRes = getSecureUrl(presignedUrl);
// return { url: fileRes.url };
try {
const fileRes = await parseUploadFile(fileName, file, mimeType);
fileUrl = getSecureUrl(fileRes?.url)?.url;
return { url: fileUrl };
} catch (err) {
throw new Parse.Error(400, err?.message);
} }
const pdfFile = new Parse.File(fileName, file, mimeType);
// Save the Parse File if needed
const pdfData = await pdfFile.save({ useMasterKey: true });
const presignedUrl = pdfData.url();
const fileRes = getSecureUrl(presignedUrl);
return { url: fileRes.url };
} else { } else {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.'); throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.');
} }
@@ -0,0 +1,33 @@
export default async function saveSignature(request) {
const { signature, userId, initials, id, title } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
try {
const signatureCls = new Parse.Object('contracts_Signature');
if (id) {
signatureCls.id = id;
}
if (initials) {
signatureCls.set('Initials', initials);
}
if (signature) {
signatureCls.set('ImageURL', signature);
}
if (title) {
signatureCls.set('SignatureName', title);
}
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
const signRes = await signatureCls.save(null, { useMasterKey: true });
return signRes;
} catch (err) {
throw err;
}
}
@@ -12,15 +12,23 @@ async function sendMailProvider(req, plan, monthchange) {
let mailgunClient; let mailgunClient;
let mailgunDomain; let mailgunDomain;
if (smtpenable) { if (smtpenable) {
transporterSMTP = createTransport({ let transporterConfig = {
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT || 465, port: process.env.SMTP_PORT || 465,
secure: smtpsecure, secure: smtpsecure,
auth: { };
// ✅ Add auth only if BOTH username & password exist
const smtpUser = process.env.SMTP_USERNAME;
const smtpPass = process.env.SMTP_PASS;
if (smtpUser && smtpPass) {
transporterConfig.auth = {
user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL, user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL,
pass: process.env.SMTP_PASS, pass: smtpPass,
}, };
}); }
transporterSMTP = createTransport(transporterConfig);
} else { } else {
if (mailgunApiKey) { if (mailgunApiKey) {
const mailgun = new Mailgun(formData); const mailgun = new Mailgun(formData);
@@ -185,7 +193,7 @@ async function sendMailProvider(req, plan, monthchange) {
} }
} }
} catch (err) { } catch (err) {
console.log('err in sendmailv3', err); console.log(`Error in sendmailv3: ${err}`);
if (fs.existsSync(testPdf)) { if (fs.existsSync(testPdf)) {
try { try {
fs.unlinkSync(testPdf); fs.unlinkSync(testPdf);
@@ -236,7 +244,7 @@ async function sendMailProvider(req, plan, monthchange) {
} }
} }
} catch (err) { } catch (err) {
console.log('err in sendmailv3', err); console.log(`Error in sendmailv3: ${err}`);
if (err) { if (err) {
return { status: 'error' }; return { status: 'error' };
} }
+18 -9
View File
@@ -61,20 +61,28 @@ let mailgunDomain;
let isMailAdapter = false; let isMailAdapter = false;
if (smtpenable) { if (smtpenable) {
try { try {
transporterMail = createTransport({ let transporterConfig = {
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT || 465, port: process.env.SMTP_PORT || 465,
secure: smtpsecure, secure: smtpsecure,
auth: { };
// ✅ Add auth only if BOTH username & password exist
const smtpUser = process.env.SMTP_USERNAME;
const smtpPass = process.env.SMTP_PASS;
if (smtpUser && smtpPass) {
transporterConfig.auth = {
user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL, user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL,
pass: process.env.SMTP_PASS, pass: smtpPass,
}, };
}); }
transporterMail = createTransport(transporterConfig);
await transporterMail.verify(); await transporterMail.verify();
isMailAdapter = true; isMailAdapter = true;
} catch (err) { } catch (err) {
isMailAdapter = false; isMailAdapter = false;
console.log('Please provide valid SMTP credentials'); console.log(`Please provide valid SMTP credentials: ${err}`);
} }
} else if (process.env.MAILGUN_API_KEY) { } else if (process.env.MAILGUN_API_KEY) {
try { try {
@@ -100,7 +108,7 @@ export const config = {
appId: serverAppId, appId: serverAppId,
logLevel: ['error'], logLevel: ['error'],
maxLimit: 500, maxLimit: 500,
maxUploadSize: '30mb', maxUploadSize: '100mb',
masterKey: process.env.MASTER_KEY, //Add your master key here. Keep it secret! masterKey: process.env.MASTER_KEY, //Add your master key here. Keep it secret!
masterKeyIps: ['0.0.0.0/0', '::/0'], // '::1' masterKeyIps: ['0.0.0.0/0', '::/0'], // '::1'
serverURL: cloudServerUrl, // Don't forget to change to https if needed serverURL: cloudServerUrl, // Don't forget to change to https if needed
@@ -110,6 +118,7 @@ export const config = {
appName: appName, appName: appName,
allowClientClassCreation: false, allowClientClassCreation: false,
allowExpiredAuthDataToken: false, allowExpiredAuthDataToken: false,
enableInsecureAuthAdapters: false,
encodeParseObjectInCloudFunction: true, encodeParseObjectInCloudFunction: true,
...(isMailAdapter === true ...(isMailAdapter === true
? { ? {
@@ -156,8 +165,8 @@ export const config = {
export const app = express(); export const app = express();
app.use(cors()); app.use(cors());
app.use(express.json({ limit: '50mb' })); app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(express.urlencoded({ limit: '100mb', extended: true }));
app.use(function (req, res, next) { app.use(function (req, res, next) {
req.headers['x-real-ip'] = getUserIP(req); req.headers['x-real-ip'] = getUserIP(req);
const publicUrl = 'https://' + req?.get('host'); const publicUrl = 'https://' + req?.get('host');
+2953 -2760
View File
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -1,10 +1,10 @@
{ {
"name": "open_sign_server", "name": "open_sign_server",
"version": "2.26.0", "version": "2.30.0",
"description": "An example Parse API server using the parse-server module", "description": "opensign server using the parse-server module",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/ParsePlatform/parse-server-example" "url": "https://github.com/OpenSignLabs/OpenSign"
}, },
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
@@ -18,8 +18,8 @@
"watch": "nodemon index.js" "watch": "nodemon index.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.879.0", "@aws-sdk/client-s3": "^3.901.0",
"@aws-sdk/s3-request-presigner": "^3.879.0", "@aws-sdk/s3-request-presigner": "^3.901.0",
"@parse/fs-files-adapter": "^3.0.0", "@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.2.0", "@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
@@ -27,41 +27,41 @@
"@signpdf/signer-p12": "^3.2.4", "@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.5", "@signpdf/signpdf": "^3.2.5",
"aws-sdk": "^2.1692.0", "aws-sdk": "^2.1692.0",
"axios": "^1.11.0", "axios": "^1.12.2",
"coherentpdf": "^2.5.5", "coherentpdf": "^2.5.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"dotenv": "^17.2.1", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"form-data": "^4.0.4", "form-data": "^4.0.4",
"generate-api-key": "^1.0.2", "generate-api-key": "^1.0.2",
"googleapis": "^159.0.0", "googleapis": "^161.0.0",
"libreoffice-convert": "^1.6.1", "libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.0.3", "mailgun.js": "^12.1.0",
"mongodb": "^6.19.0", "mongodb": "^5.9.2",
"multer": "^2.0.2", "multer": "^2.0.2",
"multer-s3": "^3.0.1", "multer-s3": "^3.0.1",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"nodemailer": "^7.0.6", "nodemailer": "^7.0.9",
"p-limit": "^7.1.1", "p-limit": "^7.1.1",
"parse": "^6.1.1", "parse": "^6.1.1",
"parse-dbtool": "^1.2.0", "parse-dbtool": "^1.2.0",
"parse-server": "^8.2.4", "parse-server": "^8.2.5",
"parse-server-api-mail-adapter": "^4.1.0", "parse-server-api-mail-adapter": "^4.1.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"posthog-node": "^5.8.1", "posthog-node": "^5.9.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"rate-limiter-flexible": "^7.2.0", "rate-limiter-flexible": "^8.0.1",
"sharp": "^0.34.3", "sharp": "^0.34.4",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"ws": "^8.18.3" "ws": "^8.18.3"
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.28.0", "@babel/eslint-parser": "^7.28.4",
"eslint": "^9.34.0", "eslint": "^9.37.0",
"jasmine": "^5.10.0", "jasmine": "^5.12.0",
"mongodb-runner": "^5.9.2", "mongodb-runner": "^5.10.0",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"nyc": "^17.1.0", "nyc": "^17.1.0",
"prettier": "^3.6.2" "prettier": "^3.6.2"
+23
View File
@@ -1,3 +1,6 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../Utils.js';
function formatFixedDate(date = new Date()) { function formatFixedDate(date = new Date()) {
const dd = String(date.getDate()).padStart(2, '0'); const dd = String(date.getDate()).padStart(2, '0');
const months = [ const months = [
@@ -75,3 +78,23 @@ export function buildDownloadFilename(formatId, ctx) {
const safeExt = ext.replace(/\.+/g, '').toLowerCase() || 'pdf'; const safeExt = ext.replace(/\.+/g, '').toLowerCase() || 'pdf';
return `${stem}.${safeExt}`; return `${stem}.${safeExt}`;
} }
export async function parseUploadFile(fileName, fileData, mimeType) {
try {
const res = await axios.post(`${cloudServerUrl}/files/${fileName}`, fileData, {
headers: {
'X-Parse-Application-Id': serverAppId,
'X-Parse-Master-Key': process.env.MASTER_KEY,
'Content-Type': mimeType,
},
});
// console.log('File uploaded:', res.data);
return res.data;
} catch (err) {
const errorMessage = err?.response?.data?.error || 'Unknown error';
const statusCode = err?.response?.status || 500;
console.log('Err in parseUploadFile', errorMessage);
throw { message: errorMessage, code: statusCode };
}
}