Merge pull request #2175 from OpenSignLabs/updates-25926972891

Merge pull request #2460 from nxglabs/sync-to-public_repo-25861367352
This commit is contained in:
Raktima
2026-05-18 10:11:45 +05:30
committed by GitHub
92 changed files with 8850 additions and 5394 deletions
+1504 -2363
View File
File diff suppressed because it is too large Load Diff
+31 -31
View File
@@ -3,11 +3,11 @@
"version": "2.37.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.18.4",
"@lottiefiles/dotlottie-react": "^0.19.0",
"@mui/icons-material": "^5.15.10",
"@mui/material": "^5.15.10",
"@pdf-lib/fontkit": "^1.1.1",
@@ -26,27 +26,28 @@
"@usewaypoint/block-text": "^0.0.7",
"@usewaypoint/document-core": "^0.0.6",
"@usewaypoint/email-builder": "^0.0.9",
"axios": "^1.13.6",
"axios": "^1.15.2",
"date-fns-tz": "^3.2.0",
"dompurify": "^3.3.3",
"dompurify": "^3.4.1",
"file-saver": "^2.0.5",
"highlight.js": "^11.9.0",
"i18next": "^25.8.17",
"highlight.js": "^11.11.1",
"i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.2",
"i18next-http-backend": "^3.0.6",
"jszip": "^3.10.1",
"juice": "^11.1.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"parse": "^8.1.0",
"pdf-lib": "^1.17.1",
"pkijs": "^3.3.3",
"prettier": "^3.8.1",
"pkijs": "^3.4.0",
"prettier": "^3.8.3",
"print-js": "^1.6.0",
"prismjs": "^1.30.0",
"process": "^0.11.10",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react": "^19.2.5",
"react-bootstrap": "^2.10.10",
"react-colorful": "^5.6.1",
"react-confetti": "^6.4.0",
@@ -54,25 +55,24 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dom": "^19.2.4",
"react-dom": "^19.2.5",
"react-gtm-module": "^2.0.11",
"react-i18next": "^16.5.6",
"react-i18next": "^17.0.6",
"react-konva": "^19.2.3",
"react-pdf": "^10.4.1",
"react-quill-new": "^3.8.3",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.3",
"react-router": "^7.13.1",
"react-router": "^7.14.2",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8",
"react-timezone-select": "^3.3.3",
"react-to-print": "^3.3.0",
"react-tooltip": "^5.30.0",
"react-tooltip": "^5.30.1",
"redux": "^5.0.1",
"regex-parser": "^2.3.1",
"serve": "^14.2.6",
"web-vitals": "^5.1.0",
"web-vitals": "^5.2.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^3.22.4",
"zustand": "^4.5.1"
@@ -82,7 +82,7 @@
"start-dev": "vite",
"dev": "vite",
"preview": "vite preview",
"start": "serve -s build",
"start": "node server.cjs",
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
"version-win": "powershell -Command \"Invoke-RestMethod -Uri 'https://api.github.com/repos/opensignlabs/opensign/releases/latest' | Select-Object -ExpandProperty tag_name | Out-File -FilePath ./public/version.txt\"",
"build-win": "npm run version-win && vite build",
@@ -116,17 +116,17 @@
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.28.6",
"@babel/runtime-corejs2": "^7.29.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/ui": "^4.0.18",
"autoprefixer": "^10.4.27",
"babel-loader": "^10.0.0",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/ui": "^4.1.5",
"autoprefixer": "^10.5.0",
"babel-loader": "^9.2.1",
"commitizen": "^4.3.1",
"concurrently": "^9.2.1",
"css-loader": "^7.1.4",
@@ -134,17 +134,17 @@
"eslint": "^9.39.4",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^28.1.0",
"lint-staged": "^16.3.3",
"postcss": "^8.5.8",
"jsdom": "^29.0.2",
"lint-staged": "^16.4.0",
"postcss": "^8.5.12",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.19",
"tmp": "^0.2.5",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0",
"vite": "^8.0.10",
"vite-plugin-svgr": "^5.2.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18"
"vitest": "^4.1.5"
},
"overrides": {
"tmp": "$tmp",
@@ -237,6 +237,8 @@
"p3": "Wenn Sie diese Option auswählen, werden die Signaturlinks gleichzeitig an alle Unterzeichner gesendet. Jeder Unterzeichner kann das Dokument nach Belieben unterzeichnen, unabhängig davon, ob andere Unterzeichner ihre Unterschriften abgeschlossen haben. Diese Methode ist schneller, erzwingt jedoch keine Reihenfolge unter den Teilnehmern.",
"p4": "Wählen Sie die Option, die am besten zu den Anforderungen Ihrer Dokumentenverarbeitung passt."
},
"strict-order": "Strikte Reihenfolge erzwingen",
"strict-order-help": "Wenn aktiviert, kann kein Unterzeichner unterschreiben oder genehmigen, bis alle vorherigen Unterzeichner/Genehmiger ihre Aktion abgeschlossen haben.",
"no": "Nein",
"auto-reminder": "Automatische Erinnerung",
"remind-once": "Einmal erinnern alle (Tage)",
@@ -260,6 +262,7 @@
"signers": "Unterzeichner",
"signers-help": "Beginnen Sie mit der Eingabe eines Kontaktnamens, um vorgeschlagene Unterzeichner aus Ihren gespeicherten Kontakten zu sehen, oder fügen Sie neue hinzu. Ordnen Sie die Reihenfolge der Unterzeichner, indem Sie sie in der gewünschten Reihenfolge hinzufügen. Verwenden Sie die '+'-Schaltfläche, um Unterzeichner hinzuzufügen, und 'x', um sie zu entfernen. Jeder Unterzeichner erhält eine E-Mail-Aufforderung, das Dokument in der aufgelisteten Reihenfolge zu unterzeichnen.",
"bcc-help": "Beginnen Sie mit der Eingabe des Namens eines Kontakts, um Vorschläge aus Ihren gespeicherten Kontakten anzuzeigen oder neue hinzuzufügen. Verwenden Sie die Schaltfläche '+', um einen Benutzer hinzuzufügen, und das 'x', um ihn zu entfernen. Die E-Mail des ausgewählten Benutzers wird als Bcc (Blindkopie) hinzugefügt. Jeder Benutzer erhält eine E-Mail-Benachrichtigung, sobald das Dokument fertig ist.",
"cc-help": "Beginnen Sie mit der Eingabe des Namens eines Kontakts, um Vorschläge aus Ihren gespeicherten Kontakten zu sehen, oder fügen Sie neue hinzu. Verwenden Sie die Schaltfläche „+“, um einen Benutzer hinzuzufügen, und „x“, um ihn zu entfernen. Die E-Mail des ausgewählten Benutzers wird als CC (Kopie) hinzugefügt. Jeder Benutzer erhält eine E-Mail-Benachrichtigung, sobald das Dokument abgeschlossen ist.",
"add-signer": "Unterzeichner hinzufügen",
"contact-not-found": "Kontakt nicht gefunden",
"add-yourself": "Sich selbst hinzufügen",
@@ -287,7 +290,7 @@
"add-webhook": "Webhook hinzufügen",
"webhook-added": "Webhook erfolgreich hinzugefügt.",
"team-disabled": "Team deaktiviert",
"Team enabled": "Team aktiviert",
"team-enabled": "Team aktiviert",
"are-you-sure": "Sind Sie sicher, dass Sie",
"disable": "deaktivieren",
"enable": "aktivieren",
@@ -312,6 +315,31 @@
"user-activated": "Benutzer aktiviert.",
"public": "Öffentlich",
"download": "Herunterladen",
"sign-offline": "Offline unterzeichnen",
"sign-offline-instructions": "Laden Sie das Dokument herunter, unterzeichnen Sie es mit einem externen Tool und laden Sie dann die unterschriebene Kopie hier hoch. Der Dokumenteigentümer wird Ihre Einreichung prüfen und entweder genehmigen oder ablehnen.",
"sign-offline-step-1": "Das unsignierte PDF herunterladen.",
"sign-offline-step-2": "Das PDF mit Ihrem bevorzugten Tool unterzeichnen.",
"sign-offline-step-3": "Das unterschriebene PDF zur Genehmigung hochladen.",
"sign-offline-upload-hint": "Das unterschriebene PDF zur Einreichung zur Genehmigung auswählen.",
"sign-offline-submitted-title": "Zur Genehmigung eingereicht",
"sign-offline-submitted-body": "Der Dokumenteigentümer wurde per E-Mail benachrichtigt, Ihre offline unterschriebene Kopie zu prüfen.",
"offline-already-submitted-title": "Dokument offline eingereicht",
"offline-already-submitted-body": "Sie haben dieses Dokument bereits zur Offline-Signaturprüfung eingereicht. Sobald es genehmigt oder abgelehnt wurde, werden Sie benachrichtigt.",
"offline-action-approve-title": "Offline-Signatur genehmigen",
"offline-action-approve-confirm": "Genehmigen",
"offline-action-reject-title": "Offline-Signatur ablehnen",
"offline-action-reject-hint": "Optional können Sie einen Grund angeben. Der Unterzeichner wird per E-Mail benachrichtigt.",
"offline-action-reject-reason-placeholder": "Grund (optional)",
"offline-action-reject-confirm": "Ablehnung bestätigen",
"offline-action-review-title": "Offline-Signatur prüfen",
"offline-action-review-hint": "Sehen Sie sich das vom Unterzeichner hochgeladene signierte PDF an, und genehmigen oder lehnen Sie es dann ab. Keine Aktion wird durchgeführt, bis Sie bestätigen.",
"offline-action-submitted-at": "Eingereicht",
"offline-action-no-preview": "Für diese Einreichung ist keine Vorschau verfügbar.",
"offline-action-open-in-new-tab": "PDF in neuem Tab öffnen",
"offline-action-invalid-link": "Dieser Prüflink ist ungültig oder abgelaufen.",
"offline-action-success": "Aktion abgeschlossen.",
"reject": "Ablehnen",
"document": "Dokument",
"view": "Ansehen",
"make-template-public": "Vorlage öffentlich machen",
"make-template-private": "Vorlage privat machen",
@@ -405,6 +433,14 @@
"certificate": "Zertifikat",
"decline": "Ablehnen",
"finish": "Fertigstellen",
"approve": "Genehmigen",
"approve-mssg": " Sind Sie sicher, dass Sie das Dokument genehmigen möchten?",
"signer-role-label": "Empfängerrolle",
"signer-role-signer": "Unterzeichner",
"signer-role-viewer": "Betrachter",
"signer-role-approver": "Genehmiger",
"signer-role-hint": "Unterzeichner muss unterschreiben. Genehmiger genehmigt ohne Unterschrift. Betrachter kann nur ansehen.",
"signer-role-upgrade-msg": "Die Rollen Betrachter und Genehmiger sind nur in den kostenpflichtigen Tarifen verfügbar. Aktualisieren Sie Ihren Tarif, um diese Rollen zuzuweisen.",
"done": "Fertig",
"mail": "E-Mail",
"sign-now": "Jetzt unterzeichnen",
@@ -543,6 +579,7 @@
"sign-url": "Signatur-URL",
"mails-sent": "E-Mails gesendet",
"mail-not-delivered": "E-Mail nicht zugestellt",
"email-not-verified-head": "E-Mail nicht verifiziert",
"document-alert": "Dokument-Warnung",
"owner-subscription-expired": "Das Abonnement des Besitzers ist abgelaufen.",
"subscription-expired": "Abonnement abgelaufen",
@@ -618,6 +655,9 @@
"document-signed-alert-7": "Dieses Dokument ist nicht mehr verfügbar.",
"document-signed-alert-8": "Das Dokument wurde bereits zum Unterzeichnen versendet.",
"yet-to-sign": "Noch zu unterzeichnen",
"viewers": "Betrachter",
"viewer-no-widgets": "Betrachter erhalten eine schreibgeschützte Kopie des Dokuments und können keine Widgets zugewiesen bekommen.",
"viewer-confirm-widget-removal": "Wenn Sie diesen Empfänger auf Betrachter umstellen, werden alle ihm bereits zugewiesenen Widgets entfernt. Fortfahren?",
"validation-alert-1": "Die Eingabe erfüllt nicht die Kriterien des regulären Ausdrucks.",
"account-already-exists": "Konto existiert bereits!",
"choose-password": "Passwort wählen",
@@ -768,6 +808,13 @@
"p3": "Wenn Sie diese Option deaktivieren, können Benutzer das Dokument direkt ohne zusätzliche Schritte unterzeichnen.",
"p4": "Bitte wählen Sie die Option, die am besten zu Ihren Anforderungen für die Dokumentenunterzeichnung passt."
},
"allow-offline-sign": "Offline-Signatur zulassen",
"allow-offline-sign-help": {
"p1": "Legen Sie fest, ob Unterzeichner eine Kopie dieses Dokuments einreichen dürfen, die mit einem externen Werkzeug signiert wurde, damit der Eigentümer sie prüfen kann.",
"p2": "Unterzeichner sehen die Option 'Offline unterzeichnen' und können eine extern signierte PDF zur Genehmigung hochladen.",
"p3": "Unterzeichner können nur in der App signieren. Die Option 'Offline unterzeichnen' wird ausgeblendet.",
"p4": "Diese Einstellung verwendet standardmäßig Ihre Kontoeinstellung und ist in kostenpflichtigen Tarifen verfügbar."
},
"advanced-options": "Erweiterte Optionen",
"hide-advanced-options": "Erweiterte Optionen ausblenden",
"download-files": "Dateien herunterladen",
@@ -830,6 +877,7 @@
"delete-folder-alert-1": "Löschen nicht möglich: Der Ordner muss leer sein.",
"defaultSign-alert": "{{widgetsType}} Widgets sind nicht für die automatische Signatur verfügbar.",
"embed-heading": "{{appName}} Vorlagen einbetten",
"other-apps-by-opensign": "Andere Apps vom OpenSign-Team",
"public-template-edit-title": "Öffentliche Vorlage bearbeiten",
"public-template-edit-mssg": "Eine öffentliche Vorlage können Sie nicht direkt bearbeiten. Um Änderungen vorzunehmen, wird die Vorlage auf privat gesetzt und Sie müssen sie anschließend wieder öffentlich machen. Möchten Sie fortfahren?",
"agree-p1": "Ich bestätige, dass ich die ",
@@ -900,6 +948,7 @@
"p3": "Durch Auswahl dieser Option wird verhindert, dass Unterzeichner während des Unterzeichnungsprozesses Ergänzungen zum Dokument vornehmen."
},
"signature-setting": "Signatur-Einstellungen",
"rotation": "Drehung",
"activation-key": "Aktivierungsschlüssel",
"paid-selfhost-help": "Bitte kontaktieren Sie {{appName}} für weitere Informationen unter ",
"remaining-credits-selfhost-help": "Verwenden Sie Premium-Credits für API-Dokumentensignaturen, den Massenversand und die {{appName}} Integration auf Ihrer Website. Sie haben {{allowedcredits}} enthaltene Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig.",
@@ -928,6 +977,8 @@
"visit-below-link": "Besuchen Sie den untenstehenden Link, um mehr zu erfahren -",
"storage-help": "Durch die Aktivierung von BYOC können Sie Ihren eigenen S3-Speicher verbinden, sodass Ihre Dateien vollständig unter Ihrer Kontrolle bleiben und keine externen Kopien gespeichert werden. Wenn Ihnen Datenautonomie wichtig ist, erwägen Sie ein Upgrade auf Teams, um diese Funktion freizuschalten.",
"daily-quota-reached": "Sie haben Ihr tägliches Kontingent erreicht. Für Unterstützung kontaktieren Sie bitte quotas@opensignlabs.com.",
"email-not-verified-send": "Bitte verifizieren Sie Ihre E-Mail-Adresse, bevor Sie E-Mails senden. Gehen Sie zu Ihren <1>Profileinstellungen</1>, um die E-Mail-Verifizierung abzuschließen.",
"email-not-verified-send-plain": "Bitte verifizieren Sie Ihre E-Mail-Adresse, bevor Sie E-Mails senden. Gehen Sie zu Ihren Profileinstellungen, um die E-Mail-Verifizierung abzuschließen.",
"enabled-signature-type": "Aktivierte Signaturtypen",
"enabled-signature-type-help": "Die Einstellung 'Aktivierte Signaturtypen' bestimmt, welche Signaturoptionen in Ihrer Organisation verfügbar sind. Wenn Sie beispielsweise die Option 'Zeichnen' deaktivieren, wird sie den Mitgliedern Ihrer Organisation im Signatur-Widget nicht angezeigt, während die anderen drei Optionen weiterhin zugänglich bleiben.",
"indexing-public-profile": "Erlaube die Indexierung des öffentlichen Profils durch Suchmaschinen",
@@ -955,6 +1006,7 @@
"please-provide-templateid": "Bitte geben Sie templateid an",
"this-template-is-not-public": "Dieses template ist nicht öffentlich",
"invalid-templateid": "Ungültige templateid",
"kiosk-no-active-role": "Diese Vorlage hat keine für die Kiosk-Signatur verfügbaren Unterzeichner- oder Genehmigerrollen.",
"contact-billing-at-opensign": "Um weitere Plätze hinzuzufügen, kontaktieren Sie bitte OpenSign™ unter <1>billing@opensignlabs.com</1> für Unterstützung.",
"title-length-alert": "Der Titel darf höchstens 250 Zeichen lang sein.",
"note-length-alert": "Die Notiz darf höchstens 200 Zeichen lang sein.",
@@ -1303,7 +1355,7 @@
"signer-form-data-for": "Formulardaten für Unterzeichner: {{signerName}} <{{signerEmail}}>",
"widget-name": "Widget-Name",
"widget-responses": "Widget-Antworten",
"formdata-download-note": "Hinweis: Signaturen, Stempel, Initialen und Bilder werden nicht exportiert.",
"formdata-download-note": "Hinweis: Signaturen, Stempel, Initialen und Bilder werden nicht exportiert. Daten von Offline-Unterzeichnern sind nicht verfügbar.",
"export-all-formdata-error": "Sie können die Formulardaten nicht exportieren, da die Vorlage doppelte Widget-Namen enthält. Bitte erstellen Sie eine neue Vorlage, um Formulardaten exportieren zu können.",
"export-formdata-error": "Sie können die Formulardaten nicht exportieren, da das Dokument doppelte Widget-Namen enthält. Bitte erstellen Sie ein neues Dokument ohne doppelte Widget-Namen, um Formulardaten exportieren zu können.",
"duplicate-widget-name-error": "Dieser Widget-Name ist bereits einem anderen Unterzeichner zugewiesen. Bitte wählen Sie einen anderen Namen.",
@@ -1416,5 +1468,95 @@
"switch-to-advanced": "Zum erweiterten Modus wechseln",
"switch-to-basic": "Zum Basis-Modus wechseln",
"use-name-as-sender": "Profilnamen als Absender verwenden",
"use-name-as-sender-help": "Wenn aktiviert, verwendet {{appName}} Ihren Profilnamen als Absender für Signaturanfragen und Abschluss-E-Mails."
"use-name-as-sender-help": "Wenn aktiviert, verwendet {{appName}} Ihren Profilnamen als Absender für Signaturanfragen und Abschluss-E-Mails.",
"acro-fields-detected": "Formularfelder erkannt",
"acro-fields-detected-msg": "Es wurden {{count}} Formularfeld(er) im hochgeladenen PDF erkannt. Möchten Sie die entsprechenden Widgets automatisch einfügen?",
"documents-count": "Anzahl der Dokumente",
"templates-count": "Anzahl der Vorlagen",
"emails-sent": "E-Mails gesendet",
"storage-used": "Speicher verwendet",
"docs-count-team-help": "Gesamtzahl der von allen Benutzern in Ihren Organisationen erstellten Dokumente.",
"docs-count-help": "Gesamtzahl der von Ihnen erstellten Dokumente.",
"templates-count-team-help": "Gesamtzahl der von allen Benutzern in Ihren Organisationen erstellten Vorlagen.",
"templates-count-help": "Gesamtzahl der von Ihnen erstellten Vorlagen.",
"emails-sent-team-help": "Gesamtzahl der in Ihren Organisationen versendeten E-Mail-Benachrichtigungen.",
"emails-sent-help": "Gesamtzahl der von Ihnen versendeten E-Mail-Benachrichtigungen.",
"storage-used-team-help": "Gesamter Speicherplatzverbrauch aller Benutzer in Ihren Organisationen.",
"storage-used-help": "Gesamter Speicherplatzverbrauch durch Sie.",
"cloud": "Cloud",
"on-premises": "On-Premises",
"plan-name-free": "OPENSIGN™ FREE",
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
"plan-name-teams": "OPENSIGN™ TEAMS",
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
"plan-subtitle-free": "Kostenlose unbegrenzte E-Signaturen, für immer.",
"plan-subtitle-professional": "Exklusiver Zugang zu erweiterten Funktionen.",
"plan-subtitle-scalable": "Skalierbare Funktionen mit Priority-Support.",
"subscribe": "Abonnieren",
"contact-us": "Kontaktiere uns",
"benefit-unlimited-digital-signatures": "Unbegrenzte digitale Signaturen",
"benefit-sign-docs-yourself": "Dokumente selbst unterschreiben",
"benefit-request-signatures": "Signaturen von anderen anfordern",
"benefit-unlimited-templates": "Unbegrenzte Vorlagen",
"benefit-14-field-types": "14 Feldtypen",
"benefit-automatic-e-signatures": "Automatische E-Signaturen",
"benefit-completion-certificates": "Abschlusszertifikate",
"benefit-send-in-order": "In Reihenfolge senden",
"benefit-organize-docs": "Dokumente in OpenSign™ Drive organisieren",
"benefit-document-templates": "Dokumentvorlagen",
"benefit-import-from-dropbox": "Import aus Dropbox",
"benefit-contact-book": "Kontaktbuch",
"benefit-document-expiry": "Unterstützung für Dokumentablauf",
"benefit-decline-document": "Unterstützung für Dokumentablehnung",
"benefit-email-notifications": "E-Mail-Benachrichtigungen",
"benefit-public-profiles": "Öffentliche Profile",
"benefit-and-much-more": "Und vieles mehr",
"benefit-everything-free": "Alles in OpenSign™ Free",
"benefit-field-validations": "Feldvalidierungen",
"benefit-regex-validations": "Reguläre-Ausdrucks-Validierungen",
"benefit-webhooks": "Webhooks",
"benefit-zapier": "Zapier-Integration",
"benefit-api-access": "API-Zugang",
"benefit-api-signatures-100": "bis zu 100 API-Signaturen",
"benefit-api-signatures-240": "bis zu 240 API-Signaturen",
"benefit-custom-email-templates": "Benutzerdefinierte E-Mail-Vorlagen",
"benefit-custom-smtp": "Eigenes Gmail- oder SMTP-Konto für den E-Mail-Versand verbinden",
"benefit-auto-reminders": "Automatische Erinnerungen",
"benefit-bulk-send-100": "Massenversand (bis zu 100 Dokumente)",
"benefit-bulk-send-240": "Massenversand (bis zu 240 Dokumente)",
"benefit-premium-profiles": "Premium öffentliche Profilbenutzernamen",
"benefit-email-verification": "E-Mail-basierte Verifizierung zur Bestätigung der Unterzeichneridentität erzwingen",
"benefit-embedded-signing": "Eingebettetes Signieren",
"benefit-everything-professional": "Alles in OpenSign™ Professional",
"benefit-teams-organizations": "Teams und Organisationen",
"benefit-share-templates-teams": "Vorlagen mit Teams teilen",
"benefit-share-templates-individuals": "Vorlagen mit Einzelpersonen teilen",
"benefit-byoc": "BYOC - Dokumente in Ihrem eigenen Cloud-Speicher aufbewahren",
"benefit-doc-id-removal": "DocumentId-Entfernung aus signierten Dokumenten",
"benefit-request-payments": "Zahlungen anfordern (demnächst)",
"benefit-mobile-app": "Mobile App (demnächst)",
"benefit-api-signatures-500": "bis zu 500 API-Signaturen",
"benefit-bulk-send-500": "Massenversand (bis zu 500 Dokumente)",
"benefit-all-features": "Alle Funktionen",
"benefit-custom-domain": "Benutzerdefinierte Domain",
"benefit-custom-branding": "Benutzerdefiniertes Branding",
"benefit-uptime-sla": "Uptime SLA",
"benefit-sso-support": "SSO-Unterstützung",
"benefit-priority-support": "Prioritätssupport",
"benefit-3-users-1500-credits": "Enthält 3 Benutzer und 1500 Premium-Credits",
"benefit-public-links": "Öffentliche Links",
"benefit-kiosk-mode": "Kiosk-Modus",
"benefit-bulk-send-1500": "Massenversand (bis zu 1500 Dokumente)",
"benefit-connect-gmail-smtp": "Eigenes Gmail- oder SMTP-Konto verbinden",
"benefit-analytics": "Analysen",
"benefit-bulksend-api-pro-month-help": "Dieser Plan enthält 100 Premium-Credits pro Monat, mit denen Sie Dokumente per API versenden, öffentliche Signierlinks teilen, Massen-Signaturanfragen ausführen, den Kiosk-Modus für Vor-Ort-Unterschriften nutzen oder Signaturen aus direkt auf Ihrer Website eingebetteten Dokumenten sammeln können. Jedes Mal, wenn Sie ein Dokument initiieren oder versenden, wird ein Credit verwendet — und dieses einzelne Dokument kann bis zu 10 Unterzeichner enthalten.",
"benefit-bulksend-api-pro-year-help": "Dieser Plan enthält 240 Premium-Credits pro Jahr, mit denen Sie Dokumente per API versenden, öffentliche Signierlinks teilen, Massen-Signaturanfragen ausführen, den Kiosk-Modus für Vor-Ort-Unterschriften nutzen oder Signaturen aus direkt auf Ihrer Website eingebetteten Dokumenten sammeln können. Jedes Mal, wenn Sie ein Dokument initiieren oder versenden, wird ein Credit verwendet — und dieses einzelne Dokument kann bis zu 10 Unterzeichner enthalten.",
"benefit-bulksend-api-team-month-help": "Dieser Plan enthält 100 Premium-Credits pro Monat, mit denen Sie Dokumente per API versenden, öffentliche Signierlinks teilen, Massen-Signaturanfragen ausführen, den Kiosk-Modus für Vor-Ort-Unterschriften nutzen oder Signaturen aus direkt auf Ihrer Website eingebetteten Dokumenten sammeln können. Jedes Mal, wenn Sie ein Dokument initiieren oder versenden, wird ein Credit verwendet — und dieses einzelne Dokument kann bis zu 10 Unterzeichner enthalten.",
"benefit-bulksend-api-team-year-help": "Dieser Plan enthält 500 Premium-Credits pro Jahr, mit denen Sie Dokumente per API versenden, öffentliche Signierlinks teilen, Massen-Signaturanfragen ausführen, den Kiosk-Modus für Vor-Ort-Unterschriften nutzen oder Signaturen aus direkt auf Ihrer Website eingebetteten Dokumenten sammeln können. Jedes Mal, wenn Sie ein Dokument initiieren oder versenden, wird ein Credit verwendet — und dieses einzelne Dokument kann bis zu 10 Unterzeichner enthalten.",
"failed-pdf-for-prefill-processing": "PDF für die Vorbefüllung konnte nicht geladen werden.",
"attach-all-role-to-signer": "Bitte weisen Sie dem Unterzeichner alle Rollen zu.",
"upgrade-to-edit": "Zum Bearbeiten upgraden",
"hide-live-chat-for-signers": "Live-Chat für Unterzeichner ausblenden",
"hide-live-chat-for-signers-help": "Wenn aktiviert, werden Live-Chat-Widgets für Unterzeichner auf Signierseiten ausgeblendet."
}
@@ -237,6 +237,8 @@
"p3": "Selecting this option will send the signing links to all signers simultaneously. Every signer can sign the document at their convenience, regardless of whether other signers have completed their signatures. This method is faster but does not enforce any signing order among the participants.",
"p4": "Select the option that best suits the needs of your document processing."
},
"strict-order": "Enforce strict order",
"strict-order-help": "When enabled, no signer can sign or approve until every previous signer/approver has completed their action.",
"no": "No",
"auto-reminder": "Auto reminder",
"remind-once": " Remind once in every (Days)",
@@ -260,6 +262,7 @@
"signers": "Signers",
"signers-help": "Begin typing a contact's name to see suggested signers from your saved contacts or add new ones. Arrange the signing order by adding signers in the desired sequence. Use the '+' button to include signers and the 'x' to remove them. Each signer will receive an email prompt to sign the document in the order listed.",
"bcc-help": "Start typing a contact's name to see suggestions from your saved contacts or add new ones. Use the '+' button to add a user and the 'x' to remove them. The selected user's email will be added as a Bcc (blind carbon copy). Each user will receive an email notification once the document is completed.",
"cc-help": "Start typing a contact's name to see suggestions from your saved contacts or add new ones. Use the '+' button to add a user and the 'x' to remove them. The selected user's email will be added as a cc (carbon copy). Each user will receive an email notification once the document is completed.",
"add-signer": "Add Signer",
"contact-not-found": "Contact not found",
"add-yourself": "Add yourself",
@@ -287,7 +290,7 @@
"add-webhook": "Add webhook",
"webhook-added": "Webhook added successfully.",
"team-disabled": "Team disabled",
"Team enabled": "Team enabled",
"team-enabled": "Team enabled",
"are-you-sure": "Are you sure you want to",
"disable": "disable",
"enable": "enable",
@@ -312,6 +315,31 @@
"user-activated": "User activated.",
"public": "Public",
"download": "Download",
"sign-offline": "Sign Offline",
"sign-offline-instructions": "Download the document, sign it using any external tool, then upload the signed copy here. The document owner will review your submission and either approve or reject it.",
"sign-offline-step-1": "Download the unsigned PDF.",
"sign-offline-step-2": "Sign the PDF using your preferred tool.",
"sign-offline-step-3": "Upload the signed PDF for owner approval.",
"sign-offline-upload-hint": "Select the signed PDF to submit for approval.",
"sign-offline-submitted-title": "Submitted for approval",
"sign-offline-submitted-body": "The document owner has been notified by email to review your offline-signed copy.",
"offline-already-submitted-title": "Document submitted offline",
"offline-already-submitted-body": "You have already submitted this document for offline signature review. The document owner will be notified once it is approved or rejected.",
"offline-action-approve-title": "Approve offline signature",
"offline-action-approve-confirm": "Approve",
"offline-action-reject-title": "Reject offline signature",
"offline-action-reject-hint": "Optionally provide a reason. The signer will be notified by email.",
"offline-action-reject-reason-placeholder": "Reason (optional)",
"offline-action-reject-confirm": "Confirm rejection",
"offline-action-review-title": "Review offline signature",
"offline-action-review-hint": "Preview the signed PDF uploaded by the signer, then approve or reject it. No action is taken until you confirm.",
"offline-action-submitted-at": "Submitted",
"offline-action-no-preview": "No preview available for this submission.",
"offline-action-open-in-new-tab": "Open PDF in new tab",
"offline-action-invalid-link": "This review link is invalid or has expired.",
"offline-action-success": "Action completed.",
"reject": "Reject",
"document": "Document",
"view": "View",
"make-template-public": "Make template public",
"make-template-private": "Make template private",
@@ -405,6 +433,14 @@
"certificate": "Certificate",
"decline": "Decline",
"finish": "Finish",
"approve": "Approve",
"approve-mssg": " Are you sure you want to approve the document ?",
"signer-role-label": "Recipient role",
"signer-role-signer": "Signer",
"signer-role-viewer": "Viewer",
"signer-role-approver": "Approver",
"signer-role-hint": "Signer must sign. Approver approves without signing. Viewer only views.",
"signer-role-upgrade-msg": "Viewer and Approver roles are available on paid plans only. Upgrade your plan to assign these roles to recipients.",
"done": "Done",
"mail": "Mail",
"sign-now": "Sign now",
@@ -543,6 +579,7 @@
"sign-url": "Sign url",
"mails-sent": "Mails Sent",
"mail-not-delivered": "Mail not delivered",
"email-not-verified-head": "Email not verified",
"document-alert": "Document alert",
"owner-subscription-expired": "Owner's subscription has expired.",
"subscription-expired": "Subscription Expired",
@@ -618,6 +655,9 @@
"document-signed-alert-7": "This Document is no longer available.",
"document-signed-alert-8": "The document has already been dispatched for signing.",
"yet-to-sign": "Yet to sign",
"viewers": "Viewers",
"viewer-no-widgets": "Viewers receive a read-only copy of the document and cannot have widgets assigned.",
"viewer-confirm-widget-removal": "Switching this recipient to Viewer will remove all widgets already assigned to them. Continue?",
"validation-alert-1": "The input does not meet the criteria set by the regular expression.",
"account-already-exists": "Account already exists!",
"choose-password": "Choose password",
@@ -768,6 +808,13 @@
"p3": "Selecting this option will disable OTP verification, allowing users to sign the document directly without additional steps.",
"p4": "Please choose the option that best suits your document signing requirements."
},
"allow-offline-sign": "Allow offline signing",
"allow-offline-sign-help": {
"p1": "Choose whether signers can submit a copy of this document signed using an external tool for owner review.",
"p2": "Signers will see a Sign Offline option and can upload an externally signed PDF for the document owner to approve.",
"p3": "Signers can only sign within the app. The Sign Offline option will be hidden.",
"p4": "This setting defaults to your account preference under Preferences and is available on paid plans."
},
"advanced-options": "Advanced options",
"hide-advanced-options": "Hide advanced options",
"download-files": "Download files",
@@ -830,6 +877,7 @@
"delete-folder-alert-1": "Unable to delete: The folder must be empty.",
"defaultSign-alert": "{{widgetsType}} widgets are not available for auto sign.",
"embed-heading": "Embed {{appName}} Templates",
"other-apps-by-opensign": "Other apps by OpenSign team",
"public-template-edit-title": "Edit Public Template",
"public-template-edit-mssg": "You cannot edit a public template directly. To make changes, the template will be set to private, and you'll need to make it public again afterward. Do you want to continue?",
"agree-p1": "I confirm that I have read and understood the ",
@@ -900,6 +948,7 @@
"p3": "Selecting this option restricts signers from making any additions to the document during the signing process."
},
"signature-setting": "Signature settings",
"rotation": "Rotation",
"activation-key": "Activation key",
"paid-selfhost-help": "Please contact to {{appName}} for more info at ",
"remaining-credits-selfhost-help": "Use premium credits for API document signing, bulk sending {{appName}} integration on your website. You have {{allowedcredits}} included credits and {{addoncredits}} additional purchased credits remaining.",
@@ -928,6 +977,8 @@
"visit-below-link": "Visit below link to know more -",
"storage-help": "Enabling BYOC lets you connect your own S3 storage so your files remain entirely under your control—no external copies retained. If data autonomy matters to you, consider upgrading to Teams to unlock this feature.",
"daily-quota-reached": "You've reached your daily quota. For assistance, please contact quotas@opensignlabs.com.",
"email-not-verified-send": "Please verify your email address before sending emails. Go to your <1>profile settings</1> to complete email verification.",
"email-not-verified-send-plain": "Please verify your email address before sending emails. Go to your profile settings to complete email verification.",
"enabled-signature-type": "Enabled Signature Types",
"enabled-signature-type-help": "The 'Enabled Signature Types' setting determines which signature options are available across your organization. For example, if you disable the 'Draw' option, members of your organization will not see it in the signature widget, while the other three options will remain accessible.",
"indexing-public-profile": "Allow indexing of public profile by search engines",
@@ -955,6 +1006,7 @@
"please-provide-templateid": "Please provide templateid",
"this-template-is-not-public": "This template is not public",
"invalid-templateid": "Invalid templateid",
"kiosk-no-active-role": "This template has no signer or approver roles available for kiosk signing.",
"contact-billing-at-opensign": "To add more seats, please contact OpenSign™ at <1>billing@opensignlabs.com</1> for assistance",
"title-length-alert": "Title must be at most 250 characters long.",
"note-length-alert": "Note must be at most 200 characters long.",
@@ -1289,6 +1341,8 @@
"disable-individual-user-smtp": "Disable individual user smtp settings",
"enable-individual-user-smtp": "Enable individual user smtp settings",
"user-smtp-general-help": "When disabled, users will not be able to configure their own SMTP settings. All emails will use the global SMTP configuration instead.",
"hide-live-chat-for-signers": "Hide live chat widget for signers",
"hide-live-chat-for-signers-help": "When enabled, the live chat widget will be hidden on all signing pages. A window variable 'opensign_hide_chat' is set to true, which can be used as a GTM trigger condition to prevent loading the widget for signers.",
"global-smtp-settings": "Global SMTP Settings",
"global-smtp-settings-help-p1": "These settings apply globally across your entire {{appName}} account. If an individual user has not configured their own email delivery settings, the system will automatically use the Global SMTP Settings defined here.",
"global-smtp-settings-help-p2": "To ensure better inbox deliverability for all signature request emails, you can connect your preferred email provider. Choose one of the following options:",
@@ -1303,7 +1357,7 @@
"signer-form-data-for": "Signer Form data for: {{signerName}} <{{signerEmail}}>",
"widget-name": "Widget Name",
"widget-responses": "Widget Responses",
"formdata-download-note": "Note: Signatures, stamps, initials and images wont be exported.",
"formdata-download-note": "Note: Signatures, stamps, initials and images wont be exported. Offline signers data will not be available.",
"export-all-formdata-error": "You can/t export form data because the template contains duplicate widget names. Please create a fresh template to be able to export form data.",
"export-formdata-error": "You can't export form data because the document contains duplicate widget names. Please create a fresh document without duplicate widget names to be able to export form data.",
"duplicate-widget-name-error": "This widget name is already assigned to another signer. Please choose a different name.",
@@ -1416,5 +1470,93 @@
"switch-to-advanced": "Switch to advanced",
"switch-to-basic": "Switch to basic",
"use-name-as-sender": "Use profile name as sender",
"use-name-as-sender-help": "When enabled, {{appName}} uses your profile name as sender for signature request and completion mail."
"use-name-as-sender-help": "When enabled, {{appName}} uses your profile name as sender for signature request and completion mail.",
"acro-fields-detected": "Form Fields Detected",
"acro-fields-detected-msg": "We detected {{count}} form field(s) in the uploaded PDF. Would you like to automatically insert corresponding widgets?",
"documents-count": "Documents count",
"templates-count": "Templates count",
"emails-sent": "Emails sent",
"storage-used": "Storage used",
"docs-count-team-help": "Total number of documents created by all users in your organizations.",
"docs-count-help": "Total number of documents created by you.",
"templates-count-team-help": "Total number of templates created by all users in your organizations.",
"templates-count-help": "Total number of templates created by you.",
"emails-sent-team-help": "Total number of email notifications sent across your organizations.",
"emails-sent-help": "Total number of email notifications sent by you.",
"storage-used-team-help": "Total storage space consumed by all users across your organizations.",
"storage-used-help": "Total storage space consumed by you.",
"cloud": "Cloud",
"on-premises": "On-premises",
"plan-name-free": "OPENSIGN™ FREE",
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
"plan-name-teams": "OPENSIGN™ TEAMS",
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
"plan-subtitle-free": "Free Unlimited E-signatures, Forever.",
"plan-subtitle-professional": "Exclusive Access to advanced features.",
"plan-subtitle-scalable": "Scalable Features with priority support.",
"subscribe": "Subscribe",
"contact-us": "Contact us",
"benefit-unlimited-digital-signatures": "Unlimited digital signatures",
"benefit-sign-docs-yourself": "Sign documents yourself",
"benefit-request-signatures": "Request signatures from others",
"benefit-unlimited-templates": "Unlimited templates",
"benefit-14-field-types": "14 field types",
"benefit-automatic-e-signatures": "Automatic e-signatures",
"benefit-completion-certificates": "Completion certificates",
"benefit-send-in-order": "Send in order",
"benefit-organize-docs": "Organize docs in OpenSign™ Drive",
"benefit-document-templates": "Document templates",
"benefit-import-from-dropbox": "Import from Dropbox",
"benefit-contact-book": "Contact book",
"benefit-document-expiry": "Document expiry support",
"benefit-decline-document": "Decline document support",
"benefit-email-notifications": "Email notifications",
"benefit-public-profiles": "Public profiles",
"benefit-and-much-more": "And much more",
"benefit-everything-free": "Everything in OpenSign™ free",
"benefit-field-validations": "Field validations",
"benefit-regex-validations": "Regular expression validations",
"benefit-webhooks": "Webhooks",
"benefit-zapier": "Zapier integration",
"benefit-api-access": "API Access",
"benefit-api-signatures-100": "upto 100 API signatures",
"benefit-api-signatures-240": "upto 240 API signatures",
"benefit-custom-email-templates": "Custom email templates",
"benefit-custom-smtp": "Connect your own Gmail or SMTP account for sending emails",
"benefit-auto-reminders": "Auto reminders",
"benefit-bulk-send-100": "Bulk send (upto 100 docs)",
"benefit-bulk-send-240": "Bulk send (upto 240 docs)",
"benefit-premium-profiles": "Premium Public profile usernames",
"benefit-email-verification": "Enforce email-based verification to confirm signer identity",
"benefit-embedded-signing": "Embedded signing",
"benefit-everything-professional": "Everything in OpenSign™ professional",
"benefit-teams-organizations": "Teams and Organizations",
"benefit-share-templates-teams": "Share Templates with teams",
"benefit-share-templates-individuals": "Share Templates with individuals",
"benefit-byoc": "BYOC - Store your documents in your own cloud storage",
"benefit-doc-id-removal": "DocumentId removal from signed docs",
"benefit-request-payments": "Request Payments (coming soon)",
"benefit-mobile-app": "Mobile app (coming soon)",
"benefit-api-signatures-500": "upto 500 API signatures",
"benefit-bulk-send-500": "Bulk send (upto 500 docs)",
"benefit-all-features": "All features",
"benefit-custom-domain": "Custom domain",
"benefit-custom-branding": "Custom branding",
"benefit-uptime-sla": "Uptime SLA",
"benefit-sso-support": "SSO support",
"benefit-priority-support": "Priority support",
"benefit-3-users-1500-credits": "Includes 3 users and 1500 Premium Credit",
"benefit-public-links": "Public links",
"benefit-kiosk-mode": "Kiosk Mode",
"benefit-bulk-send-1500": "Bulk send (upto 1500 docs)",
"benefit-connect-gmail-smtp": "Connect your own Gmail or SMTP",
"benefit-analytics": "Analytics",
"benefit-bulksend-api-pro-month-help": "This plan comes with 100 premium credits per month that let you send documents via API, share public signing links, run bulk signature requests, use Kiosk mode for in-person signing, or collect signatures from documents embedded directly on your website. Each time you initiate or send a document, one credit is used — and that single document can include up to 10 signers.",
"benefit-bulksend-api-pro-year-help": "This plan comes with 240 premium credits per year that let you send documents via API, share public signing links, run bulk signature requests, use Kiosk mode for in-person signing, or collect signatures from documents embedded directly on your website. Each time you initiate or send a document, one credit is used — and that single document can include up to 10 signers.",
"benefit-bulksend-api-team-month-help": "This plan comes with 100 premium credits per month that let you send documents via API, share public signing links, run bulk signature requests, use Kiosk mode for in-person signing, or collect signatures from documents embedded directly on your website. Each time you initiate or send a document, one credit is used — and that single document can include up to 10 signers.",
"benefit-bulksend-api-team-year-help": "This plan comes with 500 premium credits per year that let you send documents via API, share public signing links, run bulk signature requests, use Kiosk mode for in-person signing, or collect signatures from documents embedded directly on your website. Each time you initiate or send a document, one credit is used — and that single document can include up to 10 signers.",
"failed-pdf-for-prefill-processing": "Failed to load PDF for prefill processing.",
"attach-all-role-to-signer": "Please attach all roles to the signer.",
"upgrade-to-edit": "Upgrade to edit"
}
@@ -237,6 +237,8 @@
"p3": "Seleccionando esta opción se enviarán los enlaces para la firma a todos los firmantes simultáneamente. Cada firmante puede firmar el documento cuando le convenga, independientemente de si otros firmantes han completado sus firmas. Este método es más rápido, pero no impone ningún orden de firma entre los participantes.",
"p4": "Selecciona la opción que mejor se adapte a las necesidades de tu tramitación de documentos."
},
"strict-order": "Forzar orden estricto",
"strict-order-help": "Cuando está habilitado, ningún firmante puede firmar o aprobar hasta que todos los firmantes/aprobadores anteriores hayan completado su acción.",
"no": "No",
"auto-reminder": "Recordatorio automático",
"remind-once": " Recordar una vez cada (Días)",
@@ -260,6 +262,7 @@
"signers": "Firmantes",
"signers-help": "Empieza a escribir el nombre de un contacto para ver los firmantes sugeridos entre tus contactos guardados o añade otros nuevos. Organiza el orden de firma añadiendo firmantes en la secuencia deseada. Utiliza el botón «+» para añadir nuevos firmantes y la «X» para quitar todos. Cada firmante recibirá una solicitud por correo electrónico para que firme el documento según el orden indicado.",
"bcc-help": "Empieza a escribir el nombre de un contacto para ver sugerencias de tus contactos guardados o agregar nuevos. Usa el botón '+' para añadir un usuario y el botón 'x' para eliminarlo. El correo electrónico del usuario seleccionado se añadirá como Bcc (copia oculta). Cada usuario recibirá una notificación por correo electrónico una vez que el documento esté completado.",
"cc-help": "Empieza a escribir el nombre de un contacto para ver sugerencias de tus contactos guardados o añadir nuevos. Usa el botón '+' para agregar un usuario y la 'x' para eliminarlo. El correo del usuario seleccionado se añadirá como CC (copia). Cada usuario recibirá una notificación por correo una vez que el documento se complete.",
"add-signer": "Añadir firmante",
"contact-not-found": "Contacto no encontrado",
"add-yourself": "Añadirte a ti mismo",
@@ -287,7 +290,7 @@
"add-webhook": "Agregar webhook",
"webhook-added": "Webhook agregado exitosamente.",
"team-disabled": "Equipo deshabilitado",
"Team enabled": "Equipo habilitado",
"team-enabled": "Equipo habilitado",
"are-you-sure": "¿En definitiva quieres ",
"disable": "deshabilitar",
"enable": "habilitar",
@@ -312,6 +315,31 @@
"user-activated": "Usuario activado.",
"public": "Público",
"download": "Descargar",
"sign-offline": "Firmar sin conexión",
"sign-offline-instructions": "Descargue el documento, fírmelo con una herramienta externa y luego suba la copia firmada aquí. El propietario del documento revisará su envío y lo aprobará o rechazará.",
"sign-offline-step-1": "Descargar el PDF sin firmar.",
"sign-offline-step-2": "Firmar el PDF con su herramienta preferida.",
"sign-offline-step-3": "Subir el PDF firmado para aprobación del propietario.",
"sign-offline-upload-hint": "Seleccione el PDF firmado para enviarlo para su aprobación.",
"sign-offline-submitted-title": "Enviado para aprobación",
"sign-offline-submitted-body": "El propietario del documento ha sido notificado por correo electrónico para revisar su copia firmada sin conexión.",
"offline-already-submitted-title": "Documento enviado sin conexión",
"offline-already-submitted-body": "Ya ha enviado este documento para la revisión de la firma sin conexión. Se le notificará una vez que sea aprobado o rechazado.",
"offline-action-approve-title": "Aprobar firma sin conexión",
"offline-action-approve-confirm": "Aprobar",
"offline-action-reject-title": "Rechazar firma sin conexión",
"offline-action-reject-hint": "Opcionalmente, proporcione un motivo. El firmante será notificado por correo electrónico.",
"offline-action-reject-reason-placeholder": "Motivo (opcional)",
"offline-action-reject-confirm": "Confirmar rechazo",
"offline-action-review-title": "Revisar firma sin conexión",
"offline-action-review-hint": "Previsualice el PDF firmado subido por el firmante y luego apruébelo o recházelo. No se realiza ninguna acción hasta que confirme.",
"offline-action-submitted-at": "Enviado",
"offline-action-no-preview": "No hay vista previa disponible para este envío.",
"offline-action-open-in-new-tab": "Abrir PDF en nueva pestaña",
"offline-action-invalid-link": "Este enlace de revisión no es válido o ha expirado.",
"offline-action-success": "Acción completada.",
"reject": "Rechazar",
"document": "Documento",
"view": "Ver",
"make-template-public": "Convertir la plantilla en pública",
"make-template-private": "Convertir la plantilla en privada",
@@ -405,6 +433,14 @@
"certificate": "Certificado",
"decline": "Rechazar",
"finish": "Finalizar",
"approve": "Aprobar",
"approve-mssg": " ¿Está seguro de que desea aprobar el documento?",
"signer-role-label": "Rol del destinatario",
"signer-role-signer": "Firmante",
"signer-role-viewer": "Visualizador",
"signer-role-approver": "Aprobador",
"signer-role-hint": "El firmante debe firmar. El aprobador aprueba sin firmar. El visualizador solo visualiza.",
"signer-role-upgrade-msg": "Los roles de Visualizador y Aprobador solo están disponibles en los planes de pago. Actualice su plan para asignar estos roles a los destinatarios.",
"done": "Hecho",
"mail": "Correo",
"sign-now": "Firmar ahora",
@@ -543,6 +579,7 @@
"sign-url": "URL de firma",
"mails-sent": "Correos enviados",
"mail-not-delivered": "correo no entregado",
"email-not-verified-head": "Correo no verificado",
"document-alert": "Alerta de documento",
"owner-subscription-expired": "La suscripción del propietario ha expirado.",
"owner-doesnt-have-paid-plan": "El propietario no tiene un plan de pago.",
@@ -618,6 +655,9 @@
"document-signed-alert-7": "Este documento ya no está disponible.",
"document-signed-alert-8": "Este documento ya ha sido enviado para ser firmado.",
"yet-to-sign": "Todavía por firmar",
"viewers": "Visualizadores",
"viewer-no-widgets": "Los visualizadores reciben una copia de solo lectura del documento y no pueden tener widgets asignados.",
"viewer-confirm-widget-removal": "Al cambiar este destinatario a Visualizador se eliminarán todos los widgets ya asignados. ¿Continuar?",
"validation-alert-1": "Los datos ingresados no cumplen con los criterios establecidos por la expresión regular.",
"account-already-exists": "¡La cuenta ya existe!",
"choose-password": "Elegir contraseña",
@@ -768,6 +808,13 @@
"p3": "Seleccionar esta opción deshabilitará la verificación con OTP, lo cual permitirá a los usuarios firmar el documento directamente sin pasos adicionales.",
"p4": "Por favor, selecciona la opción que mejor se adapte a tus requisitos para la firma de documentos."
},
"allow-offline-sign": "Permitir firma sin conexión",
"allow-offline-sign-help": {
"p1": "Decide si los firmantes pueden enviar una copia de este documento firmada con una herramienta externa para que el propietario la revise.",
"p2": "Los firmantes verán una opción Firmar sin conexión y podrán subir un PDF firmado externamente para su aprobación.",
"p3": "Los firmantes solo podrán firmar dentro de la aplicación. La opción Firmar sin conexión se ocultará.",
"p4": "Esta configuración usa por defecto tu preferencia de cuenta y está disponible en planes de pago."
},
"advanced-options": "Opciones avanzadas",
"hide-advanced-options": "Ocultar las opciones avanzadas",
"download-files": "Descargar archivos",
@@ -830,6 +877,7 @@
"delete-folder-alert-1": "No se puede eliminar: la carpeta debe estar vacía",
"defaultSign-alert": "Los widgets {{widgetsType}} no están disponibles para la firma automática",
"embed-heading": "Incrustar plantillas de {{appName}}",
"other-apps-by-opensign": "Otras aplicaciones del equipo de OpenSign",
"public-template-edit-title": "Editar plantilla pública",
"public-template-edit-mssg": "No puede editar una plantilla pública directamente. Para realizar cambios, la plantilla se configurará como privada y luego tendrás que volver a hacerla pública. ¿Quieres continuar?",
"agree-p1": "Confirmo que he leído y comprendido el ",
@@ -894,6 +942,7 @@
"p3": "Seleccionar esta opción restringe a los firmantes de realizar cualquier adición al documento durante el proceso de firma."
},
"signature-setting": "Configuración de firmas",
"rotation": "Rotación",
"signyour-self-button": "Utilice este formulario para crear plantillas reutilizables para sus documentos.",
"requestsign-button": "Utilice esta opción para solicitar firmas de otras personas y de usted mismo en conjunto.",
"signyour-self-description": "Utilice este formulario para firmar el documento usted mismo sin agregar otros.",
@@ -928,6 +977,8 @@
"visit-below-link": "Visita el siguiente enlace para saber más -",
"storage-help": "Habilitar BYOC te permite conectar tu propio almacenamiento S3 para que tus archivos permanezcan completamente bajo tu control, sin copias externas retenidas. Si la autonomía de los datos es importante para ti, considera actualizar a Teams para desbloquear esta función.",
"daily-quota-reached": "Ha alcanzado su cuota diaria. Para obtener ayuda, comuníquese con quotas@opensignlabs.com.",
"email-not-verified-send": "Por favor, verifica tu dirección de correo antes de enviar correos. Ve a tu <1>configuración de perfil</1> para completar la verificación del correo.",
"email-not-verified-send-plain": "Por favor, verifica tu dirección de correo antes de enviar correos. Ve a la configuración de tu perfil para completar la verificación del correo.",
"enabled-signature-type": "Tipos de firma habilitados",
"enabled-signature-type-help": "La configuración de 'Tipos de firma habilitados' determina qué opciones de firma están disponibles en su organización. Por ejemplo, si desactiva la opción 'Dibujar', los miembros de su organización no la verán en el widget de firma, mientras que las otras tres opciones seguirán siendo accesibles.",
"indexing-public-profile": "Permitir la indexación del perfil público por los motores de búsqueda",
@@ -955,6 +1006,7 @@
"please-provide-templateid": "Por favor, proporcione templateid",
"this-template-is-not-public": "Esta template no es pública",
"invalid-templateid": "templateid no válida",
"kiosk-no-active-role": "Esta plantilla no tiene roles de firmante o aprobador disponibles para la firma en modo kiosko.",
"contact-billing-at-opensign": "Para agregar más asientos, comuníquese con OpenSign™ a <1>billing@opensignlabs.com</1> para obtener ayuda.",
"title-length-alert": "El título debe tener como máximo 250 caracteres.",
"note-length-alert": "La nota debe tener como máximo 200 caracteres.",
@@ -1303,7 +1355,7 @@
"signer-form-data-for": "Datos del formulario para el firmante: {{signerName}} <{{signerEmail}}>",
"widget-name": "Nombre del widget",
"widget-responses": "Respuestas del widget",
"formdata-download-note": "Nota: Las firmas, sellos, iniciales e imágenes no se exportarán.",
"formdata-download-note": "Nota: Las firmas, sellos, iniciales e imágenes no se exportarán. Los datos de los firmantes sin conexión no estarán disponibles.",
"export-all-formdata-error": "No puede exportar los datos del formulario porque la plantilla contiene nombres de widgets duplicados. Cree una nueva plantilla para poder exportar los datos.",
"export-formdata-error": "No puede exportar los datos del formulario porque el documento contiene nombres de widgets duplicados. Cree un documento nuevo sin nombres duplicados para poder exportar los datos.",
"duplicate-widget-name-error": "Este nombre de widget ya está asignado a otro firmante. Por favor elija un nombre diferente.",
@@ -1416,5 +1468,95 @@
"switch-to-advanced": "Cambiar a avanzado",
"switch-to-basic": "Cambiar a básico",
"use-name-as-sender": "Usar el nombre del perfil como remitente",
"use-name-as-sender-help": "Cuando está activado, {{appName}} usa el nombre de tu perfil como remitente para las solicitudes de firma y el correo de finalización."
"use-name-as-sender-help": "Cuando está activado, {{appName}} usa el nombre de tu perfil como remitente para las solicitudes de firma y el correo de finalización.",
"acro-fields-detected": "Campos de formulario detectados",
"acro-fields-detected-msg": "Se detectaron {{count}} campo(s) de formulario en el PDF cargado. ¿Desea insertar automáticamente los widgets correspondientes?",
"documents-count": "Cantidad de documentos",
"templates-count": "Cantidad de plantillas",
"emails-sent": "Correos enviados",
"storage-used": "Almacenamiento usado",
"docs-count-team-help": "Número total de documentos creados por todos los usuarios en tus organizaciones.",
"docs-count-help": "Número total de documentos creados por ti.",
"templates-count-team-help": "Número total de plantillas creadas por todos los usuarios en tus organizaciones.",
"templates-count-help": "Número total de plantillas creadas por ti.",
"emails-sent-team-help": "Número total de notificaciones por correo enviadas en tus organizaciones.",
"emails-sent-help": "Número total de notificaciones por correo enviadas por ti.",
"storage-used-team-help": "Espacio total de almacenamiento consumido por todos los usuarios en tus organizaciones.",
"storage-used-help": "Espacio total de almacenamiento consumido por ti.",
"cloud": "Nube",
"on-premises": "En las instalaciones",
"plan-name-free": "OPENSIGN™ FREE",
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
"plan-name-teams": "OPENSIGN™ TEAMS",
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
"plan-subtitle-free": "Firmas electrónicas ilimitadas gratuitas, para siempre.",
"plan-subtitle-professional": "Acceso exclusivo a funciones avanzadas.",
"plan-subtitle-scalable": "Funciones escalables con soporte prioritario.",
"subscribe": "Suscribirse",
"contact-us": "Contáctenos",
"benefit-unlimited-digital-signatures": "Firmas digitales ilimitadas",
"benefit-sign-docs-yourself": "Firmar documentos usted mismo",
"benefit-request-signatures": "Solicitar firmas de otros",
"benefit-unlimited-templates": "Plantillas ilimitadas",
"benefit-14-field-types": "14 tipos de campo",
"benefit-automatic-e-signatures": "Firmas electrónicas automáticas",
"benefit-completion-certificates": "Certificados de finalización",
"benefit-send-in-order": "Enviar en orden",
"benefit-organize-docs": "Organizar documentos en OpenSign™ Drive",
"benefit-document-templates": "Plantillas de documentos",
"benefit-import-from-dropbox": "Importar desde Dropbox",
"benefit-contact-book": "Libreta de contactos",
"benefit-document-expiry": "Compatibilidad con vencimiento de documentos",
"benefit-decline-document": "Compatibilidad con rechazo de documentos",
"benefit-email-notifications": "Notificaciones por correo electrónico",
"benefit-public-profiles": "Perfiles públicos",
"benefit-and-much-more": "Y mucho más",
"benefit-everything-free": "Todo en OpenSign™ Free",
"benefit-field-validations": "Validaciones de campo",
"benefit-regex-validations": "Validaciones de expresiones regulares",
"benefit-webhooks": "Webhooks",
"benefit-zapier": "Integración con Zapier",
"benefit-api-access": "Acceso a API",
"benefit-api-signatures-100": "hasta 100 firmas de API",
"benefit-api-signatures-240": "hasta 240 firmas de API",
"benefit-custom-email-templates": "Plantillas de correo electrónico personalizadas",
"benefit-custom-smtp": "Conecte su propia cuenta de Gmail o SMTP para enviar correos",
"benefit-auto-reminders": "Recordatorios automáticos",
"benefit-bulk-send-100": "Envío masivo (hasta 100 documentos)",
"benefit-bulk-send-240": "Envío masivo (hasta 240 documentos)",
"benefit-premium-profiles": "Nombres de usuario de perfil público premium",
"benefit-email-verification": "Aplicar verificación basada en correo electrónico para confirmar la identidad del firmante",
"benefit-embedded-signing": "Firma incrustada",
"benefit-everything-professional": "Todo en OpenSign™ Professional",
"benefit-teams-organizations": "Equipos y Organizaciones",
"benefit-share-templates-teams": "Compartir plantillas con equipos",
"benefit-share-templates-individuals": "Compartir plantillas con individuos",
"benefit-byoc": "BYOC - Almacena tus documentos en tu propio almacenamiento en la nube",
"benefit-doc-id-removal": "Eliminación de DocumentId de documentos firmados",
"benefit-request-payments": "Solicitar pagos (próximamente)",
"benefit-mobile-app": "Aplicación móvil (próximamente)",
"benefit-api-signatures-500": "hasta 500 firmas de API",
"benefit-bulk-send-500": "Envío masivo (hasta 500 documentos)",
"benefit-all-features": "Todas las funciones",
"benefit-custom-domain": "Dominio personalizado",
"benefit-custom-branding": "Marca personalizada",
"benefit-uptime-sla": "SLA de tiempo de actividad",
"benefit-sso-support": "Soporte SSO",
"benefit-priority-support": "Soporte prioritario",
"benefit-3-users-1500-credits": "Incluye 3 usuarios y 1500 créditos premium",
"benefit-public-links": "Enlaces públicos",
"benefit-kiosk-mode": "Modo quiosco",
"benefit-bulk-send-1500": "Envío masivo (hasta 1500 documentos)",
"benefit-connect-gmail-smtp": "Conecte su propio Gmail o SMTP",
"benefit-analytics": "Análisis",
"benefit-bulksend-api-pro-month-help": "Este plan incluye 100 créditos premium al mes que te permiten enviar documentos mediante la API, compartir enlaces públicos de firma, ejecutar solicitudes de firma masiva, usar el modo Kiosco para firmas presenciales o recopilar firmas de documentos incrustados directamente en tu sitio web. Cada vez que inicias o envías un documento, se usa un crédito — y ese mismo documento puede incluir hasta 10 firmantes.",
"benefit-bulksend-api-pro-year-help": "Este plan incluye 240 créditos premium al año que te permiten enviar documentos mediante la API, compartir enlaces públicos de firma, ejecutar solicitudes de firma masiva, usar el modo Kiosco para firmas presenciales o recopilar firmas de documentos incrustados directamente en tu sitio web. Cada vez que inicias o envías un documento, se usa un crédito — y ese mismo documento puede incluir hasta 10 firmantes.",
"benefit-bulksend-api-team-month-help": "Este plan incluye 100 créditos premium al mes que te permiten enviar documentos mediante la API, compartir enlaces públicos de firma, ejecutar solicitudes de firma masiva, usar el modo Kiosco para firmas presenciales o recopilar firmas de documentos incrustados directamente en tu sitio web. Cada vez que inicias o envías un documento, se usa un crédito — y ese mismo documento puede incluir hasta 10 firmantes.",
"benefit-bulksend-api-team-year-help": "Este plan incluye 500 créditos premium al año que te permiten enviar documentos mediante la API, compartir enlaces públicos de firma, ejecutar solicitudes de firma masiva, usar el modo Kiosco para firmas presenciales o recopilar firmas de documentos incrustados directamente en tu sitio web. Cada vez que inicias o envías un documento, se usa un crédito — y ese mismo documento puede incluir hasta 10 firmantes.",
"failed-pdf-for-prefill-processing": "No se pudo cargar el PDF para el procesamiento de prellenado.",
"attach-all-role-to-signer": "Por favor, asigna todos los roles al firmante.",
"upgrade-to-edit": "Actualiza para editar",
"hide-live-chat-for-signers": "Ocultar chat en vivo para firmantes",
"hide-live-chat-for-signers-help": "Cuando está activado, los widgets de chat en vivo se ocultarán para los firmantes en las páginas de firma."
}
@@ -237,6 +237,8 @@
"p3": "La sélection de cette option enverra les liens de signature à tous les signataires simultanément. Chaque signataire peut signer le document à sa convenance, que d'autres signataires aient ou non complété leur signature. Cette méthode est plus rapide mais n'impose aucun ordre de signature entre les participants.",
"p4": "Sélectionnez l'option qui correspond le mieux aux besoins de votre traitement de documents."
},
"strict-order": "Imposer l'ordre strict",
"strict-order-help": "Lorsqu'il est activé, aucun signataire ne peut signer ou approuver tant que tous les signataires/approbateurs précédents n'ont pas terminé leur action.",
"no": "Non",
"auto-reminder": "Rappel automatique",
"remind-once": "Rappeler une fois tous les (jours)",
@@ -260,6 +262,7 @@
"signers": "Signataires",
"signers-help": "Commencez à saisir le nom d'un contact pour voir les signataires suggérés par vos contacts enregistrés ou en ajouter de nouveaux. Organisez l'ordre de signature en ajoutant des signataires dans l'ordre souhaité. Utilisez le bouton « + » pour inclure les signataires et le « x » pour les supprimer. Chaque signataire recevra un e-mail invité à signer le document dans l'ordre indiqué.",
"bcc-help": "Commencez à taper le nom d'un contact pour voir les suggestions parmi vos contacts enregistrés ou en ajouter de nouveaux. Utilisez le bouton '+' pour ajouter un utilisateur et le bouton 'x' pour le supprimer. L'adresse e-mail de l'utilisateur sélectionné sera ajoutée en Bcc (copie carbone invisible). Chaque utilisateur recevra une notification par e-mail une fois le document terminé.",
"cc-help": "Commencez à saisir le nom d'un contact pour voir des suggestions à partir de vos contacts enregistrés ou en ajouter de nouveaux. Utilisez le bouton « + » pour ajouter un utilisateur et le « x » pour le supprimer. L'e-mail de l'utilisateur sélectionné sera ajouté en CC (copie carbone). Chaque utilisateur recevra une notification par e-mail une fois le document terminé.",
"add-signer": "Ajouter un signataire",
"contact-not-found": "Contact introuvable",
"add-yourself": "Ajoutez-vous",
@@ -312,6 +315,31 @@
"user-activated": "Activé par l'utilisateur.",
"public": "Public",
"download": "Télécharger",
"sign-offline": "Signer hors ligne",
"sign-offline-instructions": "Téléchargez le document, signez-le avec un outil externe, puis téléversez la copie signée ici. Le propriétaire du document examinera votre soumission et l'approuvera ou la rejettera.",
"sign-offline-step-1": "Télécharger le PDF non signé.",
"sign-offline-step-2": "Signer le PDF avec votre outil préféré.",
"sign-offline-step-3": "Téléverser le PDF signé pour approbation du propriétaire.",
"sign-offline-upload-hint": "Sélectionner le PDF signé pour le soumettre à l'approbation.",
"sign-offline-submitted-title": "Soumis pour approbation",
"sign-offline-submitted-body": "Le propriétaire du document a été notifié par e-mail pour examiner votre copie signée hors ligne.",
"offline-already-submitted-title": "Document soumis hors ligne",
"offline-already-submitted-body": "Vous avez déjà soumis ce document pour la vérification de la signature hors ligne. Vous serez notifié une fois qu'il sera approuvé ou rejeté.",
"offline-action-approve-title": "Approuver la signature hors ligne",
"offline-action-approve-confirm": "Approuver",
"offline-action-reject-title": "Rejeter la signature hors ligne",
"offline-action-reject-hint": "Fournissez éventuellement un motif. Le signataire sera notifié par e-mail.",
"offline-action-reject-reason-placeholder": "Motif (facultatif)",
"offline-action-reject-confirm": "Confirmer le rejet",
"offline-action-review-title": "Examiner la signature hors ligne",
"offline-action-review-hint": "Prévisualisez le PDF signé téléversé par le signataire, puis approuvez-le ou rejetez-le. Aucune action n'est prise avant confirmation.",
"offline-action-submitted-at": "Soumis",
"offline-action-no-preview": "Aucun aperçu disponible pour cette soumission.",
"offline-action-open-in-new-tab": "Ouvrir le PDF dans un nouvel onglet",
"offline-action-invalid-link": "Ce lien de révision est invalide ou a expiré.",
"offline-action-success": "Action effectuée.",
"reject": "Rejeter",
"document": "Document",
"view": "voir",
"make-template-public": "Rendre le modèle public",
"make-template-private": "Rendre le modèle privé",
@@ -405,6 +433,14 @@
"certificate": "Certificat",
"decline": "refusé",
"finish": "terminé",
"approve": "Approuver",
"approve-mssg": " Êtes-vous sûr de vouloir approuver le document ?",
"signer-role-label": "Rôle du destinataire",
"signer-role-signer": "Signataire",
"signer-role-viewer": "Lecteur",
"signer-role-approver": "Approbateur",
"signer-role-hint": "Le signataire doit signer. L'approbateur approuve sans signer. Le lecteur peut uniquement consulter.",
"signer-role-upgrade-msg": "Les rôles Lecteur et Approbateur sont uniquement disponibles dans les forfaits payants. Mettez à niveau votre forfait pour attribuer ces rôles aux destinataires.",
"done": "Terminé",
"mail": "Mail",
"sign-now": "Signez maintenant",
@@ -543,6 +579,7 @@
"sign-url": "Signer l'URL",
"mails-sent": "Mails envoyés avec succès !",
"mail-not-delivered": "Courrier non distribué",
"email-not-verified-head": "E-mail non vérifié",
"document-alert": "Alerte document",
"owner-subscription-expired": "L'abonnement du propriétaire a expiré.",
"owner-doesnt-have-paid-plan": "Le propriétaire n'a pas de plan payant.",
@@ -618,6 +655,9 @@
"document-signed-alert-7": "Ce document n'est plus disponible.",
"document-signed-alert-8": "Le document a déjà été envoyé pour signature.",
"yet-to-sign": "Reste à signer",
"viewers": "Lecteurs",
"viewer-no-widgets": "Les lecteurs reçoivent une copie en lecture seule du document et ne peuvent pas avoir de widgets assignés.",
"viewer-confirm-widget-removal": "Le passage de ce destinataire en Lecteur supprimera tous les widgets déjà assignés. Continuer ?",
"validation-alert-1": "L'entrée ne répond pas aux critères définis par l'expression régulière.",
"account-already-exists": "Le compte existe déjà!",
"choose-password": "Choisissez un mot de passe",
@@ -768,6 +808,13 @@
"p3": "La sélection de cette option désactivera la vérification OTP, permettant aux utilisateurs de signer le document directement sans étapes supplémentaires.",
"p4": "Veuillez choisir l'option qui correspond le mieux à vos exigences en matière de signature de documents."
},
"allow-offline-sign": "Autoriser la signature hors ligne",
"allow-offline-sign-help": {
"p1": "Choisissez si les signataires peuvent soumettre une copie de ce document signée avec un outil externe pour examen par le propriétaire.",
"p2": "Les signataires verront une option Signer hors ligne et pourront téléverser un PDF signé en externe pour validation.",
"p3": "Les signataires ne peuvent signer que dans l'application. L'option Signer hors ligne sera masquée.",
"p4": "Ce paramètre prend par défaut votre préférence de compte et est disponible sur les forfaits payants."
},
"advanced-options": "Options avancées",
"hide-advanced-options": "Masquer les options avancées",
"download-files": "Télécharger des fichiers",
@@ -777,7 +824,7 @@
"server-down": "Impossible de se connecter au serveur {{appName}}. Si vous hébergez vous-même {{appName}}, veuillez vous assurer que toutes les étapes de la documentation ont été correctement suivies. Si vous exécutez {{appName}} localement, vous y accédez peut-être via un numéro de port incorrect.",
"admin-exists": "L'administrateur existe déjà. Veuillez vous connecter à l'application en utilisant les informations d'identification de l'administrateur afin de gérer les utilisateurs.",
"enable-tour": "Activer la visite",
"istourenabled-otp-help": {
"istourenabled-help": {
"p1": "Cela permettra une visite guidée pour les signataires, fournissant des instructions pendant le processus de signature.",
"p2": "Cela désactivera la visite guidée, garantissant ainsi une expérience de signature plus rapide et ininterrompue.",
"p3": "Conseil: si vos signataires connaissent {{appName}} et l'utilisent fréquemment, vous souhaiterez peut-être désactiver la visite guidée pour une expérience plus fluide. Choisissez l'option qui correspond le mieux à vos besoins en matière de signature de documents."
@@ -830,6 +877,7 @@
"delete-folder-alert-1": "Impossible de supprimer : le dossier doit être vide.",
"defaultSign-alert": "Les widgets {{widgetsType}} ne sont pas disponibles pour la signature automatique.",
"embed-heading": "Intégrer des modèles {{appName}}",
"other-apps-by-opensign": "Autres applications par l'équipe OpenSign",
"public-template-edit-title": "Modifier le modèle public",
"public-template-edit-mssg": "Vous ne pouvez pas modifier directement un modèle public. Pour apporter des modifications, le modèle sera défini comme privé et vous devrez ensuite le rendre à nouveau public. Voulez-vous continuer ?",
"agree-p1": "Je confirme avoir lu et compris les ",
@@ -894,6 +942,7 @@
"p3": "Sélectionner cette option empêche les signataires d'apporter des ajouts au document pendant le processus de signature."
},
"signature-setting": "Paramètres de signature",
"rotation": "Rotation",
"change-signer": "Changer de signataire",
"signyour-self-button": "Utilisez cette option pour signer le document vous-même sans en ajouter d'autres.",
"requestsign-button": "Utilisez cette option pour demander les signatures des autres et de vous-même.",
@@ -928,6 +977,8 @@
"visit-below-link": "Visitez le lien ci-dessous pour en savoir plus -",
"storage-help": "Activer BYOC vous permet de connecter votre propre stockage S3 afin que vos fichiers restent entièrement sous votre contrôle, sans copie externe conservée. Si l'autonomie des données est importante pour vous, envisagez de passer à l'offre Teams pour débloquer cette fonctionnalité.",
"daily-quota-reached": "Vous avez atteint votre quota quotidien. Pour obtenir de l'aide, veuillez contacter quotas@opensignlabs.com.",
"email-not-verified-send": "Veuillez vérifier votre adresse e-mail avant d'envoyer des e-mails. Accédez à vos <1>paramètres de profil</1> pour terminer la vérification de l'e-mail.",
"email-not-verified-send-plain": "Veuillez vérifier votre adresse e-mail avant d'envoyer des e-mails. Accédez à vos paramètres de profil pour terminer la vérification de l'e-mail.",
"enabled-signature-type": "Types de signature activés",
"enabled-signature-type-help": "Le paramètre 'Types de signature activés' détermine quelles options de signature sont disponibles dans votre organisation. Par exemple, si vous désactivez l'option 'Dessiner', les membres de votre organisation ne la verront pas dans le widget de signature, tandis que les trois autres options resteront accessibles.",
"indexing-public-profile": "Autoriser l'indexation du profil public par les moteurs de recherche",
@@ -955,6 +1006,7 @@
"please-provide-templateid": "Veuillez fournir templateid",
"this-template-is-not-public": "Ce template n'est pas public",
"invalid-templateid": "templateid invalide",
"kiosk-no-active-role": "Ce modèle n'a aucun rôle de signataire ou d'approbateur disponible pour la signature en mode kiosque.",
"contact-billing-at-opensign": "Pour ajouter plus de places, veuillez contacter OpenSign™ à l'adresse <1>billing@opensignlabs.com</1> pour obtenir de l'aide.",
"title-length-alert": "Le titre doit comporter au maximum 250 caractères.",
"note-length-alert": "La note doit comporter au maximum 200 caractères.",
@@ -1303,7 +1355,7 @@
"signer-form-data-for": "Données du formulaire pour le signataire : {{signerName}} <{{signerEmail}}>",
"widget-name": "Nom du widget",
"widget-responses": "Réponses du widget",
"formdata-download-note": "Remarque : Les signatures, tampons, initiales et images ne seront pas exportés.",
"formdata-download-note": "Remarque : Les signatures, tampons, initiales et images ne seront pas exportés. Les données des signataires hors ligne ne seront pas disponibles.",
"export-all-formdata-error": "Vous ne pouvez pas exporter les données du formulaire car le modèle contient des noms de widgets en double. Veuillez créer un nouveau modèle pour pouvoir exporter les données.",
"export-formdata-error": "Vous ne pouvez pas exporter les données du formulaire car le document contient des noms de widgets en double. Veuillez créer un nouveau document sans doublons pour pouvoir exporter les données.",
"duplicate-widget-name-error": "Ce nom de widget est déjà attribué à un autre signataire. Veuillez choisir un autre nom.",
@@ -1416,5 +1468,96 @@
"switch-to-advanced": "Passer en mode avancé",
"switch-to-basic": "Passer en mode basique",
"use-name-as-sender": "Utiliser le nom du profil comme expéditeur",
"use-name-as-sender-help": "Lorsque cette option est activée, {{appName}} utilise le nom de votre profil comme expéditeur pour les demandes de signature et l'e-mail de finalisation."
"use-name-as-sender-help": "Lorsque cette option est activée, {{appName}} utilise le nom de votre profil comme expéditeur pour les demandes de signature et l'e-mail de finalisation.",
"acro-fields-detected": "Champs de formulaire détectés",
"acro-fields-detected-msg": "Nous avons détecté {{count}} champ(s) de formulaire dans le PDF téléchargé. Souhaitez-vous insérer automatiquement les widgets correspondants ?",
"documents-count": "Nombre de documents",
"templates-count": "Nombre de modèles",
"emails-sent": "E-mails envoyés",
"storage-used": "Stockage utilisé",
"docs-count-team-help": "Nombre total de documents créés par tous les utilisateurs de vos organisations.",
"docs-count-help": "Nombre total de documents que vous avez créés.",
"templates-count-team-help": "Nombre total de modèles créés par tous les utilisateurs de vos organisations.",
"templates-count-help": "Nombre total de modèles que vous avez créés.",
"emails-sent-team-help": "Nombre total de notifications e-mail envoyées dans vos organisations.",
"emails-sent-help": "Nombre total de notifications e-mail envoyées par vous.",
"storage-used-team-help": "Espace de stockage total consommé par tous les utilisateurs de vos organisations.",
"storage-used-help": "Espace de stockage total consommé par vous.",
"cloud": "Cloud",
"on-premises": "Sur site",
"plan-name-free": "OPENSIGN™ FREE",
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
"plan-name-teams": "OPENSIGN™ TEAMS",
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
"plan-subtitle-free": "Signatures électroniques illimitées gratuites, pour toujours.",
"plan-subtitle-professional": "Accès exclusif aux fonctionnalités avancées.",
"plan-subtitle-scalable": "Fonctionnalités évolutives avec support prioritaire.",
"subscribe": "S'abonner",
"contact-us": "Nous contacter",
"benefit-unlimited-digital-signatures": "Signatures numériques illimitées",
"benefit-sign-docs-yourself": "Signer des documents vous-même",
"benefit-request-signatures": "Demander des signatures à d'autres",
"benefit-unlimited-templates": "Modèles illimités",
"benefit-14-field-types": "14 types de champ",
"benefit-automatic-e-signatures": "Signatures électroniques automatiques",
"benefit-completion-certificates": "Certificats d'achèvement",
"benefit-send-in-order": "Envoyer dans l'ordre",
"benefit-organize-docs": "Organiser les documents dans OpenSign™ Drive",
"benefit-document-templates": "Modèles de documents",
"benefit-import-from-dropbox": "Importer depuis Dropbox",
"benefit-contact-book": "Carnet de contacts",
"benefit-document-expiry": "Prise en charge de l'expiration des documents",
"benefit-decline-document": "Prise en charge du refus de documents",
"benefit-email-notifications": "Notifications par e-mail",
"benefit-public-profiles": "Profils publics",
"benefit-and-much-more": "Et bien plus encore",
"benefit-everything-free": "Tout dans OpenSign™ Free",
"benefit-field-validations": "Validations de champ",
"benefit-regex-validations": "Validations par expression régulière",
"benefit-webhooks": "Webhooks",
"benefit-zapier": "Intégration Zapier",
"benefit-api-access": "Accès API",
"benefit-api-signatures-100": "jusqu'à 100 signatures API",
"benefit-api-signatures-240": "jusqu'à 240 signatures API",
"benefit-custom-email-templates": "Modèles d'e-mail personnalisés",
"benefit-custom-smtp": "Connectez votre propre compte Gmail ou SMTP pour envoyer des e-mails",
"benefit-auto-reminders": "Rappels automatiques",
"benefit-bulk-send-100": "Envoi en masse (jusqu'à 100 documents)",
"benefit-bulk-send-240": "Envoi en masse (jusqu'à 240 documents)",
"benefit-premium-profiles": "Noms d'utilisateur de profil public premium",
"benefit-email-verification": "Appliquer la vérification par e-mail pour confirmer l'identité du signataire",
"benefit-embedded-signing": "Signature intégrée",
"benefit-everything-professional": "Tout dans OpenSign™ Professional",
"benefit-teams-organizations": "Équipes et Organisations",
"benefit-share-templates-teams": "Partager des modèles avec des équipes",
"benefit-share-templates-individuals": "Partager des modèles avec des individus",
"benefit-byoc": "BYOC - Stockez vos documents dans votre propre stockage cloud",
"benefit-doc-id-removal": "Suppression de l'ID de document des documents signés",
"benefit-request-payments": "Demander des paiements (bientôt disponible)",
"benefit-mobile-app": "Application mobile (bientôt disponible)",
"benefit-api-signatures-500": "jusqu'à 500 signatures API",
"benefit-bulk-send-500": "Envoi en masse (jusqu'à 500 documents)",
"benefit-all-features": "Toutes les fonctionnalités",
"benefit-custom-domain": "Domaine personnalisé",
"benefit-custom-branding": "Identité visuelle personnalisée",
"benefit-uptime-sla": "SLA de disponibilité",
"benefit-sso-support": "Support SSO",
"benefit-priority-support": "Support prioritaire",
"benefit-3-users-1500-credits": "Comprend 3 utilisateurs et 1500 crédits premium",
"benefit-public-links": "Liens publics",
"benefit-kiosk-mode": "Mode kiosque",
"benefit-bulk-send-1500": "Envoi en masse (jusqu'à 1500 documents)",
"benefit-connect-gmail-smtp": "Connectez votre propre Gmail ou SMTP",
"benefit-analytics": "Analyses",
"something-went-wrong-mssg": "Une erreur s'est produite, actualiser cette page peut résoudre le problème.",
"benefit-bulksend-api-pro-month-help": "Ce forfait inclut 100 crédits premium par mois qui vous permettent d'envoyer des documents via l'API, de partager des liens publics de signature, de lancer des demandes de signature en masse, d'utiliser le mode Kiosque pour les signatures en personne ou de collecter des signatures à partir de documents intégrés directement à votre site web. À chaque fois que vous initiez ou envoyez un document, un crédit est utilisé — et ce document peut inclure jusqu'à 10 signataires.",
"benefit-bulksend-api-pro-year-help": "Ce forfait inclut 240 crédits premium par an qui vous permettent d'envoyer des documents via l'API, de partager des liens publics de signature, de lancer des demandes de signature en masse, d'utiliser le mode Kiosque pour les signatures en personne ou de collecter des signatures à partir de documents intégrés directement à votre site web. À chaque fois que vous initiez ou envoyez un document, un crédit est utilisé — et ce document peut inclure jusqu'à 10 signataires.",
"benefit-bulksend-api-team-month-help": "Ce forfait inclut 100 crédits premium par mois qui vous permettent d'envoyer des documents via l'API, de partager des liens publics de signature, de lancer des demandes de signature en masse, d'utiliser le mode Kiosque pour les signatures en personne ou de collecter des signatures à partir de documents intégrés directement à votre site web. À chaque fois que vous initiez ou envoyez un document, un crédit est utilisé — et ce document peut inclure jusqu'à 10 signataires.",
"benefit-bulksend-api-team-year-help": "Ce forfait inclut 500 crédits premium par an qui vous permettent d'envoyer des documents via l'API, de partager des liens publics de signature, de lancer des demandes de signature en masse, d'utiliser le mode Kiosque pour les signatures en personne ou de collecter des signatures à partir de documents intégrés directement à votre site web. À chaque fois que vous initiez ou envoyez un document, un crédit est utilisé — et ce document peut inclure jusqu'à 10 signataires.",
"failed-pdf-for-prefill-processing": "Impossible de charger le PDF pour le traitement du préremplissage.",
"attach-all-role-to-signer": "Veuillez attribuer tous les rôles au signataire.",
"upgrade-to-edit": "Mettre à niveau pour modifier",
"hide-live-chat-for-signers": "Masquer le chat en direct pour les signataires",
"hide-live-chat-for-signers-help": "Lorsqu'activé, les widgets de chat en direct seront masqués pour les signataires sur les pages de signature."
}
@@ -237,6 +237,8 @@
"p3": "इस विकल्प का चयन करने पर हस्ताक्षर लिंक सभी हस्ताक्षरकर्ताओं को एक साथ भेजे जाएंगे। प्रत्येक हस्ताक्षरकर्ता अपनी सुविधानुसार दस्तावेज़ पर हस्ताक्षर कर सकता है, भले ही अन्य हस्ताक्षरकर्ताओं ने अपने हस्ताक्षर पूरे कर लिए हों या नहीं। यह विधि तेज़ है लेकिन प्रतिभागियों के बीच किसी भी हस्ताक्षर क्रम को लागू नहीं करती है।",
"p4": "वह विकल्प चुनें जो आपके दस्तावेज़ प्रसंस्करण की आवश्यकताओं के लिए सबसे उपयुक्त हो।"
},
"strict-order": "सख्त क्रम लागू करें",
"strict-order-help": "जब सक्षम होता है, तो कोई भी हस्ताक्षरकर्ता तब तक हस्ताक्षर या अनुमोदन नहीं कर सकता जब तक कि पिछले सभी हस्ताक्षरकर्ता/अनुमोदक अपनी कार्रवाई पूरी न कर लें।",
"no": "नहीं",
"auto-reminder": "स्वतः अनुस्मारक",
"remind-once": " प्रत्येक (दिन) में एक बार याद दिलाएं",
@@ -260,6 +262,7 @@
"signers": "हस्ताक्षरकर्ता",
"signers-help": "अपने सहेजे गए संपर्कों से सुझाए गए हस्ताक्षरकर्ताओं को देखने के लिए किसी संपर्क का नाम टाइप करना शुरू करें या नए जोड़ें। वांछित क्रम में हस्ताक्षरकर्ताओं को जोड़कर हस्ताक्षर क्रम की व्यवस्था करें। हस्ताक्षरकर्ताओं को शामिल करने के लिए '+' बटन और उन्हें हटाने के लिए 'x' का उपयोग करें। प्रत्येक हस्ताक्षरकर्ता को सूचीबद्ध क्रम में दस्तावेज़ पर हस्ताक्षर करने के लिए एक ईमेल संकेत प्राप्त होगा।",
"bcc-help": "अपने सहेजे गए संपर्कों से सुझाव देखने के लिए किसी संपर्क का नाम टाइप करना शुरू करें या नए जोड़ें। किसी उपयोगकर्ता को जोड़ने के लिए '+' बटन और उसे हटाने के लिए 'x' का उपयोग करें। चयनित उपयोगकर्ता का ईमेल Bcc (ब्लाइंड कार्बन कॉपी) के रूप में जोड़ा जाएगा। दस्तावेज़ पूरा होने के बाद प्रत्येक उपयोगकर्ता को एक ईमेल सूचना प्राप्त होगी।",
"cc-help": "सुझाव देखने के लिए अपने सेव किए गए कॉन्टैक्ट्स में से किसी कॉन्टैक्ट का नाम टाइप करना शुरू करें या नए जोड़ें। यूज़र जोड़ने के लिए '+' बटन और हटाने के लिए 'x' का उपयोग करें। चुने गए यूज़र का ईमेल CC (कार्बन कॉपी) के रूप में जोड़ा जाएगा। दस्तावेज़ पूर्ण होने पर प्रत्येक यूज़र को एक ईमेल सूचना मिलेगी।",
"add-signer": "हस्ताक्षरकर्ता जोड़ें",
"contact-not-found": "संपर्क नहीं मिला",
"add-yourself": "स्वयं को जोड़ें",
@@ -287,7 +290,7 @@
"add-webhook": "वेबहूक जोड़ें",
"webhook-added": "वेबहूक सफलतापूर्वक जोड़ा गया।",
"team-disabled": "टीम अक्षम",
"Team enabled": "टीम सक्षम",
"team-enabled": "टीम सक्षम",
"are-you-sure": "क्या आप वाकई चाहते हैं",
"disable": "अक्षम करें",
"enable": "सक्षम करें",
@@ -312,6 +315,31 @@
"user-activated": "उपयोगकर्ता सक्रिय।",
"public": "सार्वजनिक",
"download": "डाउनलोड करें",
"sign-offline": "ऑफ़लाइन हस्ताक्षर करें",
"sign-offline-instructions": "दस्तावेज़ डाउनलोड करें, इसे किसी बाहरी टूल से हस्ताक्षरित करें, फिर यहाँ हस्ताक्षरित प्रति अपलोड करें। दस्तावेज़ का मालिक आपकी सबमिशन की समीक्षा करके उसे स्वीकृत या अस्वीकार करेगा।",
"sign-offline-step-1": "बिना हस्ताक्षर का PDF डाउनलोड करें।",
"sign-offline-step-2": "अपने पसंदीदा टूल से PDF हस्ताक्षरित करें।",
"sign-offline-step-3": "मालिक की स्वीकृति के लिए हस्ताक्षरित PDF अपलोड करें।",
"sign-offline-upload-hint": "स्वीकृति के लिए सबमिट करने हेतु हस्ताक्षरित PDF चुनें।",
"sign-offline-submitted-title": "स्वीकृति के लिए सबमिट किया गया",
"sign-offline-submitted-body": "दस्तावेज़ के मालिक को आपकी ऑफ़लाइन हस्ताक्षरित प्रति की समीक्षा करने के लिए ईमेल द्वारा सूचित किया गया है।",
"offline-already-submitted-title": "दस्तावेज़ ऑफ़लाइन सबमिट किया गया",
"offline-already-submitted-body": "आपने यह दस्तावेज़ पहले ही ऑफ़लाइन हस्ताक्षर समीक्षा के लिए सबमिट कर दिया है। स्वीकृति या अस्वीकृति होने पर आपको सूचित किया जाएगा।",
"offline-action-approve-title": "ऑफ़लाइन हस्ताक्षर स्वीकृत करें",
"offline-action-approve-confirm": "स्वीकृत करें",
"offline-action-reject-title": "ऑफ़लाइन हस्ताक्षर अस्वीकार करें",
"offline-action-reject-hint": "वैकल्पिक रूप से कारण बताएं। हस्ताक्षरकर्ता को ईमेल द्वारा सूचित किया जाएगा।",
"offline-action-reject-reason-placeholder": "कारण (वैकल्पिक)",
"offline-action-reject-confirm": "अस्वीकृति की पुष्टि करें",
"offline-action-review-title": "ऑफ़लाइन हस्ताक्षर की समीक्षा करें",
"offline-action-review-hint": "हस्ताक्षरकर्ता द्वारा अपलोड किए गए हस्ताक्षरित PDF का पूर्वावलोकन करें, फिर उसे स्वीकृत या अस्वीकार करें। पुष्टि करने तक कोई कार्रवाई नहीं की जाएगी।",
"offline-action-submitted-at": "सबमिट किया गया",
"offline-action-no-preview": "इस सबमिशन के लिए कोई पूर्वावलोकन उपलब्ध नहीं है।",
"offline-action-open-in-new-tab": "PDF नए टैब में खोलें",
"offline-action-invalid-link": "यह समीक्षा लिंक अमान्य है या समाप्त हो गई है।",
"offline-action-success": "कार्रवाई पूर्ण हुई।",
"reject": "अस्वीकार करें",
"document": "दस्तावेज़",
"view": "देखें",
"make-template-public": "टेम्पलेट को सार्वजनिक करें",
"make-template-private": "टेम्पलेट को निजी बनाएं",
@@ -405,6 +433,14 @@
"certificate": "प्रमाण पत्र",
"decline": "अस्वीकार करें",
"finish": "समाप्त करें",
"approve": "स्वीकृत करें",
"approve-mssg": " क्या आप वाकई दस्तावेज़ को स्वीकृत करना चाहते हैं?",
"signer-role-label": "प्राप्तकर्ता की भूमिका",
"signer-role-signer": "हस्ताक्षरकर्ता",
"signer-role-viewer": "दर्शक",
"signer-role-approver": "अनुमोदक",
"signer-role-hint": "हस्ताक्षरकर्ता को हस्ताक्षर करना होगा। अनुमोदक बिना हस्ताक्षर किए अनुमोदन करता है। दर्शक केवल देखता है।",
"signer-role-upgrade-msg": "दर्शक और अनुमोदक भूमिकाएँ केवल सशुल्क योजनाओं में उपलब्ध हैं। प्राप्तकर्ताओं को ये भूमिकाएँ सौंपने के लिए अपनी योजना अपग्रेड करें।",
"done": "हो गया",
"mail": "मेल",
"sign-now": "अभी हस्ताक्षर करें",
@@ -543,6 +579,7 @@
"sign-url": "यूआरएल पर हस्ताक्षर करें",
"mails-sent": "मेल भेजे गए",
"mail-not-delivered": "मेल वितरित नहीं किया गया",
"email-not-verified-head": "ईमेल सत्यापित नहीं है",
"document-alert": "दस्तावेज़ चेतावनी",
"owner-subscription-expired": "मालिक की सदस्यता समाप्त हो गई है।",
"subscription-expired": "सदस्यता समाप्त",
@@ -618,6 +655,9 @@
"document-signed-alert-7": "यह दस्तावेज़ अब उपलब्ध नहीं है।",
"document-signed-alert-8": "दस्तावेज़ पर हस्ताक्षर के लिए पहले ही भेज दिया गया है।",
"yet-to-sign": "अभी हस्ताक्षर करना बाकी है",
"viewers": "दर्शक",
"viewer-no-widgets": "दर्शकों को दस्तावेज़ की केवल-पढ़ने योग्य प्रति मिलती है और उन्हें विजेट असाइन नहीं किए जा सकते।",
"viewer-confirm-widget-removal": "इस प्राप्तकर्ता को दर्शक में बदलने पर उन्हें पहले से असाइन किए गए सभी विजेट हटा दिए जाएंगे। जारी रखें?",
"validation-alert-1": "इनपुट नियमित अभिव्यक्ति द्वारा निर्धारित मानदंडों को पूरा नहीं करता है।",
"account-already-exists": "खाता पहले से मौजूद है!",
"choose-password": "पासवर्ड चुनें",
@@ -768,6 +808,13 @@
"p3": "इस विकल्प का चयन करने पर ओटीपी सत्यापन अक्षम हो जाएगा, जिससे उपयोगकर्ता अतिरिक्त चरणों के बिना सीधे दस्तावेज़ पर हस्ताक्षर कर सकेंगे।",
"p4": "कृपया वह विकल्प चुनें जो आपके दस्तावेज़ हस्ताक्षर आवश्यकताओं के लिए सबसे उपयुक्त हो।"
},
"allow-offline-sign": "ऑफ़लाइन हस्ताक्षर की अनुमति दें",
"allow-offline-sign-help": {
"p1": "चुनें कि क्या हस्ताक्षरकर्ता बाहरी टूल से हस्ताक्षरित इस दस्तावेज़ की एक प्रति स्वामी की समीक्षा के लिए जमा कर सकते हैं।",
"p2": "हस्ताक्षरकर्ताओं को ऑफ़लाइन हस्ताक्षर विकल्प दिखाई देगा और वे बाहरी रूप से हस्ताक्षरित PDF स्वीकृति के लिए अपलोड कर सकते हैं।",
"p3": "हस्ताक्षरकर्ता केवल एप के भीतर ही हस्ताक्षर कर सकते हैं। ऑफ़लाइन हस्ताक्षर विकल्प छिपा दिया जाएगा।",
"p4": "यह सेटिंग डिफ़ॉल्ट रूप से आपकी खाता प्राथमिकता का उपयोग करती है और भुगतान योजनाओं पर उपलब्ध है।"
},
"advanced-options": "उन्नत विकल्प",
"hide-advanced-options": "उन्नत विकल्प छिपाएँ",
"download-files": "फ़ाइलें डाउनलोड करें",
@@ -830,6 +877,7 @@
"delete-folder-alert-1": "हटाने में असमर्थ: फ़ोल्डर खाली होना चाहिए।",
"defaultSign-alert": "{{widgetsType}} विजेट स्वतः हस्ताक्षर के लिए उपलब्ध नहीं हैं।",
"embed-heading": "{{appName}} टेम्पलेट एम्बेड करें",
"other-apps-by-opensign": "OpenSign टीम के अन्य ऐप्स",
"public-template-edit-title": "सार्वजनिक टेम्पलेट संपादित करें",
"public-template-edit-mssg": "आप सीधे सार्वजनिक टेम्पलेट संपादित नहीं कर सकते। परिवर्तन करने के लिए, टेम्पलेट को निजी पर सेट किया जाएगा, और आपको बाद में इसे फिर से सार्वजनिक करना होगा। क्या आप जारी रखना चाहते हैं?",
"agree-p1": "मैं पुष्टि करता हूं कि मैंने पढ़ लिया है और समझ लिया है ",
@@ -900,6 +948,7 @@
"p3": "इस विकल्प का चयन करने से हस्ताक्षरकर्ताओं को हस्ताक्षर प्रक्रिया के दौरान दस्तावेज़ में कोई भी সংযোজন करने से प्रतिबंधित किया जाता है।"
},
"signature-setting": "हस्ताक्षर सेटिंग्स",
"rotation": "घुमाव",
"activation-key": "सक्रियण कुंजी",
"paid-selfhost-help": "अधिक जानकारी के लिए कृपया {{appName}} से संपर्क करें ",
"remaining-credits-selfhost-help": "अपनी वेबसाइट पर एपीआई दस्तावेज़ हस्ताक्षर, थोक भेजने {{appName}} एकीकरण के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे गए क्रेडिट शेष हैं।",
@@ -928,6 +977,8 @@
"visit-below-link": "अधिक जानने के लिए नीचे दिए गए लिंक पर जाएं -",
"storage-help": "BYOC सक्षम करने से आप अपना स्वयं का S3 संग्रहण कनेक्ट कर सकते हैं ताकि आपकी फ़ाइलें पूरी तरह से आपके नियंत्रण में रहें - कोई बाहरी प्रतियां बरकरार न रहें। यदि डेटा स्वायत्तता आपके लिए मायने रखती है, तो इस सुविधा को अनलॉक करने के लिए टीमों में अपग्रेड करने पर विचार करें।",
"daily-quota-reached": "आपने अपना दैनिक कोटा पूरा कर लिया है। सहायता के लिए, कृपया quotas@opensignlabs.com पर संपर्क करें।",
"email-not-verified-send": "ईमेल भेजने से पहले कृपया अपना ईमेल पता सत्यापित करें। ईमेल सत्यापन पूरा करने के लिए अपनी <1>प्रोफ़ाइल सेटिंग्स</1> में जाएँ।",
"email-not-verified-send-plain": "ईमेल भेजने से पहले कृपया अपना ईमेल पता सत्यापित करें। ईमेल सत्यापन पूरा करने के लिए अपनी प्रोफ़ाइल सेटिंग्स में जाएँ।",
"enabled-signature-type": "सक्षम हस्ताक्षर प्रकार",
"enabled-signature-type-help": "'सक्षम हस्ताक्षर प्रकार' सेटिंग यह निर्धारित करती है कि आपके संगठन में कौन से हस्ताक्षर विकल्प उपलब्ध हैं। उदाहरण के लिए, यदि आप 'ड्रा' विकल्प को अक्षम करते हैं, तो आपके संगठन के सदस्य इसे हस्ताक्षर विजेट में नहीं देख पाएंगे, जबकि अन्य तीन विकल्प पहुंच योग्य रहेंगे।",
"indexing-public-profile": "खोज इंजन द्वारा सार्वजनिक प्रोफ़ाइल को अनुक्रमित करने की अनुमति दें",
@@ -955,6 +1006,7 @@
"please-provide-templateid": "कृपया टेम्पलेट आईडी प्रदान करें",
"this-template-is-not-public": "यह टेम्पलेट सार्वजनिक नहीं है",
"invalid-templateid": "अमान्य टेम्पलेट आईडी",
"kiosk-no-active-role": "इस टेम्पलेट में कियोस्क हस्ताक्षर के लिए कोई हस्ताक्षरकर्ता या अनुमोदक भूमिका उपलब्ध नहीं है।",
"contact-billing-at-opensign": "अधिक सीटें जोड़ने के लिए, कृपया सहायता के लिए <1>billing@opensignlabs.com</1> पर OpenSign™ से संपर्क करें",
"title-length-alert": "शीर्षक अधिकतम 250 वर्ण लंबा होना चाहिए।",
"note-length-alert": "टिप्पणी अधिकतम 200 वर्ण लंबी होनी चाहिए।",
@@ -1303,7 +1355,7 @@
"signer-form-data-for": "हस्ताक्षरकर्ता के लिए फ़ॉर्म डेटा: {{signerName}} <{{signerEmail}}>",
"widget-name": "विजेट नाम",
"widget-responses": "विजेट प्रतिक्रियाएँ",
"formdata-download-note": "नोट: हस्ताक्षर, स्टैम्प, प्रारंभिक अक्षर और छवियाँ निर्यात नहीं की जाएँगी।",
"formdata-download-note": "नोट: हस्ताक्षर, स्टैम्प, प्रारंभिक अक्षर और छवियाँ निर्यात नहीं की जाएँगी। ऑफ़लाइन हस्ताक्षरकर्ताओं का डेटा उपलब्ध नहीं होगा।",
"export-all-formdata-error": "आप फ़ॉर्म डेटा निर्यात नहीं कर सकते क्योंकि टेम्पलेट में डुप्लिकेट विजेट नाम हैं। कृपया नया टेम्पलेट बनाएं ताकि आप फ़ॉर्म डेटा निर्यात कर सकें।",
"export-formdata-error": "आप फ़ॉर्म डेटा निर्यात नहीं कर सकते क्योंकि दस्तावेज़ में डुप्लिकेट विजेट नाम हैं। कृपया नया दस्तावेज़ बनाएं जिसमें डुप्लिकेट विजेट नाम न हों।",
"duplicate-widget-name-error": "यह विजेट नाम पहले से ही किसी अन्य साइनर को सौंपा गया है। कृपया कोई दूसरा नाम चुनें।",
@@ -1416,5 +1468,95 @@
"switch-to-advanced": "एडवांस्ड पर स्विच करें",
"switch-to-basic": "बेसिक पर स्विच करें",
"use-name-as-sender": "प्रेषक के रूप में प्रोफ़ाइल नाम का उपयोग करें",
"use-name-as-sender-help": "सक्षम होने पर, {{appName}} हस्ताक्षर अनुरोध और पूर्णता ईमेल के लिए आपके प्रोफ़ाइल नाम को प्रेषक के रूप में उपयोग करता है।"
"use-name-as-sender-help": "सक्षम होने पर, {{appName}} हस्ताक्षर अनुरोध और पूर्णता ईमेल के लिए आपके प्रोफ़ाइल नाम को प्रेषक के रूप में उपयोग करता है।",
"acro-fields-detected": "फॉर्म फ़ील्ड मिले",
"acro-fields-detected-msg": "अपलोड किए गए PDF में {{count}} फॉर्म फ़ील्ड पाए गए। क्या आप स्वचालित रूप से संबंधित विजेट सम्मिलित करना चाहेंगे?",
"documents-count": "दस्तावेज़ों की संख्या",
"templates-count": "टेम्पलेट्स की संख्या",
"emails-sent": "भेजे गए ईमेल",
"storage-used": "उपयोग किया गया स्टोरेज",
"docs-count-team-help": "आपकी संगठनों में सभी उपयोगकर्ताओं द्वारा बनाए गए दस्तावेज़ों की कुल संख्या।",
"docs-count-help": "आपके द्वारा बनाए गए दस्तावेज़ों की कुल संख्या।",
"templates-count-team-help": "आपकी संगठनों में सभी उपयोगकर्ताओं द्वारा बनाए गए टेम्पलेट्स की कुल संख्या।",
"templates-count-help": "आपके द्वारा बनाए गए टेम्पलेट्स की कुल संख्या।",
"emails-sent-team-help": "आपकी संगठनों में भेजी गई ईमेल सूचनाओं की कुल संख्या।",
"emails-sent-help": "आपके द्वारा भेजी गई ईमेल सूचनाओं की कुल संख्या।",
"storage-used-team-help": "आपकी संगठनों में सभी उपयोगकर्ताओं द्वारा उपयोग किया गया कुल स्टोरेज स्पेस।",
"storage-used-help": "आपके द्वारा उपयोग किया गया कुल स्टोरेज स्पेस।",
"cloud": "क्लाउड",
"on-premises": "ऑन-प्रिमाइसेस",
"plan-name-free": "OPENSIGN™ FREE",
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
"plan-name-teams": "OPENSIGN™ TEAMS",
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
"plan-subtitle-free": "निःशुल्क असीमित ई-हस्ताक्षर, हमेशा के लिए।",
"plan-subtitle-professional": "उन्नत सुविधाओं तक विशेष पहुँच।",
"plan-subtitle-scalable": "प्राथमिकता समर्थन के साथ स्केलेबल सुविधाएँ।",
"subscribe": "सदस्यता लें",
"contact-us": "हमसे संपर्क करें",
"benefit-unlimited-digital-signatures": "असीमित डिजिटल हस्ताक्षर",
"benefit-sign-docs-yourself": "स्वयं दस्तावेज़ पर हस्ताक्षर करें",
"benefit-request-signatures": "दूसरों से हस्ताक्षर का अनुरोध करें",
"benefit-unlimited-templates": "असीमित टेम्पलेट",
"benefit-14-field-types": "14 फ़ील्ड प्रकार",
"benefit-automatic-e-signatures": "स्वचालित ई-हस्ताक्षर",
"benefit-completion-certificates": "समापन प्रमाण पत्र",
"benefit-send-in-order": "क्रम में भेजें",
"benefit-organize-docs": "OpenSign™ Drive में दस्तावेज़ व्यवस्थित करें",
"benefit-document-templates": "दस्तावेज़ टेम्पलेट",
"benefit-import-from-dropbox": "Dropbox से आयात करें",
"benefit-contact-book": "संपर्क पुस्तक",
"benefit-document-expiry": "दस्तावेज़ समाप्ति समर्थन",
"benefit-decline-document": "दस्तावेज़ अस्वीकृति समर्थन",
"benefit-email-notifications": "ईमेल सूचनाएं",
"benefit-public-profiles": "सार्वजनिक प्रोफ़ाइल",
"benefit-and-much-more": "और बहुत कुछ",
"benefit-everything-free": "OpenSign™ Free में सब कुछ",
"benefit-field-validations": "फ़ील्ड सत्यापन",
"benefit-regex-validations": "नियमित अभिव्यक्ति सत्यापन",
"benefit-webhooks": "वेबहुक",
"benefit-zapier": "Zapier एकीकरण",
"benefit-api-access": "API एक्सेस",
"benefit-api-signatures-100": "100 तक API हस्ताक्षर",
"benefit-api-signatures-240": "240 तक API हस्ताक्षर",
"benefit-custom-email-templates": "कस्टम ईमेल टेम्पलेट",
"benefit-custom-smtp": "ईमेल भेजने के लिए अपना Gmail या SMTP खाता कनेक्ट करें",
"benefit-auto-reminders": "स्वचालित अनुस्मारक",
"benefit-bulk-send-100": "बल्क भेजें (100 दस्तावेज़ तक)",
"benefit-bulk-send-240": "बल्क भेजें (240 दस्तावेज़ तक)",
"benefit-premium-profiles": "प्रीमियम सार्वजनिक प्रोफ़ाइल उपयोगकर्ता नाम",
"benefit-email-verification": "हस्ताक्षरकर्ता की पहचान की पुष्टि के लिए ईमेल-आधारित सत्यापन लागू करें",
"benefit-embedded-signing": "एम्बेडेड हस्ताक्षर",
"benefit-everything-professional": "OpenSign™ Professional में सब कुछ",
"benefit-teams-organizations": "टीम और संगठन",
"benefit-share-templates-teams": "टीमों के साथ टेम्पलेट साझा करें",
"benefit-share-templates-individuals": "व्यक्तियों के साथ टेम्पलेट साझा करें",
"benefit-byoc": "BYOC - अपने दस्तावेज़ अपने क्लाउड स्टोरेज में संग्रहीत करें",
"benefit-doc-id-removal": "हस्ताक्षरित दस्तावेज़ों से DocumentId हटाना",
"benefit-request-payments": "भुगतान का अनुरोध (जल्द आ रहा है)",
"benefit-mobile-app": "मोबाइल ऐप (जल्द आ रहा है)",
"benefit-api-signatures-500": "500 तक API हस्ताक्षर",
"benefit-bulk-send-500": "बल्क भेजें (500 दस्तावेज़ तक)",
"benefit-all-features": "सभी सुविधाएँ",
"benefit-custom-domain": "कस्टम डोमेन",
"benefit-custom-branding": "कस्टम ब्रांडिंग",
"benefit-uptime-sla": "अपटाइम SLA",
"benefit-sso-support": "SSO समर्थन",
"benefit-priority-support": "प्राथमिकता समर्थन",
"benefit-3-users-1500-credits": "3 उपयोगकर्ता और 1500 प्रीमियम क्रेडिट शामिल हैं",
"benefit-public-links": "सार्वजनिक लिंक",
"benefit-kiosk-mode": "कियोस्क मोड",
"benefit-bulk-send-1500": "बल्क भेजें (1500 दस्तावेज़ तक)",
"benefit-connect-gmail-smtp": "अपना Gmail या SMTP कनेक्ट करें",
"benefit-analytics": "विश्लेषण",
"benefit-bulksend-api-pro-month-help": "इस प्लान में प्रति माह 100 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
"benefit-bulksend-api-pro-year-help": "इस प्लान में प्रति वर्ष 240 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
"benefit-bulksend-api-team-month-help": "इस प्लान में प्रति माह 100 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
"benefit-bulksend-api-team-year-help": "इस प्लान में प्रति वर्ष 500 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
"failed-pdf-for-prefill-processing": "प्रीफिल प्रोसेसिंग के लिए PDF लोड करने में विफल।",
"attach-all-role-to-signer": "कृपया सभी भूमिकाएँ साइनर से जोड़ें।",
"upgrade-to-edit": "एडिट करने के लिए अपग्रेड करें",
"hide-live-chat-for-signers": "साइनर के लिए लाइव चैट छुपाएं",
"hide-live-chat-for-signers-help": "सक्षम होने पर, साइनिंग पेजों पर साइनर के लिए लाइव चैट विजेट छुपाए जाएंगे।"
}
@@ -237,6 +237,8 @@
"p3": "Selezionando questa opzione, i link di firma verranno inviati a tutti i firmatari contemporaneamente. Ogni firmatario può firmare il documento a sua discrezione, indipendentemente dal fatto che altri firmatari abbiano completato le firme. Questo metodo è più veloce ma non impone un ordine di firma tra i partecipanti.",
"p4": "Seleziona l'opzione che meglio si adatta alle esigenze del tuo processo documentale."
},
"strict-order": "Imponi ordine rigoroso",
"strict-order-help": "Se abilitato, nessun firmatario può firmare o approvare finché tutti i firmatari/approvatori precedenti non hanno completato la loro azione.",
"no": "No",
"auto-reminder": "Promemoria automatico",
"remind-once": "Ricorda una volta ogni (giorni)",
@@ -260,6 +262,7 @@
"signers": "Firmatari",
"signers-help": "Inizia a digitare il nome di un contatto per vedere i firmatari suggeriti dai tuoi contatti salvati o aggiungerne di nuovi. Organizza l'ordine di firma aggiungendo firmatari nella sequenza desiderata. Usa il pulsante '+' per includere firmatari e 'x' per rimuoverli. Ogni firmatario riceverà una notifica via email per firmare il documento nell'ordine elencato.",
"bcc-help": "it-Start it-typing it-a it-contact's it-name it-to it-see it-suggestions it-from it-your it-saved it-contacts it-or it-add it-new it-ones. it-Use it-the '+' it-button it-to it-add it-a it-user it-and it-the 'x' it-to it-remove it-them. it-The it-selected it-user's it-email it-will it-be it-added it-as it-a it-Bcc (it-blind it-carbon it-copy). it-Each it-user it-will it-receive it-an it-email it-notification it-once it-the it-document it-is it-completed.",
"cc-help": "Inizia a digitare il nome di un contatto per vedere i suggerimenti dai tuoi contatti salvati oppure aggiungerne di nuovi. Usa il pulsante '+' per aggiungere un utente e la 'x' per rimuoverlo. L'email dell'utente selezionato verrà aggiunta come CC (copia conoscenza). Ogni utente riceverà una notifica via email una volta completato il documento.",
"add-signer": "Aggiungi firmatario",
"contact-not-found": "Contatto non trovato",
"add-yourself": "Aggiungi te stesso",
@@ -287,7 +290,7 @@
"add-webhook": "Aggiungi Webhook",
"webhook-added": "Webhook aggiunto con successo.",
"team-disabled": "Team disabilitato",
"Team enabled": "Team abilitato",
"team-enabled": "Team abilitato",
"are-you-sure": "Sei sicuro di voler",
"disable": "disabilitare",
"enable": "abilitare",
@@ -312,6 +315,31 @@
"user-activated": "Utente attivato.",
"public": "Pubblico",
"download": "Scarica",
"sign-offline": "Firma offline",
"sign-offline-instructions": "Scarica il documento, firmalo con uno strumento esterno, quindi carica qui la copia firmata. Il proprietario del documento esaminerà l'invio e lo approverà o rifiuterà.",
"sign-offline-step-1": "Scarica il PDF non firmato.",
"sign-offline-step-2": "Firma il PDF con il tuo strumento preferito.",
"sign-offline-step-3": "Carica il PDF firmato per l'approvazione del proprietario.",
"sign-offline-upload-hint": "Seleziona il PDF firmato da inviare per l'approvazione.",
"sign-offline-submitted-title": "Inviato per approvazione",
"sign-offline-submitted-body": "Il proprietario del documento è stato notificato via e-mail per esaminare la copia firmata offline.",
"offline-already-submitted-title": "Documento inviato offline",
"offline-already-submitted-body": "Hai già inviato questo documento per la revisione della firma offline. Riceverai una notifica una volta approvato o rifiutato.",
"offline-action-approve-title": "Approva firma offline",
"offline-action-approve-confirm": "Approva",
"offline-action-reject-title": "Rifiuta firma offline",
"offline-action-reject-hint": "Facoltativamente, fornisci un motivo. Il firmatario verrà notificato via e-mail.",
"offline-action-reject-reason-placeholder": "Motivo (facoltativo)",
"offline-action-reject-confirm": "Conferma rifiuto",
"offline-action-review-title": "Esamina firma offline",
"offline-action-review-hint": "Visualizza in anteprima il PDF firmato caricato dal firmatario, quindi approvalo o rifiutalo. Nessuna azione viene intrapresa fino alla conferma.",
"offline-action-submitted-at": "Inviato",
"offline-action-no-preview": "Nessuna anteprima disponibile per questo invio.",
"offline-action-open-in-new-tab": "Apri PDF in una nuova scheda",
"offline-action-invalid-link": "Questo link di revisione non è valido o è scaduto.",
"offline-action-success": "Azione completata.",
"reject": "Rifiuta",
"document": "Documento",
"view": "Visualizza",
"make-template-public": "Rendi il modello pubblico",
"make-template-private": "Rendi il modello privato",
@@ -405,6 +433,14 @@
"certificate": "Certificato",
"decline": "Rifiuta",
"finish": "Completa",
"approve": "Approva",
"approve-mssg": " Sei sicuro di voler approvare il documento?",
"signer-role-label": "Ruolo del destinatario",
"signer-role-signer": "Firmatario",
"signer-role-viewer": "Visualizzatore",
"signer-role-approver": "Approvatore",
"signer-role-hint": "Il firmatario deve firmare. L'approvatore approva senza firmare. Il visualizzatore può solo visualizzare.",
"signer-role-upgrade-msg": "I ruoli Visualizzatore e Approvatore sono disponibili solo nei piani a pagamento. Esegui l'upgrade del tuo piano per assegnare questi ruoli ai destinatari.",
"done": "Fatto",
"mail": "Email",
"sign-now": "Firma ora",
@@ -543,6 +579,7 @@
"sign-url": "URL Firma",
"mails-sent": "Mail inviate",
"mail-not-delivered": "Mail non consegnata",
"email-not-verified-head": "Email non verificata",
"document-alert": "Avviso Documento",
"owner-subscription-expired": "L'abbonamento del proprietario è scaduto.",
"owner-doesnt-have-paid-plan": "Il proprietario non ha un piano a pagamento.",
@@ -618,6 +655,9 @@
"document-signed-alert-7": "Questo documento non è più disponibile.",
"document-signed-alert-8": "Il documento è già stato inviato per la firma.",
"yet-to-sign": "Ancora da firmare",
"viewers": "Visualizzatori",
"viewer-no-widgets": "I visualizzatori ricevono una copia in sola lettura del documento e non possono avere widget assegnati.",
"viewer-confirm-widget-removal": "Cambiando questo destinatario in Visualizzatore verranno rimossi tutti i widget già assegnati. Continuare?",
"validation-alert-1": "L'input non soddisfa i criteri impostati dall'espressione regolare.",
"account-already-exists": "L'account esiste già!",
"choose-password": "Scegli Password",
@@ -768,6 +808,13 @@
"p3": "Selezionando questa opzione, disabiliterai la verifica OTP, consentendo agli utenti di firmare il documento direttamente senza passaggi aggiuntivi.",
"p4": "Scegli l'opzione che meglio si adatta alle tue esigenze di firma dei documenti."
},
"allow-offline-sign": "Consenti firma offline",
"allow-offline-sign-help": {
"p1": "Scegli se i firmatari possono inviare una copia di questo documento firmata tramite uno strumento esterno per la revisione del proprietario.",
"p2": "I firmatari vedranno l'opzione Firma offline e potranno caricare un PDF firmato esternamente per l'approvazione.",
"p3": "I firmatari possono firmare solo all'interno dell'app. L'opzione Firma offline sarà nascosta.",
"p4": "Questa impostazione utilizza come predefinita la tua preferenza account ed è disponibile sui piani a pagamento."
},
"advanced-options": "Opzioni avanzate",
"hide-advanced-options": "Nascondi opzioni avanzate",
"download-files": "Scarica file",
@@ -830,6 +877,7 @@
"delete-folder-alert-1": "Impossibile eliminare: la cartella deve essere vuota.",
"defaultSign-alert": "I widget {{widgetsType}} non sono disponibili per la firma automatica.",
"embed-heading": "Incorpora modelli {{appName}}",
"other-apps-by-opensign": "Altre app del team OpenSign",
"public-template-edit-title": "Modifica Modello Pubblico",
"public-template-edit-mssg": "Non puoi modificare direttamente un modello pubblico. Per apportare modifiche, il modello sarà impostato come privato e dovrai renderlo pubblico di nuovo successivamente. Vuoi continuare?",
"agree-p1": "Confermo di aver letto e compreso il",
@@ -900,6 +948,7 @@
"p3": "Selezionando questa opzione, i firmatari non potranno apportare alcuna aggiunta al documento durante il processo di firma."
},
"signature-setting": "Impostazioni Firma",
"rotation": "Rotazione",
"activation-key": "Chiave di attivazione",
"paid-selfhost-help": "Si prega di contattare {{appName}} per ulteriori informazioni all'indirizzo ",
"remaining-credits-selfhost-help": "Utilizza i crediti premium per la firma di documenti tramite API, l'invio in massa e l'integrazione di {{appName}} sul tuo sito web. Hai {{allowedcredits}} crediti inclusi e {{addoncredits}} crediti aggiuntivi acquistati rimanenti.",
@@ -928,6 +977,8 @@
"visit-below-link": "Visita il link qui sotto per saperne di più -",
"storage-help": "Abilitare BYOC ti consente di collegare il tuo archivio S3 in modo che i tuoi file rimangano completamente sotto il tuo controllo, senza copie esterne conservate. Se l'autonomia dei dati è importante per te, considera l'upgrade a Teams per sbloccare questa funzionalità.",
"daily-quota-reached": "Hai raggiunto la tua quota giornaliera. Per assistenza, contatta quotas@opensignlabs.com.",
"email-not-verified-send": "Verifica il tuo indirizzo email prima di inviare email. Vai alle <1>impostazioni del profilo</1> per completare la verifica dellemail.",
"email-not-verified-send-plain": "Verifica il tuo indirizzo email prima di inviare email. Vai alle impostazioni del profilo per completare la verifica dellemail.",
"enabled-signature-type": "Tipi di firma abilitati",
"enabled-signature-type-help": "L'impostazione 'Tipi di firma abilitati' determina quali opzioni di firma sono disponibili nella tua organizzazione. Ad esempio, se disabiliti l'opzione 'Disegna', i membri della tua organizzazione non la vedranno nel widget della firma, mentre le altre tre opzioni resteranno accessibili.",
"indexing-public-profile": "Consenti l'indicizzazione del profilo pubblico dai motori di ricerca",
@@ -955,6 +1006,7 @@
"please-provide-templateid": "Si prega di fornire templateid",
"this-template-is-not-public": "Questo template non è pubblico",
"invalid-templateid": "templateid non valido",
"kiosk-no-active-role": "Questo modello non ha ruoli di firmatario o approvatore disponibili per la firma in modalità chiosco.",
"contact-billing-at-opensign": " Per aggiungere altri posti, contattare OpenSign™ all'indirizzo <1>billing@opensignlabs.com</1> per assistenza.",
"title-length-alert": "Il titolo può contenere al massimo 250 caratteri.",
"note-length-alert": "La nota può contenere al massimo 200 caratteri.",
@@ -1303,7 +1355,7 @@
"signer-form-data-for": "Dati del modulo per il firmatario: {{signerName}} <{{signerEmail}}>",
"widget-name": "Nome del widget",
"widget-responses": "Risposte del widget",
"formdata-download-note": "Nota: Firme, timbri, iniziali e immagini non verranno esportati.",
"formdata-download-note": "Nota: Firme, timbri, iniziali e immagini non verranno esportati. I dati dei firmatari offline non saranno disponibili.",
"export-all-formdata-error": "Non è possibile esportare i dati del modulo perché il modello contiene nomi di widget duplicati. Creare un nuovo modello per poter esportare i dati.",
"export-formdata-error": "Non è possibile esportare i dati del modulo perché il documento contiene nomi di widget duplicati. Creare un nuovo documento senza nomi duplicati per poter esportare i dati.",
"duplicate-widget-name-error": "Questo nome di widget è già assegnato a un altro firmatario. Scegli un nome diverso.",
@@ -1416,5 +1468,95 @@
"switch-to-advanced": "Passa alla modalità avanzata",
"switch-to-basic": "Passa alla modalità base",
"use-name-as-sender": "Usa il nome del profilo come mittente",
"use-name-as-sender-help": "Quando abilitato, {{appName}} usa il nome del tuo profilo come mittente per le richieste di firma e l'email di completamento."
"use-name-as-sender-help": "Quando abilitato, {{appName}} usa il nome del tuo profilo come mittente per le richieste di firma e l'email di completamento.",
"acro-fields-detected": "Campi modulo rilevati",
"acro-fields-detected-msg": "Sono stati rilevati {{count}} campo/i modulo nel PDF caricato. Vuoi inserire automaticamente i widget corrispondenti?",
"documents-count": "Numero di documenti",
"templates-count": "Numero di modelli",
"emails-sent": "Email inviate",
"storage-used": "Spazio utilizzato",
"docs-count-team-help": "Numero totale di documenti creati da tutti gli utenti nelle tue organizzazioni.",
"docs-count-help": "Numero totale di documenti creati da te.",
"templates-count-team-help": "Numero totale di modelli creati da tutti gli utenti nelle tue organizzazioni.",
"templates-count-help": "Numero totale di modelli creati da te.",
"emails-sent-team-help": "Numero totale di notifiche email inviate nelle tue organizzazioni.",
"emails-sent-help": "Numero totale di notifiche email inviate da te.",
"storage-used-team-help": "Spazio di archiviazione totale consumato da tutti gli utenti nelle tue organizzazioni.",
"storage-used-help": "Spazio di archiviazione totale consumato da te.",
"cloud": "Cloud",
"on-premises": "On-premise",
"plan-name-free": "OPENSIGN™ FREE",
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
"plan-name-teams": "OPENSIGN™ TEAMS",
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
"plan-subtitle-free": "Firme elettroniche illimitate gratuite, per sempre.",
"plan-subtitle-professional": "Accesso esclusivo a funzionalità avanzate.",
"plan-subtitle-scalable": "Funzionalità scalabili con supporto prioritario.",
"subscribe": "Abbonati",
"contact-us": "Contattaci",
"benefit-unlimited-digital-signatures": "Firme digitali illimitate",
"benefit-sign-docs-yourself": "Firma i documenti tu stesso",
"benefit-request-signatures": "Richiedi firme ad altri",
"benefit-unlimited-templates": "Modelli illimitati",
"benefit-14-field-types": "14 tipi di campo",
"benefit-automatic-e-signatures": "Firme elettroniche automatiche",
"benefit-completion-certificates": "Certificati di completamento",
"benefit-send-in-order": "Invia in ordine",
"benefit-organize-docs": "Organizza i documenti in OpenSign™ Drive",
"benefit-document-templates": "Modelli di documento",
"benefit-import-from-dropbox": "Importa da Dropbox",
"benefit-contact-book": "Rubrica contatti",
"benefit-document-expiry": "Supporto per la scadenza dei documenti",
"benefit-decline-document": "Supporto per il rifiuto dei documenti",
"benefit-email-notifications": "Notifiche via e-mail",
"benefit-public-profiles": "Profili pubblici",
"benefit-and-much-more": "E molto altro",
"benefit-everything-free": "Tutto in OpenSign™ Free",
"benefit-field-validations": "Validazioni dei campi",
"benefit-regex-validations": "Validazioni con espressioni regolari",
"benefit-webhooks": "Webhook",
"benefit-zapier": "Integrazione Zapier",
"benefit-api-access": "Accesso API",
"benefit-api-signatures-100": "fino a 100 firme API",
"benefit-api-signatures-240": "fino a 240 firme API",
"benefit-custom-email-templates": "Modelli e-mail personalizzati",
"benefit-custom-smtp": "Collega il tuo account Gmail o SMTP per l'invio di e-mail",
"benefit-auto-reminders": "Promemoria automatici",
"benefit-bulk-send-100": "Invio in massa (fino a 100 documenti)",
"benefit-bulk-send-240": "Invio in massa (fino a 240 documenti)",
"benefit-premium-profiles": "Nomi utente del profilo pubblico premium",
"benefit-email-verification": "Imponi la verifica via e-mail per confermare l'identità del firmatario",
"benefit-embedded-signing": "Firma incorporata",
"benefit-everything-professional": "Tutto in OpenSign™ Professional",
"benefit-teams-organizations": "Team e Organizzazioni",
"benefit-share-templates-teams": "Condividi modelli con i team",
"benefit-share-templates-individuals": "Condividi modelli con individui",
"benefit-byoc": "BYOC - Archivia i tuoi documenti nel tuo cloud storage",
"benefit-doc-id-removal": "Rimozione del DocumentId dai documenti firmati",
"benefit-request-payments": "Richiedi pagamenti (prossimamente)",
"benefit-mobile-app": "App mobile (prossimamente)",
"benefit-api-signatures-500": "fino a 500 firme API",
"benefit-bulk-send-500": "Invio in massa (fino a 500 documenti)",
"benefit-all-features": "Tutte le funzionalità",
"benefit-custom-domain": "Dominio personalizzato",
"benefit-custom-branding": "Branding personalizzato",
"benefit-uptime-sla": "SLA di disponibilità",
"benefit-sso-support": "Supporto SSO",
"benefit-priority-support": "Supporto prioritario",
"benefit-3-users-1500-credits": "Include 3 utenti e 1500 crediti premium",
"benefit-public-links": "Link pubblici",
"benefit-kiosk-mode": "Modalità chiosco",
"benefit-bulk-send-1500": "Invio in massa (fino a 1500 documenti)",
"benefit-connect-gmail-smtp": "Collega il tuo Gmail o SMTP",
"benefit-analytics": "Analisi",
"benefit-bulksend-api-pro-month-help": "Questo piano include 100 crediti premium al mese che ti permettono di inviare documenti tramite API, condividere link pubblici per la firma, eseguire richieste di firma in blocco, usare la modalità Kiosk per la firma di persona o raccogliere firme da documenti incorporati direttamente nel tuo sito web. Ogni volta che avvii o invii un documento, viene utilizzato 1 credito — e quel singolo documento può includere fino a 10 firmatari.",
"benefit-bulksend-api-pro-year-help": "Questo piano include 240 crediti premium all'anno che ti permettono di inviare documenti tramite API, condividere link pubblici per la firma, eseguire richieste di firma in blocco, usare la modalità Kiosk per la firma di persona o raccogliere firme da documenti incorporati direttamente nel tuo sito web. Ogni volta che avvii o invii un documento, viene utilizzato 1 credito — e quel singolo documento può includere fino a 10 firmatari.",
"benefit-bulksend-api-team-month-help": "Questo piano include 100 crediti premium al mese che ti permettono di inviare documenti tramite API, condividere link pubblici per la firma, eseguire richieste di firma in blocco, usare la modalità Kiosk per la firma di persona o raccogliere firme da documenti incorporati direttamente nel tuo sito web. Ogni volta che avvii o invii un documento, viene utilizzato 1 credito — e quel singolo documento può includere fino a 10 firmatari.",
"benefit-bulksend-api-team-year-help": "Questo piano include 500 crediti premium all'anno che ti permettono di inviare documenti tramite API, condividere link pubblici per la firma, eseguire richieste di firma in blocco, usare la modalità Kiosk per la firma di persona o raccogliere firme da documenti incorporati direttamente nel tuo sito web. Ogni volta che avvii o invii un documento, viene utilizzato 1 credito — e quel singolo documento può includere fino a 10 firmatari.",
"failed-pdf-for-prefill-processing": "Impossibile caricare il PDF per l'elaborazione del precompilamento.",
"attach-all-role-to-signer": "Per favore, assegna tutti i ruoli al firmatario.",
"upgrade-to-edit": "Esegui l'upgrade per modificare",
"hide-live-chat-for-signers": "Nascondi chat dal vivo per i firmatari",
"hide-live-chat-for-signers-help": "Se attivato, i widget di chat dal vivo saranno nascosti per i firmatari nelle pagine di firma."
}
@@ -237,6 +237,8 @@
"p3": "이 옵션을 선택하면 서명 링크가 모든 서명자에게 동시에 전송됩니다. 각 서명자는 다른 서명자가 서명을 완료했는지 여부에 관계없이 편리한 시간에 문서에 서명할 수 있습니다. 이 방법은 더 빠르지만 참가자 간에 서명 순서를 강제하지 않습니다.",
"p4": "문서 처리 요구에 가장 적합한 옵션을 선택하세요."
},
"strict-order": "엄격한 순서 적용",
"strict-order-help": "활성화하면 이전의 모든 서명자/승인자가 작업을 완료할 때까지 어떤 서명자도 서명하거나 승인할 수 없습니다.",
"no": "아니요",
"auto-reminder": "자동 알림",
"remind-once": " 매 (일)마다 한 번 알림",
@@ -260,6 +262,7 @@
"signers": "서명자",
"signers-help": "연락처 이름을 입력하기 시작하면 저장된 연락처에서 추천 서명자가 표시되거나 새 연락처를 추가할 수 있습니다. 원하는 순서대로 서명자를 추가하여 서명 순서를 정렬하세요. '+' 버튼을 눌러 서명자를 포함시키고 'x' 버튼을 눌러 제거하세요. 각 서명자는 지정된 순서대로 문서에 서명하라는 이메일 안내를 받게 됩니다.",
"bcc-help": "연락처 이름을 입력하기 시작하면 저장된 연락처에서 제안된 항목이 표시되거나 새 연락처를 추가할 수 있습니다. '+' 버튼을 사용하여 사용자를 추가하고 'x' 버튼을 사용하여 제거하세요. 선택한 사용자의 이메일은 숨은 참조(Bcc)로 추가됩니다. 문서가 완료되면 각 사용자에게 이메일 알림이 전송됩니다.",
"cc-help": "연락처 이름을 입력하기 시작하면 저장된 연락처에서 추천이 표시되며, 새 연락처를 추가할 수도 있습니다. 사용자를 추가하려면 '+' 버튼을, 제거하려면 'x'를 사용하세요. 선택한 사용자의 이메일이 CC(참조)로 추가됩니다. 문서가 완료되면 각 사용자에게 이메일 알림이 전송됩니다.",
"add-signer": "서명자 추가",
"contact-not-found": "연락처를 찾을 수 없습니다",
"add-yourself": "본인 추가",
@@ -287,7 +290,7 @@
"add-webhook": "웹훅 추가",
"webhook-added": "웹훅이 성공적으로 추가되었습니다.",
"team-disabled": "팀 비활성화됨",
"Team enabled": "팀 활성화됨",
"team-enabled": "팀 활성화됨",
"are-you-sure": "정말로 하시겠습니까?",
"disable": "비활성화",
"enable": "활성화",
@@ -312,6 +315,31 @@
"user-activated": "유저 활성화됨.",
"public": "공개",
"download": "다운로드",
"sign-offline": "오프라인 서명",
"sign-offline-instructions": "문서를 다운로드하고 외부 도구로 서명한 후 서명된 사본을 여기에 업로드하세요. 문서 소유자가 제출물을 검토하여 승인하거나 거부합니다.",
"sign-offline-step-1": "서명되지 않은 PDF를 다운로드합니다.",
"sign-offline-step-2": "선호하는 도구를 사용하여 PDF에 서명합니다.",
"sign-offline-step-3": "소유자 승인을 위해 서명된 PDF를 업로드합니다.",
"sign-offline-upload-hint": "승인을 위해 제출할 서명된 PDF를 선택하세요.",
"sign-offline-submitted-title": "승인을 위해 제출됨",
"sign-offline-submitted-body": "문서 소유자에게 오프라인 서명 사본 검토를 위한 이메일이 전송되었습니다.",
"offline-already-submitted-title": "문서가 오프라인으로 제출되었습니다",
"offline-already-submitted-body": "이 문서는 이미 오프라인 서명 검토를 위해 제출되었습니다. 승인 또는 거부되면 알림이 전송됩니다.",
"offline-action-approve-title": "오프라인 서명 승인",
"offline-action-approve-confirm": "승인",
"offline-action-reject-title": "오프라인 서명 거부",
"offline-action-reject-hint": "선택적으로 이유를 제공하세요. 서명자에게 이메일로 알림이 전송됩니다.",
"offline-action-reject-reason-placeholder": "이유 (선택 사항)",
"offline-action-reject-confirm": "거부 확인",
"offline-action-review-title": "오프라인 서명 검토",
"offline-action-review-hint": "서명자가 업로드한 서명된 PDF를 미리 보고 승인하거나 거부하세요. 확인하기 전까지는 아무런 조치가 취해지지 않습니다.",
"offline-action-submitted-at": "제출됨",
"offline-action-no-preview": "이 제출에 대한 미리 보기를 사용할 수 없습니다.",
"offline-action-open-in-new-tab": "새 탭에서 PDF 열기",
"offline-action-invalid-link": "이 검토 링크가 유효하지 않거나 만료되었습니다.",
"offline-action-success": "작업이 완료되었습니다.",
"reject": "거부",
"document": "문서",
"view": "보기",
"make-template-public": "템플릿 공개하기",
"make-template-private": "템플릿 비공개하기",
@@ -405,6 +433,14 @@
"certificate": "증명서",
"decline": "거절",
"finish": "완료",
"approve": "승인",
"approve-mssg": " 문서를 승인하시겠습니까?",
"signer-role-label": "수신자 역할",
"signer-role-signer": "서명자",
"signer-role-viewer": "열람자",
"signer-role-approver": "승인자",
"signer-role-hint": "서명자는 서명해야 합니다. 승인자는 서명 없이 승인합니다. 열람자는 문서만 볼 수 있습니다.",
"signer-role-upgrade-msg": "열람자 및 승인자 역할은 유료 요금제에서만 제공됩니다. 수신자에게 이러한 역할을 지정하려면 요금제를 업그레이드하세요.",
"done": "완료",
"mail": "메일",
"sign-now": "지금 서명",
@@ -543,6 +579,7 @@
"sign-url": "서명 URL",
"mails-sent": "메일 전송됨",
"mail-not-delivered": "메일이 전달되지 않음",
"email-not-verified-head": "이메일이 인증되지 않았습니다",
"document-alert": "문서 알림",
"owner-subscription-expired": "소유자의 구독이 만료되었습니다.",
"subscription-expired": "구독 만료",
@@ -618,6 +655,9 @@
"document-signed-alert-7": "이 문서는 더 이상 사용할 수 없습니다.",
"document-signed-alert-8": "문서가 이미 서명을 위해 발송되었습니다.",
"yet-to-sign": "서명 대기 중",
"viewers": "열람자",
"viewer-no-widgets": "열람자는 문서의 읽기 전용 사본을 받으며 위젯을 배정받을 수 없습니다.",
"viewer-confirm-widget-removal": "이 수신자를 열람자로 변경하면 이미 배정된 모든 위젯이 제거됩니다. 계속하시겠습니까?",
"validation-alert-1": "입력이 정규식으로 설정된 기준을 충족하지 않습니다.",
"account-already-exists": "계정이 이미 존재합니다!",
"choose-password": "비밀번호 선택",
@@ -768,6 +808,13 @@
"p3": "이 옵션을 선택하면 OTP 인증이 비활성화되어 사용자가 추가 단계 없이 직접 문서에 서명할 수 있습니다.",
"p4": "문서 서명 요구 사항에 가장 적합한 옵션을 선택하세요."
},
"allow-offline-sign": "오프라인 서명 허용",
"allow-offline-sign-help": {
"p1": "서명자가 외부 도구로 서명한 문서 사본을 소유자 검토를 위해 제출할 수 있는지 선택하세요.",
"p2": "서명자에게 오프라인 서명 옵션이 표시되며 외부에서 서명된 PDF를 소유자 승인용으로 업로드할 수 있습니다.",
"p3": "서명자는 앱 내에서만 서명할 수 있으며 오프라인 서명 옵션은 숨겨집니다.",
"p4": "이 설정은 계정 기본 설정을 따르며 유료 요금제에서 사용할 수 있습니다."
},
"advanced-options": "고급 옵션",
"hide-advanced-options": "고급 옵션 숨기기",
"download-files": "파일 다운로드",
@@ -830,6 +877,7 @@
"delete-folder-alert-1": "삭제할 수 없음: 폴더가 비어 있어야 합니다.",
"defaultSign-alert": "{{widgetsType}} 위젯은 자동 서명에 사용할 수 없습니다.",
"embed-heading": "{{appName}} 템플릿 포함",
"other-apps-by-opensign": "OpenSign 팀의 다른 앱",
"public-template-edit-title": "공개 템플릿 편집",
"public-template-edit-mssg": "공개 템플릿을 직접 편집할 수 없습니다. 변경하려면 템플릿이 비공개로 설정되며 나중에 다시 공개해야 합니다. 계속하시겠습니까?",
"agree-p1": "나는 ",
@@ -900,6 +948,7 @@
"p3": "이 옵션을 선택하면 서명자가 서명 과정 중에 문서에 추가 작업을 할 수 없습니다."
},
"signature-setting": "서명 설정",
"rotation": "회전",
"activation-key": "활성화 키",
"paid-selfhost-help": "자세한 내용은 {{appName}}에 문의하세요. ",
"remaining-credits-selfhost-help": "API 문서 서명, 대량 전송, 웹사이트에 {{appName}} 통합에 프리미엄 크레딧을 사용하세요. 포함된 크레딧 {{allowedcredits}}개와 추가 구매한 크레딧 {{addoncredits}}개가 남아 있습니다.",
@@ -928,6 +977,8 @@
"visit-below-link": "자세한 내용은 아래 링크를 방문하세요 -",
"storage-help": "BYOC를 활성화하면 자체 S3 저장소를 연결하여 파일이 외부 사본 없이 완전히 제어할 수 있습니다. 데이터 자율성이 중요하다면 팀으로 업그레이드하여 이 기능을 잠금 해제하는 것을 고려해보세요.",
"daily-quota-reached": "일일 할당량에 도달했습니다. 도움이 필요하시면 quotas@opensignlabs.com으로 문의하세요.",
"email-not-verified-send": "이메일을 보내기 전에 이메일 주소를 인증해 주세요. 이메일 인증을 완료하려면 <1>프로필 설정</1>으로 이동하세요.",
"email-not-verified-send-plain": "이메일을 보내기 전에 이메일 주소를 인증해 주세요. 이메일 인증을 완료하려면 프로필 설정으로 이동하세요.",
"enabled-signature-type": "활성화된 서명 유형",
"enabled-signature-type-help": "'활성화된 서명 유형' 설정은 조직 전체에서 사용할 수 있는 서명 옵션을 결정합니다. 예를 들어 '그리기' 옵션을 비활성화하면 조직 구성원은 서명 위젯에서 해당 옵션을 볼 수 없지만 다른 세 가지 옵션은 계속 사용할 수 있습니다.",
"indexing-public-profile": "검색 엔진에 의한 공개 프로필 색인 허용",
@@ -955,6 +1006,7 @@
"please-provide-templateid": "템플릿 ID를 입력하세요.",
"this-template-is-not-public": "이 템플릿은 공개되지 않았습니다.",
"invalid-templateid": "잘못된 템플릿 ID입니다.",
"kiosk-no-active-role": "이 템플릿에는 키오스크 서명에 사용할 수 있는 서명자 또는 승인자 역할이 없습니다.",
"contact-billing-at-opensign": "더 많은 좌석을 추가하려면 <1>billing@opensignlabs.com</1>으로 OpenSign™에 문의하여 도움을 받으세요.",
"title-length-alert": "제목은 최대 250자여야 합니다.",
"note-length-alert": "메모는 최대 200자여야 합니다.",
@@ -1303,7 +1355,7 @@
"signer-form-data-for": "서명자 양식 데이터: {{signerName}} <{{signerEmail}}>",
"widget-name": "위젯 이름",
"widget-responses": "위젯 응답",
"formdata-download-note": "참고: 서명, 스탬프, 이니셜 및 이미지는 내보내지지 않습니다.",
"formdata-download-note": "참고: 서명, 스탬프, 이니셜 및 이미지는 내보내지지 않습니다. 오프라인 서명자의 데이터는 사용할 수 없습니다.",
"export-all-formdata-error": "템플릿에 중복된 위젯 이름이 포함되어 있어 양식 데이터를 내보낼 수 없습니다. 양식 데이터를 내보내려면 새 템플릿을 만드십시오.",
"export-formdata-error": "문서에 중복된 위젯 이름이 포함되어 있어 양식 데이터를 내보낼 수 없습니다. 양식 데이터를 내보내려면 중복된 위젯 이름이 없는 새 문서를 만드십시오.",
"duplicate-widget-name-error": "이 위젯 이름은 이미 다른 서명자에게 할당되었습니다. 다른 이름을 선택하십시오.",
@@ -1416,5 +1468,95 @@
"switch-to-advanced": "고급 모드로 전환",
"switch-to-basic": "기본 모드로 전환",
"use-name-as-sender": "프로필 이름을 발신자로 사용",
"use-name-as-sender-help": "활성화하면 {{appName}}에서 서명 요청 및 완료 이메일의 발신자로 프로필 이름을 사용합니다."
"use-name-as-sender-help": "활성화하면 {{appName}}에서 서명 요청 및 완료 이메일의 발신자로 프로필 이름을 사용합니다.",
"acro-fields-detected": "양식 필드 감지됨",
"acro-fields-detected-msg": "업로드된 PDF에서 {{count}}개의 양식 필드가 감지되었습니다. 해당 위젯을 자동으로 삽입하시겠습니까?",
"documents-count": "문서 수",
"templates-count": "템플릿 수",
"emails-sent": "전송된 이메일",
"storage-used": "사용한 저장공간",
"docs-count-team-help": "조직 내 모든 사용자가 생성한 문서의 총 개수입니다.",
"docs-count-help": "내가 생성한 문서의 총 개수입니다.",
"templates-count-team-help": "조직 내 모든 사용자가 생성한 템플릿의 총 개수입니다.",
"templates-count-help": "내가 생성한 템플릿의 총 개수입니다.",
"emails-sent-team-help": "조직 전체에서 전송된 이메일 알림의 총 개수입니다.",
"emails-sent-help": "내가 전송한 이메일 알림의 총 개수입니다.",
"storage-used-team-help": "조직 전체의 모든 사용자가 사용한 총 저장공간입니다.",
"storage-used-help": "내가 사용한 총 저장공간입니다.",
"cloud": "클라우드",
"on-premises": "온프레미스",
"plan-name-free": "OPENSIGN™ FREE",
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
"plan-name-teams": "OPENSIGN™ TEAMS",
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
"plan-subtitle-free": "무료 무제한 전자 서명, 영원히.",
"plan-subtitle-professional": "고급 기능에 대한 독점 액세스.",
"plan-subtitle-scalable": "우선 지원을 제공하는 확장 가능한 기능.",
"subscribe": "구독",
"contact-us": "문의하기",
"benefit-unlimited-digital-signatures": "무제한 디지털 서명",
"benefit-sign-docs-yourself": "직접 문서에 서명",
"benefit-request-signatures": "타인에게 서명 요청",
"benefit-unlimited-templates": "무제한 템플릿",
"benefit-14-field-types": "14가지 필드 유형",
"benefit-automatic-e-signatures": "자동 전자 서명",
"benefit-completion-certificates": "완료 인증서",
"benefit-send-in-order": "순서대로 전송",
"benefit-organize-docs": "OpenSign™ Drive에서 문서 정리",
"benefit-document-templates": "문서 템플릿",
"benefit-import-from-dropbox": "Dropbox에서 가져오기",
"benefit-contact-book": "연락처 목록",
"benefit-document-expiry": "문서 만료 지원",
"benefit-decline-document": "문서 거절 지원",
"benefit-email-notifications": "이메일 알림",
"benefit-public-profiles": "공개 프로필",
"benefit-and-much-more": "그 외 더 많은 기능",
"benefit-everything-free": "OpenSign™ Free의 모든 기능",
"benefit-field-validations": "필드 유효성 검사",
"benefit-regex-validations": "정규식 유효성 검사",
"benefit-webhooks": "웹훅",
"benefit-zapier": "Zapier 통합",
"benefit-api-access": "API 액세스",
"benefit-api-signatures-100": "최대 100개 API 서명",
"benefit-api-signatures-240": "최대 240개 API 서명",
"benefit-custom-email-templates": "맞춤형 이메일 템플릿",
"benefit-custom-smtp": "이메일 발송을 위한 Gmail 또는 SMTP 계정 연결",
"benefit-auto-reminders": "자동 알림",
"benefit-bulk-send-100": "일괄 전송 (최대 100개 문서)",
"benefit-bulk-send-240": "일괄 전송 (최대 240개 문서)",
"benefit-premium-profiles": "프리미엄 공개 프로필 사용자 이름",
"benefit-email-verification": "서명자 신원 확인을 위한 이메일 인증 적용",
"benefit-embedded-signing": "임베디드 서명",
"benefit-everything-professional": "OpenSign™ Professional의 모든 기능",
"benefit-teams-organizations": "팀 및 조직",
"benefit-share-templates-teams": "팀과 템플릿 공유",
"benefit-share-templates-individuals": "개인과 템플릿 공유",
"benefit-byoc": "BYOC - 자체 클라우드 스토리지에 문서 저장",
"benefit-doc-id-removal": "서명된 문서에서 DocumentId 제거",
"benefit-request-payments": "결제 요청 (출시 예정)",
"benefit-mobile-app": "모바일 앱 (출시 예정)",
"benefit-api-signatures-500": "최대 500개 API 서명",
"benefit-bulk-send-500": "일괄 전송 (최대 500개 문서)",
"benefit-all-features": "모든 기능",
"benefit-custom-domain": "사용자 지정 도메인",
"benefit-custom-branding": "사용자 지정 브랜딩",
"benefit-uptime-sla": "가동 시간 SLA",
"benefit-sso-support": "SSO 지원",
"benefit-priority-support": "우선 지원",
"benefit-3-users-1500-credits": "3명의 사용자와 1500 프리미엄 크레딧 포함",
"benefit-public-links": "공개 링크",
"benefit-kiosk-mode": "키오스크 모드",
"benefit-bulk-send-1500": "일괄 전송 (최대 1500개 문서)",
"benefit-connect-gmail-smtp": "Gmail 또는 SMTP 연결",
"benefit-analytics": "분석",
"benefit-bulksend-api-pro-month-help": "이 플랜에는 월 100개의 프리미엄 크레딧이 포함되어 API로 문서를 전송하고, 공개 서명 링크를 공유하고, 대량 서명 요청을 실행하고, 대면 서명을 위한 Kiosk 모드를 사용하거나, 웹사이트에 직접 임베드된 문서에서 서명을 수집할 수 있습니다. 문서를 시작하거나 전송할 때마다 크레딧 1개가 사용되며 — 해당 문서 1건에는 최대 10명의 서명자를 포함할 수 있습니다.",
"benefit-bulksend-api-pro-year-help": "이 플랜에는 연 240개의 프리미엄 크레딧이 포함되어 API로 문서를 전송하고, 공개 서명 링크를 공유하고, 대량 서명 요청을 실행하고, 대면 서명을 위한 Kiosk 모드를 사용하거나, 웹사이트에 직접 임베드된 문서에서 서명을 수집할 수 있습니다. 문서를 시작하거나 전송할 때마다 크레딧 1개가 사용되며 — 해당 문서 1건에는 최대 10명의 서명자를 포함할 수 있습니다.",
"benefit-bulksend-api-team-month-help": "이 플랜에는 월 100개의 프리미엄 크레딧이 포함되어 API로 문서를 전송하고, 공개 서명 링크를 공유하고, 대량 서명 요청을 실행하고, 대면 서명을 위한 Kiosk 모드를 사용하거나, 웹사이트에 직접 임베드된 문서에서 서명을 수집할 수 있습니다. 문서를 시작하거나 전송할 때마다 크레딧 1개가 사용되며 — 해당 문서 1건에는 최대 10명의 서명자를 포함할 수 있습니다.",
"benefit-bulksend-api-team-year-help": "이 플랜에는 연 500개의 프리미엄 크레딧이 포함되어 API로 문서를 전송하고, 공개 서명 링크를 공유하고, 대량 서명 요청을 실행하고, 대면 서명을 위한 Kiosk 모드를 사용하거나, 웹사이트에 직접 임베드된 문서에서 서명을 수집할 수 있습니다. 문서를 시작하거나 전송할 때마다 크레딧 1개가 사용되며 — 해당 문서 1건에는 최대 10명의 서명자를 포함할 수 있습니다.",
"failed-pdf-for-prefill-processing": "프리필 처리를 위해 PDF를 불러오지 못했습니다.",
"attach-all-role-to-signer": "서명자에게 모든 역할을 연결해 주세요.",
"upgrade-to-edit": "편집하려면 업그레이드하세요",
"hide-live-chat-for-signers": "서명자에게 라이브 채팅 숨기기",
"hide-live-chat-for-signers-help": "활성화하면 서명 페이지에서 서명자에게 라이브 채팅 위젯이 숨겨집니다."
}
+5
View File
@@ -0,0 +1,5 @@
{
"rewrites": [
{ "source": "/:path((?!\\.well-known/).*)", "destination": "/index.html" }
]
}
Binary file not shown.
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env node
/* eslint-env node */
/* eslint-disable no-console */
// Tiny static file server for the production build.
// - Serves real files from ./build (including dotfile dirs like .well-known)
// - Falls back to /index.html only when the requested path does not exist
// (SPA client-side routing).
const http = require("node:http");
const fs = require("node:fs");
const path = require("node:path");
const root = path.join(__dirname, "build");
const port = Number(process.env.PORT) || 3000;
const host = process.env.HOST || "0.0.0.0";
const mime = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".map": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".ico": "image/x-icon",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".txt": "text/plain; charset=utf-8",
".pdf": "application/pdf",
".csv": "text/csv; charset=utf-8",
".wasm": "application/wasm",
".xml": "application/xml; charset=utf-8"
};
function contentType(filePath) {
// Files under /.well-known/ are JSON by convention (WebAuthn, apple-app-site-association, etc.)
// and are extensionless, so explicitly set application/json there.
const rel = path.relative(root, filePath).split(path.sep).join("/");
if (rel.startsWith(".well-known/")) {
return "application/json; charset=utf-8";
}
return mime[path.extname(filePath).toLowerCase()] || "application/octet-stream";
}
function applyWellKnownCors(req, res, filePath) {
const rel = path.relative(root, filePath).split(path.sep).join("/");
if (!rel.startsWith(".well-known/")) return;
// .well-known/* must be readable cross-origin (e.g. WebAuthn related-origins
// requests come from any RP host). Echo the request origin when present so
// credentialed fetches still work; otherwise allow all.
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin || "*");
res.setHeader("Vary", "Origin");
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
}
function safeJoin(reqPath) {
let decoded;
try {
decoded = decodeURIComponent(reqPath.split("?")[0].split("#")[0]);
} catch {
return null;
}
const resolved = path.normalize(path.join(root, decoded));
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
return resolved;
}
function cacheControl(filePath) {
const ext = path.extname(filePath).toLowerCase();
const relativePath = path.relative(root, filePath);
const isBuildAsset =
relativePath === "assets" ||
relativePath.startsWith(`assets${path.sep}`);
if (ext === ".html" || ext === "") {
return "no-cache";
}
return isBuildAsset
? "public, max-age=31536000, immutable"
: "no-cache";
}
function streamFile(req, res, filePath, stats) {
const headers = {
"Content-Type": contentType(filePath),
"Content-Length": stats.size,
"Last-Modified": stats.mtime.toUTCString(),
"Cache-Control": cacheControl(filePath)
};
applyWellKnownCors(req, res, filePath);
if (req.method === "HEAD") {
res.writeHead(200, headers);
return res.end();
}
const stream = fs.createReadStream(filePath);
stream.on("error", () => {
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "text/plain" });
return res.end("Internal Server Error");
}
res.destroy();
});
res.on("close", () => {
if (!stream.destroyed) stream.destroy();
});
res.on("error", () => {
if (!stream.destroyed) stream.destroy();
});
res.writeHead(200, headers);
stream.pipe(res);
}
function sendIndex(req, res) {
const indexPath = path.join(root, "index.html");
fs.stat(indexPath, (err, stats) => {
if (err || !stats.isFile()) {
res.writeHead(500, { "Content-Type": "text/plain" });
return res.end("index.html not found");
}
streamFile(req, res, indexPath, stats);
});
}
const server = http.createServer((req, res) => {
if (req.method === "OPTIONS") {
const filePath = safeJoin(req.url || "/");
if (filePath) applyWellKnownCors(req, res, filePath);
res.writeHead(204);
return res.end();
}
if (req.method !== "GET" && req.method !== "HEAD") {
res.writeHead(405, { Allow: "GET, HEAD, OPTIONS" });
return res.end();
}
const reqUrl = req.url || "/";
const filePath = safeJoin(reqUrl);
if (!filePath) {
res.writeHead(400);
return res.end("Bad Request");
}
fs.stat(filePath, (err, stats) => {
if (!err && stats.isFile()) {
return streamFile(req, res, filePath, stats);
}
if (!err && stats.isDirectory()) {
const indexInDir = path.join(filePath, "index.html");
return fs.stat(indexInDir, (e, s) => {
if (!e && s.isFile()) return streamFile(req, res, indexInDir, s);
return sendIndex(req, res);
});
}
// No file at this path → SPA fallback to index.html
sendIndex(req, res);
});
});
server.listen(port, host, () => {
console.log(`Serving ${root} on http://${host}:${port}`);
});
@@ -21,6 +21,7 @@ import {
formatCSVDate,
normalizeKey,
loadPdfOnce,
hasSignatureWidget,
} from "../../utils";
import { useDispatch, useSelector } from "react-redux";
import {
@@ -216,13 +217,10 @@ const BulkSendUi = (props) => {
const checkSignatureAndRoles = (placeholders = []) => {
const filtered = placeholders.filter((x) => x?.Role !== "prefill");
// Viewer/approver roles do not require a signature widget; only
// signer-role placeholders must have one.
const isSignExist =
filtered?.length > 0 &&
filtered.every((p) =>
p?.placeHolder?.some((h) =>
h?.pos?.some((x) => x?.type === "signature")
)
);
filtered?.length > 0 && filtered.every((p) => hasSignatureWidget(p));
setIsSignatureExist(isSignExist);
setIsVacantRoles(filtered?.some((x) => !x.signerObjId));
};
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
@@ -0,0 +1,52 @@
import { useGuidelinesContext } from "../../context/GuidelinesContext";
/**
* Canvas-level guidelines that live inside the main scroll container (not
* inside any individual page div). This allows the guideline lines to span
* across page boundaries seamlessly when dragging a widget from the side
* panel onto the document.
*/
const CanvasGuidelines = () => {
const { canvasGuideRefs } = useGuidelinesContext();
return (
<>
{/* Horizontal guidelines */}
{/* top guide */}
<div
ref={(el) => {
canvasGuideRefs.current.top = el;
}}
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: 0, display: "none" }}
/>
{/* bottom guide */}
<div
ref={(el) => {
canvasGuideRefs.current.bottom = el;
}}
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: 0, display: "none" }}
/>
{/* Vertical guidelines */}
{/* left guide */}
<div
ref={(el) => {
canvasGuideRefs.current.left = el;
}}
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: 0, display: "none" }}
/>
{/* right guide */}
<div
ref={(el) => {
canvasGuideRefs.current.right = el;
}}
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: 0, display: "none" }}
/>
</>
);
};
export default CanvasGuidelines;
@@ -1,77 +1,93 @@
import { useDragLayer } from "react-dnd";
import { defaultWidthHeight } from "../../constant/Utils";
import { defaultWidthHeight, getContainerScale } from "../../constant/Utils";
import { useEffect, useRef } from "react";
function DragGuideLinesLayer(props) {
const { showGuidelines } = props;
const { showCanvasGuidelines, canvasContainerRef } = props;
const prevDragState = useRef({ isDragging: false, x: 0, y: 0 });
const { isDraggingWidget, offset, itemType } = useDragLayer((monitor) => ({
const { isDraggingWidget, sourceOffset, itemType } = useDragLayer(
(monitor) => ({
isDraggingWidget: monitor.isDragging(),
offset: monitor.getClientOffset(),
// Use getSourceClientOffset - this gives the top-left position of the
// drag source, which exactly matches WidgetsDragPreview's position.
sourceOffset: monitor.getSourceClientOffset(),
itemType: monitor.getItem()
}));
})
);
useEffect(() => {
// Hide guidelines when dragging stops
if (!isDraggingWidget) {
showGuidelines(false);
showCanvasGuidelines(false);
return;
}
if (!offset) return;
//getting container by id
const container = document.getElementById("container");
if (!container) return;
if (itemType?.text) {
// Calculate the position of the dragged element relative to the container
// Get the container's bounding rectangle (position and size in the viewport)
const containerRect = container.getBoundingClientRect();
if (!sourceOffset || !itemType?.text) return;
// Compute the X and Y coordinates of the dragged item inside the container
// by subtracting the container's top-left offset from the current drag offset
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
// Get the canvas container to calculate position relative to the full
// document canvas (which spans all pages).
const canvasContainer = canvasContainerRef?.current;
if (!canvasContainer) return;
// Check if theres any change in the dragging state or the dragged elements position
// Compare the current drag state with the previously stored one
const scale = props.scale || 1; // ✅ get scale
const containerRect = canvasContainer.getBoundingClientRect();
// ✅ Divide by scale so guidelines live in the same coordinate space
// as the absolute-positioned CanvasGuidelines divs inside the scaled canvas.
// Without this, at 1.5x zoom a viewport pixel maps to 1/1.5 canvas pixels,
// so the guideline lines appear further right/down than the drag preview.
const x = (sourceOffset.x - containerRect.left) / scale;
const y = (sourceOffset.y - containerRect.top) / scale;
// Check if there's any change in position
const hasStateChanged =
prevDragState.current.isDragging !== isDraggingWidget || // Drag started or stopped
prevDragState.current.x !== x || // X position changed
prevDragState.current.y !== y; // Y position changed
prevDragState.current.isDragging !== isDraggingWidget ||
prevDragState.current.x !== x ||
prevDragState.current.y !== y;
if (hasStateChanged) {
// Update the previous drag state with the latest values
prevDragState.current = { isDragging: isDraggingWidget, x, y };
if (isDraggingWidget) {
// Adjust the current drag coordinates relative to the initial widgets button position
const getXPosition = props?.signBtnPosition?.[0]
? x - props.signBtnPosition[0].xPos
: x;
const getYPosition = props?.signBtnPosition?.[0]
? y - props.signBtnPosition[0].yPos
: y;
// Get the default width and height of the dragged checkbox/radio widget type
const el = document.getElementById("checkbox&radio-preview");
let rect;
if (el) {
rect = el.getBoundingClientRect();
}
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const height =
defaultWidthHeight(itemType?.text)?.height * containerScale;
const width = defaultWidthHeight(itemType?.text)?.width * containerScale;
const widget = {
type: itemType?.text,
width: defaultWidthHeight(itemType?.text).width,
height: defaultWidthHeight(itemType?.text).height
Width: width / (containerScale * props?.scale),
Height: height / (containerScale * props?.scale)
};
const getWidth =
rect?.width || props?.posWidth(widget, props?.isSignYourself);
const getHeight =
rect?.height || props?.posHeight(widget, props?.isSignYourself);
// ✅ rect dimensions also need to be divided by scale because
// getBoundingClientRect() returns scaled (viewport) pixel sizes,
// but posWidth/posHeight return unscaled canvas sizes.
const getWidth = rect?.width
? rect.width / scale
: props?.posWidth(widget);
const getHeight = rect?.height
? rect.height / scale
: props?.posHeight(widget);
// Display the alignment guidelines based on the current drag position and widget size
showGuidelines(true, getXPosition, getYPosition, getWidth, getHeight);
showCanvasGuidelines(true, x, y, getWidth, getHeight);
}
}
}
}, [isDraggingWidget, offset, itemType, showGuidelines]);
}, [
isDraggingWidget,
sourceOffset,
itemType,
showCanvasGuidelines,
props.scale
]);
}
export default DragGuideLinesLayer;
@@ -39,6 +39,8 @@ const EditTemplate = ({
Note: template?.Note || "",
Description: template?.Description || "",
SendinOrder: template?.SendinOrder ? `${template?.SendinOrder}` : "false",
SendInOrderStrict:
template?.SendInOrderStrict === true ? "true" : "false",
AutomaticReminders: template?.AutomaticReminders || false,
RemindOnceInEvery: template?.RemindOnceInEvery || 5,
IsEnableOTP: template?.IsEnableOTP ? `${template?.IsEnableOTP}` : "false",
@@ -50,6 +52,7 @@ const EditTemplate = ({
? template?.NotifyOnSignatures
: false,
Bcc: template?.Bcc,
Cc: template?.Cc,
RedirectUrl: template?.RedirectUrl || "",
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
@@ -190,6 +193,8 @@ const EditTemplate = ({
SaveFileSize(buffer.length, pdfUrl, tenantId, userId);
}
const isChecked = formData.SendinOrder === "true" ? true : false;
const isStrictOrder =
isChecked && formData.SendInOrderStrict === "true";
const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true;
const AutoReminder = formData?.AutomaticReminders || false;
const IsEnableOTP = formData.IsEnableOTP === "true" ? true : false;
@@ -212,6 +217,7 @@ const EditTemplate = ({
...formData,
...(pdfUrl ? { URL: pdfUrl } : {}),
SendinOrder: isChecked,
SendInOrderStrict: isStrictOrder,
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
AllowModifications: allowModify,
@@ -238,6 +244,18 @@ const EditTemplate = ({
}
};
const handleCc = (data) => {
if (data && data.length > 0) {
const trimEmail = data.map((item) => ({
objectId: item?.value,
Name: item?.label,
Email: item?.email
}));
setIsUpdate(true);
setFormData((prev) => ({ ...prev, Cc: trimEmail }));
}
};
const handleEditTemplateClose = () => {
if (isUpdate) {
setShowConfirm(true);
@@ -411,6 +429,28 @@ const EditTemplate = ({
<div className="text-[12px]">{t("no")}</div>
</div>
</div>
{formData.SendinOrder === "true" && (
<div className="flex items-center gap-[8px] ml-[8px] mt-[4px] mb-[5px]">
<input
type="checkbox"
className="op-checkbox op-checkbox-xs"
name="SendInOrderStrict"
checked={formData.SendInOrderStrict === "true"}
onChange={(e) =>
setFormData({
...formData,
SendInOrderStrict: e.target.checked ? "true" : "false"
})
}
/>
<span
className="text-[12px]"
title={t("strict-order-help")}
>
{t("strict-order")}
</span>
</div>
)}
</div>
<div className="text-xs mt-3">
<label className="block">
@@ -551,12 +591,23 @@ const EditTemplate = ({
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
zindex={50}
helpText={t("bcc-help")}
isCaptureAllData
isAddYourSelfCheckbox={isAddYourSelfCheckbox}
/>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Cc")}
initialData={template?.Cc}
onChange={handleCc}
zindex={50}
helpText={t("cc-help")}
isCaptureAllData
isAddYourSelfCheckbox={isAddYourSelfCheckbox}
/>
</div>
<div className="text-xs mt-2">
<label className="block">{t("redirect-url")}</label>
<input
@@ -1,27 +1,51 @@
const Guidelines = ({ x1, x2, y1, y2 }) => {
import { useGuidelinesContext } from "../../context/GuidelinesContext";
const Guidelines = ({ pageNumber }) => {
const { guideRefs } = useGuidelinesContext();
return (
<>
{/* Horizontal guidelines */}
{/* top guide */}{" "}
{/* top guide */}
<div
ref={(el) => {
if (!guideRefs.current[pageNumber])
guideRefs.current[pageNumber] = {};
guideRefs.current[pageNumber].top = el;
}}
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: y1 }}
style={{ top: 0, display: "none" }}
/>
{/* bottom guide */}
<div
ref={(el) => {
if (!guideRefs.current[pageNumber])
guideRefs.current[pageNumber] = {};
guideRefs.current[pageNumber].bottom = el;
}}
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: y2 }}
style={{ top: 0, display: "none" }}
/>
{/* Vertical guidelines */}
{/* left guide */}
<div
ref={(el) => {
if (!guideRefs.current[pageNumber])
guideRefs.current[pageNumber] = {};
guideRefs.current[pageNumber].left = el;
}}
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: x1 }}
style={{ left: 0, display: "none" }}
/>
{/* right guide */}
<div
ref={(el) => {
if (!guideRefs.current[pageNumber])
guideRefs.current[pageNumber] = {};
guideRefs.current[pageNumber].right = el;
}}
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: x2 }}
style={{ left: 0, display: "none" }}
/>
</>
);
+11 -5
View File
@@ -29,6 +29,8 @@ function Header(props) {
const [isReorderModal, setIsReorderModal] = useState(false);
const mergePdfInputRef = useRef(null);
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
const isViewerSigner = false;
const finishLabel = t("finish");
//function for show decline alert
const handleDeclinePdfAlert = async () => {
if (props?.handleDecline) {
@@ -293,7 +295,7 @@ function Header(props) {
*/}
{props?.currentSigner && (
<div className="flex items-center" data-tut="reactourFifth">
{props?.decline && (
{props?.decline && !isViewerSigner && (
<div
onClick={() => handleDeclinePdfAlert()}
className="text-[red] border-none font-[650] text-[14px] mr-2"
@@ -318,13 +320,15 @@ function Header(props) {
: t("send")}
</div>
) : (
!isViewerSigner && (
<div
data-tut="reactourThird"
onClick={() => props?.embedWidgetsData()}
className="border-none font-[650] text-[14px] op-link op-link-primary no-underline"
>
{t("finish")}
{finishLabel}
</div>
)
)}
<input
type="file"
@@ -579,7 +583,7 @@ function Header(props) {
<span className="hidden lg:block">{t("download")}</span>
</button>
)}
{!props?.isSelfSign && (
{!props?.isSelfSign && !isViewerSigner && (
<button
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
onClick={() => handleDeclinePdfAlert()}
@@ -597,13 +601,15 @@ function Header(props) {
<span className="hidden lg:block">{t("download")}</span>
</button>
)}
{!isViewerSigner && (
<button
type="button"
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
onClick={() => props?.embedWidgetsData()}
>
{t("finish")}
{finishLabel}
</button>
)}
</>
)}
</div>
@@ -681,7 +687,7 @@ function Header(props) {
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
onClick={() => props?.embedWidgetsData()}
>
{t("finish")}
{finishLabel}
</button>
</div>
)}
+19 -10
View File
@@ -4,7 +4,6 @@ import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleRemoveWidgets,
reorderPdfPages
@@ -13,6 +12,10 @@ import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
import PageReorderModal from "./PageReorderModal";
import {
clearAcroFields,
isPdfPasswordProtected
} from "../../utils/acroFieldExtractor";
function PdfTools(props) {
const { t } = useTranslation();
@@ -45,7 +48,7 @@ function PdfTools(props) {
}
}
} catch (e) {
console.log("error in delete pdf page", e);
console.error("Delete pdf page error", e);
}
};
@@ -77,15 +80,16 @@ function PdfTools(props) {
return;
}
try {
let uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await getFileAsArrayBuffer(file);
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
await isPdfPasswordProtected(uploadedPdfBytes);
uploadedPdfBytes = await clearAcroFields(uploadedPdfBytes); // best effort cleanup to prevent stale data
} catch (error) {
if (error?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
uploadedPdfBytes = await clearAcroFields(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
@@ -95,22 +99,27 @@ function PdfTools(props) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
uploadedPdfBytes = await clearAcroFields(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
return;
}
} else {
alert(t("provide-password"));
return;
}
} else {
console.log("Err ", err);
console.error("Decryption error ", error);
alert(t("error-uploading-pdf"));
return;
}
}
} else {
console.error("File upload error ", error);
alert(t("error-uploading-pdf"));
return;
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
@@ -156,7 +165,7 @@ function PdfTools(props) {
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
console.error("Reorder PDF pages error", e);
}
setIsReorderModal(false);
};
+139 -95
View File
@@ -34,6 +34,22 @@ import DatePicker from "react-datepicker";
import DateWidgetModal from "../../primitives/DateWidgetModal";
import { dateFormat } from "../../utils";
/**
* Custom input UI for DatePicker
* - Declaring custom input outside resolve issue of the component recreates the input component identity on each state update, which can break re-open behavior after first use
*/
const DatePickerCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content "
onClick={onClick}
ref={ref}
>
{value}
<i className={`${value ? "ml-[5px]" : "w-20"} fa-light fa-calendar `}></i>
</div>
));
DatePickerCustomInput.displayName = "DatePickerCustomInput";
function Placeholder(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
@@ -55,7 +71,11 @@ function Placeholder(props) {
const [isToday, setIsToday] = useState(
props.pos?.options?.response === "today" ? true : false
);
const [selectedFormatIndex, setselectedFormatIndex] = useState(0);
const selectedFormatIndex = dateFormatList?.findIndex(
(item) =>
item.format === selectDate?.format ||
item?.format === props.pos?.options?.validation?.format
);
const isSelfSign = props.isSignYourself || props.isSelfSign;
const startDate = props?.pos?.options?.response
@@ -66,7 +86,7 @@ function Placeholder(props) {
: "";
useEffect(() => {
const getPdfPageWidth = props.pdfOriginalWH.find(
(data) => data.pageNumber === props.pageNumber
(data) => data.pageNumber === props.widgetPageNum
);
setContainerScale(props.containerWH.width / getPdfPageWidth.width);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -175,13 +195,6 @@ function Placeholder(props) {
updateDate.push(dateObj);
});
setDateFormatList(updateDate);
setselectedFormatIndex(
updateDate?.findIndex(
(item) =>
item.format === selectDate?.format ||
item?.format === props.pos?.options?.validation?.format
)
);
};
useEffect(() => {
@@ -202,7 +215,11 @@ function Placeholder(props) {
props?.data?.Role !== "prefill" &&
props.data?.Id === props.uniqueId
) {
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
props.setCurrWidgetsDetails &&
props.setCurrWidgetsDetails({
...props.pos,
pageNumber: props.widgetPageNum
});
if (props?.ispublicTemplate) {
props.handleUserDetails();
} else {
@@ -214,19 +231,24 @@ function Placeholder(props) {
const updateZindex = handleHeighlightWidget(
getCurrentSignerPos,
props.pos.key,
props.pageNumber
props.widgetPageNum
);
const updatesignerPos = props.xyPosition.map((x) =>
x.Id === props.uniqueId ? { ...x, placeHolder: updateZindex } : x
);
props.setXyPosition(updatesignerPos);
}
props?.closeWidgetTour?.();
dispatch(setIsShowModal({ [props.pos.key]: true }));
}
}
//placeholder, template flow
else if (props.isPlaceholder && !props.isDragging) {
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
props.setCurrWidgetsDetails &&
props.setCurrWidgetsDetails({
...props.pos,
pageNumber: props.widgetPageNum
});
if (props?.data?.Role !== "prefill") {
//this condition is used open signers attach modal but it should only open when widgets already selected
if (props?.currWidgetsDetails?.key === props.pos?.key) {
@@ -235,6 +257,8 @@ function Placeholder(props) {
props.setUniqueId && props.setUniqueId(props.data.Id);
props?.setRoleName("");
} else if (props?.data?.Role === "prefill") {
props?.handleClosePrefillTour?.();
props?.closeWidgetTour?.();
dispatch(setIsShowModal({ [props.pos.key]: true }));
props.setUniqueId(props?.data?.Id);
props?.setRoleName("prefill");
@@ -286,7 +310,10 @@ function Placeholder(props) {
) {
props.setUniqueId(props?.data?.Id);
}
props.setCurrWidgetsDetails(props.pos);
props?.setCurrWidgetsDetails?.({
...props.pos,
pageNumber: props.widgetPageNum
});
};
//function to set required state value onclick on widget's copy icon
const handleCopyPlaceholder = (e) => {
@@ -307,7 +334,10 @@ function Placeholder(props) {
props.setUniqueId(props?.data?.Id);
}
props.setIsPageCopy(true);
props.setCurrWidgetsDetails(props.pos);
props?.setCurrWidgetsDetails?.({
...props.pos,
pageNumber: props.widgetPageNum
});
};
const setCellCount = (key, newCount) => {
@@ -319,7 +349,7 @@ function Placeholder(props) {
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const updatedPlaceHolder = getPlaceHolder.map((ph) => {
if (ph.pageNumber !== props.pageNumber) return ph;
if (ph.pageNumber !== props.widgetPageNum) return ph;
const newPos = ph.pos.map((p) =>
p.key === key
? { ...p, options: { ...p.options, cellCount: newCount } }
@@ -453,12 +483,18 @@ function Placeholder(props) {
<i
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
props.setCurrWidgetsDetails(props.pos);
props?.setCurrWidgetsDetails?.({
...props.pos,
pageNumber: props.widgetPageNum
});
setIsDateModal(!isDateModal);
e.stopPropagation();
setClickonWidget(props.pos);
if (props.data) {
props.setCurrWidgetsDetails(props.pos);
props?.setCurrWidgetsDetails?.({
...props.pos,
pageNumber: props.widgetPageNum
});
props.setUniqueId(props.data.Id);
const checkIndex = props.xyPosition.findIndex(
(data) => data.Id === props.data.Id
@@ -467,11 +503,17 @@ function Placeholder(props) {
}
}}
onTouchEnd={(e) => {
props.setCurrWidgetsDetails(props.pos);
props?.setCurrWidgetsDetails?.({
...props.pos,
pageNumber: props.widgetPageNum
});
e.stopPropagation();
setIsDateModal(!isDateModal);
if (props.data) {
props.setCurrWidgetsDetails(props.pos);
props?.setCurrWidgetsDetails?.({
...props.pos,
pageNumber: props.widgetPageNum
});
props.setUniqueId(props.data.Id);
const checkIndex = props.xyPosition.findIndex(
(data) => data.Id === props.data.Id
@@ -502,11 +544,19 @@ function Placeholder(props) {
showGuidelines(false);
//condition for template and placeholder flow
if (props.data) {
props.handleDeleteWidget(props.pos.key, props.data.Id);
props.handleDeleteWidget(
props.pos.key,
props.data.Id,
props.widgetPageNum
);
}
//condition for signyour-self flow
else {
props.handleDeleteWidget(props.pos.key);
props.handleDeleteWidget(
props.pos.key,
null,
props.widgetPageNum
);
}
}}
//for mobile and tablet touch event
@@ -515,11 +565,19 @@ function Placeholder(props) {
showGuidelines(false);
//condition for template and placeholder flow
if (props.data) {
props.handleDeleteWidget(props.pos.key, props.data?.Id);
props.handleDeleteWidget(
props.pos.key,
props.data?.Id,
props.widgetPageNum
);
}
//condition for signyour-self flow
else {
props.handleDeleteWidget(props.pos.key);
props.handleDeleteWidget(
props.pos.key,
null,
props.widgetPageNum
);
}
}}
></i>
@@ -527,29 +585,18 @@ function Placeholder(props) {
)
);
};
const xPos = (pos, signYourself) => {
const xPos = (pos) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const resizePos = pos.xPosition;
if (signYourself) {
return resizePos * containerScale * props.scale;
} else {
//checking both condition mobile and desktop view
if (pos.isMobile && pos.scale) {
if (props.scale > 1) {
return resizePos * pos.scale * containerScale * props.scale;
} else {
return resizePos * pos.scale * containerScale;
}
} else {
return resizePos * containerScale * props.scale;
}
}
return resizePos * containerScale; // ✅ desktop: account for Page scale
};
const yPos = (pos, signYourself) => {
const yPos = (pos) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
@@ -557,29 +604,17 @@ function Placeholder(props) {
);
const resizePos = pos.yPosition;
if (signYourself) {
return resizePos * containerScale * props.scale;
} else {
//checking both condition mobile and desktop view
if (pos.isMobile && pos.scale) {
if (props.scale > 1) {
return resizePos * pos.scale * containerScale * props.scale;
} else {
return resizePos * pos.scale * containerScale;
}
} else {
return resizePos * containerScale * props.scale;
}
}
return resizePos * containerScale; // ✅ desktop fix
};
// function to calculate font size
const calculateFont = (size, isMinHeight) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.widgetPageNum,
props.containerWH
);
const fontSize = (size || 12) * containerScale * props.scale;
const fontSize = (size || 12) * containerScale;
// isMinHeight to set text box minimum height
if (isMinHeight) {
return fontSize * 1.5 + "px";
@@ -624,18 +659,6 @@ function Placeholder(props) {
}
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content "
onClick={onClick}
ref={ref}
>
{value}
<i className={`${value ? "ml-[5px]" : "w-20"} fa-light fa-calendar `}></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleChangeFormat = (e) => {
const selectedIndex = e.target.value;
e.stopPropagation();
@@ -693,20 +716,27 @@ function Placeholder(props) {
{props.pos?.options?.response &&
props.pos.key !== props?.currWidgetsDetails?.key &&
props.pos.type === textWidget ? (
<textarea
<span
readOnly
onClick={() => {
onClick={(e) => {
// Prevent the click from bubbling up to the Document onClick handler
// which would immediately clear currWidgetsDetails and make the
// widget appear non-clickable after a response has been saved.
e.stopPropagation();
if (props?.ispublicTemplate) return;
props.setCurrWidgetsDetails &&
props.setCurrWidgetsDetails(props.pos);
props?.setCurrWidgetsDetails?.({
...props.pos,
pageNumber: props.widgetPageNum
});
props?.setRoleName && props?.setRoleName("prefill");
props.setUniqueId && props.setUniqueId(props?.data?.Id);
}}
style={{
fontFamily: "Arial, sans-serif",
position: "absolute",
left: xPos(props.pos, props.isSignYourself),
top: yPos(props.pos, props.isSignYourself),
left: xPos(props.pos),
top: yPos(props.pos),
...(props.pos?.Width ? { width: props.pos?.Width - 5 } : {}),
...(props.pos?.Height ? { height: props.pos?.Height } : {}),
fontSize: fontSize,
@@ -715,10 +745,10 @@ function Placeholder(props) {
cursor: getCursor(),
resize: "none",
background: "transparent",
width: props.posWidth(props.pos, props.isSignYourself),
height: props.posHeight(props.pos, props.isSignYourself),
width: props.posWidth(props.pos),
height: props.pos?.Height,
whiteSpace: "pre-wrap",
overflow: "hidden",
overflow: "visible",
outline: "none",
border: "none"
}}
@@ -726,10 +756,11 @@ function Placeholder(props) {
{
props.pos?.options?.response
}
</textarea>
</span>
) : (
<Rnd
id={props.pos.key}
scale={props.scale}
data-tut={props.pos.key === props.unSignedWidgetId ? "IsSigned" : ""}
key={props.pos.key}
cancel=".cell-size-handle, .icon"
@@ -739,7 +770,12 @@ function Placeholder(props) {
? false
: !props.isFreeResize &&
(props.pos.Width
? props.pos.Width / props.pos.Height
? [90, 270].includes(props.pos.options?.rotation)
? props.pos.Height / props.pos.Width
: props.pos.Width / props.pos.Height
: [90, 270].includes(props.pos.options?.rotation)
? defaultWidthHeight(props.pos.type).height /
defaultWidthHeight(props.pos.type).width
: defaultWidthHeight(props.pos.type).width /
defaultWidthHeight(props.pos.type).height)
}
@@ -770,9 +806,9 @@ function Placeholder(props) {
borderRadius: "2px",
cursor: getCursor(),
zIndex:
props.pos.type === "date"
? props.pos.key === props?.currWidgetsDetails?.key
? 99 + 1
props.pos.key === props?.currWidgetsDetails?.key
? props.pos.type === "date"
? 100
: 99
: props?.pos?.zIndex
? props.pos.zIndex
@@ -795,7 +831,8 @@ function Placeholder(props) {
d.x,
d.y,
d.node.offsetWidth,
d.node.offsetHeight
d.node.offsetHeight,
props.widgetPageNum
);
}}
size={{
@@ -803,12 +840,16 @@ function Placeholder(props) {
props.pos.type === radioButtonWidget ||
props.pos.type === "checkbox"
? "auto"
: props.posWidth(props.pos, props.isSignYourself),
: [90, 270].includes(props.pos.options?.rotation)
? props.posHeight(props.pos)
: props.posWidth(props.pos),
height:
props.pos.type === radioButtonWidget ||
props.pos.type === "checkbox"
? "auto"
: props.posHeight(props.pos, props.isSignYourself)
: [90, 270].includes(props.pos.options?.rotation)
? props.posWidth(props.pos)
: props.posHeight(props.pos)
}}
minHeight={
props.pos.type === cellsWidget
@@ -821,10 +862,11 @@ function Placeholder(props) {
props?.setIsResize?.(true);
showGuidelines(
true,
xPos(props.pos, props.isSignYourself),
yPos(props.pos, props.isSignYourself),
xPos(props.pos),
yPos(props.pos),
ref.offsetWidth,
ref.offsetHeight
ref.offsetHeight,
props.widgetPageNum
);
}}
onResize={(e, dir, ref, delta, position) => {
@@ -833,19 +875,19 @@ function Placeholder(props) {
position.x,
position.y,
ref.offsetWidth,
ref.offsetHeight
ref.offsetHeight,
props.widgetPageNum
);
}}
onResizeStop={(e, direction, ref) => {
setTimeout(() => props?.setIsResize?.(false), 50);
props?.handleSignYourselfImageResize?.(
props?.handleWidgetResize?.(
ref,
props.pos.key,
props.xyPosition,
props.setXyPosition,
props.index,
containerScale,
props.scale,
props.data && props.data.Id,
props.isResize
);
@@ -859,12 +901,13 @@ function Placeholder(props) {
event,
dragElement,
props.data?.Id,
props.pos?.key
props.pos?.key,
props.widgetPageNum
);
}}
position={{
x: xPos(props.pos, props.isSignYourself),
y: yPos(props.pos, props.isSignYourself)
x: xPos(props.pos),
y: yPos(props.pos)
}}
disableDragging={handleDragging()}
>
@@ -916,8 +959,8 @@ function Placeholder(props) {
<div
className="flex items-stretch justify-center"
style={{
left: xPos(props.pos, props.isSignYourself),
top: yPos(props.pos, props.isSignYourself),
left: xPos(props.pos),
top: yPos(props.pos),
width: "100%",
height: "100%",
zIndex: "10"
@@ -952,6 +995,7 @@ function Placeholder(props) {
xPos={props.xPos}
calculateFont={calculateFont}
setCellCount={setCellCount}
isPrefillModal={props?.isPrefillModal}
/>
</div>
</Rnd>
@@ -1056,7 +1100,7 @@ function Placeholder(props) {
selectDate?.format
)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
customInput={<DatePickerCustomInput />}
onChange={(date) => {
setIsToday(false);
const isDateChange = true;
@@ -71,8 +71,8 @@ function PlaceholderCopy(props) {
newPageNumber !== props.pageNumber
) {
// Fallback widget dimensions (in case not defined)
const widgetHeight = newPlaceholder?.height || 60;
const widgetWidth = newPlaceholder?.width || 150;
const widgetHeight = newPlaceholder?.Height || 60;
const widgetWidth = newPlaceholder?.Width || 150;
//Calculate scaling ratio between source and target page
const heightRatio = targetPageHeight / sourcePageHeight;
@@ -281,13 +281,11 @@ function PlaceholderCopy(props) {
const filteredOld = oldPlaceholders.filter(
(page) => page.pageNumber < start || page.pageNumber > end
);
return {
...signersData,
placeHolder: [...filteredOld, ...newPlaceholderPosition]
};
});
// No mutation
props.setXyPosition(updatedSignerPlaceholder);
}
@@ -45,8 +45,11 @@ function PlaceholderType(props) {
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId);
// prefer the latest response value over any default value
const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
//props?.isPrefillModal is used to handle in create template flow when user use use-template button
//then prefill details should not be reflect on pdf document it should only show in modal
const widgetData = !props?.isPrefillModal
? (props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "")
: "";
const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [imgUrl, setImgUrl] = useState("");
@@ -64,6 +67,7 @@ function PlaceholderType(props) {
height: "100%"
};
useEffect(() => {
if (!props?.isPrefillModal) {
if (type !== "date") {
if (type && type === "checkbox") {
setSelectedCheckbox(response || defaultData || []);
@@ -73,6 +77,7 @@ function PlaceholderType(props) {
setwidgetValue(widgetData);
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos, widgetData, type]);
@@ -94,10 +99,24 @@ function PlaceholderType(props) {
));
ExampleCustomInput.displayName = "ExampleCustomInput";
// Extract string label from a radio/checkbox value entry which may be
// either a plain string or an object like { name: string, checked: boolean }
const getRadioLabel = (data) => {
if (typeof data === "string") return data;
if (data && typeof data === "object" && data.name != null)
return String(data.name);
return "";
};
const handleRadioCheck = (data) => {
const defaultData =
widgetValue?.trim() || props.pos?.options?.defaultValue?.trim() || "";
if (!props?.isPrefillModal) {
const defaultData = widgetValue
? widgetValue?.trim()
: props.pos?.options?.defaultValue
? props.pos?.options?.defaultValue?.trim()
: "";
return defaultData === data?.trim();
}
};
//function is used to get prefill image's signedUrl after expired
useEffect(() => {
@@ -117,7 +136,9 @@ function PlaceholderType(props) {
setImgUrl(props.pos.SignUrl);
}
};
if (!props?.isPrefillModal) {
loadImage();
}
}, [props.pos.SignUrl]);
const formatWidgetName = () => {
@@ -176,12 +197,25 @@ function PlaceholderType(props) {
}, [props?.startDate, props?.selectDate?.format]);
switch (type) {
case "signature":
case "signature": {
const sigRotation = props.pos.options?.rotation;
const sigIsSwapped = [90, 270].includes(sigRotation);
const sigImgStyle = sigRotation
? sigIsSwapped &&
props.pos.signatureType === "type" &&
props.pos.Width &&
props.pos.Height
? {
transform: `rotate(${sigRotation}deg) scaleX(${props.pos.Width / props.pos.Height}) scaleY(${props.pos.Height / props.pos.Width})`
}
: { transform: `rotate(${sigRotation}deg)` }
: undefined;
return props.pos.SignUrl ? (
<img
alt="signature"
draggable="false"
src={props.pos.SignUrl}
style={sigImgStyle}
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
/>
) : (
@@ -191,15 +225,19 @@ function PlaceholderType(props) {
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
: "11px",
...(sigRotation
? { transform: `rotate(${sigRotation}deg)` }
: {})
}}
className="font-medium"
className={`${sigIsSwapped ? "whitespace-nowrap" : ""} font-medium`}
>
{formatWidgetName()}
</div>
)}
</div>
);
}
case "stamp":
return props.pos.SignUrl ? (
<img
@@ -228,32 +266,60 @@ function PlaceholderType(props) {
const checkBoxLayout = props.pos.options?.layout || "vertical";
const isMultipleCheckbox =
props.pos.options?.values?.length > 0 ? true : false;
const checkboxSize = parseFloat(fontSize);
// Scaled gaps
const checkboxGapX = `${checkboxSize * 0.8}px`; // was gap-x-2 (fixed 8px)
const checkboxGapY = `${checkboxSize * 0.4}px`; // was gap-y-[3px] (fixed 3px)
const checkBoxWrapperClass = `flex items-start whitespace-pre-wrap ${
checkBoxLayout === "horizontal"
? `flex-row flex-wrap lg:py-[1.6px] ${isMultipleCheckbox ? "gap-x-2" : ""}`
: `flex-col ${isMultipleCheckbox ? "gap-y-[3px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
? `flex-row flex-wrap lg:py-[1.6px]`
: `flex-col`
}`;
return (
<div
className={checkBoxWrapperClass}
style={{ zIndex: props.isSignYourself && "99" }}
style={{
zIndex: props.isSignYourself && "99",
width: "100%",
height: "100%",
overflow: "hidden",
//Scaled gap with same condition as before
gap: isMultipleCheckbox
? checkBoxLayout === "horizontal"
? checkboxGapX // was gap-x-2
: checkboxGapY // was gap-y-[3px]
: undefined
}}
>
{props.pos.options?.values?.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className={`mb-0 flex items-center gap-1`}
style={{
fontSize: fontSize,
color: fontColor,
gap: `${checkboxSize * 0.2}px` //scaled inner gap (was gap-1)
}}
className="mb-0 flex items-center" //removed gap-1 (fixed 4px)
>
<input
id={`checkbox-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize }}
style={{
width: fontSize,
height: fontSize,
flexShrink: 0 //prevent flex compression
}}
className="op-checkbox rounded-[1px]"
disabled={props.isNeedSign && isReadOnly}
type="checkbox"
readOnly
checked={!!selectCheckbox(ind, selectedCheckbox)}
checked={
!props?.isPrefillModal
? !!selectCheckbox(ind, selectedCheckbox)
: false
}
/>
{!props.pos.options?.isHideLabel && (
<span className="leading-none">{data}</span>
@@ -327,12 +393,25 @@ function PlaceholderType(props) {
<i className="fa-light fa-circle-chevron-down mr-1 "></i>
</div>
);
case "initials":
case "initials": {
const iniRotation = props.pos.options?.rotation;
const iniIsSwapped = [90, 270].includes(iniRotation);
const iniImgStyle = iniRotation
? iniIsSwapped &&
props.pos.signatureType === "type" &&
props.pos.Width &&
props.pos.Height
? {
transform: `rotate(${iniRotation}deg) scaleX(${props.pos.Width / props.pos.Height}) scaleY(${props.pos.Height / props.pos.Width})`
}
: { transform: `rotate(${iniRotation}deg)` }
: undefined;
return props.pos.SignUrl ? (
<img
alt="initials"
draggable="false"
src={props.pos.SignUrl}
style={iniImgStyle}
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
/>
) : (
@@ -342,7 +421,11 @@ function PlaceholderType(props) {
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
: "11px",
...(iniRotation
? { transform: `rotate(${iniRotation}deg)` }
: {}),
...(iniIsSwapped ? { whiteSpace: "nowrap" } : {})
}}
className="font-medium text-center"
>
@@ -351,6 +434,7 @@ function PlaceholderType(props) {
)}
</div>
);
}
case "name":
return iswidgetEnable ? (
<textarea
@@ -503,24 +587,51 @@ function PlaceholderType(props) {
case radioButtonWidget:
const radioLayout = props.pos.options?.layout || "vertical";
const isOnlyOneBtn = props.pos.options?.values?.length > 0 ? true : false;
const radioSize = parseFloat(fontSize);
//Scaled gaps based on radioSize
const scaledGapX = `${radioSize * 0.8}px`; // horizontal gap between items
const scaledGapY = `${radioSize * 0.4}px`; // vertical gap between items
const radioWrapperClass = `flex items-start whitespace-pre-wrap ${
radioLayout === "horizontal"
? `flex-row flex-wrap lg:py-[1.6px] ${isOnlyOneBtn ? "gap-x-[10px]" : ""}`
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
? `flex-row flex-wrap lg:py-[1.6px]`
: `flex-col`
}`;
return (
<div className={radioWrapperClass}>
<div
className={radioWrapperClass}
style={{
width: "100%",
height: "100%",
overflow: "hidden",
//Scaled gap with condition same as before
gap: isOnlyOneBtn
? radioLayout === "horizontal"
? scaledGapX // was gap-x-[10px]
: scaledGapY // was gap-y-[5px]
: undefined
}}
>
{props.pos.options?.values.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 flex items-center gap-1"
style={{
fontSize: fontSize,
color: fontColor,
gap: `${radioSize * 0.2}px` //scaled inner gap (was gap-1)
}}
className="mb-0 flex items-center" //removed text-xs
>
<input
readOnly
id={`radio-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize, lineHeight: 2 }}
style={{
width: fontSize,
height: fontSize,
flexShrink: 0
}}
className={`op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative ${
handleRadioCheck(data) ? "checked-radio" : ""
}`}
@@ -529,7 +640,9 @@ function PlaceholderType(props) {
checked={handleRadioCheck(data)}
/>
{!props.pos.options?.isHideLabel && (
<span className="leading-none">{data?.trim()}</span>
<span className="leading-none">
{getRadioLabel(data).trim()}
</span>
)}
</label>
</div>
@@ -25,8 +25,7 @@ import AddContact from "../../primitives/AddContact";
import Loader from "../../primitives/Loader";
import { useDispatch, useSelector } from "react-redux";
import {
resetWidgetState,
setPrefillImg
setPrefillImg,
} from "../../redux/reducers/widgetSlice";
import * as utils from "../../utils";
import Draw from "./tab/Draw";
@@ -122,6 +121,7 @@ function PrefillWidgetModal(props) {
const dispatch = useDispatch();
const canvasRefs = useRef({});
const [penColor, setPenColor] = useState("blue");
const prefillImg = useSelector((state) => state.widget.prefillImg);
// Track already loaded image keys so they don't increment multiple times
const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates
@@ -161,9 +161,6 @@ function PrefillWidgetModal(props) {
return flatArray || [];
}, [props.prefillData]);
useEffect(() => {
dispatch(resetWidgetState([]));
}, []);
// Reset loader state when modal closes
useEffect(() => {
if (!props?.isPrefillModal) {
@@ -176,10 +173,10 @@ function PrefillWidgetModal(props) {
useEffect(() => {
//function is used to save all image base64 in redux state to display prefill images
const savePrefillImg = async () => {
const prefillImg = await utils?.savePrefillImg(props.xyPosition);
if (Array.isArray(prefillImg)) {
const prefillImage = await utils?.savePrefillImg(props.xyPosition);
if (Array.isArray(prefillImage)) {
setLoading(true);
prefillImg.forEach((img) => dispatch(setPrefillImg(img)));
prefillImage.forEach((img) => dispatch(setPrefillImg(img)));
}
setLoading(false);
};
@@ -465,7 +462,7 @@ function PrefillWidgetModal(props) {
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`checkbox-${position.key + ind}`}
id={`modal-checkbox-${position.key + ind}`}
className="mt-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
checked={selectCheckbox(ind, position)}
@@ -474,7 +471,7 @@ function PrefillWidgetModal(props) {
}
/>
<label
htmlFor={`checkbox-${position.key + ind}`}
htmlFor={`modal-checkbox-${position.key + ind}`}
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
>
{data}
@@ -597,14 +594,14 @@ function PrefillWidgetModal(props) {
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`radio-${position.key + ind}`}
id={`modal-radio-${position.key + ind}`}
className="mt-[2px] op-radio op-radio-xs"
type="radio"
checked={handleRadioCheck(data, position)}
onChange={() => handleWidgetDetails(position, data)}
/>
<label
htmlFor={`radio-${position.key + ind}`}
htmlFor={`modal-radio-${position.key + ind}`}
className="text-xs mb-0 ml-[2px] cursor-pointer"
>
{data}
@@ -623,6 +620,7 @@ function PrefillWidgetModal(props) {
{position.options?.name}
</span>
<Draw
key={position.key + "_" + (prefillImg?.length || 0)}
penColor={penColor}
canvasRef={getCanvasRef(position.key)}
currWidgetsDetails={position}
@@ -1,5 +1,6 @@
import { useRef, useState } from "react";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslation } from "react-i18next";
import {
color,
darkenColor,
@@ -10,6 +11,7 @@ import {
const cursor =
"cursor-[url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAASElEQVR4nGNgwAMkJSUbpKSkOvCpIaT5PxSTbogUQjMYMwxeIIXmVFIxA8UGDDyQGg0DnIDi6JKUlCxHMqCeZAOghjSAMD5FAKfeaURdUFxCAAAAAElFTkSuQmCC'),_pointer]";
const RecipientList = (props) => {
const { t } = useTranslation();
const [animationParent] = useAutoAnimate();
const [isHover, setIsHover] = useState();
const [isPrefill, setIsPrefill] = useState(false);
@@ -6,10 +6,13 @@ import { PDFDocument } from "pdf-lib";
import {
base64ToArrayBuffer,
decryptPdf,
flattenPdf,
getFileAsArrayBuffer
} from "../../constant/Utils";
import { maxFileSize } from "../../constant/const";
import {
clearAcroFields,
isPdfPasswordProtected
} from "../../utils/acroFieldExtractor";
function RenderAllPdfPage(props) {
const { t } = useTranslation();
@@ -95,15 +98,17 @@ function RenderAllPdfPage(props) {
return;
}
try {
let uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await getFileAsArrayBuffer(file);
await isPdfPasswordProtected(uploadedPdfBytes);
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
uploadedPdfBytes = await clearAcroFields(uploadedPdfBytes); // best effort cleanup to prevent stale data
} catch (error) {
console.error("Error merging PDF:", error);
if (error?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
uploadedPdfBytes = await clearAcroFields(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
@@ -113,22 +118,27 @@ function RenderAllPdfPage(props) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
uploadedPdfBytes = await clearAcroFields(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
return;
}
} else {
alert(t("provide-password"));
return;
}
} else {
console.log("Err ", err);
console.error("Decryption error ", error);
alert(t("error-uploading-pdf"));
return;
}
}
} else {
console.error("File upload error ", error);
alert(t("error-uploading-pdf"));
return;
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
File diff suppressed because it is too large Load Diff
@@ -17,27 +17,6 @@ function SignerListPlace(props) {
<sup onClick={() => props.setIsTour && props.setIsTour(true)}>
<i className="ml-1 cursor-pointer fa-light fa-question rounded-full border-[1px] border-base-content text-[11px] py-[1px] px-[3px]"></i>
</sup>
{/* <span className="absolute text-xs z-[30] mt-1 ml-0.5">
{props?.title === "Roles" && (
<>
<a data-tooltip-id="my-tooltip">
<sup>
<i className="fa-light fa-question rounded-full border-[1px] border-base-content text-[11px] py-[1px] px-[3px]"></i>
</sup>
</a>
<Tooltip id="my-tooltip" className="z-[100]">
<div className="max-w-[450px] 2xl:max-w-[500px] p-[1px]">
<p className="font-bold pb-[1px]">{t("role-help.p1")}</p>
<p>{t("role-help.p2")} </p>
<p className="font-bold">{t("role-help.p3")}</p>
<p>{t("role-help.p4")}</p>
<p className="font-bold">{t("role-help.p5")}</p>
<p>{t("role-help.p6")}</p>
</div>
</Tooltip>
</>
)}
</span> */}
</span>
</div>
<div className="overflow-auto hide-scrollbar max-h-[180px]">
@@ -28,6 +28,7 @@ const WidgetNameModal = (props) => {
isReadOnly: false,
cellCount: 5,
});
const [rotation, setRotation] = useState(0);
const [isValid, setIsValid] = useState(true);
const statusArr = ["Required", "Optional"];
const [signatureType, setSignatureType] = useState([]);
@@ -81,6 +82,7 @@ const WidgetNameModal = (props) => {
cellCount: props.defaultdata?.options?.cellCount || 5,
});
setLastSubmittedName(props.defaultdata?.options?.name || "");
setRotation(props.defaultdata?.options?.rotation || 0);
} else {
setFormdata({
...formdata,
@@ -88,6 +90,7 @@ const WidgetNameModal = (props) => {
cellCount: props.defaultdata?.options?.cellCount || 5,
});
setLastSubmittedName(props.defaultdata?.options?.name || "");
setRotation(props.defaultdata?.options?.rotation || 0);
}
if (signTypes.length > 0) {
@@ -123,7 +126,7 @@ const WidgetNameModal = (props) => {
} else if (isDefaultSignTypeOnly) {
alert(t("expect-default-one-signature-type"));
} else {
const data = { ...formdata, signatureType };
const data = { ...formdata, signatureType, rotation };
props.handleData(data, props.defaultdata?.type);
}
} else {
@@ -154,6 +157,7 @@ const WidgetNameModal = (props) => {
textvalidate: "",
cellCount: 5,
});
setRotation(0);
setSignatureType(signTypes);
}
};
@@ -380,6 +384,25 @@ const WidgetNameModal = (props) => {
</div>
</div>
)}
{isSignOrInitials && (
<div className="mb-[0.75rem]">
<label className="text-[14px] mb-[0.7rem]">
{t("rotation")}
</label>
<div className="ml-[7px] flex items-center gap-[10px]">
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs w-[120px]"
value={rotation}
onChange={(e) => setRotation(parseInt(e.target.value))}
>
<option value={0}>0°</option>
<option value={90}>90°</option>
<option value={180}>180°</option>
<option value={270}>270°</option>
</select>
</div>
</div>
)}
{!props?.isSelfSign && props?.roleName !== "prefill" && (
<div className="mb-[0.75rem]">
<label htmlFor="hint" className="text-[13px]">
@@ -7,49 +7,89 @@ import {
} from "../../constant/Utils";
export default function WidgetsDragPreview(props) {
const { widgetType, isDragging, initialOffset, currentOffset } = useDragLayer(
(monitor) => ({
widgetType: monitor.getItem(),
isDragging: monitor.isDragging(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset()
})
);
// 🎯 useDragLayer is used to track drag state globally (without attaching to a specific draggable item)
const { widgetType, isDragging, currentOffset } = useDragLayer((monitor) => ({
widgetType: monitor.getItem(), // 📦 current dragged item data
isDragging: monitor.isDragging(), // 🟢 whether dragging is active
currentOffset: monitor.getSourceClientOffset() // 📍 current cursor position
}));
// 📌 Default options for checkbox/radio preview
const options = [{ name: "Option-1" }, { name: "Option-2" }];
if (!isDragging) return null;
if (!initialOffset || !currentOffset) return null;
// ❌ Do not render preview if drag is not active or position data is missing
// isDragging → ensures preview is shown ONLY while user is actively dragging
// currentOffset → provides current mouse position; without it we can't place the preview correctly
if (!isDragging || !currentOffset) return null;
// 📍 Extract current cursor position
const { x, y } = currentOffset;
const getWidgetDetails = defaultWidthHeight(widgetType?.text);
const widget = {
type: widgetType?.text,
width: getWidgetDetails?.width,
height: getWidgetDetails?.height
};
const calculateFont = () => {
// 📐 Get scale of PDF container (helps map coordinates correctly)
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const fontSize = 12 * containerScale * props.scale;
// 📏 Calculate scaled height & width based on widget type
const height = defaultWidthHeight(widgetType?.text)?.height * containerScale;
const width = defaultWidthHeight(widgetType?.text)?.width * containerScale;
// 📦 Widget dimensions adjusted relative to PDF scale and zoom
const widget = {
type: widgetType?.text,
Width: width / (containerScale * props?.scale),
Height: height / (containerScale * props?.scale)
};
// 🔤 Font size calculator (currently static but can be dynamic later)
const calculateFont = () => {
const fontSize = 12;
return fontSize;
};
// ✅ KEY FIX: Get the scroll container's bounding rect so the preview
// renders at the same visual position as the real widget drop target.
// Without this, the preview uses raw viewport coords while the PDF
// canvas is offset by the container's scroll position.
const scrollEl = props.canvasContainerRef?.current;
// 📦 Get container position relative to viewport
const containerRect = scrollEl?.getBoundingClientRect?.() ?? {
left: 0,
top: 0
};
// 📜 Get scroll offsets (important when container is scrolled)
const scrollLeft = scrollEl?.scrollLeft ?? 0;
const scrollTop = scrollEl?.scrollTop ?? 0;
// 🔄 Convert viewport coordinates → PDF coordinate system
const scale = props.scale || 1;
// 🎯 Adjust X coordinate based on container offset, scroll, and zoom
const adjustedX = (x - containerRect.left + scrollLeft) / scale;
// 🎯 Adjust Y coordinate based on container offset, scroll, and zoom
const adjustedY = (y - containerRect.top + scrollTop) / scale;
return (
// 🧱 Full-screen overlay layer for drag preview (non-interactive)
// pointer-events-none ensures it doesn't block mouse interactions
<div className="fixed inset-0 pointer-events-none z-[9999]">
<div
className="absolute"
style={{
transform: `translate(${x}px, ${y}px)`
position: "absolute", // 📍 Positioned relative to overlay
transform: `translate(${adjustedX}px, ${adjustedY}px)` // 🚀 Move preview to correct position
}}
>
{/* 🔘 Render checkbox/radio preview */}
{widgetType.text === "checkbox" ||
widgetType.text === radioButtonWidget ? (
<div
id="checkbox&radio-preview"
style={{
// 🎨 Dynamic background depending on state (signing / assigned user etc.)
background: handleBackground(
props?.data,
props?.isNeedSign,
@@ -58,34 +98,35 @@ export default function WidgetsDragPreview(props) {
}}
className="border-red-600 rounded-[2px] border-[1px]"
>
{options.map((x, id) => {
return (
{/* 🔁 Render options list */}
{options.map((x, id) => (
<div key={id}>
<label
style={{ fontSize: calculateFont(), color: "black" }}
className={`mb-0 flex items-center gap-1`}
className="mb-0 flex items-center gap-1"
>
<input
style={{
width: calculateFont(),
height: calculateFont()
}}
className={`${widgetType.text === "checkbox" ? "op-checkbox" : "op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative"} rounded-[1px]`}
type={
widgetType.text === "checkbox" ? "checkbox" : "radio"
}
// 📏 Input size based on font
style={{ width: calculateFont(), height: calculateFont() }}
className={`${
widgetType.text === "checkbox"
? "op-checkbox"
: "op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative"
} rounded-[1px]`}
type={widgetType.text === "checkbox" ? "checkbox" : "radio"}
/>
{/* 🏷️ Option label */}
<span>{x?.name}</span>
</label>
</div>
);
})}
))}
</div>
) : (
// 🧾 Render generic widget preview (text, signature, etc.)
<div
style={{
width: props?.posWidth(widget, props?.isSignYourself),
height: props?.posHeight(widget, props?.isSignYourself),
width: props?.posWidth(widget), // 📏 dynamic width
height: props?.posHeight(widget), // 📏 dynamic height
background: handleBackground(
props?.data,
props?.isNeedSign,
@@ -94,6 +135,7 @@ export default function WidgetsDragPreview(props) {
}}
className="border-red-600 rounded-[2px] border-[1px] flex items-center justify-center"
>
{/* 🏷️ Widget type label */}
<span className="text-center text-[10px] font-medium">
{widgetType?.text}
</span>
@@ -193,7 +193,10 @@ function WidgetsValueModal(props) {
// below useEffect is used to focus text widgets when user open modal
useEffect(() => {
if (widgetRef?.current) {
const clearFocus = setTimeout(() => widgetRef?.current.focus(), 10);
const clearFocus = setTimeout(
() => widgetRef?.current.focus({ preventScroll: true }),
10
);
return () => clearTimeout(clearFocus);
}
}, [widgetRef.current]);
@@ -343,9 +346,9 @@ function WidgetsValueModal(props) {
prev.map((signer) => {
if (signer.Id !== uniqueId) return signer;
// Find the placeholder index for current page
const index = signer.placeHolder.findIndex(
(x) => x.pageNumber === pageNumber
// Find the placeholder index for the page containing the current widget
const index = signer.placeHolder.findIndex((x) =>
x.pos?.some((p) => p.key === currWidgetsDetails?.key)
);
// Get updated placeholder list
const updatedPlaceholders = onSaveImage(
@@ -386,9 +389,9 @@ function WidgetsValueModal(props) {
}
}
} else {
const index = props?.xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber;
});
const index = props?.xyPosition?.findIndex(
(p) => p.pageNumber === (currWidgetsDetails?.pageNumber || pageNumber)
);
const getImage = onSaveImage(
signatureType,
props?.xyPosition,
@@ -445,8 +448,8 @@ function WidgetsValueModal(props) {
prevState.map((signer) => {
if (signer.Id !== uniqueId) return signer;
const placeholderIndex = signer.placeHolder.findIndex(
(x) => x.pageNumber === pageNumber
const placeholderIndex = signer.placeHolder.findIndex((x) =>
x.pos?.some((p) => p.key === currWidgetsDetails?.key)
);
const updatedPlaceholders = onSaveSign(
signType,
@@ -483,9 +486,9 @@ function WidgetsValueModal(props) {
);
}
} else {
const index = props?.xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber;
});
const index = props?.xyPosition?.findIndex(
(p) => p.pageNumber === (currWidgetsDetails?.pageNumber || pageNumber)
);
const getUpdatePosition = onSaveSign(
signType,
props?.xyPosition,
@@ -1577,7 +1580,14 @@ function WidgetsValueModal(props) {
<div
className={`border-[1px] border-gray-300 rounded-[2px] pt-1 px-2.5 ${radioWrapperClass}`}
>
{currWidgetsDetails?.options?.values.map((data, ind) => (
{currWidgetsDetails?.options?.values.map((data, ind) => {
const label =
typeof data === "string"
? data
: data && typeof data === "object" && data.name != null
? String(data.name)
: "";
return (
<div key={ind} className="text-base-content select-none-cls">
<label
// htmlFor={`radio-${currWidgetsDetails?.key + ind}`}
@@ -1587,16 +1597,17 @@ function WidgetsValueModal(props) {
id={`radio-${currWidgetsDetails?.key + ind}`}
className={`op-radio op-radio-xs mt-1`}
type="radio"
value={data}
checked={handleRadioCheck(data?.trim())}
value={label}
checked={handleRadioCheck(label?.trim())}
onChange={(e) => {
handleCheckRadio(e.target.value?.trim());
}}
/>
<span>{data}</span>
<span>{label}</span>
</label>
</div>
))}
);
})}
</div>
);
case textWidget:
@@ -1766,8 +1777,8 @@ function WidgetsValueModal(props) {
prev.map((signer) => {
if (signer.Id !== uniqueId) return signer;
const idx = signer?.placeHolder?.findIndex(
(p) => p.pageNumber === pageNumber
const idx = signer?.placeHolder?.findIndex((p) =>
p.pos?.some((w) => w.key === widgetKey)
);
if (idx === -1) return signer;
@@ -1823,9 +1834,7 @@ function WidgetsValueModal(props) {
);
//get current index of widget
const currentIndex = editableWidgets.findIndex(
(item) =>
item.widget.key === currWidgetsDetails?.key &&
item.pageNumber === pageNumber
(item) => item.widget.key === currWidgetsDetails?.key
);
//get totoal widget length
const totalItems = editableWidgets?.length;
@@ -1848,7 +1857,10 @@ function WidgetsValueModal(props) {
}
dispatch(setIsShowModal({ [nextWidgetDetails?.key]: true }));
props.setCurrWidgetsDetails(nextWidgetDetails);
props.setCurrWidgetsDetails({
...nextWidgetDetails,
pageNumber: nextItem?.pageNumber
});
}
};
@@ -1924,7 +1936,7 @@ function WidgetsValueModal(props) {
const editableWidgets = widgetsPosition?.placeHolder?.flatMap((page) =>
page.pos
.filter((widget) => !widget.options?.isReadOnly)
.map((widget) => widget)
.map((widget) => ({ ...widget, pageNumber: page.pageNumber }))
);
const getcurrentwidget = editableWidgets?.find(
(data) => data?.key === currWidgetsDetails?.key
@@ -1989,7 +2001,9 @@ function WidgetsValueModal(props) {
<>
<div className="p-1 mt-3">
<span className="text-base text-base-content">
{t("finish-mssg")}
{
t("finish-mssg")
}
</span>
</div>
<div className="flex gap-3 items-center mt-4">
@@ -1998,7 +2012,9 @@ function WidgetsValueModal(props) {
className="op-btn op-btn-primary op-btn-sm px-4"
onClick={() => handleFinish()}
>
{t("finish")}
{
t("finish")
}
</button>
<button
type="button"
@@ -13,7 +13,8 @@ const SelectSigners = (props) => {
setSignersData,
uniqueId,
isRemove,
handleAddUser
handleAddUser,
isSubscribe
} = props;
const [userList, setUserList] = useState([]);
const [selected, setSelected] = useState();
@@ -59,7 +60,7 @@ const SelectSigners = (props) => {
};
const handleAdd = () => {
if (userData && userData.objectId) {
handleAddUser(userData);
const addedId = handleAddUser(userData);
if (props.closePopup) {
props.closePopup();
}
@@ -154,7 +154,7 @@ const SignersInput = (props) => {
{props.label ? props.label : t("signers")}
{props.required && <span className="text-red-500 text-[13px]">*</span>}
<span
className={`z-[${props?.helptextZindex ? props.helptextZindex : 30}] absolute ml-1 text-xs`}
className={`z-[${props?.zindex ? props.zindex : 30}] absolute ml-1 text-xs`}
>
<Tooltip
id={`${props.label ? props.label : "signers"}-tooltip`}
@@ -163,7 +163,9 @@ const SignersInput = (props) => {
</span>
</label>
<div className="flex gap-x-[5px]">
<div className="w-full z-40">
<div
className={`w-full z-[${props?.zindex ? props.zindex : 40}]`}
>
<AsyncSelect
onSortEnd={onSortEnd}
distance={4}
+638 -75
View File
@@ -1,12 +1,20 @@
import axios from "axios";
import moment from "moment";
import { PDFDocument, rgb, degrees } from "pdf-lib";
import {
PDFDocument,
rgb,
degrees,
PDFName,
StandardFonts,
PDFArray,
PDFDict
} from "pdf-lib";
import Parse from "parse";
import { appInfo } from "./appinfo";
import { saveAs } from "file-saver";
import printModule from "print-js";
import fontkit from "@pdf-lib/fontkit";
import { themeColor } from "./const";
import { SCALE_STEPS, themeColor } from "./const";
import { format, toZonedTime } from "date-fns-tz";
import i18n from "../i18n";
import {
@@ -108,7 +116,7 @@ export const getUserCountry = async () => {
// `getSecureUrl` is used to return local secure url if local files
export const getSecureUrl = async (url) => {
const fileUrl = new URL(url)?.pathname?.includes("files");
const fileUrl = new URL(url)?.pathname?.includes("/files/");
if (fileUrl) {
try {
const fileRes = await Parse.Cloud.run("fileupload", { url: url });
@@ -268,23 +276,21 @@ export const contractUsers = async () => {
};
//function for resize image and update width and height for mulitisigners
export const handleImageResize = (
export const handleWidgetResize = (
ref,
key,
signerPos,
setSignerPos,
pageNumber,
containerScale,
scale,
signerId,
showResize
) => {
// Compute widget dimensions only once
const { offsetWidth, offsetHeight } = ref;
const factor = scale * containerScale || 1;
const factor = containerScale || 1;
const widgetWidth = offsetWidth / factor;
const widgetHeight = offsetHeight / factor;
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
const filterSignerPos = signerPos.filter((data) => data.Id === signerId);
if (filterSignerPos.length > 0) {
@@ -297,11 +303,18 @@ export const handleImageResize = (
const getPosData = getXYdata;
const addSignPos = getPosData.map((url) => {
if (url.key === key) {
// For rotated widgets (90°/270°), the visual dimensions are swapped.
// Store the original-orientation dimensions in the data model.
const rotation = url.options?.rotation || 0;
const isRotSwapped = [90, 270].includes(rotation);
const storedWidth = isRotSwapped ? widgetHeight : widgetWidth;
const storedHeight = isRotSwapped ? widgetWidth : widgetHeight;
const widgetDims = { Width: storedWidth, Height: storedHeight };
// Base fields for every resized signature
const base = {
...url,
Width: widgetWidth,
Height: widgetHeight,
Width: storedWidth,
Height: storedHeight,
IsResize: showResize ? true : false
};
// If it's a “type” signature, regenerate the image and options
@@ -745,14 +758,13 @@ export const convertPNGtoJPEG = (base64Data) => {
};
//function for resize image and update width and height for sign-yourself
export const handleSignYourselfImageResize = (
export const handleSignYourselfWidgetResize = (
ref,
key,
xyPosition,
setXyPosition,
index,
containerScale,
scale
containerScale
) => {
// Guard against bad index
if (!xyPosition[index]) {
@@ -762,10 +774,9 @@ export const handleSignYourselfImageResize = (
// Compute widget dimensions only once
const { offsetWidth, offsetHeight } = ref;
const factor = scale * containerScale || 1;
const factor = containerScale || 1;
const widgetWidth = offsetWidth / factor;
const widgetHeight = offsetHeight / factor;
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
// Single pass to update only the targeted index/key
const updated = xyPosition.map((item, idx) => {
@@ -776,11 +787,19 @@ export const handleSignYourselfImageResize = (
pos: item.pos.map((url) => {
if (url.key !== key) return url;
// For rotated widgets (90°/270°), the visual dimensions are swapped.
// Store the original-orientation dimensions in the data model.
const rotation = url.options?.rotation || 0;
const isRotSwapped = [90, 270].includes(rotation);
const storedWidth = isRotSwapped ? widgetHeight : widgetWidth;
const storedHeight = isRotSwapped ? widgetWidth : widgetHeight;
const widgetDims = { Width: storedWidth, Height: storedHeight };
// Base fields for every resized signature
const base = {
...url,
Width: widgetWidth,
Height: widgetHeight,
Width: storedWidth,
Height: storedHeight,
IsResize: true
};
@@ -813,7 +832,8 @@ export const signPdfFun = async (
documentId,
signerObjectId,
objectId,
widgets
widgets,
activity
) => {
let isCustomCompletionMail = false;
try {
@@ -841,6 +861,8 @@ export const signPdfFun = async (
}
let base64Sign = getSignature?.SignUrl;
let suffixbase64 = "";
if (base64Sign) {
//check https type signature (default signature exist) then convert in base64
const isUrl = base64Sign.includes("https");
if (isUrl) {
@@ -854,14 +876,16 @@ export const signPdfFun = async (
//change image width and height to 300/120 in png base64
const imagebase64 = await changeImageWH(base64Sign);
//remove suffiix of base64 (without type)
const suffixbase64 = imagebase64 && imagebase64.split(",").pop();
suffixbase64 = imagebase64 && imagebase64.split(",").pop();
}
const params = {
pdfFile: base64Url,
docId: documentId,
userId: signerObjectId,
isCustomCompletionMail: isCustomCompletionMail,
signature: suffixbase64
signature: suffixbase64,
activity: activity || "Signed"
};
const resSignPdf = await Parse.Cloud.run("signPdf", params);
if (resSignPdf) {
@@ -877,6 +901,11 @@ export const signPdfFun = async (
};
} else if (e?.message?.includes("password")) {
return { status: "error", message: "PFX file password is invalid." };
} else if (
e?.code === 119 ||
e?.message?.toLowerCase?.().includes("strict signing order")
) {
return { status: "error", message: e.message };
} else {
return { status: "error", message: "something went wrong." };
}
@@ -938,6 +967,7 @@ export const createDocument = async (
? { NotifyOnSignatures: Doc?.NotifyOnSignatures }
: {};
const Bcc = Doc?.Bcc?.length > 0 ? { Bcc: Doc?.Bcc } : {};
const Cc = Doc?.Cc?.length > 0 ? { Cc: Doc?.Cc } : {};
const RedirectUrl = Doc?.RedirectUrl
? { RedirectUrl: Doc?.RedirectUrl }
: {};
@@ -971,6 +1001,7 @@ export const createDocument = async (
AutomaticReminders: Doc?.AutomaticReminders || false,
RemindOnceInEvery: parseInt(Doc?.RemindOnceInEvery || 5),
IsEnableOTP: Doc?.IsEnableOTP || false,
SendInOrderStrict: Doc?.SendInOrderStrict || false,
IsTourEnabled: Doc?.IsTourEnabled || false,
AllowModifications: Doc?.AllowModifications || false,
TimeToCompleteDays: parseInt(Doc?.TimeToCompleteDays) || 15,
@@ -980,6 +1011,7 @@ export const createDocument = async (
...SenderName,
...SenderMail,
...Bcc,
...Cc,
...RedirectUrl,
...TemplateId,
...PenColors
@@ -991,20 +1023,24 @@ export const createDocument = async (
if (AutomaticReminders && reminderCount > 15) {
return { status: "error", id: "only-15-reminder-allowed" };
}
const url = `${localStorage.getItem("baseUrl")}classes/contracts_Document`;
const url = `${localStorage.getItem("baseUrl")}functions/createdocumentfromapp`;
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
try {
const res = await axios.post(url, data, {
const res = await axios.post(
url,
{ document: data },
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
}
});
}
);
if (res) {
const result = res.data;
const result = res.data.result;
return { status: "success", id: result.objectId, data: result };
}
@@ -1139,10 +1175,7 @@ export const onChangeInput = (
textWidgetHeight &&
!value
) {
return {
...position,
Height: textWidgetHeight
};
return { ...position, Height: textWidgetHeight };
} else {
return {
...position,
@@ -1198,10 +1231,7 @@ export const onChangeInput = (
textWidgetHeight &&
!value
) {
return {
...positionData,
Height: textWidgetHeight
};
return { ...positionData, Height: textWidgetHeight };
} else {
return {
...positionData,
@@ -1694,13 +1724,14 @@ export function onSaveImage(
defaultStampImg,
defaultStampType
) {
// let getIMGWH;
let widgetName;
const isSignOrInitials =
widgetsType && ["signature", "initials"].includes(widgetsType);
//get current page position
const getXYData = xyPosition[index].pos;
const updateXYData = getXYData.map((position) => {
if (position.key === signKey) {
widgetName = position?.options?.name;
return {
...position,
SignUrl: imgUrl || image?.src || defaultStampImg,
@@ -1720,14 +1751,17 @@ export function onSaveImage(
}
return obj;
});
// condition when user upload(stamp) then apply it all related to widgets
// condition when user apply auto sign feature for any type upload image (signature,initials,stamp,image)
if (isAutoSign) {
const updatedArray = updateXYposition.map((page) => ({
...page,
pos: page.pos.map(
(item) =>
// below condition to exclude apply all for image widget
item.type === widgetsType // && item.type !== "image"
// below condition is check if image widget name same then apply to auto sign other wise not
// and for other widget like signature,stamp,initials apply auto sign for all remaining widgets
item.type === widgetsType &&
(item.type !== "image" || item.options.name === widgetName)
? {
...item,
SignUrl: image?.src || defaultStampImg,
@@ -2076,7 +2110,7 @@ export const embedWidgetsToDoc = async (
// Size and spacing settings
const checkboxSize = fontSize - 1; // checkbox diameter
const checkboxTextGapFromLeft = fontSize + 3.4; // gap between box and its label
const verticalGap = fontSize + 3.4; // gap between two rows (vertical layout)
const verticalGap = fontSize + 5.5; // gap between two options (vertical layout)
let horizontalGap = 0; // will compute after drawing each label
if (position?.options?.values?.length > 0) {
position.options.values.forEach((item, ind) => {
@@ -2326,8 +2360,8 @@ export const embedWidgetsToDoc = async (
// Initial “cursor” positions (from your existing helpers)
let currentX = xPos(position) + 2;
let currentY = yPos(position) + 3;
// Vertical gap between two radiorows
const verticalGap = fontSize + 3;
// Vertical gap between two options
const verticalGap = fontSize + 5;
// Well compute horizontalGap on the fly—after drawing each label
// Initialize to zero (will be set after first option is placed)
let horizontalGap = 0;
@@ -2406,13 +2440,39 @@ export const embedWidgetsToDoc = async (
// 6. Set to readonly (if required)
radioGroup.enableReadOnly();
} else {
const widgetRotation = position?.options?.rotation || 0;
const isSwapped = [90, 270].includes(widgetRotation);
// Use visual (swapped) dimensions to compute the correct position
const signature = {
x: xPos(position),
y: yPos(position),
width: widgetWidth,
height: widgetHeight
width: isSwapped ? widgetHeight : widgetWidth,
height: isSwapped ? widgetWidth : widgetHeight
};
const imageOptions = getWidgetPosition(page, signature, 1, getSize);
if (widgetRotation) {
const pageRotation = page.getRotation().angle;
const combinedRotation = degrees(
(imageOptions.rotate?.angle || pageRotation) - widgetRotation
);
const visualWidth = imageOptions.width;
const visualHeight = imageOptions.height;
if (isSwapped) {
// Override with original (unswapped) dimensions for the actual image
imageOptions.width = visualHeight;
imageOptions.height = visualWidth;
}
imageOptions.rotate = combinedRotation;
// Adjust position to compensate for pdf-lib rotation pivot (bottom-left corner)
if (widgetRotation === 90) {
imageOptions.y += visualHeight;
} else if (widgetRotation === 180) {
imageOptions.x += imageOptions.width;
imageOptions.y += imageOptions.height;
} else if (widgetRotation === 270) {
imageOptions.x += visualWidth;
}
}
page.drawImage(img, imageOptions);
}
} catch (err) {
@@ -3268,19 +3328,52 @@ export const convertBase64ToFile = async (pdfName, pdfBase64, imgType) => {
console.log("error in convertbase64tofile", e);
}
};
export const onClickZoomIn = (zoomPercent, setScale, setZoomPercent) => {
const newPercent = zoomPercent + 10;
setZoomPercent(newPercent);
setScale(1 + newPercent / 100);
};
export const onClickZoomOut = (zoomPercent, setZoomPercent, setScale) => {
if (zoomPercent > 0) {
const newPercent = Math.max(0, zoomPercent - 10);
setZoomPercent(newPercent);
setScale(1 + newPercent / 100);
}
// Shared ref that holds a pending scroll restore job for button zoom.
// RenderPdf reads this in its scale-change useEffect (same pattern as pinch).
export const pendingButtonZoomScrollRef = { current: null };
/**
* Call this with the scroll container ref so the zoom utils can snapshot
* the current scroll position BEFORE setScale triggers a re-render.
*
* Anchors zoom to the visible center of the viewport, so the page content
* under your eyes stays in place rather than jumping to the top.
*/
const captureScrollForButtonZoom = (scrollContainerRef, scale, newScale) => {
const el = scrollContainerRef?.current;
if (!el) return;
const scaleRatio = newScale / scale;
// Anchor point = center of the visible scroll container
const midRelX = el.clientWidth / 2;
const midRelY = el.clientHeight / 2;
const docX = el.scrollLeft + midRelX;
const docY = el.scrollTop + midRelY;
const newScrollLeft = Math.max(0, docX * scaleRatio - midRelX);
const newScrollTop = Math.max(0, docY * scaleRatio - midRelY);
pendingButtonZoomScrollRef.current = { newScrollLeft, newScrollTop };
};
export const onClickZoomIn = (scale, setScale, scrollContainerRef) => {
// Find last step smaller than current scale
const nextScale = SCALE_STEPS.find((s) => s > scale);
if (!nextScale) return;
captureScrollForButtonZoom(scrollContainerRef, scale, nextScale);
setScale(nextScale);
};
export const onClickZoomOut = (scale, setScale, scrollContainerRef) => {
// Find last step smaller than current scale
const prevScale = [...SCALE_STEPS].reverse().find((s) => s < scale);
if (!prevScale || prevScale < 1.0) return;
captureScrollForButtonZoom(scrollContainerRef, scale, prevScale);
setScale(prevScale);
};
//function to use remove widgets from current page when user want to rotate page
export const handleRemoveWidgets = (
setSignerPos,
@@ -3527,37 +3620,504 @@ export const handleHeighlightWidget = (
return updateZindex;
};
/**
* FlattenPdf is used to remove existing widgets if present any and flatten pdf.
* FlattenPdf renders field values as static content and removes the interactive
* form layer. Signatures are stripped entirely. Non-widget annotations (links,
* comments, stamps) are preserved.
* @param {string | Uint8Array | ArrayBuffer} pdfFile - pdf file.
* @returns {Promise<Uint8Array>} flatPdf - pdf file in unit8arry
* @returns {Promise<Uint8Array>} flatPdf - PDF file in Uint8Array
*/
export const flattenPdf = async (pdfFile) => {
const pdfDoc = await PDFDocument.load(pdfFile);
// Get the form
const form = pdfDoc.getForm();
// fetch form fields
const fields = form.getFields();
// remove form all existing fields and their widgets
if (fields && fields?.length > 0) {
const pdfDoc = await PDFDocument.load(pdfFile, { ignoreEncryption: true });
let form;
try {
form = pdfDoc.getForm();
} catch {
// No form, nothing to flatten
return await pdfDoc.save({ useObjectStreams: false });
}
const pages = pdfDoc.getPages();
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const zapf = await pdfDoc.embedFont(StandardFonts.ZapfDingbats);
const fields = form.getFields();
for (const field of fields) {
while (field.acroField.getWidgets().length) {
field.acroField.removeWidget(0);
const type = field.constructor.name;
if (type === "PDFSignature") {
continue;
}
form.removeField(field);
}
} catch (err) {
console.log("err while removing field from pdf", err);
const widgets = _safeGetWidgets(field);
for (const widget of widgets) {
const rect = _getWidgetRect(widget, pdfDoc);
const page = _getWidgetPage(pdfDoc, pages, widget);
if (!rect || !page) continue;
_drawWidgetBox(page, rect);
if (type === "PDFTextField") {
_drawTextField(page, field, rect, helvetica);
} else if (type === "PDFCheckBox") {
_drawCheckBox(page, field, rect, zapf);
} else if (type === "PDFRadioGroup") {
_drawRadioGroup(page, field, widget, rect);
} else if (type === "PDFDropdown") {
_drawDropdown(page, field, rect, helvetica);
} else if (type === "PDFOptionList") {
_drawOptionList(page, field, rect, helvetica);
} else if (type === "PDFButton") {
// Push buttons are interactive controls, not meaningful data fields.
}
}
// 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 flatPdf = await pdfDoc.save({ useObjectStreams: false });
return flatPdf;
}
if (fields.length > 0) {
_removeWidgetAnnotations(pdfDoc);
}
return await pdfDoc.save({ useObjectStreams: false });
};
/* ---- flattenPdf private helpers ---- */
function _safeGetWidgets(field) {
try {
return field.acroField?.getWidgets?.() || [];
} catch {
return [];
}
}
function _getWidgetRect(widget, pdfDoc) {
try {
const r = widget.getRectangle?.();
if (
r &&
isFinite(r.x) &&
isFinite(r.y) &&
isFinite(r.width) &&
isFinite(r.height)
) {
return r;
}
} catch {
/* fall through to manual extraction */
}
try {
const rectArr = widget.dict?.lookup?.(PDFName.of("Rect"));
if (!rectArr || typeof rectArr.size !== "function" || rectArr.size() !== 4)
return null;
const x1 = _numberFromPdfObject(rectArr.get(0));
const y1 = _numberFromPdfObject(rectArr.get(1));
const x2 = _numberFromPdfObject(rectArr.get(2));
const y2 = _numberFromPdfObject(rectArr.get(3));
if ([x1, y1, x2, y2].some((v) => !isFinite(v))) return null;
return {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1)
};
} catch {
return null;
}
}
function _numberFromPdfObject(obj) {
if (!obj) return NaN;
if (typeof obj.asNumber === "function") return obj.asNumber();
if (typeof obj.numberValue === "function") return obj.numberValue();
return Number(obj?.value ?? obj);
}
function _getWidgetPage(pdfDoc, pages, widget) {
try {
const pRef = widget.P?.();
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
try {
const pRef = widget.dict?.get?.(PDFName.of("P"));
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
// Fallback: search page annots
try {
for (const page of pages) {
const annots = page.node.lookupMaybe(PDFName.of("Annots"), PDFArray);
if (!annots) continue;
for (let i = 0; i < annots.size(); i++) {
const ref = annots.get(i);
if (ref === widget.ref) return page;
}
}
} catch {
/* fall through */
}
return null;
}
function _drawWidgetBox(page, rect) {
try {
page.drawRectangle({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
borderWidth: 0.6,
borderColor: rgb(0.65, 0.65, 0.65)
});
} catch {
/* best effort */
}
}
function _drawTextField(page, field, rect, font) {
let text = "";
try {
text = field.getText?.() ?? "";
} catch {
text = "";
}
text = String(text ?? "");
if (!text) return;
let multiline = false;
try {
multiline = field.isMultiline?.() ?? false;
} catch {
/* ignore */
}
let comb = false;
try {
comb = field.isCombed?.() ?? false;
} catch {
/* ignore */
}
if (comb) {
_drawCombText(page, text, rect, font);
return;
}
if (multiline || text.includes("\n")) {
_drawMultilineText(page, text, rect, font);
return;
}
const fontSize = _fitSingleLineFontSize(text, rect, font);
const baselineY = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(text, {
x: rect.x + 2,
y: baselineY,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4)
});
}
function _drawMultilineText(page, text, rect, font) {
const lines = String(text).replace(/\r/g, "").split("\n");
const fontSize = Math.max(
8,
Math.min(11, rect.height / Math.max(lines.length + 0.5, 2))
);
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
lineHeight
});
y -= lineHeight;
}
}
function _drawCombText(page, text, rect, font) {
const chars = String(text).split("");
const count = Math.max(chars.length, 1);
const cellWidth = rect.width / count;
const fontSize = Math.max(8, Math.min(12, rect.height - 4));
chars.forEach((ch, i) => {
const textWidth = font.widthOfTextAtSize(ch, fontSize);
const x = rect.x + i * cellWidth + (cellWidth - textWidth) / 2;
const y = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(ch, {
x,
y,
size: fontSize,
font,
color: rgb(0, 0, 0)
});
if (i < count - 1) {
try {
page.drawLine({
start: { x: rect.x + (i + 1) * cellWidth, y: rect.y },
end: {
x: rect.x + (i + 1) * cellWidth,
y: rect.y + rect.height
},
thickness: 0.4,
color: rgb(0.75, 0.75, 0.75)
});
} catch {
/* best effort */
}
}
});
}
function _fitSingleLineFontSize(text, rect, font) {
let size = Math.min(12, rect.height - 4);
size = Math.max(size, 6);
while (size > 6) {
const width = font.widthOfTextAtSize(text, size);
if (width <= rect.width - 4) return size;
size -= 0.5;
}
return 6;
}
function _drawCheckBox(page, field, rect, zapf) {
let checked = false;
try {
checked = field.isChecked();
} catch {
checked = false;
}
if (!checked) return;
const size = Math.max(8, Math.min(rect.width, rect.height) - 4);
page.drawText("\u2714", {
x: rect.x + Math.max(1, (rect.width - size * 0.7) / 2),
y: rect.y + Math.max(1, (rect.height - size) / 2),
size,
font: zapf,
color: rgb(0, 0, 0)
});
}
function _drawRadioGroup(page, field, widget, rect) {
let selected = null;
try {
selected = field.getSelected();
} catch {
selected = null;
}
if (!selected) return;
let widgetOnValue = null;
try {
widgetOnValue = widget.getOnValue?.();
} catch {
/* ignore */
}
if (!widgetOnValue) {
try {
const ap = widget.dict?.lookupMaybe?.(PDFName.of("AP"), PDFDict);
const n = ap?.lookupMaybe?.(PDFName.of("N"), PDFDict);
if (n) {
const keys = n.keys();
for (const k of keys) {
const name = k?.decodeText?.() ?? k?.encodedName ?? String(k);
if (name !== "/Off" && name !== "Off") {
widgetOnValue = name.replace(/^\//, "");
break;
}
}
}
} catch {
/* ignore */
}
}
const selectedStr = String(selected).replace(/^\//, "");
const onStr = String(widgetOnValue ?? "").replace(/^\//, "");
if (!onStr || selectedStr !== onStr) return;
// Circle outline
try {
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: rect.width / 2 - 1,
yScale: rect.height / 2 - 1,
borderWidth: 0.8,
borderColor: rgb(0, 0, 0)
});
} catch {
/* best effort */
}
// Inner filled dot (drawn as a filled ellipse instead of a text glyph
// so we avoid WinAnsi encoding issues with bullet characters)
try {
const r = Math.min(rect.width, rect.height) / 4;
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: r,
yScale: r,
color: rgb(0, 0, 0)
});
} catch {
/* best effort */
}
}
function _drawDropdown(page, field, rect, font) {
let text = "";
try {
const selected = field.getSelected?.();
if (Array.isArray(selected)) {
text = selected.join(", ");
} else {
text = selected ?? "";
}
} catch {
text = "";
}
text = String(text ?? "");
if (!text) return;
const fontSize = _fitSingleLineFontSize(text, rect, font);
page.drawText(text, {
x: rect.x + 2,
y: rect.y + Math.max(2, (rect.height - fontSize) / 2),
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 12)
});
}
function _drawOptionList(page, field, rect, font) {
let selected = [];
try {
selected = field.getSelected?.() || [];
} catch {
selected = [];
}
if (!Array.isArray(selected)) {
selected = [selected].filter(Boolean);
}
if (!selected.length) return;
const lines = selected.map((v) => String(v));
const fontSize = Math.max(
8,
Math.min(11, rect.height / Math.max(lines.length + 0.5, 2))
);
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4)
});
y -= lineHeight;
}
}
/** Remove only Widget annotations; preserve links, stamps, comments, etc. */
function _removeWidgetAnnotations(pdfDoc) {
for (const page of pdfDoc.getPages()) {
try {
const annotationsRef = page.node.get(PDFName.of("Annots"));
if (!annotationsRef) continue;
const annotations = pdfDoc.context.lookup(annotationsRef);
if (!annotations || !annotations.asArray) continue;
const filtered = annotations.asArray().filter((annotRef) => {
try {
const annot = pdfDoc.context.lookup(annotRef);
const subtype = annot?.get(PDFName.of("Subtype"));
return subtype?.toString() !== "/Widget";
} catch {
return true;
}
});
if (filtered.length === 0) {
page.node.delete(PDFName.of("Annots"));
} else {
page.node.set(PDFName.of("Annots"), pdfDoc.context.obj(filtered));
}
} catch {
/* best effort */
}
}
try {
pdfDoc.catalog.delete(PDFName.of("AcroForm"));
} catch {
/* best effort */
}
}
export const mailTemplate = (param) => {
const appName = "OpenSign™";
const logo = `<div style='padding:10px'><img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' /></div>`;
@@ -3944,7 +4504,7 @@ export const sendEmailToSigners = async (
let senderPhone = pdfDetails?.[0]?.ExtUserPtr?.Phone;
let signerMail = signersdata.slice();
if (pdfDetails?.[0]?.SendinOrder && pdfDetails?.[0]?.SendinOrder === true) {
signerMail.splice(1);
signerMail = signerMail[0] ? [signerMail[0]] : [];
}
for (let i = 0; i < signerMail.length; i++) {
try {
@@ -4036,15 +4596,18 @@ export const sendEmailToSigners = async (
localExpireDate: localExpireDate,
signingUrl: signPdf
};
// Pick a role-appropriate default template (viewers get a "view"
// template instead of the "sign" template).
const defaultTemplate = mailTemplate(mailparam);
let params = {
extUserId: owner?.objectId,
recipient: signerMail[i].Email,
subject: replaceVar?.subject
? replaceVar?.subject
: mailTemplate(mailparam).subject,
: defaultTemplate.subject,
replyto: senderEmail,
from: from,
html: replaceVar?.body ? replaceVar?.body : mailTemplate(mailparam).body
html: replaceVar?.body ? replaceVar?.body : defaultTemplate.body
};
sendMail = await axios.post(url, params, { headers: headers });
+3
View File
@@ -3,6 +3,9 @@ export const templateCls = "contracts_Template";
export const documentCls = "contracts_Document";
export const themeColor = "#47a3ad";
export const iconColor = "#686968";
export const SCALE_STEPS = [
0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0
];
// Dynamic icon color function for better dark mode visibility
export const getThemeIconColor = () => {
const theme = document.documentElement.getAttribute("data-theme");
+100 -20
View File
@@ -1,32 +1,112 @@
import { createContext, useContext, useState, useCallback } from "react";
import { createContext, useContext, useRef, useCallback, useMemo } from "react";
const GuidelinesContext = createContext();
export const GuidelinesProvider = ({ children }) => {
const [guideline, setGuideline] = useState({
show: false,
x1: 0,
x2: 0,
y1: 0,
y2: 0
});
// Per-page refs: { [pageNum]: { top, bottom, left, right } }
// Each page mounts its own <Guidelines> and registers its DOM elements here.
// This avoids React state updates (and the resulting re-renders of the entire
// component tree) on every drag/resize pixel movement.
const guideRefs = useRef({});
// Track which page's guidelines are currently visible so we can hide them
const activePageRef = useRef(null);
const showGuidelines = useCallback((show, x, y, width, height) => {
if (show) {
setGuideline({
show: true,
x1: x,
x2: x + width - 1.5,
y1: y,
y2: y + height - 1
});
} else {
setGuideline((prev) => ({ ...prev, show: false }));
// Canvas-level refs: single set of guideline DOM elements that live in the
// main scroll container (not inside any page div). Used by DragGridLinesLayer
// when dragging widgets from the side panel so gridlines seamlessly span
// across page boundaries and perfectly track the drag preview.
const canvasGuideRefs = useRef({});
// Per-page guidelines used by Placeholder for on-page widget drag/resize
const showGuidelines = useCallback(
(show, x, y, width, height, targetPage) => {
if (!show) {
// Explicitly hiding: hide whatever is active
if (
activePageRef.current != null &&
guideRefs.current[activePageRef.current]
) {
const prev = guideRefs.current[activePageRef.current];
if (prev.top) prev.top.style.display = "none";
if (prev.bottom) prev.bottom.style.display = "none";
if (prev.left) prev.left.style.display = "none";
if (prev.right) prev.right.style.display = "none";
}
activePageRef.current = null;
return;
}
// Check if target page's refs are available before hiding old ones.
// This prevents guidelines from disappearing when the target page
// can't be resolved (e.g. cursor between pages during drag-from-panel).
const refs = guideRefs.current[targetPage];
if (!refs || !refs.top || !refs.bottom || !refs.left || !refs.right)
return;
// Hide the previously active page's guidelines when switching pages
if (
activePageRef.current != null &&
activePageRef.current !== targetPage &&
guideRefs.current[activePageRef.current]
) {
const prev = guideRefs.current[activePageRef.current];
if (prev.top) prev.top.style.display = "none";
if (prev.bottom) prev.bottom.style.display = "none";
if (prev.left) prev.left.style.display = "none";
if (prev.right) prev.right.style.display = "none";
}
refs.top.style.display = "";
refs.bottom.style.display = "";
refs.left.style.display = "";
refs.right.style.display = "";
refs.top.style.top = `${y}px`;
refs.bottom.style.top = `${y + height - 1}px`;
refs.left.style.left = `${x}px`;
refs.right.style.left = `${x + width - 1.5}px`;
activePageRef.current = targetPage;
},
[]
);
// Canvas-level guidelines used by DragGridLinesLayer for panel drags.
// These live outside page divs so they seamlessly span page boundaries.
const showCanvasGuidelines = useCallback((show, x, y, width, height) => {
const refs = canvasGuideRefs.current;
if (!refs.top || !refs.bottom || !refs.left || !refs.right) return;
if (!show) {
refs.top.style.display = "none";
refs.bottom.style.display = "none";
refs.left.style.display = "none";
refs.right.style.display = "none";
return;
}
refs.top.style.display = "";
refs.bottom.style.display = "";
refs.left.style.display = "";
refs.right.style.display = "";
refs.top.style.top = `${y}px`;
refs.bottom.style.top = `${y + height - 1}px`;
refs.left.style.left = `${x}px`;
refs.right.style.left = `${x + width - 1.5}px`;
}, []);
// Memoize the context value so it never changes, preventing consumer re-renders
const value = useMemo(
() => ({
guideRefs,
showGuidelines,
canvasGuideRefs,
showCanvasGuidelines
}),
[showGuidelines, showCanvasGuidelines]
);
return (
<GuidelinesContext.Provider value={{ guideline, showGuidelines }}>
<GuidelinesContext.Provider value={value}>
{children}
</GuidelinesContext.Provider>
);
@@ -0,0 +1,15 @@
import { createContext, useContext, useRef } from "react";
const ScrollContext = createContext(null);
export const ScrollProvider = ({ children }) => {
const scrollRef = useRef(null);
return (
<ScrollContext.Provider value={{ scrollRef }}>
{children}
</ScrollContext.Provider>
);
};
export const useScroll = () => useContext(ScrollContext);
+157 -84
View File
@@ -1,110 +1,183 @@
import { useEffect, useRef } from "react";
export default function usePdfPinchZoom(ref, scale, setScale, setZoomPercent) {
const initialDistanceRef = useRef(null);
const startScaleRef = useRef(scale);
const currentScaleRef = useRef(scale);
const pinchScaleRef = useRef(null); // stores relative scale during a gesture
const frameRef = useRef(null);
const MIN_SCALE = 1.0;
const MAX_SCALE = 4.0;
// keep a reference to the latest scale value
useEffect(() => {
const element = ref.current;
currentScaleRef.current = scale;
// once React commits the new scale after a pinch we clear the transform
if (element && !pinchScaleRef.current) {
element.style.transform = "";
// Calculate straight-line distance between two touch points
function getDistance(t1, t2) {
const dx = t1.clientX - t2.clientX;
const dy = t1.clientY - t2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
}, [scale, ref]);
// Calculate the midpoint (center) between two touch points
function getMidpoint(t1, t2) {
return {
x: (t1.clientX + t2.clientX) / 2,
y: (t1.clientY + t2.clientY) / 2
};
}
// Clamp a value between min and max bounds
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
/**
* usePdfPinchZoom
*
* Handles two-finger pinch-to-zoom on the PDF scroll container.
*
* Key design decision flicker-free zooming:
* During the gesture, we mutate the transform div's style DIRECTLY via DOM
* (zero React re-renders). Only on touchEnd do we call setScale() once,
* committing the final value to React state. This eliminates the flicker
* that occurs when setScale() fires on every touchmove frame and forces
* the entire PDF tree (Document, Pages, Widgets) to re-render mid-gesture.
*
* @param {React.RefObject} containerRef - Ref to the outer scrollable div (scrollRef in RenderPdf)
* @param {number} scale - Current scale value from React state
* @param {function} setScale - React state setter for scale
* @param {function} setZoomPercent - React state setter for zoom % display
*/
export default function usePdfPinchZoom(
containerRef,
scale,
setScale,
setZoomPercent
) {
// Keep a mutable ref in sync with scale prop so event handlers always
// read the latest value without being re-registered on every scale change
const scaleRef = useRef(scale);
// Stores the pending requestAnimationFrame id (null = none pending)
const rafRef = useRef(null);
// Sync scaleRef whenever the scale prop changes from outside (e.g. zoom buttons)
useEffect(() => {
scaleRef.current = scale;
}, [scale]);
useEffect(() => {
const element = ref.current;
if (!element) return;
const el = containerRef?.current;
if (!el) return;
const getDistance = (touches) => {
const [touch1, touch2] = touches;
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.hypot(dx, dy);
// Locate the inner content div that has `transform: scale(...)` applied.
// We use an explicit data attribute (data-pdf-transform-target) set on that
// div in RenderPdf.jsx — more reliable than sniffing [style*='transform']
// which could accidentally match other elements.
const getTransformTarget = () =>
el.querySelector("[data-pdf-transform-target]");
// Mutable pinch state — stored as a plain object (not refs/state) because
// we only need it inside event handlers; no React re-render required.
const pinch = {
active: false, // Whether a two-finger gesture is in progress
startDist: 0, // Finger distance at gesture start
startScale: 1, // Scale value at gesture start
startMid: { x: 0, y: 0 }, // Midpoint between fingers at gesture start (viewport coords)
startScrollLeft: 0, // Container scrollLeft at gesture start
startScrollTop: 0, // Container scrollTop at gesture start
currentScale: 1 // Latest scale reached during the gesture
};
const handleTouchStart = (e) => {
if (e.touches.length === 2) {
initialDistanceRef.current = getDistance(e.touches);
startScaleRef.current = currentScaleRef.current;
element.style.transformOrigin = "0 0";
// touchstart: record initial finger positions and scroll state
function onTouchStart(e) {
// Only activate for exactly two fingers
if (e.touches.length !== 2) return;
const t1 = e.touches[0];
const t2 = e.touches[1];
pinch.active = true;
pinch.startDist = getDistance(t1, t2);
pinch.startScale = scaleRef.current;
pinch.currentScale = scaleRef.current;
pinch.startMid = getMidpoint(t1, t2);
// Snapshot scroll position so we can recompute it proportionally as scale changes
pinch.startScrollLeft = el.scrollLeft;
pinch.startScrollTop = el.scrollTop;
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2 && initialDistanceRef.current) {
// touchmove: update the DOM transform directly — NO React state updates here
function onTouchMove(e) {
if (!pinch.active || e.touches.length !== 2) return;
// Prevent browser's native scroll/zoom from interfering
e.preventDefault();
const newDistance = getDistance(e.touches);
const pinchScale = newDistance / initialDistanceRef.current;
const newScale = Math.max(1, startScaleRef.current * pinchScale);
// store relative scale so it can be applied on top of current width
pinchScaleRef.current = newScale / startScaleRef.current;
if (!frameRef.current) {
frameRef.current = requestAnimationFrame(() => {
element.style.transform = `scale(${pinchScaleRef.current})`;
frameRef.current = null;
const t1 = e.touches[0];
const t2 = e.touches[1];
// Derive new scale from how much the finger distance has changed
const dist = getDistance(t1, t2);
const ratio = dist / pinch.startDist;
const newScale = clamp(pinch.startScale * ratio, MIN_SCALE, MAX_SCALE);
// Skip tiny changes to avoid unnecessary DOM writes and rAF scheduling
if (Math.abs(newScale - pinch.currentScale) < 0.008) return;
pinch.currentScale = newScale;
// Cancel any previously queued frame before scheduling a new one
// (ensures we only process the most recent touch position per frame)
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
const transformTarget = getTransformTarget();
if (!transformTarget) return;
// ✅ KEY — mutate the DOM directly, bypassing React entirely.
// This is what prevents flickering: no state update = no re-render
// of <Document>, <Page> components, or widgets during the gesture.
transformTarget.style.transform = `scale(${newScale})`;
// Recompute scroll so the pinch midpoint stays anchored on screen.
// Formula: new scroll = (old scroll × scaleFactor) + (midpoint offset × (scaleFactor - 1))
const rect = el.getBoundingClientRect();
const midRelX = pinch.startMid.x - rect.left;
const midRelY = pinch.startMid.y - rect.top;
const scaleFactor = newScale / pinch.startScale;
el.scrollLeft =
pinch.startScrollLeft * scaleFactor + midRelX * (scaleFactor - 1);
el.scrollTop =
pinch.startScrollTop * scaleFactor + midRelY * (scaleFactor - 1);
});
}
}
};
const handleTouchEnd = () => {
if (pinchScaleRef.current) {
let relativeScale = pinchScaleRef.current;
const inlineTransform = element.style.transform;
if (inlineTransform && inlineTransform.startsWith("scale(")) {
const value = parseFloat(inlineTransform.slice(6, -1));
if (!isNaN(value)) {
relativeScale = value;
}
}
const finalScale = Math.max(1, startScaleRef.current * relativeScale);
const percent = parseFloat(((finalScale - 1) * 100).toFixed(2));
const unchanged =
Math.abs(finalScale - currentScaleRef.current) < 0.001;
// touchend / touchcancel: commit the final scale to React state exactly once
function onTouchEnd(e) {
// If a second finger is still down, the gesture is still active — wait
if (e.touches.length >= 2) return;
if (!pinch.active) return;
pinchScaleRef.current = null;
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
pinch.active = false;
if (unchanged) {
// if scale didn't actually change, reset transform right away
element.style.transform = "";
} else {
// keep transform applied until state update reflects the new scale
element.style.transform = `scale(${relativeScale})`;
}
const finalScale = pinch.currentScale;
// ✅ Single setScale call per gesture — triggers exactly one React re-render
// to sync props with what the DOM already shows visually. No visible change,
// no flicker, but state is now correctly updated for the rest of the app.
if (finalScale !== scaleRef.current) {
setScale(finalScale);
setZoomPercent(percent);
// ensure refs reflect the latest value before next gesture starts
currentScaleRef.current = finalScale;
startScaleRef.current = finalScale;
if (setZoomPercent) {
// Display value e.g. 150 for 1.5x zoom
setZoomPercent(Math.round(finalScale * 100));
}
}
}
initialDistanceRef.current = null;
};
element.addEventListener("touchstart", handleTouchStart, {
passive: false
});
element.addEventListener("touchmove", handleTouchMove, { passive: false });
element.addEventListener("touchend", handleTouchEnd);
element.addEventListener("touchcancel", handleTouchEnd);
// Attach all listeners. touchmove must be non-passive so e.preventDefault() works.
el.addEventListener("touchstart", onTouchStart, { passive: false });
el.addEventListener("touchmove", onTouchMove, { passive: false });
el.addEventListener("touchend", onTouchEnd, { passive: true });
el.addEventListener("touchcancel", onTouchEnd, { passive: true });
// Cleanup: remove listeners and cancel any in-flight animation frame on unmount
return () => {
element.removeEventListener("touchstart", handleTouchStart);
element.removeEventListener("touchmove", handleTouchMove);
element.removeEventListener("touchend", handleTouchEnd);
element.removeEventListener("touchcancel", handleTouchEnd);
el.removeEventListener("touchstart", onTouchStart);
el.removeEventListener("touchmove", onTouchMove);
el.removeEventListener("touchend", onTouchEnd);
el.removeEventListener("touchcancel", onTouchEnd);
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [ref, setScale, setZoomPercent]);
}, [containerRef, setScale, setZoomPercent]);
}
+3
View File
@@ -9,6 +9,7 @@ import Parse from "parse";
import "./polyfills";
import { serverUrl_fn } from "./constant/appinfo";
import "./i18n";
import { ScrollProvider } from "./context/ScrollPdfContext";
const appId =
import.meta.env.VITE_APPID || process.env.REACT_APP_APPID || "opensign";
@@ -29,7 +30,9 @@ if (savedTheme === "dark") {
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<ScrollProvider>
<App />
</ScrollProvider>
</Provider>
);
+4 -2
View File
@@ -15,7 +15,8 @@ export const formJson = {
redirectRoute: "placeHolderSign",
Cls: documentCls,
signers: true,
bcc: true
bcc: true,
cc: true
},
//json form for template
template: {
@@ -23,6 +24,7 @@ export const formJson = {
redirectRoute: "template",
msgVar: "Template",
Cls: templateCls,
bcc: true
bcc: true,
cc: true
}
};
+127 -27
View File
@@ -8,7 +8,6 @@ import SignersInput from "../components/shared/fields/SignersInput";
import PageNotFound from "./PageNotFound";
import { SaveFileSize } from "../constant/saveFileSize";
import {
flattenPdf,
generatePdfName,
generateTitleFromFilename,
getSecureUrl,
@@ -33,6 +32,10 @@ import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../redux/reducers/userReducer";
import { withSessionValidation } from "../utils";
import {
clearAcroFields,
isPdfPasswordProtected
} from "../utils/acroFieldExtractor";
// `Form` render all type of Form on this basis of their provided in path
function Form() {
@@ -62,6 +65,7 @@ const Forms = (props) => {
Note: "",
TimeToCompleteDays: 15,
SendinOrder: "false",
SendInOrderStrict: "false",
password: "",
file: "",
remindOnceInEvery: 5,
@@ -70,14 +74,16 @@ const Forms = (props) => {
IsTourEnabled: "false",
NotifyOnSignatures: "",
Bcc: [],
Cc: [],
RedirectUrl: "",
AllowModifications: false,
AllowModifications: false
});
const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]);
const [fileload, setfileload] = useState(false);
const [percentage, setpercentage] = useState(0);
const [isReset, setIsReset] = useState(false);
const [isInitializing, setIsInitializing] = useState(true);
const [isAlert, setIsAlert] = useState({ type: "success", message: "" });
const [isSubmit, setIsSubmit] = useState(false);
const [isPassword, setIsPassword] = useState(false);
@@ -85,6 +91,7 @@ const Forms = (props) => {
const [isCorrectPass, setIsCorrectPass] = useState(true);
const [isAdvanceOpt, setIsAdvanceOpt] = useState(false);
const [bcc, setBcc] = useState([]);
const [cc, setCc] = useState([]);
const pensList = ["blue", "red", "black"];
const [selectedColors, setSelectedColors] = useState(pensList);
@@ -117,12 +124,16 @@ const Forms = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initializeValues = async () => {
try {
setFormData((obj) => ({
...obj,
NotifyOnSignatures: true,
SendinOrder: sendinorder,
IsTourEnabled: istourenabled
IsTourEnabled: istourenabled,
}));
} finally {
setIsInitializing(false);
}
};
// `removeFile` is used to reset progress, percentage and remove file if exists
@@ -160,8 +171,9 @@ const Forms = (props) => {
if (file.type === "application/pdf") {
try {
const buffer = await getFileAsArrayBuffer(file);
const flat = await flattenPdf(buffer);
pdfBuffers.push(flat);
await isPdfPasswordProtected(buffer);
const newBuffer = await clearAcroFields(buffer); // best effort cleanup to prevent stale data
pdfBuffers.push(newBuffer);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
@@ -170,13 +182,11 @@ const Forms = (props) => {
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
pdfBuffers.push(flatPdf);
const newBuffer = await clearAcroFields(res); // best effort cleanup to prevent stale data
pdfBuffers.push(newBuffer);
} catch (err) {
removeFile(e);
if (err?.response?.status === 401) {
// setIsPassword(true);
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
@@ -187,9 +197,8 @@ const Forms = (props) => {
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
pdfBuffers.push(flatPdf);
const newBuffer = await clearAcroFields(res); // best effort cleanup to prevent stale data
pdfBuffers.push(newBuffer);
} catch (err) {
console.error(
"Incorrect password or decryption failed",
@@ -204,26 +213,30 @@ const Forms = (props) => {
alert(
t("incorrect-password-for-file", { file: file.name })
);
return;
}
} else {
console.error("password not provided");
console.error("Password not provided");
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
setIsDecrypting(false);
setfileload(false);
removeFile(e);
return;
}
} else {
console.log("Error uploading file: ", err?.response);
console.error("File upload error: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
removeFile(e);
return;
}
}
} else {
console.log("err ", err);
console.error("File upload error: ", err);
removeFile(e);
return;
}
}
} else if (file.type.includes("image/")) {
@@ -280,7 +293,7 @@ const Forms = (props) => {
} catch (err) {
setfileload(false);
removeFile(e);
console.log("err in docx to pdf ", err);
console.error("Docx to PDF conversion error: ", err);
const error =
t("docx-error");
if (err?.code === 209) {
@@ -345,7 +358,7 @@ const Forms = (props) => {
setSelectedFiles([]);
}
} catch (error) {
console.error("error ", error);
console.error("Document save form error: ", error);
if (error?.code === 209) {
dispatch(sessionStatus(false));
} else {
@@ -420,6 +433,11 @@ const Forms = (props) => {
return;
}
object.set("SendinOrder", isChecked);
// Strict-order is only meaningful when SendinOrder is true.
object.set(
"SendInOrderStrict",
isChecked && formData.SendInOrderStrict === "true"
);
object.set("AutomaticReminders", AutomaticReminders);
object.set("RemindOnceInEvery", remindOnceInEvery);
object.set("IsTourEnabled", isTourEnabled);
@@ -455,6 +473,15 @@ const Forms = (props) => {
}));
object.set("Bcc", Bcc);
}
if (cc && cc.length > 0) {
const Cc = cc.map((x) => ({
__type: "Pointer",
className: "contracts_Contactbook",
objectId: x.objectId
}));
object.set("Cc", Cc);
}
const ExtCls = JSON.parse(localStorage.getItem("Extand_Class"));
object.set("ExtUserPtr", {
__type: "Pointer",
@@ -475,6 +502,7 @@ const Forms = (props) => {
if (res) {
setSigners([]);
setBcc([]);
setCc([]);
setSelectedColors(pensList);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
@@ -506,7 +534,7 @@ const Forms = (props) => {
navigate(`/${props?.redirectRoute}/${res.id}`);
}
} catch (err) {
console.log("err ", err);
console.error("Document save form error: ", err);
if (err?.code === 209) {
dispatch(sessionStatus(false));
} else if (err.message === "only 15 reminder allowed") {
@@ -553,11 +581,22 @@ const Forms = (props) => {
setBcc(trimEmail);
}
};
const handleCc = (data) => {
if (data && data.length > 0) {
const trimEmail = data.map((item) => ({
objectId: item?.value,
Name: item?.label,
Email: item?.email
}));
setCc(trimEmail);
}
};
const handleReset = () => {
setIsReset(true);
setSigners([]);
setBcc([]);
setCc([]);
setSelectedColors(pensList);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
@@ -604,8 +643,13 @@ const Forms = (props) => {
const pdfFile = await decryptPdf(formData?.file, formData?.password);
setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(name, [...flatPdf], "application/pdf");
const newArraybuffer = await clearAcroFields(res); // best effort cleanup to prevent stale data
const pdfToSave = new Uint8Array(newArraybuffer);
const parseFile = new Parse.File(
name,
[...pdfToSave],
"application/pdf"
);
await parseFile.save({
progress: (progressValue, loaded, total) => {
if (progressValue !== null) {
@@ -648,7 +692,7 @@ const Forms = (props) => {
setIsPassword(true);
setIsCorrectPass(false);
} else {
console.log("Error uploading file: ", err?.response);
console.error("File upload error: ", err?.response);
setFormData((prev) => ({ ...prev, password: "" }));
setIsDecrypting(false);
if (inputFileRef.current) {
@@ -696,10 +740,10 @@ const Forms = (props) => {
};
return (
<div
className={`${isSubmit ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
className={`${isSubmit || isInitializing ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
>
{isAlert?.message && <Alert type={isAlert.type}>{isAlert.message}</Alert>}
{isSubmit ? (
{isSubmit || isInitializing ? (
<div className="flex flex-col justify-center items-center h-[100vh]">
<Loader />
</div>
@@ -857,7 +901,7 @@ const Forms = (props) => {
label={t("signers")}
onChange={handleSigners}
isReset={isReset}
helptextZindex={50}
zindex={50}
isAddYourSelfCheckbox
required
/>
@@ -887,7 +931,7 @@ const Forms = (props) => {
/>
) : (
<div className="flex flex-col md:flex-row w-full mt-4 md:mt-10 gap-3">
<div className="card bg-base-100 rounded-box flex-grow p-3 ">
<div className="card bg-base-100 rounded-box flex-grow-0 w-full p-3">
{props.title !== "New Template" ? (
<SelectFolder
onSuccess={handleFolder}
@@ -958,6 +1002,27 @@ const Forms = (props) => {
<div className="text-center">{t("no")}</div>
</div>
</div>
{formData.SendinOrder === "true" && (
<div className="flex items-center gap-2 ml-2 mt-1 mb-1">
<input
type="checkbox"
className="op-checkbox op-checkbox-xs"
name="SendInOrderStrict"
checked={formData.SendInOrderStrict === "true"}
onChange={(e) =>
setFormData({
...formData,
SendInOrderStrict: e.target.checked
? "true"
: "false"
})
}
/>
<span title={t("strict-order-help")}>
{t("strict-order")}
</span>
</div>
)}
</div>
</>
)}
@@ -1059,6 +1124,29 @@ const Forms = (props) => {
<div className="text-center">{t("no")}</div>
</div>
</div>
{formData.SendinOrder === "true" && (
<div className="flex items-center gap-2 ml-2 mt-1 mb-1">
<input
type="checkbox"
className="op-checkbox op-checkbox-xs"
name="SendInOrderStrict"
checked={
formData.SendInOrderStrict === "true"
}
onChange={(e) =>
setFormData({
...formData,
SendInOrderStrict: e.target.checked
? "true"
: "false"
})
}
/>
<span title={t("strict-order-help")}>
{t("strict-order")}
</span>
</div>
)}
</div>
</>
)}
@@ -1093,12 +1181,24 @@ const Forms = (props) => {
initialData={bcc}
onChange={handleBcc}
isReset={isReset}
helptextZindex={50}
zindex={50}
helpText={t("bcc-help")}
isCaptureAllData
isAddYourSelfCheckbox
/>
)}
{props.cc && (
<SignersInput
label={t("Cc")}
initialData={cc}
onChange={handleCc}
isReset={isReset}
zindex={50}
helpText={t("cc-help")}
isCaptureAllData
isAddYourSelfCheckbox
/>
)}
</>
)}
</div>
@@ -1114,7 +1214,7 @@ const Forms = (props) => {
></div>
)}
{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-0 w-full">
{formData?.autoreminder === true &&
props.title !== "New Template" && (
<div className="text-xs mt-2">
+76 -35
View File
@@ -49,7 +49,7 @@ import {
widgetDataValue,
getOriginalWH,
handleCheckResponse,
convertJpegToPng
convertJpegToPng,
} from "../constant/Utils";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
@@ -72,12 +72,14 @@ import TextFontSetting from "../components/pdf/TextFontSetting";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import * as utils from "../utils";
import { useWindowSize } from "../hook/useWindowSize";
import { useScroll } from "../context/ScrollPdfContext";
function PdfRequestFiles(
) {
const { t } = useTranslation();
const dispatch = useDispatch();
const windowSize = useWindowSize();
const { scrollRef } = useScroll();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const isShowModal = useSelector((state) => state.widget.isShowModal);
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
@@ -228,6 +230,7 @@ function PdfRequestFiles(
}
}, [redirectTimeLeft, isredirectCanceled, redirectUrl]);
const fetchTenantDetails = async (contactId) => {
const user = JSON.parse(
localStorage.getItem(
@@ -278,6 +281,7 @@ function PdfRequestFiles(
const tenantSignTypes = await fetchTenantDetails(contactId);
// `currUserId` will be contactId or extUserId
let currUserId;
//getting document details
const documentData = await contractDocument(docId);
// Filter out 'prefill' roles from the Placeholder array
@@ -384,8 +388,10 @@ function PdfRequestFiles(
);
if (checkCurrentUser) {
const widgetId = [];
for (let placeholder of checkCurrentUser.placeHolder) {
for (let item of placeholder.pos) {
// Viewers and approvers may not have any placeholders/widgets
// attached, so guard against an undefined placeHolder array.
for (let placeholder of checkCurrentUser.placeHolder || []) {
for (let item of placeholder.pos || []) {
widgetId.push(item.key);
}
}
@@ -395,6 +401,7 @@ function PdfRequestFiles(
}
}
}
// Approved entries also count toward audit trail completion summary.
const audittrailData = documentData?.[0]?.AuditTrail?.filter(
(data) => data.Activity === "Signed"
);
@@ -402,7 +409,8 @@ function PdfRequestFiles(
documentData?.[0]?.AuditTrail?.some(
(data) =>
data?.UserPtr?.objectId === currUserId &&
data.Activity === "Signed"
(data.Activity === "Signed"
)
) || false;
if (checkAlreadySign) {
setAlreadySign(true);
@@ -453,7 +461,9 @@ function PdfRequestFiles(
const getSignerData = documentData[0].Signers.find(
(data) => data.objectId === placeholder?.signerObjId
);
placeholdersOrSigners.push(getSignerData);
placeholdersOrSigners.push({
...getSignerData
});
} else {
placeholdersOrSigners.push(placeholder);
}
@@ -696,6 +706,17 @@ function PdfRequestFiles(
const documentData = await contractDocument(docId);
if (documentData && documentData.length > 0) {
const url = documentData[0]?.SignedUrl || documentData[0]?.URL;
const isDeclined = documentData?.[0]?.IsDeclined || false;
if (isDeclined) {
setIsDecline({
isDeclined,
currnt: "another",
email: documentData?.[0]?.DeclineBy?.email || "",
reason: documentData?.[0]?.DeclineReason || ""
});
setIsUiLoading(false);
return;
}
//convert document url in array buffer format to use embed widgets in pdf using pdf-lib
const arrayBuffer = await convertPdfArrayBuffer(url);
if (arrayBuffer === "Error") {
@@ -760,6 +781,7 @@ function PdfRequestFiles(
contactId,
objectId,
widgets,
"Signed"
);
if (resSign && resSign.status === "success") {
dispatch(setTypedSignFont("Fasthand"));
@@ -784,7 +806,9 @@ function PdfRequestFiles(
updatedDoc.updatedPdfDetails?.[0]?.Placeholders?.filter(
(x) => x.Role !== "prefill"
);
const newIndex = index + 1;
// Skip viewer placeholders when computing the next signer
// to notify (viewers do not gate sequential signing).
let newIndex = index + 1;
const usermail = {
Email: removePrefill[newIndex]?.email || ""
};
@@ -982,14 +1006,15 @@ function PdfRequestFiles(
setIsDragging(true);
};
//function for set and update x and y postion after drag and drop signature tab
const handleStop = (event, dragElement, signerId, key) => {
const handleStop = (event, dragElement, signerId, key, widgetPageNumber) => {
if (!isResize && isDragging) {
const effectivePageNumber = widgetPageNumber || pageNumber;
let updateSignPos = [...signerPos];
const signerObjId = signerId ? signerId : uniqueId;
const keyValue = key ? key : dragKey;
const containerScale = getContainerScale(
pdfOriginalWH,
pageNumber,
effectivePageNumber,
containerWH
);
if (keyValue >= 0) {
@@ -1005,7 +1030,7 @@ function PdfRequestFiles(
const getPlaceHolder = filterSignerPos[0]?.placeHolder;
//get position of current pagenumber
const getPageNumer = getPlaceHolder?.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === effectivePageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
@@ -1014,15 +1039,15 @@ function PdfRequestFiles(
if (url.key === keyValue) {
return {
...url,
xPosition: dragElement.x / (containerScale * scale),
yPosition: dragElement.y / (containerScale * scale)
xPosition: dragElement.x / containerScale,
yPosition: dragElement.y / containerScale
};
}
return url;
});
//update new position of current page number
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === effectivePageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -1048,11 +1073,12 @@ function PdfRequestFiles(
setIsTextSetting(value);
};
const handleSaveFontSize = () => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
const filterSignerPos = signerPos?.filter((data) => data.Id === uniqueId);
if (filterSignerPos) {
const placehoder = filterSignerPos[0]?.placeHolder;
const getPageNumer = placehoder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
@@ -1075,7 +1101,7 @@ function PdfRequestFiles(
//update new position of current page number
const newUpdateSignPos = placehoder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === widgetPageNumber) {
return { ...obj, pos: updateSignPos };
}
return obj;
@@ -1399,10 +1425,10 @@ function PdfRequestFiles(
};
const clickOnZoomIn = () => {
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
onClickZoomIn(scale, setScale, scrollRef);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
onClickZoomOut(scale, setScale, scrollRef);
};
const handleDownloadBtn = async () => {
const url = pdfDetails?.[0]?.SignedUrl || pdfDetails?.[0]?.URL;
@@ -1419,7 +1445,8 @@ function PdfRequestFiles(
await fetchUrl(url, docName);
};
const handleDeclineMssg = () => {
const user = pdfDetails[0]?.DeclineBy?.email;
const user = pdfDetails[0]?.DeclineBy?.email || isDecline?.email;
const reason = pdfDetails[0]?.DeclineReason || isDecline?.reason;
return (
<div>
{t("decline-alert-3")}
@@ -1429,8 +1456,7 @@ function PdfRequestFiles(
</div>
<div className="mt-2">
{" "}
<span className="font-medium">{t("reason")}</span> :{" "}
{pdfDetails[0]?.DeclineReason}{" "}
<span className="font-medium">{t("reason")}</span> : {reason}{" "}
</div>
</div>
);
@@ -1528,16 +1554,20 @@ function PdfRequestFiles(
const widgetValue = widgetDataValue(dragTypeValue, parseUser);
//adding and updating drop position in array when user drop signature button in div
if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divWidth = divRef.current.getBoundingClientRect().width;
const divHeight = divRef.current.getBoundingClientRect().height;
// Use the current page container (id="container") so that the
// height reflects one page, not the entire multi-page document.
const containerEl =
document.getElementById("container") || divRef.current;
const divWidth = containerEl.getBoundingClientRect().width;
const divHeight = containerEl.getBoundingClientRect().height;
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
//onclick put placeholder center on pdf
xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2,
yPosition:
(divHeight / 2 - widgetHeight / 2) / (containerScale * scale),
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
key: key,
@@ -1646,18 +1676,19 @@ function PdfRequestFiles(
setFontSize(12);
setFontColor("black");
}
setCurrWidgetsDetails(dropObj);
setCurrWidgetsDetails({ ...dropObj, pageNumber: pageNumber });
}
};
//function for delete signature block
const handleDeleteWidget = (key, Id) => {
const handleDeleteWidget = (key, Id, widgetPageNumber) => {
const effectivePageNumber = widgetPageNumber || pageNumber;
const updateData = [];
const filterSignerPos = signerPos?.filter((data) => data.Id === Id);
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === effectivePageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos.filter(
@@ -1667,7 +1698,7 @@ function PdfRequestFiles(
if (getXYdata.length > 0) {
updateData.push(getXYdata);
const newUpdatePos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === effectivePageNumber) {
return { ...obj, pos: updateData[0] };
}
return obj;
@@ -1682,7 +1713,7 @@ function PdfRequestFiles(
setSignerPos(newUpdateSigner);
} else {
const getRemainPage = filterSignerPos[0]?.placeHolder?.filter(
(data) => data.pageNumber !== pageNumber
(data) => data.pageNumber !== effectivePageNumber
);
//condition to check placeholder length is greater than 1 do not need to remove whole placeholder
//array only resove particular widgets
@@ -1785,7 +1816,8 @@ function PdfRequestFiles(
!isExpired &&
!alreadySign &&
!isCompleted?.isCertificate &&
!isDecline?.isDeclined && (
!isDecline?.isDeclined &&
(
<AgreementSign
setIsAgree={setIsAgree}
showFirstWidget={showFirstWidget}
@@ -1935,7 +1967,7 @@ function PdfRequestFiles(
xyPosition={signerPos}
setXyPosition={setSignerPos}
allPages={allPages}
pageNumber={pageNumber}
pageNumber={currWidgetsDetails?.pageNumber || pageNumber}
signKey={currWidgetsDetails?.key}
Id={uniqueId}
widgetType={currWidgetsDetails?.type}
@@ -2085,6 +2117,7 @@ function PdfRequestFiles(
<RenderPdf
setIsPageCopy={setIsPageCopy}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
pdfOriginalWH={pdfOriginalWH}
pdfNewWidth={pdfNewWidth}
pdfDetails={pdfDetails}
@@ -2123,6 +2156,7 @@ function PdfRequestFiles(
isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
closeWidgetTour={closeTour}
/>
)}
</div>
@@ -2156,7 +2190,9 @@ function PdfRequestFiles(
</>
)}
{unsignedSigners.length > 0 && (
{unsignedSigners.length > 0 &&
(() => {
return (
<>
<div className="mx-2 pr-2 pt-2 pb-1 text-[15px] text-base-content font-semibold border-b-[1px] border-base-300">
<span>
@@ -2166,7 +2202,10 @@ function PdfRequestFiles(
</sup>
</span>
</div>
<div data-tut="reactourFirst" className="mt-[5px]">
<div
data-tut="reactourFirst"
className="mt-[5px]"
>
{unsignedSigners.map((obj, ind) => (
<div key={ind}>
<SignerListComponent
@@ -2178,7 +2217,8 @@ function PdfRequestFiles(
))}
</div>
</>
)}
);
})()}
{(defaultSignImg || myInitial) &&
!alreadySign &&
currentSigner && (
@@ -2198,7 +2238,8 @@ function PdfRequestFiles(
</div>
{pdfDetails[0]?.AllowModifications &&
currentSigner &&
!alreadySign && (
!alreadySign &&
(
<div data-tut="reactourFourth">
<WidgetComponent
pdfUrl={pdfUrl}
@@ -2226,7 +2267,7 @@ function PdfRequestFiles(
finishDocument={handleSignPdf}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
index={pageNumber}
index={currWidgetsDetails?.pageNumber || pageNumber}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
allowCellResize={false}
+128 -48
View File
@@ -40,7 +40,9 @@ import {
generatePdfName,
getOriginalWH,
defaultMailBody,
defaultMailSubject
defaultMailSubject,
flattenPdf,
base64ToArrayBuffer,
} from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf";
import { useNavigate } from "react-router";
@@ -50,7 +52,7 @@ import WidgetNameModal from "../components/pdf/WidgetNameModal";
import { SaveFileSize } from "../constant/saveFileSize";
import { useDispatch, useSelector } from "react-redux";
import PdfTools from "../components/pdf/PdfTools";
import { useTranslation } from "react-i18next";
import { useTranslation, Trans } from "react-i18next";
import RotateAlert from "../components/RotateAlert";
import Loader from "../primitives/Loader";
import ModalUi from "../primitives/ModalUi";
@@ -66,10 +68,12 @@ import { resetWidgetState, setPrefillImg } from "../redux/reducers/widgetSlice";
import EditDocument from "../components/pdf/EditTemplate";
import CustomizeMail from "../components/pdf/CustomizeMail";
import { useWindowSize } from "../hook/useWindowSize";
import { useScroll } from "../context/ScrollPdfContext";
function PlaceHolderSign() {
const { t } = useTranslation();
const copyUrlRef = useRef(null);
const { scrollRef } = useScroll();
const dispatch = useDispatch();
const windowSize = useWindowSize();
const prefillImg = useSelector((state) => state.widget.prefillImg);
@@ -172,6 +176,7 @@ function PlaceHolderSign() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//function to fetch tenant Details
const fetchTenantDetails = async () => {
const user = JSON.parse(
@@ -243,7 +248,7 @@ function PlaceHolderSign() {
//getting document details
const documentData = await contractDocument(
documentId,
"Bcc"
"Bcc,Cc"
);
if (documentData && documentData.length > 0) {
@@ -348,7 +353,7 @@ function PlaceHolderSign() {
...x,
Id: placeholder[index]?.Id,
Role: placeholder[index]?.Role,
blockColor: placeholder[index]?.blockColor
blockColor: placeholder[index]?.blockColor,
}));
setPrefillSigner([utils?.prefillObj()]);
setSignerPos(placeholder);
@@ -384,10 +389,14 @@ function PlaceHolderSign() {
...matchingSigner,
Role: x.Role ? x.Role : matchingSigner.Role,
Id: x.Id,
blockColor: x.blockColor
blockColor: x.blockColor,
};
} else {
return { Role: x.Role, Id: x.Id, blockColor: x.blockColor };
return {
Role: x.Role,
Id: x.Id,
blockColor: x.blockColor,
};
}
});
if (prefillPlaceholder) {
@@ -441,7 +450,11 @@ function PlaceHolderSign() {
setSignerPos(documentData[0].Placeholders);
if (placeholder.length > 0) {
let updatedSigners = placeholder.map((x) => {
return { Role: x.Role, Id: x.Id, blockColor: x.blockColor };
return {
Role: x.Role,
Id: x.Id,
blockColor: x.blockColor,
};
});
setSignersData(updatedSigners);
setIsSelectId(0);
@@ -540,16 +553,20 @@ function PlaceHolderSign() {
}));
dropData = placeHolder.pos;
} else if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divWidth = divRef.current.getBoundingClientRect().width;
const divHeight = divRef.current.getBoundingClientRect().height;
// Use the current page container (id="container") so that the
// height reflects one page, not the entire multi-page document.
const containerEl =
document.getElementById("container") || divRef.current;
const divWidth = containerEl.getBoundingClientRect().width;
const divHeight = containerEl.getBoundingClientRect().height;
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
//onclick put placeholder center on pdf
xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2,
yPosition:
(divHeight / 2 - widgetHeight / 2) / (containerScale * scale),
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
key: key,
@@ -706,7 +723,7 @@ function PlaceHolderSign() {
} else if (dragTypeValue === radioButtonWidget) {
setIsRadio(true);
}
setCurrWidgetsDetails(dropObj);
setCurrWidgetsDetails({ ...dropObj, pageNumber: pageNumber });
}
}
};
@@ -718,6 +735,7 @@ function PlaceHolderSign() {
setPdfLoad(true);
};
//function for save x and y position and show signature tab on that position
const handleTabDrag = (key) => {
setDragKey(key);
@@ -725,10 +743,11 @@ function PlaceHolderSign() {
};
//function for set and update x and y postion after drag and drop signature tab
const handleStop = (event, dragElement, signerId, key) => {
const handleStop = (event, dragElement, signerId, key, widgetPageNumber) => {
setFontColor();
setFontSize();
if (!isResize && isDragging) {
const effectivePageNumber = widgetPageNumber || pageNumber;
const dataNewPlace = addZIndex(signerPos, key, setZIndex);
let updateSignPos = [...signerPos];
updateSignPos.splice(0, updateSignPos.length, ...dataNewPlace);
@@ -736,7 +755,7 @@ function PlaceHolderSign() {
const keyValue = key ? key : dragKey;
const containerScale = getContainerScale(
pdfOriginalWH,
pageNumber,
effectivePageNumber,
containerWH
);
if (keyValue >= 0) {
@@ -746,22 +765,22 @@ function PlaceHolderSign() {
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === effectivePageNumber
);
if (getPageNumer.length > 0) {
const addSignPos = getPageNumer?.[0]?.pos?.map((url) => {
if (url.key === keyValue) {
return {
...url,
xPosition: dragElement.x / (containerScale * scale),
yPosition: dragElement.y / (containerScale * scale)
xPosition: dragElement.x / containerScale,
yPosition: dragElement.y / containerScale
};
}
return url;
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === effectivePageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -782,13 +801,14 @@ function PlaceHolderSign() {
setTimeout(() => setIsDragging(false), 200);
};
//function for delete signature block
const handleDeleteWidget = (key, Id) => {
const handleDeleteWidget = (key, Id, widgetPageNumber) => {
const effectivePageNumber = widgetPageNumber || pageNumber;
const updateData = [];
const filterSignerPos = signerPos.filter((data) => data.Id === Id);
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === effectivePageNumber
);
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos.filter(
@@ -798,7 +818,7 @@ function PlaceHolderSign() {
if (getXYdata.length > 0) {
updateData.push(getXYdata);
const newUpdatePos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === effectivePageNumber) {
return { ...obj, pos: updateData[0] };
}
return obj;
@@ -813,7 +833,7 @@ function PlaceHolderSign() {
setSignerPos(newUpdateSigner);
} else {
const getRemainPage = filterSignerPos[0].placeHolder.filter(
(data) => data.pageNumber !== pageNumber
(data) => data.pageNumber !== effectivePageNumber
);
//condition to check placeholder length is greater than 1 do not need to remove whole placeholder
//array only resove particular widgets
@@ -987,6 +1007,7 @@ function PlaceHolderSign() {
alert(t("atleast-one-recipient-alert"));
} else if (isPlaceholderExist && unassignedWidget.length === 0) {
const IsSignerNotExist = filterPrefill?.filter((x) => !x.signerObjId);
// below condition is used to hightlight the widget whose value is not provided by signer
if (IsSignerNotExist && IsSignerNotExist?.length > 0) {
setSignerExistModal(true);
setCurrWidgetsDetails(IsSignerNotExist[0]?.placeHolder?.[0]?.pos[0]);
@@ -995,7 +1016,6 @@ function PlaceHolderSign() {
}
}
};
useEffect(() => {
const timer = setTimeout(() => {
if (
@@ -1099,20 +1119,20 @@ function PlaceHolderSign() {
}
}
);
setIsLoading({ isLoad: false });
setIsUiLoading(false);
setIsSendAlert({ mssg: "confirm", alert: true });
if (docTitle) {
const updatedPdfDetails = [...pdfDetails];
updatedPdfDetails[0].Name = docTitle;
setPdfDetails(updatedPdfDetails);
}
setIsLoading({ isLoad: false });
setIsUiLoading(false);
setIsSendAlert({ mssg: "confirm", alert: true });
const ownerId = pdfDetails[0].ExtUserPtr?.UserId?.objectId;
const firstSigner = signersdata[0];
const isOwner = firstSigner?.UserId?.objectId === ownerId;
if (pdfDetails[0]?.SendinOrder && isOwner) {
setIsCurrUser(ownerId);
setIsSend(true);
setIsCurrUser(currentUser?.objectId);
} else {
const currentSigner = signersdata?.find(
(x) => x?.UserId?.objectId === ownerId
@@ -1152,7 +1172,10 @@ function PlaceHolderSign() {
`${pdfDetails?.[0].objectId}/${signerMail[i].Email}/${objectId}/${sendMail}`
);
let signPdf = `${hostUrl}/login/${encodeBase64}`;
shareLinkList.push({ signerEmail: signerMail[i].Email, url: signPdf });
shareLinkList.push({
signerEmail: signerMail[i].Email,
url: signPdf
});
}
return shareLinkList.map((data, ind) => {
return (
@@ -1289,6 +1312,7 @@ function PlaceHolderSign() {
isHideLabel,
layout
) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
const isPrefill = signerPos.some(
(x) => x?.Role === "prefill" && x.Id === uniqueId
);
@@ -1296,7 +1320,7 @@ function PlaceHolderSign() {
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
const textSize = fontSize || currWidgetsDetails?.options?.fontSize;
@@ -1395,7 +1419,7 @@ function PlaceHolderSign() {
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === widgetPageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -1419,6 +1443,7 @@ function PlaceHolderSign() {
setFontColor();
};
const handleWidgetdefaultdata = (defaultdata, isSignWidget) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
if (isSignWidget) {
const updatedPdfDetails = [...pdfDetails];
const signtypes = defaultdata.signatureType || signatureType;
@@ -1432,7 +1457,7 @@ function PlaceHolderSign() {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
@@ -1484,6 +1509,7 @@ function PlaceHolderSign() {
...position.options,
name: defaultdata.name,
hint: defaultdata?.hint || "",
rotation: defaultdata?.rotation || 0,
...(defaultdata?.penColors?.length > 0 && {
penColors: defaultdata?.penColors
})
@@ -1508,7 +1534,7 @@ function PlaceHolderSign() {
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === widgetPageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -1586,12 +1612,13 @@ function PlaceHolderSign() {
setIsAddUser({ [id]: true });
};
//`handleAddUser` function to use add new user
const handleAddUser = (data, signerObjId) => {
const handleAddUser = (data, signerObjId, signerRole) => {
const id = signerObjId ? signerObjId : uniqueId;
setRoleName("");
setCurrWidgetsDetails({});
let assignedId = id;
if (isAddSigner) {
handleAddNewRecipients(data);
assignedId = handleAddNewRecipients(data, signerRole);
} else {
if (data && data.objectId) {
const signerPtr = {
@@ -1633,23 +1660,25 @@ function PlaceHolderSign() {
}
}
setSignersData(updateSigner);
const index = signersdata.findIndex(
const index = updateSigner.findIndex(
(x) => x.Id === id || x.objectId === id
);
setIsSelectId(index);
}
}
return assignedId;
};
//function to add new signer in document signers list
const handleAddNewRecipients = (data) => {
const handleAddNewRecipients = (data, signerRole) => {
const newId = randomId();
const backgroundColor = color[signersdata.length];
signersdata.push({
const newSigner = {
...data,
className: "contracts_Contactbook",
Id: newId,
blockColor: backgroundColor
});
blockColor: backgroundColor,
};
setSignersData((prev) => [...(prev || []), newSigner]);
const signerPosObj = {
signerPtr: {
__type: "Pointer",
@@ -1658,11 +1687,12 @@ function PlaceHolderSign() {
},
signerObjId: data.objectId,
blockColor: backgroundColor,
Id: newId
Id: newId,
};
setSignerPos((prev) => [...prev, signerPosObj]);
setSignerPos((prev) => [...(prev || []), signerPosObj]);
setUniqueId(newId);
setIsSelectId(signersdata.length - 1);
setIsSelectId(signersdata.length);
return newId;
};
const closePopup = () => {
@@ -1702,6 +1732,9 @@ function PlaceHolderSign() {
}
];
const handleClosePrefillTour = () => {
setIsSendAlert({});
};
// `handleDeleteUser` function is used to delete record and placeholder when user click on delete which is place next user name in recipients list
const handleDeleteUser = (Id) => {
const updateSigner = signersdata
@@ -1737,10 +1770,10 @@ function PlaceHolderSign() {
}
};
const clickOnZoomIn = () => {
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
onClickZoomIn(scale, setScale, scrollRef);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
onClickZoomOut(scale, setScale, scrollRef);
};
//`handleRotationFun` function is used to roatate pdf particular page
const handleRotationFun = async (rotateDegree) => {
@@ -1796,6 +1829,15 @@ function PlaceHolderSign() {
}))
}
: {};
const Cc = updateDocument?.[0]?.Cc?.length
? {
Cc: updateDocument?.[0]?.Cc?.map((x) => ({
__type: "Pointer",
className: "contracts_Contactbook",
objectId: x.objectId
}))
}
: {};
const RedirectUrl = updateDocument?.[0]?.RedirectUrl
? { RedirectUrl: updateDocument?.[0]?.RedirectUrl }
: {};
@@ -1823,6 +1865,7 @@ function PlaceHolderSign() {
parseInt(updateDocument?.[0]?.RemindOnceInEvery) || 0,
...penColors,
...Bcc,
...Cc,
...RedirectUrl,
AllowModifications: updateDocument?.[0]?.AllowModifications || false
};
@@ -1869,7 +1912,7 @@ function PlaceHolderSign() {
isOpen={signerExistModal}
/>
<Tour
onRequestClose={() => setIsSendAlert({})}
onRequestClose={handleClosePrefillTour}
steps={prefillWidgetTour}
isOpen={isSendAlert.mssg === "prefill"}
/>
@@ -1954,6 +1997,35 @@ function PlaceHolderSign() {
</div>
) : mailStatus === "failed" ? (
<p>{t("mail-failed")} </p>
) : mailStatus === "emailnotverified" ? (
<div>
<p>
<Trans
i18nKey="email-not-verified-send"
components={{
1: (
<a
href="/profile"
className="text-blue-700 underline cursor-pointer"
/>
)
}}
/>
</p>
<div className="flex justify-center mt-2">
<button
onClick={() => {
setIsSend(false);
setSignerPos([]);
navigate("/report/1MwEuxLEkF");
}}
type="button"
className="op-btn op-btn-ghost text-base-content"
>
{t("close")}
</button>
</div>
</div>
) : (
<div className="mb-[10px]">
{!pdfDetails[0]?.SendinOrder &&
@@ -1971,7 +2043,8 @@ function PlaceHolderSign() {
)}
{mailStatus !== "quotareached" &&
mailStatus !== "failed" && (
mailStatus !== "failed" &&
mailStatus !== "emailnotverified" && (
<div
className={
mailStatus === "success"
@@ -2003,6 +2076,7 @@ function PlaceHolderSign() {
)}
</div>
{mailStatus !== "success" &&
mailStatus !== "emailnotverified" &&
isCurrUser &&
pdfDetails[0]?.SendinOrder && (
<div className="op-divider text-base-content mx-[0%] my-1 font-medium">
@@ -2010,6 +2084,7 @@ function PlaceHolderSign() {
</div>
)}
{mailStatus !== "success" &&
mailStatus !== "emailnotverified" &&
pdfDetails[0]?.SendinOrder &&
isCurrUser && (
<div
@@ -2052,7 +2127,7 @@ function PlaceHolderSign() {
xyPosition={signerPos}
setXyPosition={setSignerPos}
allPages={allPages}
pageNumber={pageNumber}
pageNumber={currWidgetsDetails?.pageNumber || pageNumber}
signKey={currWidgetsDetails?.key}
Id={uniqueId}
widgetType={currWidgetsDetails?.type}
@@ -2143,7 +2218,9 @@ function PlaceHolderSign() {
>
{containerWH?.width && (
<RenderPdf
scrollRef={scrollRef}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
pdfNewWidth={pdfNewWidth}
pdfDetails={pdfDetails}
signerPos={signerPos}
@@ -2189,6 +2266,7 @@ function PlaceHolderSign() {
isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
handleClosePrefillTour={handleClosePrefillTour}
/>
)}
</div>
@@ -2298,7 +2376,7 @@ function PlaceHolderSign() {
setPageNumber={setPageNumber}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
index={pageNumber}
index={currWidgetsDetails?.pageNumber || pageNumber}
isSave={true}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
@@ -2330,6 +2408,8 @@ function PlaceHolderSign() {
closePopup={closePopup}
signersData={signersdata}
signerPos={signerPos}
setSignerPos={setSignerPos}
setSignersData={setSignersData}
isAddYourSelfCheckbox
/>
)}
+2 -4
View File
@@ -262,10 +262,7 @@ const Preferences = () => {
</div>
)}
<h1 className="ml-4 mt-3 text-lg mb-2 font-semibold text-base-content">
{appName} {t("Preferences")}{" "}
<span>
<Tooltip message={`${appName} ${t("Preferences")}`} />
</span>
{appName} {t("Preferences")}
</h1>
<div className="flex justify-center items-center mt-2">
<div
@@ -525,6 +522,7 @@ const Preferences = () => {
</div>
</div>
<div className="mb-6">
<label
htmlFor="sender-name-toggle"
+89 -40
View File
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect, useMemo } from "react";
import { PDFDocument } from "pdf-lib";
import "../styles/signature.css";
import Parse from "parse";
@@ -36,7 +36,9 @@ import {
addWidgetSelfsignOptions,
getOriginalWH,
signatureTypes,
isEmptyValue
isEmptyValue,
flattenPdf,
base64ToArrayBuffer
} from "../constant/Utils";
import { useParams } from "react-router";
import Tour from "../primitives/Tour";
@@ -73,12 +75,15 @@ import { useWindowSize } from "../hook/useWindowSize";
import {
applyNumberFormulasToPages,
} from "../utils";
import { useScroll } from "../context/ScrollPdfContext";
//For signYourself inProgress section signer can add sign and complete doc sign.
function SignYourSelf() {
const { t } = useTranslation();
const { docId } = useParams();
const dispatch = useDispatch();
const windowSize = useWindowSize();
const { scrollRef } = useScroll();
const isShowModal = useSelector((state) => state.widget.isShowModal);
const appName =
"OpenSign™";
@@ -148,9 +153,15 @@ function SignYourSelf() {
const [unSignedWidgetId, setUnSignedWidgetId] = useState("");
const [widgetsTour, setWidgetsTour] = useState(false);
const [owner, setOwner] = useState({});
const index = xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber;
});
// Memoize the xyPosition index for the widget modal to avoid recomputing on every render
const widgetModalIndex = useMemo(
() =>
xyPosition?.findIndex(
(obj) =>
obj.pageNumber === (currWidgetsDetails?.pageNumber || pageNumber)
),
[xyPosition, currWidgetsDetails?.pageNumber, pageNumber]
);
const rowLevel =
localStorage.getItem("rowlevel") &&
JSON.parse(localStorage.getItem("rowlevel"));
@@ -176,6 +187,7 @@ function SignYourSelf() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const updateSize = () => {
if (divRef.current) {
@@ -362,16 +374,20 @@ function SignYourSelf() {
defaultWidthHeight(dragTypeValue).height * containerScale;
//adding and updating drop position in array when user drop signature button in div
if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divWidth = divRef.current.getBoundingClientRect().width;
const divHeight = divRef.current.getBoundingClientRect().height;
// Use the current page container (id="container") so that the
// height reflects one page, not the entire multi-page document.
const containerEl =
document.getElementById("container") || divRef.current;
const divWidth = containerEl.getBoundingClientRect().width;
const divHeight = containerEl.getBoundingClientRect().height;
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
//onclick put placeholder center on pdf
xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2,
yPosition:
(divHeight / 2 - widgetHeight / 2) / (containerScale * scale),
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
key: key,
@@ -448,8 +464,12 @@ function SignYourSelf() {
});
const updateData = filterDropPos?.[0].pos;
const newSignPos = updateData.concat(dropData);
let xyPos = { pageNumber: pageNumber, pos: newSignPos };
xyPosition?.splice(index, 1, xyPos);
const xyPos = { pageNumber, pos: newSignPos };
setXyPosition((prev) => {
const updated = [...prev];
updated.splice(index, 1, xyPos);
return updated;
});
} else {
const xyPos = { pageNumber: pageNumber, pos: dropData };
setXyPosition((prev) => [...prev, xyPos]);
@@ -471,7 +491,7 @@ function SignYourSelf() {
setFontColor("black");
}
dispatch(setIsShowModal({ [key]: true }));
setCurrWidgetsDetails(dropObj);
setCurrWidgetsDetails({ ...dropObj, pageNumber: pageNumber });
};
//`handleResend` function is used to resend otp for email verification
@@ -797,34 +817,44 @@ function SignYourSelf() {
setIsDragging(true);
};
//function for set and update x and y postion after drag and drop signature tab
const handleStop = (event, dragElement) => {
const handleStop = (
event,
dragElement,
_signerId,
_key,
widgetPageNumber
) => {
setFontSize();
setFontColor();
if (!isResize && isDragging && dragElement) {
event.preventDefault();
const effectivePageNumber = widgetPageNumber || pageNumber;
const containerScale = getContainerScale(
pdfOriginalWH,
pageNumber,
effectivePageNumber,
containerWH
);
if (dragKey >= 0) {
const filterDropPos = xyPosition?.filter(
(data) => data.pageNumber === pageNumber
const effectiveIndex = xyPosition?.findIndex(
(object) => object.pageNumber === effectivePageNumber
);
if (filterDropPos?.length > 0) {
const addSign = xyPosition[index]?.pos?.map((url) => {
const filterDropPos = xyPosition?.filter(
(data) => data.pageNumber === effectivePageNumber
);
if (filterDropPos?.length > 0 && effectiveIndex >= 0) {
const addSign = xyPosition[effectiveIndex]?.pos?.map((url) => {
if (url.key === dragKey) {
return {
...url,
xPosition: dragElement.x / (containerScale * scale),
yPosition: dragElement.y / (containerScale * scale)
xPosition: dragElement.x / containerScale,
yPosition: dragElement.y / containerScale
};
}
return url;
});
const newUpdateUrl = xyPosition.map((obj, ind) => {
if (ind === index) {
if (ind === effectiveIndex) {
return { ...obj, pos: addSign };
}
return obj;
@@ -843,6 +873,7 @@ function SignYourSelf() {
setPdfOriginalWH(pdfWHObj);
setPdfLoad(true);
};
//function for change page numver of pdf
function changePage(offset) {
setSignBtnPosition([]);
@@ -872,15 +903,22 @@ function SignYourSelf() {
setSignBtnPosition([xySignature]);
};
//function for delete signature block
const handleDeleteWidget = (key) => {
const handleDeleteWidget = (key, _Id, widgetPageNumber) => {
const effectivePageNumber = widgetPageNumber || pageNumber;
const effectiveIndex = xyPosition?.findIndex(
(object) => object.pageNumber === effectivePageNumber
);
setCurrWidgetsDetails({});
const updateResizeData = [];
let filterData = xyPosition[index].pos.filter((data) => data.key !== key);
if (effectiveIndex < 0) return;
let filterData = xyPosition[effectiveIndex].pos.filter(
(data) => data.key !== key
);
//delete and update block position
if (filterData.length > 0) {
updateResizeData.push(filterData);
const newUpdatePos = xyPosition.map((obj, ind) => {
if (ind === index) {
if (ind === effectiveIndex) {
return { ...obj, pos: updateResizeData[0] };
}
return obj;
@@ -889,7 +927,7 @@ function SignYourSelf() {
setXyPosition(newUpdatePos);
} else {
const getRemainPage = xyPosition.filter(
(data) => data.pageNumber !== pageNumber
(data) => data.pageNumber !== effectivePageNumber
);
if (getRemainPage && getRemainPage.length > 0) {
@@ -993,8 +1031,9 @@ function SignYourSelf() {
isHideLabel,
layout
) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
const getPageNumer = xyPosition.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
const widgetLayout =
@@ -1036,7 +1075,7 @@ function SignYourSelf() {
return position;
});
const updateXYposition = xyPosition.map((obj, ind) => {
if (ind === index) {
if (obj?.pageNumber === widgetPageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -1057,8 +1096,9 @@ function SignYourSelf() {
setIsTextSetting(value);
};
const handleSaveFontSize = () => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
const getPageNumer = xyPosition.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
const addSignPos = getPageNumer?.[0]?.pos?.map((position) => {
@@ -1076,7 +1116,7 @@ function SignYourSelf() {
return position;
});
const updateXYposition = xyPosition.map((obj, ind) => {
if (ind === index) {
if (obj?.pageNumber === widgetPageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -1089,9 +1129,10 @@ function SignYourSelf() {
};
const setCellCount = (key, newCount) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
setXyPosition((prev) => {
const getPageNumer = prev.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((p) =>
@@ -1100,7 +1141,9 @@ function SignYourSelf() {
: p
);
return prev.map((obj, ind) =>
ind === index ? { ...obj, pos: updatePos } : obj
obj?.pageNumber === widgetPageNumber
? { ...obj, pos: updatePos }
: obj
);
}
return prev;
@@ -1108,13 +1151,14 @@ function SignYourSelf() {
};
const handleWidgetdefaultdata = (defaultdata) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
const newFontSize =
defaultdata?.fontSize !== undefined ? defaultdata.fontSize : fontSize;
const newFontColor =
defaultdata?.fontColor !== undefined ? defaultdata.fontColor : fontColor;
const getPageNumer = xyPosition.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((position) => {
@@ -1153,7 +1197,7 @@ function SignYourSelf() {
return position;
});
const updateXYposition = xyPosition.map((obj, ind) =>
ind === index ? { ...obj, pos: updatePos } : obj
obj?.pageNumber === widgetPageNumber ? { ...obj, pos: updatePos } : obj
);
setXyPosition(applyNumberFormulasToPages(updateXYposition));
}
@@ -1182,10 +1226,10 @@ function SignYourSelf() {
setCurrWidgetsDetails({});
};
const clickOnZoomIn = () => {
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
onClickZoomIn(scale, setScale, scrollRef);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
onClickZoomOut(scale, setScale, scrollRef);
};
//`handleRotationFun` function is used to roatate pdf particular page
const handleRotationFun = async (rotateDegree) => {
@@ -1240,6 +1284,10 @@ function SignYourSelf() {
styles: { fontSize: "13px" }
}
];
const closeWidgetTour = () => {
setWidgetsTour(false);
};
return (
<>
{isLoading.isLoad ? (
@@ -1289,7 +1337,7 @@ function SignYourSelf() {
showNumber={false}
showNavigation={false}
showNavigationNumber={false}
onRequestClose={() => setWidgetsTour(false)}
onRequestClose={closeWidgetTour}
steps={WidgetTourConfig}
isOpen={widgetsTour}
/>
@@ -1377,7 +1425,7 @@ function SignYourSelf() {
xyPosition={xyPosition}
setXyPosition={setXyPosition}
allPages={allPages}
pageNumber={pageNumber}
pageNumber={currWidgetsDetails?.pageNumber || pageNumber}
signKey={currWidgetsDetails?.key}
widgetType={currWidgetsDetails?.type}
pdfOriginalWH={pdfOriginalWH}
@@ -1421,6 +1469,7 @@ function SignYourSelf() {
{containerWH?.width && (
<RenderPdf
pageNumber={pageNumber}
setPageNumber={setPageNumber}
pdfOriginalWH={pdfOriginalWH}
pdfNewWidth={pdfNewWidth}
successEmail={successEmail}
@@ -1439,7 +1488,6 @@ function SignYourSelf() {
pdfLoad={pdfLoad}
setPdfLoad={setPdfLoad}
setXyPosition={setXyPosition}
index={index}
containerWH={containerWH}
setIsPageCopy={setIsPageCopy}
setIsCheckbox={setIsCheckbox}
@@ -1460,6 +1508,7 @@ function SignYourSelf() {
divRef={divRef}
currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal}
closeWidgetTour={closeWidgetTour}
unSignedWidgetId={unSignedWidgetId}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
@@ -1469,7 +1518,7 @@ function SignYourSelf() {
</div>
</div>
<div className="w-full md:w-[23%] bg-base-100 overflow-y-auto hide-scrollbar">
<div className={`max-h-screen`}>
<div className="max-h-screen">
{!isCompleted ? (
<WidgetComponent
pdfUrl={pdfUrl}
@@ -1499,7 +1548,7 @@ function SignYourSelf() {
setPageNumber={setPageNumber}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
index={index}
index={widgetModalIndex} //current page index
isSave={true}
signatureTypes={signatureTypes}
/>
+144 -36
View File
@@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from "react";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { useParams, useNavigate } from "react-router";
import { useParams, useNavigate, Link } from "react-router";
import axios from "axios";
import "../styles/signature.css";
import WidgetComponent from "../components/pdf/WidgetComponent";
@@ -39,8 +39,11 @@ import {
defaultMailBody,
defaultMailSubject,
handleDeleteWidget,
nonPresentMaskCss
nonPresentMaskCss,
flattenPdf,
base64ToArrayBuffer
} from "../constant/Utils";
import { PDFDocument } from "pdf-lib";
import RenderPdf from "../components/pdf/RenderPdf";
import "../styles/AddUser.css";
import EditTemplate from "../components/pdf/EditTemplate";
@@ -50,7 +53,7 @@ import DropdownWidgetOption from "../components/pdf/DropdownWidgetOption";
import Parse from "parse";
import { useDispatch, useSelector } from "react-redux";
import PdfTools from "../components/pdf/PdfTools";
import { useTranslation } from "react-i18next";
import { useTranslation, Trans } from "react-i18next";
import RotateAlert from "../components/RotateAlert";
import ModalUi from "../primitives/ModalUi";
import TourContentWithBtn from "../primitives/TourContentWithBtn";
@@ -67,12 +70,14 @@ import CustomizeMail from "../components/pdf/CustomizeMail";
import { resetWidgetState, setPrefillImg } from "../redux/reducers/widgetSlice";
import ShareButton from "../primitives/ShareButton";
import { useWindowSize } from "../hook/useWindowSize";
import { useScroll } from "../context/ScrollPdfContext";
const TemplatePlaceholder = () => {
const { t } = useTranslation();
const copyUrlRef = useRef(null);
const { templateId } = useParams();
const windowSize = useWindowSize();
const { scrollRef } = useScroll();
const dispatch = useDispatch();
const { prefillImg, isBulkLoader } = useSelector((state) => state.widget);
const divRef = useRef(null);
@@ -91,6 +96,7 @@ const TemplatePlaceholder = () => {
const [isSelectListId, setIsSelectId] = useState();
const [isSendAlert, setIsSendAlert] = useState(false);
const [isCreateDocModal, setIsCreateDocModal] = useState(false);
const [isPublicFlow, setIsPublicFlow] = useState(false);
//'signersName' variable used to show all signer's name that do not have a signature widget assigned
const [signersName, setSignersName] = useState("");
const [showRotateAlert, setShowRotateAlert] = useState({
@@ -175,6 +181,7 @@ const TemplatePlaceholder = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const updateSize = () => {
if (divRef.current) {
@@ -329,10 +336,14 @@ const TemplatePlaceholder = () => {
...matchingSigner,
Role: x.Role ? x.Role : matchingSigner.Role,
Id: x.Id,
blockColor: x.blockColor
blockColor: x.blockColor,
};
} else {
return { Role: x.Role, Id: x.Id, blockColor: x.blockColor };
return {
Role: x.Role,
Id: x.Id,
blockColor: x.blockColor,
};
}
});
const prefillPlaceholder = documentData[0]?.Placeholders.find(
@@ -377,7 +388,11 @@ const TemplatePlaceholder = () => {
);
if (signerPlaceholder) {
let updatedSigners = signerPlaceholder.map((x) => {
return { Role: x.Role, Id: x.Id, blockColor: x.blockColor };
return {
Role: x.Role,
Id: x.Id,
blockColor: x.blockColor,
};
});
setUniqueId(updatedSigners[0]?.Id);
setSignersData(updatedSigners);
@@ -474,16 +489,20 @@ const TemplatePlaceholder = () => {
}));
dropData = placeHolder.pos;
} else if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divWidth = divRef.current.getBoundingClientRect().width;
const divHeight = divRef.current.getBoundingClientRect().height;
// Use the current page container (id="container") so that the
// height reflects one page, not the entire multi-page document.
const containerEl =
document.getElementById("container") || divRef.current;
const divWidth = containerEl.getBoundingClientRect().width;
const divHeight = containerEl.getBoundingClientRect().height;
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
//onclick put placeholder center on pdf
xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2,
yPosition:
(divHeight / 2 - widgetHeight / 2) / (containerScale * scale),
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
key: key,
@@ -639,7 +658,7 @@ const TemplatePlaceholder = () => {
setFontSize(12);
setFontColor("black");
}
setCurrWidgetsDetails(dropObj);
setCurrWidgetsDetails({ ...dropObj, pageNumber: pageNumber });
} else {
setIsReceipent(false);
}
@@ -662,6 +681,7 @@ const TemplatePlaceholder = () => {
setPdfOriginalWH(pdfWHObj);
setPdfLoad(true);
};
//function for save x and y position and show signature tab on that position
const handleTabDrag = (key) => {
setDragKey(key);
@@ -669,10 +689,11 @@ const TemplatePlaceholder = () => {
};
//function for set and update x and y postion after drag and drop signature tab
const handleStop = (event, dragElement, signerId, key) => {
const handleStop = (event, dragElement, signerId, key, widgetPageNumber) => {
setFontColor();
setFontSize();
if (!isResize && isDragging) {
const effectivePageNumber = widgetPageNumber || pageNumber;
const dataNewPlace = addZIndex(signerPos, key, setZIndex);
let updateSignPos = [...signerPos];
updateSignPos.splice(0, updateSignPos.length, ...dataNewPlace);
@@ -680,7 +701,7 @@ const TemplatePlaceholder = () => {
const keyValue = key ? key : dragKey;
const containerScale = getContainerScale(
pdfOriginalWH,
pageNumber,
effectivePageNumber,
containerWH
);
if (keyValue >= 0) {
@@ -691,22 +712,22 @@ const TemplatePlaceholder = () => {
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === effectivePageNumber
);
if (getPageNumer.length > 0) {
const addSignPos = getPageNumer?.[0]?.pos?.map((url) => {
if (url.key === keyValue) {
return {
...url,
xPosition: dragElement.x / (containerScale * scale),
yPosition: dragElement.y / (containerScale * scale)
xPosition: dragElement.x / containerScale,
yPosition: dragElement.y / containerScale
};
}
return url;
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === effectivePageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -727,8 +748,13 @@ const TemplatePlaceholder = () => {
setTimeout(() => setIsDragging(false), 200);
};
//function is used to delete widgets
const handleDeleteWidgetObj = (key, Id) => {
const res = handleDeleteWidget(key, Id, pageNumber, signerPos);
const handleDeleteWidgetObj = (key, Id, widgetPageNumber) => {
const res = handleDeleteWidget(
key,
Id,
widgetPageNumber || pageNumber,
signerPos
);
if (res) {
setSignerPos(res);
}
@@ -849,6 +875,14 @@ const TemplatePlaceholder = () => {
}));
templateCls.set("Bcc", Bcc);
}
if (pdfDetails[0]?.Cc?.length) {
const Cc = pdfDetails[0]?.Cc.map((x) => ({
__type: "Pointer",
className: "contracts_Contactbook",
objectId: x.objectId
}));
templateCls.set("Cc", Cc);
}
const res = await templateCls.save();
if (res && pdfUrl) {
pdfDetails[0] = { ...pdfDetails[0], URL: pdfUrl };
@@ -921,6 +955,15 @@ const TemplatePlaceholder = () => {
}))
}
: {};
const Cc = pdfDetails[0]?.Cc?.length
? {
Cc: pdfDetails[0]?.Cc?.map((x) => ({
__type: "Pointer",
className: "contracts_Contactbook",
objectId: x.objectId
}))
}
: {};
const RedirectUrl = pdfDetails[0]?.RedirectUrl
? { RedirectUrl: pdfDetails[0]?.RedirectUrl }
: {};
@@ -946,6 +989,7 @@ const TemplatePlaceholder = () => {
TimeToCompleteDays:
parseInt(pdfDetails?.[0]?.TimeToCompleteDays) || 15,
...Bcc,
...Cc,
...RedirectUrl
};
const updateTemplate = new Parse.Object("contracts_Template");
@@ -994,7 +1038,7 @@ const TemplatePlaceholder = () => {
);
} else if (res?.status === "unattach signer") {
setIsUiLoading(false);
showAlert("danger", "please attach all role to signer");
showAlert("danger", t("attach-all-role-to-signer"));
} else if (res?.status === "success") {
setDocumentId(res.id);
const ownerId = pdfDetails[0].ExtUserPtr?.UserId?.objectId;
@@ -1014,6 +1058,10 @@ const TemplatePlaceholder = () => {
}
setIsMailModal(true);
} else if (res?.status === "error") {
const message = res?.message || "something-went-wrong-mssg";
setIsUiLoading(false);
showAlert("danger", t(message));
}
} catch (e) {
console.log("error in create document function", e);
@@ -1163,12 +1211,13 @@ const TemplatePlaceholder = () => {
setIsModalRole(true);
setPrevRole(roleName || "");
setRoleName("");
closeAddRoleTour();
};
// `handleAddRole` function is called when use click on add button in addRole modal
// save Role in entry in signerList and user
const handleAddRole = (e) => {
e.preventDefault();
e?.preventDefault?.();
const count = signersdata.length > 0 ? signersdata.length + 1 : 1;
const Id = randomId();
const index = signersdata.length;
@@ -1317,6 +1366,15 @@ const TemplatePlaceholder = () => {
}))
}
: {};
const Cc = updateTemplate?.[0]?.Cc?.length
? {
Cc: updateTemplate?.[0]?.Cc?.map((x) => ({
__type: "Pointer",
className: "contracts_Contactbook",
objectId: x.objectId
}))
}
: {};
const RedirectUrl = updateTemplate?.[0]?.RedirectUrl
? { RedirectUrl: updateTemplate?.[0]?.RedirectUrl }
: {};
@@ -1344,6 +1402,7 @@ const TemplatePlaceholder = () => {
parseInt(updateTemplate?.[0]?.RemindOnceInEvery) || 0,
...penColors,
...Bcc,
...Cc,
...RedirectUrl,
AllowModifications: updateTemplate?.[0]?.AllowModifications || false
};
@@ -1375,6 +1434,7 @@ const TemplatePlaceholder = () => {
isHideLabel,
layout
) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
const isPrefill = signerPos.some(
(x) => x?.Role === "prefill" && x.Id === uniqueId
);
@@ -1383,7 +1443,7 @@ const TemplatePlaceholder = () => {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
@@ -1483,7 +1543,7 @@ const TemplatePlaceholder = () => {
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === widgetPageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -1508,6 +1568,7 @@ const TemplatePlaceholder = () => {
};
const handleWidgetdefaultdata = (defaultdata, isSignWidget) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
if (isSignWidget) {
const updatedPdfDetails = [...pdfDetails];
const signtypes = defaultdata.signatureType || signatureType;
@@ -1521,7 +1582,7 @@ const TemplatePlaceholder = () => {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
(data) => data.pageNumber === widgetPageNumber
);
if (getPageNumer.length > 0) {
@@ -1573,6 +1634,7 @@ const TemplatePlaceholder = () => {
...position.options,
name: defaultdata.name,
hint: defaultdata?.hint || "",
rotation: defaultdata?.rotation || 0,
...(defaultdata?.penColors?.length > 0 && {
penColors: defaultdata?.penColors
})
@@ -1597,7 +1659,7 @@ const TemplatePlaceholder = () => {
});
const newUpdateSignPos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
if (obj.pageNumber === widgetPageNumber) {
return { ...obj, pos: addSignPos };
}
return obj;
@@ -1625,10 +1687,11 @@ const TemplatePlaceholder = () => {
setIsCheckbox(false);
};
const setCellCount = (key, newCount) => {
const widgetPageNumber = currWidgetsDetails?.pageNumber || pageNumber;
const updated = signerPos.map((signer) => {
if (signer.Id !== uniqueId) return signer;
const placeHolder = signer.placeHolder.map((ph) => {
if (ph.pageNumber !== pageNumber) return ph;
if (ph.pageNumber !== widgetPageNumber) return ph;
const pos = ph.pos.map((p) =>
p.key === key
? { ...p, options: { ...p.options, cellCount: newCount } }
@@ -1642,10 +1705,10 @@ const TemplatePlaceholder = () => {
};
const clickOnZoomIn = () => {
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
onClickZoomIn(scale, setScale, scrollRef);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
onClickZoomOut(scale, setScale, scrollRef);
};
//`handleRotationFun` function is used to roatate pdf particular page
const handleRotationFun = async (rotateDegree) => {
@@ -1698,6 +1761,10 @@ const TemplatePlaceholder = () => {
styles: { fontSize: "13px" }
}
];
const handleClosePrefillTour = () => {
setUnSignedWidgetId("");
};
const copytoclipboard = (text) => {
copytoData(text);
if (copyUrlRef.current) {
@@ -1719,7 +1786,10 @@ const TemplatePlaceholder = () => {
`${documentId}/${signerMail[i].Email}/${objectId}/${sendMail}`
);
let signPdf = `${hostUrl}/login/${encodeBase64}`;
shareLinkList.push({ signerEmail: signerMail[i].Email, url: signPdf });
shareLinkList.push({
signerEmail: signerMail[i].Email,
url: signPdf
});
}
return shareLinkList.map((data, ind) => {
return (
@@ -1772,6 +1842,9 @@ const TemplatePlaceholder = () => {
navigate(`/recipientSignPdf/${docId}`);
}
};
const closeAddRoleTour = () => {
setIsAddRole(false);
};
return (
<>
{isLoading.isLoad ? (
@@ -1803,7 +1876,7 @@ const TemplatePlaceholder = () => {
/>
{isAddRole && (
<Tour
onRequestClose={() => setIsAddRole(false)}
onRequestClose={closeAddRoleTour}
steps={tourAddRole}
isOpen={isAddRole}
/>
@@ -1817,12 +1890,13 @@ const TemplatePlaceholder = () => {
)}
{unSignedWidgetId && (
<Tour
onRequestClose={() => setUnSignedWidgetId("")}
onRequestClose={handleClosePrefillTour}
steps={textFieldTour}
isOpen={true}
/>
)}
{/* this component used to render all pdf pages in left side */}
<RenderAllPdfPage
allPages={allPages}
@@ -1964,7 +2038,7 @@ const TemplatePlaceholder = () => {
xyPosition={signerPos}
setXyPosition={setSignerPos}
allPages={allPages}
pageNumber={pageNumber}
pageNumber={currWidgetsDetails?.pageNumber || pageNumber}
signKey={currWidgetsDetails?.key}
Id={uniqueId}
widgetType={currWidgetsDetails?.type}
@@ -2001,6 +2075,7 @@ const TemplatePlaceholder = () => {
{containerWH?.width && (
<RenderPdf
pageNumber={pageNumber}
setPageNumber={setPageNumber}
pdfNewWidth={pdfNewWidth}
pdfDetails={pdfDetails}
signerPos={signerPos}
@@ -2047,6 +2122,8 @@ const TemplatePlaceholder = () => {
isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
isPrefillModal={isPrefillModal}
handleClosePrefillTour={handleClosePrefillTour}
/>
)}
</div>
@@ -2189,10 +2266,10 @@ const TemplatePlaceholder = () => {
<PrefillWidgetModal
isPrefillModal={isPrefillModal}
prefillData={signerPos.find((x) => x.Role === "prefill")}
forms={forms}
setForms={setForms}
xyPosition={signerPos}
forms={forms}
setXyPosition={setSignerPos}
setForms={setForms}
handleCreateDocument={handleCreateDocument}
handleClosePrefillModal={handleClosePrefillModal}
handleAddUser={handleAddUser}
@@ -2213,7 +2290,7 @@ const TemplatePlaceholder = () => {
setPageNumber={setPageNumber}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
index={pageNumber}
index={currWidgetsDetails?.pageNumber || pageNumber}
isSave={true}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
@@ -2281,6 +2358,34 @@ const TemplatePlaceholder = () => {
</div>
) : mailStatus === "failed" ? (
<p>{t("mail-failed")} </p>
) : mailStatus === "emailnotverified" ? (
<div>
<p>
<Trans
i18nKey="email-not-verified-send"
components={{
1: (
<a
href="/profile"
className="text-blue-700 underline cursor-pointer"
/>
)
}}
/>
</p>
<div className="flex justify-center mt-2">
<button
onClick={() => {
setIsSend(false);
navigate("/report/1MwEuxLEkF");
}}
type="button"
className="op-btn op-btn-ghost text-base-content"
>
{t("close")}
</button>
</div>
</div>
) : (
<div className="mb-[10px]">
{!pdfDetails[0]?.SendinOrder &&
@@ -2294,7 +2399,8 @@ const TemplatePlaceholder = () => {
)}
</div>
)}
{mailStatus !== "quotareached" && (
{mailStatus !== "quotareached" &&
mailStatus !== "emailnotverified" && (
<div
className={
mailStatus === "success"
@@ -2329,6 +2435,7 @@ const TemplatePlaceholder = () => {
)}
</div>
{mailStatus !== "success" &&
mailStatus !== "emailnotverified" &&
currUserId &&
pdfDetails[0]?.SendinOrder && (
<div className="op-divider text-base-content mx-[0%] my-1 font-medium">
@@ -2336,6 +2443,7 @@ const TemplatePlaceholder = () => {
</div>
)}
{mailStatus !== "success" &&
mailStatus !== "emailnotverified" &&
currUserId &&
pdfDetails[0]?.SendinOrder && (
<div
+24
View File
@@ -10,6 +10,30 @@ if (typeof Promise.withResolvers === "undefined") {
};
}
// 🔐 Polyfill crypto.randomUUID for non-secure contexts (LAN IP, old browsers)
if (typeof globalThis.crypto === "undefined") {
globalThis.crypto = {};
}
if (!globalThis.crypto.randomUUID) {
globalThis.crypto.randomUUID = function () {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// RFC 4122 version 4
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
return [...bytes]
.map(
(b, i) =>
([4, 6, 8, 10].includes(i) ? "-" : "") +
b.toString(16).padStart(2, "0")
)
.join("");
};
}
// Usage:
// const { promise, resolve, reject } = Promise.withResolvers()
// console.log(promise, resolve, reject) // Promise { <pending> } [Function (anonymous)] [Function (anonymous)]
+30 -2
View File
@@ -1,9 +1,11 @@
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import "../styles/signature.css";
import Loader from "./Loader";
import Tooltip from "./Tooltip";
const ModalUi = ({
id,
children,
title,
isOpen,
@@ -18,19 +20,45 @@ const ModalUi = ({
helpText = ""
}) => {
const { t } = useTranslation();
const dialogRef = useRef(null);
const width = reduceWidth;
const isBottom = position === "bottom" ? "items-end pb-2 !bg-black/10" : "";
const crossBtnColor = crossColor ?? "text-base-content";
const hideScrollBar = !showScrollBar ? "hide-scrollbar" : "";
// Keep bottom-sheet modals above the mobile virtual keyboard by tracking the
// visual viewport (which shrinks when the keyboard opens) and adjusting the
// dialog's height/top to match, so the modal never slides behind the keyboard.
useEffect(() => {
if (!isOpen || position !== "bottom" || !window.visualViewport) return;
const vp = window.visualViewport;
const updatePosition = () => {
if (!dialogRef.current) return;
dialogRef.current.style.height = `${vp.height}px`;
dialogRef.current.style.top = `${vp.offsetTop}px`;
};
updatePosition();
vp.addEventListener("resize", updatePosition);
vp.addEventListener("scroll", updatePosition);
return () => {
vp.removeEventListener("resize", updatePosition);
vp.removeEventListener("scroll", updatePosition);
};
}, [isOpen, position]);
return (
<>
{isOpen && (
<dialog
id="selectSignerModal"
ref={dialogRef}
id={id || "selectSignerModal"}
className={`${isBottom} op-modal op-modal-open`}
style={{
overlay: { zIndex: 1000 },
content: { zIndex: 1001, overflow: "visible" } // Ensure modal doesnt clip content
content: { zIndex: 1001, overflow: "visible" } // Ensure modal doesn't clip content
}}
>
{isLoader && (
@@ -42,10 +42,8 @@ function CustomModal(props) {
{props?.headMsg && props?.headMsg}
</h3>
{!isExtendExpiry && (
<div className="p-[10px] px-[20px]">
<p className="text-[15px] text-base-content">
<div className="p-[10px] px-[20px] text-[15px] text-base-content">
{props.bodyMssg && props.bodyMssg}
</p>
</div>
)}
{!isExtendExpiry && (
@@ -27,7 +27,7 @@ import {
import BulkSendUi from "../../components/bulksend/BulkSendUi";
import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import DownloadPdfZip from "../../primitives/DownloadPdfZip";
import { useElSize } from "../../hook/useElSize";
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
@@ -65,7 +65,7 @@ const DocumentsReport = (props) => {
const [userDetails, setUserDetails] = useState({});
const [isNextStep, setIsNextStep] = useState({});
const [isBulkSend, setIsBulkSend] = useState({});
const [templateDeatils, setTemplateDetails] = useState({});
const [templateDetails, setTemplateDetails] = useState({});
const [placeholders, setPlaceholders] = useState([]);
const [isLoader, setIsLoader] = useState({});
const [isModal, setIsModal] = useState({});
@@ -88,6 +88,7 @@ const DocumentsReport = (props) => {
const [isPrefillModal, setIsPrefillModal] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const [error, setError] = useState("");
const [resendErrMail, setResendErrMail] = useState("");
const [isMailModal, setIsMailModal] = useState(false);
const [customizeMail, setCustomizeMail] = useState({
body: { basic: "", advanced: "" },
@@ -267,10 +268,10 @@ const DocumentsReport = (props) => {
async (templateRes, placeholder, signer) => {
setIsPrefillModal({});
const res = await createDocument(
[templateRes || templateDeatils],
[templateRes || templateDetails],
placeholder || xyPosition,
signer || signerList,
templateRes?.URL || templateDeatils?.URL,
templateRes?.URL || templateDetails?.URL,
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -283,8 +284,8 @@ const DocumentsReport = (props) => {
);
const handleUseTemplate = async (templateId, item) => {
try {
const templateDeatils = await fetchTemplate(templateId);
const templateData = templateDeatils.data && templateDeatils.data.result;
const templateRes = await fetchTemplate(templateId);
const templateData = templateRes.data && templateRes.data.result;
if (!templateData.error) {
setTemplateDetails(templateData);
setXyPosition(templateData?.Placeholders);
@@ -445,29 +446,19 @@ const DocumentsReport = (props) => {
const jsonSender = JSON.parse(senderUser);
setIsRevoke({});
setActLoader({ [`${item.objectId}`]: true });
const data = {
IsDeclined: true,
DeclineReason: reason,
DeclineBy: {
__type: "Pointer",
className: "_User",
objectId: jsonSender?.objectId
}
const params = {
docId: item.objectId,
reason: reason,
userId: jsonSender?.objectId,
};
await axios
.put(
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${
item.objectId
}`,
data,
{
.post(`${localStorage.getItem("baseUrl")}functions/declinedoc`, params, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
)
})
.then(async (result) => {
const res = result.data;
if (res) {
@@ -643,6 +634,7 @@ const DocumentsReport = (props) => {
company_name: doc?.ExtUserPtr?.Company || "",
signing_url: signPdf
};
const subject =
doc?.RequestSubject ||
doc?.ExtUserPtr?.TenantId?.RequestSubject ||
@@ -691,11 +683,11 @@ const DocumentsReport = (props) => {
setIsResendMail({});
}
else {
showAlert("danger", t("something-went-wrong-mssg"));
setResendErrMail(t("something-went-wrong-mssg"));
}
} catch (err) {
console.error("sendmail error", err);
showAlert("danger", t("something-went-wrong-mssg"));
setResendErrMail(t("something-went-wrong-mssg"));
} finally {
setIsNextStep({});
setUserDetails({});
@@ -961,8 +953,8 @@ const DocumentsReport = (props) => {
signerList,
setIsPrefillModal,
scale,
templateDeatils?.URL,
[templateDeatils],
templateDetails?.URL,
[templateDetails],
prefillImg,
extClass?.[0]?.UserId?.objectId,
);
@@ -979,7 +971,7 @@ const DocumentsReport = (props) => {
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
showAlert("danger", "please attach all role to signer");
showAlert("danger", t("attach-all-role-to-signer"));
} else if (res?.status === "success") {
setDocumentId(res.id);
setActLoader({});
@@ -991,6 +983,9 @@ const DocumentsReport = (props) => {
console.error("fetchTenantDetails error", e);
alert(t("user-not-exist"));
}
} else if (res?.status === "error") {
const message = res?.message || "something-went-wrong-mssg";
showAlert("danger", t(message));
}
setIsSubmit(false);
setActLoader({});
@@ -1015,7 +1010,10 @@ const DocumentsReport = (props) => {
`${documentId}/${signerMail[i].Email}/${objectId}/${sendMail}`
);
let signPdf = `${hostUrl}/login/${encodeBase64}`;
shareLinkList.push({ signerEmail: signerMail[i].Email, url: signPdf });
shareLinkList.push({
signerEmail: signerMail[i].Email,
url: signPdf
});
}
return shareLinkList.map((data, ind) => {
return (
@@ -1530,14 +1528,32 @@ const DocumentsReport = (props) => {
<div className="w-full h-[100px] flex justify-center items-center z-30">
<Loader />
</div>
) : (
<>
{!extClass?.[0]?.UserId?.emailVerified ? (
<div className="mx-[20px] mt-[15px] mb-[20px]">
<Trans
i18nKey="email-not-verified-send"
components={{
1: (
<Link
to="/profile"
className="text-blue-700 underline cursor-pointer"
/>
)
}}
/>
</div>
) : (
<BulkSendUi
Placeholders={placeholders}
item={templateDeatils}
item={templateDetails}
handleClose={handleQuickSendClose}
signatureType={signatureType}
/>
)}
</>
)}
</ModalUi>
)}
{isShare[item.objectId] && (
@@ -1639,7 +1655,7 @@ const DocumentsReport = (props) => {
{isNextStep[user.Id] && (
<div className="relative">
{actLoader[user.Id] && (
<div className="absolute w-full h-full flex justify-center items-center bg-black bg-opacity-30 z-30">
<div className="absolute w-full h-full flex justify-center items-center bg-black bg-opacity-30 z-[60]">
<Loader />
</div>
)}
@@ -1655,6 +1671,7 @@ const DocumentsReport = (props) => {
message={t("resend-mail-help")}
/>
</div>
<div className="w-full flex flex-col gap-2 text-base-content relative">
<div>
<label
className="text-xs ml-1"
@@ -1711,6 +1728,7 @@ const DocumentsReport = (props) => {
smallscreen
/>
</div>
</div>
<button
type="submit"
className="op-btn op-btn-primary"
@@ -1841,6 +1859,30 @@ const DocumentsReport = (props) => {
</button>
)}
</div>
<ModalUi
isOpen={resendErrMail}
id="error-modal"
title={t("error")}
handleClose={() => setResendErrMail("")}
>
<div className="mx-[20px] mb-[20px] mt-[10px]">
{resendErrMail === "emailnotverified" ? (
<Trans
i18nKey="email-not-verified-send"
components={{
1: (
<Link
to="/profile"
className="text-blue-700 underline cursor-pointer"
/>
)
}}
/>
) : (
<p>{resendErrMail}</p>
)}
</div>
</ModalUi>
<CustomizeMail
setIsMailModal={setIsMailModal}
setCustomizeMail={setCustomizeMail}
@@ -1866,6 +1908,8 @@ const DocumentsReport = (props) => {
? t("mails-sent")
: mailStatus === "quotareached"
? t("quota-mail-head")
: mailStatus === "emailnotverified"
? t("email-not-verified-head")
: t("mail-not-delivered")
}
handleClose={() => {
@@ -1894,6 +1938,20 @@ const DocumentsReport = (props) => {
<div className="flex flex-col gap-y-3">
<div className="my-3">{handleShareList()}</div>
</div>
) : mailStatus === "emailnotverified" ? (
<p>
<Trans
i18nKey="email-not-verified-send"
components={{
1: (
<a
href="/profile"
className="text-blue-700 underline cursor-pointer"
/>
)
}}
/>
</p>
) : (
<div className="mb-[10px]">
{mailStatus === "dailyquotareached" ? (
@@ -1912,7 +1970,9 @@ const DocumentsReport = (props) => {
{mailStatus !== "quotareached" && (
<div
className={
mailStatus === "success" ? "flex justify-center mt-1" : ""
mailStatus === "success" || mailStatus === "emailnotverified"
? "flex justify-center mt-1"
: ""
}
>
{currUserId && (
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from "react";
import pad from "../../assets/images/pad.svg";
import { useLocation, useNavigate } from "react-router";
import { Link, useLocation, useNavigate } from "react-router";
import axios from "axios";
import ModalUi from "../../primitives/ModalUi";
import Alert from "../../primitives/Alert";
@@ -23,9 +23,7 @@ import {
import BulkSendUi from "../../components/bulksend/BulkSendUi";
import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo";
import {
useTranslation
} from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { useElSize } from "../../hook/useElSize";
import LottieWithLoader from "../../primitives/DotLottieReact";
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
@@ -40,9 +38,11 @@ const isSignExist = (placeholders = []) => {
const isSignature =
Array.isArray(placeholders) &&
placeholders?.length > 0 &&
placeholders.every((p) =>
p?.placeHolder?.some((h) => h?.pos?.some((x) => x?.type === "signature"))
placeholders.every((p) => {
return p?.placeHolder?.some((h) =>
h?.pos?.some((x) => x?.type === "signature")
);
});
return isSignature;
};
@@ -314,7 +314,7 @@ const TemplatesReport = (props) => {
setActLoader({});
}
});
//function is called when there ther no any prefill role widget exist then create direct document and navigate
//function is called when there are no any prefill role widget exist then create direct document and navigate
const navigatePageToDoc = utils.withSessionValidation(
async (templateRes, placeholder, signer) => {
setIsPrefillModal({});
@@ -939,7 +939,7 @@ const TemplatesReport = (props) => {
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
showAlert("danger", "please attach all role to signer");
showAlert("danger", t("attach-all-role-to-signer"));
} else if (res?.status === "success") {
setDocumentId(res.id);
setActLoader({});
@@ -996,6 +996,9 @@ const TemplatesReport = (props) => {
} else {
alert(t("user-not-exist"));
}
} else if (res?.status === "error") {
const message = res?.message || "something-went-wrong-mssg";
showAlert("danger", t(message));
}
setIsSubmit(false);
});
@@ -1012,7 +1015,10 @@ const TemplatesReport = (props) => {
`${documentId}/${signerMail[i].Email}/${objectId}/${sendMail}`
);
let signPdf = `${hostUrl}/login/${encodeBase64}`;
shareLinkList.push({ signerEmail: signerMail[i].Email, url: signPdf });
shareLinkList.push({
signerEmail: signerMail[i].Email,
url: signPdf
});
}
return shareLinkList.map((data, ind) => {
return (
@@ -1400,6 +1406,22 @@ const TemplatesReport = (props) => {
<div className="w-full h-[100px] flex justify-center items-center z-30">
<Loader />
</div>
) : (
<>
{!extClass?.[0]?.UserId?.emailVerified ? (
<div className="mx-[20px] mt-[15px] mb-[20px]">
<Trans
i18nKey="email-not-verified-send"
components={{
1: (
<Link
to="/profile"
className="text-blue-700 underline cursor-pointer"
/>
)
}}
/>
</div>
) : (
<BulkSendUi
Placeholders={placeholders}
@@ -1408,6 +1430,8 @@ const TemplatesReport = (props) => {
signatureType={signatureType}
/>
)}
</>
)}
</ModalUi>
)}
{isShare[item.objectId] && (
@@ -1473,7 +1497,7 @@ const TemplatesReport = (props) => {
{isNextStep[user.Id] && (
<div className="relative">
{actLoader[user.Id] && (
<div className="absolute w-full h-full flex justify-center items-center bg-black bg-opacity-30 z-30">
<div className="absolute w-full h-full flex justify-center items-center bg-black bg-opacity-30 z-[60]">
<Loader />
</div>
)}
@@ -1489,6 +1513,7 @@ const TemplatesReport = (props) => {
message={t("resend-mail-help")}
/>
</div>
<div className="w-full flex flex-col gap-2 text-base-content relative">
<div>
<label
className="text-xs ml-1"
@@ -1527,7 +1552,6 @@ const TemplatesReport = (props) => {
className="op-link op-link-primary"
onClick={(e) => handleSwitch(e)}
>
Switch to{" "}
{emailEditorType === "basic"
? t("switch-to-advanced")
: t("switch-to-basic")}
@@ -1546,6 +1570,7 @@ const TemplatesReport = (props) => {
smallscreen
/>
</div>
</div>
<button
type="submit"
className="op-btn op-btn-primary"
@@ -1723,6 +1748,20 @@ const TemplatesReport = (props) => {
</div>
) : mailStatus === "failed" ? (
<p>{t("mail-failed")} </p>
) : mailStatus === "emailnotverified" ? (
<p>
<Trans
i18nKey="email-not-verified-send"
components={{
1: (
<a
href="/profile"
className="text-blue-700 underline cursor-pointer"
/>
)
}}
/>
</p>
) : (
<div className="mb-[10px]">
{!templateDetails?.SendinOrder &&
+2 -1
View File
@@ -130,7 +130,8 @@
font-size: 14px;
cursor: pointer;
z-index: 2;
background: white;
filter: drop-shadow(1px 1px white) drop-shadow(1px -1px white)
drop-shadow(1px 0px white) drop-shadow(-1px 1px white)
}
.ScrollbarsCustom-TrackY {
@@ -0,0 +1,68 @@
import { PDFDocument } from "pdf-lib";
export const clearAcroFields = async (pdfFile) => {
const pdfDoc = await PDFDocument.load(pdfFile, { ignoreEncryption: true });
try {
const acroFormEntry = pdfDoc.catalog.get(PDFName.of("AcroForm"));
const acroForm = pdfDoc.context.lookupMaybe
? pdfDoc.context.lookupMaybe(acroFormEntry)
: pdfDoc.context.lookup(acroFormEntry);
if (acroForm && typeof acroForm.set === "function") {
// Avoid pdf-lib form APIs here; some malformed PDFs crash while
// iterating/removing fields. Clearing /Fields directly is safer.
acroForm.set(PDFName.of("Fields"), pdfDoc.context.obj([]));
acroForm.delete(PDFName.of("XFA"));
acroForm.delete(PDFName.of("SigFlags"));
}
} catch {
// If AcroForm is malformed, continue with page annotation cleanup.
}
for (const page of pdfDoc.getPages()) {
try {
const annotationsRef = page.node.get(PDFName.of("Annots"));
if (!annotationsRef) continue;
const annotations = pdfDoc.context.lookup(annotationsRef);
if (!annotations || !annotations.asArray) continue;
const filtered = annotations.asArray().filter((annotRef) => {
try {
const annot = pdfDoc.context.lookup(annotRef);
const subtype = annot?.get(PDFName.of("Subtype"));
return subtype?.toString() !== "/Widget";
} catch {
return true;
}
});
if (filtered.length === 0) {
page.node.delete(PDFName.of("Annots"));
} else {
page.node.set(PDFName.of("Annots"), pdfDoc.context.obj(filtered));
}
} catch {
// best effort cleanup
}
}
try {
pdfDoc.catalog.delete(PDFName.of("AcroForm"));
} catch {
// best effort cleanup
}
return await pdfDoc.save({ useObjectStreams: false });
};
export async function isPdfPasswordProtected(pdfBytes) {
try {
await PDFDocument.load(pdfBytes);
return false; // PDF opened normally
} catch (error) {
console.error("Error in isPdfPasswordProtected:", error);
throw error; // Other error
}
}
+56 -12
View File
@@ -7,7 +7,8 @@ import {
embedWidgetsToDoc,
randomId,
getBase64FromUrl,
drawWidget
drawWidget,
getSignedUrl
} from "../constant/Utils";
import { PDFDocument } from "pdf-lib";
@@ -121,6 +122,7 @@ export const isValidPrefill = (prefillData) => {
}
}
};
//function to use create document from templateAdd commentMore actions
export const handleCheckPrefillCreateDoc = async (
xyPosition,
@@ -132,13 +134,21 @@ export const handleCheckPrefillCreateDoc = async (
prefillImg,
userId
) => {
const pdfArrayBuffer = await convertPdfArrayBuffer(updatedPdfUrl);
let url;
let pdfArrayBuffer;
try {
url = await getSignedUrl(updatedPdfUrl, "", pdfDetails?.[0]?.objectId);
pdfArrayBuffer = await convertPdfArrayBuffer(url);
if (pdfArrayBuffer === "Error") {
return { status: "error", message: "failed-pdf-for-prefill-processing" };
}
} catch (e) {
return { status: "error", message: "failed-pdf-for-prefill-processing" };
}
const prefillData = xyPosition.find((x) => x.Role === "prefill");
if (prefillData) {
const res = isValidPrefill(prefillData);
if (res) {
return res;
}
if (res) return res;
}
const removePrefill = xyPosition.filter((data) => data.Role !== "prefill");
const isAllAttachSigner =
@@ -147,8 +157,7 @@ export const handleCheckPrefillCreateDoc = async (
setIsPrefillModal(false);
const prefillDetails = xyPosition.find((data) => data.Role === "prefill");
let signedUrl;
//condition to check prefill widgets exit or not if exist then embed prefill widgets value in template
//and then create document
//condition to check prefill widgets exit or not if exist then embed prefill widgets value in template and then create document
if (prefillDetails) {
signedUrl = await handleEmbedPrefillToDoc(
prefillDetails,
@@ -158,7 +167,7 @@ export const handleCheckPrefillCreateDoc = async (
userId
);
} else {
signedUrl = pdfDetails[0]?.URL;
signedUrl = url;
}
const isSendDoc = true;
const res = await createDocument(
@@ -168,9 +177,7 @@ export const handleCheckPrefillCreateDoc = async (
signedUrl || updatedPdfUrl,
isSendDoc
);
if (res.status === "success") {
return res;
} else if (res.status === "error") {
if (res?.status === "success" || res?.status === "error") {
return res;
}
} else {
@@ -229,10 +236,47 @@ export const handleSignersList = (item) => {
blockColor: x.blockColor
};
} else {
return { Role: x.Role, Id: x.Id, blockColor: x.blockColor };
return {
Role: x.Role,
Id: x.Id,
blockColor: x.blockColor
};
}
});
return updatedSigners;
};
export const normalizeKey = (value) => value?.trim()?.toLowerCase() || "";
export const handleUpdateTemplateLinks = async (
xyPosition,
templateId,
mode
) => {
//save prefill widgets updated response in contracts_templateLinks class instead of contracts_Template
//so that prefill values do not overwrite the original template
try {
const getPrefillData = xyPosition?.find((x) => x?.Role === "prefill");
if (getPrefillData) {
const prefillPlaceholder = getPrefillData?.placeHolder || [];
const query = new Parse.Query("contracts_templateLinks");
const templatePtr = {
__type: "Pointer",
className: "contracts_Template",
objectId: templateId
};
query.equalTo("TemplatePtr", templatePtr);
query.equalTo("Type", mode);
let templateLink = await query.first();
if (!templateLink) {
templateLink = new Parse.Object("contracts_templateLinks");
templateLink.set("Type", mode);
templateLink.set("TemplatePtr", templatePtr);
}
templateLink.set("Placeholders", prefillPlaceholder);
await templateLink.save();
}
} catch (e) {
console.error("handleUpdateTemplate error", e);
}
};
+13 -4
View File
@@ -66,10 +66,11 @@ export const saveToMySign = async (widget) => {
};
// helper: does this signer already have any signature widget?
export const hasSignatureWidget = (s) =>
(s.placeHolder ?? []).some((ph) =>
export const hasSignatureWidget = (s) => {
return (s.placeHolder ?? []).some((ph) =>
(ph?.pos ?? []).some((p) => p?.type === "signature")
);
};
export const widgetNamesArr = (placeholders, signerId) => {
const widgetNames =
@@ -490,11 +491,19 @@ export const loadPdfOnce = async (url) => {
};
export const mailModalHead = (sendinOrder, mailStatus, isOwner) => {
const HEAD = { success: "mails-sent", quotareached: "quota-mail-head" };
const HEAD = {
success: "mails-sent",
quotareached: "quota-mail-head",
emailnotverified: "email-not-verified-head"
};
return sendinOrder
? isOwner
? "mail-status-head"
? mailStatus === "emailnotverified"
? "email-not-verified-head"
: "mail-status-head"
: mailStatus === "emailnotverified"
? "email-not-verified-head"
: "mails-sent"
: (HEAD[mailStatus] ?? "mail-not-delivered");
};
+491 -32
View File
@@ -2,7 +2,15 @@ import dotenv from 'dotenv';
import { format, toZonedTime } from 'date-fns-tz';
import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
import crypto from 'node:crypto';
import { PDFDocument, rgb } from 'pdf-lib';
import {
PDFDocument,
PDFName,
rgb,
degrees,
// StandardFonts,
PDFArray,
PDFDict,
} from 'pdf-lib';
import { parseUploadFile } from './utils/fileUtils.js';
dotenv.config({ quiet: true });
@@ -149,42 +157,485 @@ export function generateId(length) {
}
/**
* FlattenPdf is used to remove existing widgets if present any and flatten pdf.
* FlattenPdf renders field values as static content and removes the interactive
* form layer. Signatures are stripped entirely. Non-widget annotations (links,
* comments, stamps) are preserved.
* @param {string | Uint8Array | ArrayBuffer} pdfFile - pdf file.
* @returns {Promise<Uint8Array>} flatPdf - pdf file in unit8arry
* @returns {Promise<Uint8Array>} flatPdf - pdf file in Uint8Array
*/
export const flattenPdf = async pdfFile => {
const pdfDoc = await PDFDocument.load(pdfFile, { ignoreEncryption: true });
try {
const pdfDoc = await PDFDocument.load(pdfFile);
// Get the form
const form = pdfDoc.getForm();
// fetch form fields
const fields = form.getFields();
// remove form all existing fields and their widgets
if (fields && fields?.length > 0) {
const acroFormEntry = pdfDoc.catalog.get(PDFName.of('AcroForm'));
const acroForm = pdfDoc.context.lookupMaybe
? pdfDoc.context.lookupMaybe(acroFormEntry)
: pdfDoc.context.lookup(acroFormEntry);
if (acroForm && typeof acroForm.set === 'function') {
// Avoid pdf-lib form APIs here; some malformed PDFs crash while
// iterating/removing fields. Clearing /Fields directly is safer.
acroForm.set(PDFName.of('Fields'), pdfDoc.context.obj([]));
acroForm.delete(PDFName.of('XFA'));
acroForm.delete(PDFName.of('SigFlags'));
}
} catch {
// If AcroForm is malformed, continue with page annotation cleanup.
}
for (const page of pdfDoc.getPages()) {
try {
for (const field of fields) {
while (field.acroField.getWidgets().length) {
field.acroField.removeWidget(0);
const annotationsRef = page.node.get(PDFName.of('Annots'));
if (!annotationsRef) continue;
const annotations = pdfDoc.context.lookup(annotationsRef);
if (!annotations || !annotations.asArray) continue;
const filtered = annotations.asArray().filter(annotRef => {
try {
const annot = pdfDoc.context.lookup(annotRef);
const subtype = annot?.get(PDFName.of('Subtype'));
return subtype?.toString() !== '/Widget';
} catch {
return true;
}
form.removeField(field);
});
if (filtered.length === 0) {
page.node.delete(PDFName.of('Annots'));
} else {
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
}
} catch (err) {
console.log('err while removing field from pdf', err);
} catch {
// best effort cleanup
}
}
// 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 flatPdf = await pdfDoc.save({ useObjectStreams: false });
return flatPdf;
} catch (err) {
console.log('err ', err);
throw new Error('error in pdf');
try {
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
} catch {
// best effort cleanup
}
return await pdfDoc.save({ useObjectStreams: false });
};
/* ---- flattenPdf private helpers ---- */
function _safeGetWidgetsServer(field) {
try {
return field.acroField?.getWidgets?.() || [];
} catch {
return [];
}
}
function _getWidgetRectServer(widget, pdfDoc) {
try {
const r = widget.getRectangle?.();
if (r && isFinite(r.x) && isFinite(r.y) && isFinite(r.width) && isFinite(r.height)) {
return r;
}
} catch {
/* fall through to manual extraction */
}
try {
const rectArr = widget.dict?.lookup?.(PDFName.of('Rect'));
if (!rectArr || typeof rectArr.size !== 'function' || rectArr.size() !== 4) return null;
const x1 = _numberFromPdfObjectServer(rectArr.get(0));
const y1 = _numberFromPdfObjectServer(rectArr.get(1));
const x2 = _numberFromPdfObjectServer(rectArr.get(2));
const y2 = _numberFromPdfObjectServer(rectArr.get(3));
if ([x1, y1, x2, y2].some(v => !isFinite(v))) return null;
return {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
};
} catch {
return null;
}
}
function _numberFromPdfObjectServer(obj) {
if (!obj) return NaN;
if (typeof obj.asNumber === 'function') return obj.asNumber();
if (typeof obj.numberValue === 'function') return obj.numberValue();
return Number(obj?.value ?? obj);
}
function _getWidgetPageServer(pdfDoc, pages, widget) {
try {
const pRef = widget.P?.();
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
try {
const pRef = widget.dict?.get?.(PDFName.of('P'));
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
// Fallback: search page annots
try {
for (const page of pages) {
const annots = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
if (!annots) continue;
for (let i = 0; i < annots.size(); i++) {
const ref = annots.get(i);
if (ref === widget.ref) return page;
}
}
} catch {
/* fall through */
}
return null;
}
function _drawWidgetBoxServer(page, rect) {
try {
page.drawRectangle({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
borderWidth: 0.6,
borderColor: rgb(0.65, 0.65, 0.65),
});
} catch {
/* best effort */
}
}
function _drawTextFieldServer(page, field, rect, font) {
let text = '';
try {
text = field.getText?.() ?? '';
} catch {
text = '';
}
text = String(text ?? '');
if (!text) return;
let multiline = false;
try {
multiline = field.isMultiline?.() ?? false;
} catch {
/* ignore */
}
let comb = false;
try {
comb = field.isCombed?.() ?? false;
} catch {
/* ignore */
}
if (comb) {
_drawCombTextServer(page, text, rect, font);
return;
}
if (multiline || text.includes('\n')) {
_drawMultilineTextServer(page, text, rect, font);
return;
}
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
const baselineY = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(text, {
x: rect.x + 2,
y: baselineY,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
});
}
function _drawMultilineTextServer(page, text, rect, font) {
const lines = String(text).replace(/\r/g, '').split('\n');
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
lineHeight,
});
y -= lineHeight;
}
}
function _drawCombTextServer(page, text, rect, font) {
const chars = String(text).split('');
const count = Math.max(chars.length, 1);
const cellWidth = rect.width / count;
const fontSize = Math.max(8, Math.min(12, rect.height - 4));
chars.forEach((ch, i) => {
const textWidth = font.widthOfTextAtSize(ch, fontSize);
const x = rect.x + i * cellWidth + (cellWidth - textWidth) / 2;
const y = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(ch, {
x,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
});
if (i < count - 1) {
try {
page.drawLine({
start: { x: rect.x + (i + 1) * cellWidth, y: rect.y },
end: { x: rect.x + (i + 1) * cellWidth, y: rect.y + rect.height },
thickness: 0.4,
color: rgb(0.75, 0.75, 0.75),
});
} catch {
/* best effort */
}
}
});
}
function _fitSingleLineFontSizeServer(text, rect, font) {
let size = Math.min(12, rect.height - 4);
size = Math.max(size, 6);
while (size > 6) {
const width = font.widthOfTextAtSize(text, size);
if (width <= rect.width - 4) return size;
size -= 0.5;
}
return 6;
}
function _drawCheckBoxServer(page, field, rect, zapf) {
let checked = false;
try {
checked = field.isChecked();
} catch {
checked = false;
}
if (!checked) return;
const size = Math.max(8, Math.min(rect.width, rect.height) - 4);
page.drawText('\u2714', {
x: rect.x + Math.max(1, (rect.width - size * 0.7) / 2),
y: rect.y + Math.max(1, (rect.height - size) / 2),
size,
font: zapf,
color: rgb(0, 0, 0),
});
}
function _drawRadioGroupServer(page, field, widget, rect) {
let selected = null;
try {
selected = field.getSelected();
} catch {
selected = null;
}
if (!selected) return;
let widgetOnValue = null;
try {
widgetOnValue = widget.getOnValue?.();
} catch {
/* ignore */
}
if (!widgetOnValue) {
try {
const ap = widget.dict?.lookupMaybe?.(PDFName.of('AP'), PDFDict);
const n = ap?.lookupMaybe?.(PDFName.of('N'), PDFDict);
if (n) {
const keys = n.keys();
for (const k of keys) {
const name = k?.decodeText?.() ?? k?.encodedName ?? String(k);
if (name !== '/Off' && name !== 'Off') {
widgetOnValue = name.replace(/^\//, '');
break;
}
}
}
} catch {
/* ignore */
}
}
const selectedStr = String(selected).replace(/^\//, '');
const onStr = String(widgetOnValue ?? '').replace(/^\//, '');
if (!onStr || selectedStr !== onStr) return;
// Circle outline
try {
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: rect.width / 2 - 1,
yScale: rect.height / 2 - 1,
borderWidth: 0.8,
borderColor: rgb(0, 0, 0),
});
} catch {
/* best effort */
}
// Inner filled dot
try {
const r = Math.min(rect.width, rect.height) / 4;
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: r,
yScale: r,
color: rgb(0, 0, 0),
});
} catch {
/* best effort */
}
}
function _drawDropdownServer(page, field, rect, font) {
let text = '';
try {
const selected = field.getSelected?.();
if (Array.isArray(selected)) {
text = selected.join(', ');
} else {
text = selected ?? '';
}
} catch {
text = '';
}
text = String(text ?? '');
if (!text) return;
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
page.drawText(text, {
x: rect.x + 2,
y: rect.y + Math.max(2, (rect.height - fontSize) / 2),
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 12),
});
}
function _drawOptionListServer(page, field, rect, font) {
let selected = [];
try {
selected = field.getSelected?.() || [];
} catch {
selected = [];
}
if (!Array.isArray(selected)) {
selected = [selected].filter(Boolean);
}
if (!selected.length) return;
const lines = selected.map(v => String(v));
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
});
y -= lineHeight;
}
}
/** Remove only Widget annotations; preserve links, stamps, comments, etc. */
function _removeWidgetAnnotationsServer(pdfDoc) {
for (const page of pdfDoc.getPages()) {
try {
const annotationsRef = page.node.get(PDFName.of('Annots'));
if (!annotationsRef) continue;
const annotations = pdfDoc.context.lookup(annotationsRef);
if (!annotations || !annotations.asArray) continue;
const filtered = annotations.asArray().filter(annotRef => {
try {
const annot = pdfDoc.context.lookup(annotRef);
const subtype = annot?.get(PDFName.of('Subtype'));
return subtype?.toString() !== '/Widget';
} catch {
return true;
}
});
if (filtered.length === 0) {
page.node.delete(PDFName.of('Annots'));
} else {
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
}
} catch {
/* best effort */
}
}
try {
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
} catch {
/* best effort */
}
}
// Format date and time for the selected timezone
export const formatTimeInTimezone = (date, timezone) => {
const nyDate = timezone && toZonedTime(date, timezone);
@@ -196,7 +647,7 @@ export const formatTimeInTimezone = (date, timezone) => {
// `getSecureUrl` is used to return local secure url if local files
export const getSecureUrl = url => {
const fileUrl = new URL(url)?.pathname?.includes('files');
const fileUrl = new URL(url)?.pathname?.includes('/files/');
if (fileUrl) {
try {
const file = getSignedLocalUrl(url);
@@ -288,13 +739,21 @@ export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
? format(zonedDate, `${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`, { timeZone })
: formatTimeInTimezone(date, timeZone);
}
export const randomId = () => {
const randomBytes = crypto.getRandomValues(new Uint16Array(1));
const randomValue = randomBytes[0];
const randomDigit = 1000 + (randomValue % 9000);
return randomDigit;
};
export const randomId = (digit = 8) => {
// 1. Grab a cryptographically-secure 32-bit random value
// Use crypto for stronger randomness
const randomBytes = crypto.getRandomValues(new Uint32Array(1));
const raw = randomBytes[0]; // 0 … 4,294,967,295
// Calculate the min and max for the given digit length
const min = Math.pow(10, digit - 1); // e.g., digit=3 → 100
const max = Math.pow(10, digit) - 1; // e.g., digit=3 → 999
const range = max - min + 1;
// Collapse random value into the range and shift
return min + (raw % range);
};
export const handleValidImage = async Placeholder => {
const updatedPlaceholders = [];
@@ -61,7 +61,7 @@ async function deleteLocalFile(fileUrl) {
try {
const url = new URL(fileUrl);
const filePath = decodeURIComponent(url.pathname);
if (!filePath.includes('files')) return;
if (!filePath.includes('/files/')) return;
const localPath = url?.pathname?.split(`/files/${serverAppId}/`)?.pop();
@@ -1,9 +1,5 @@
import axios from 'axios';
import { appName, cloudServerUrl, serverAppId } from '../../../Utils.js';
const serverUrl = cloudServerUrl;
const appId = serverAppId;
const masterKey = process.env.MASTER_KEY;
import { appName } from '../../../Utils.js';
import sendSystemMail from '../../parsefunction/sendSystemMail.js';
// Constants (adjust to your preference)
export const OTP_LENGTH = 6;
@@ -60,12 +56,7 @@ export async function sendDeleteOtpEmail(extUser, otp) {
</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 });
return sendSystemMail({ params });
}
export function msUntil(nowMs, futureMs) {
+2
View File
@@ -63,6 +63,7 @@ import getSignature from './parsefunction/getSignature.js';
import updateEmailTemplates from './parsefunction/updateEmailTemplates.js';
import triggerEvent from './parsefunction/triggerEvent.js';
import setWidgetPreferences from './parsefunction/setWidgetPreferences.js';
import createDocumentFromApp from './parsefunction/createDocumentFromApp.js';
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
@@ -136,3 +137,4 @@ Parse.Cloud.define('getdefaultsignature', getSignature);
Parse.Cloud.define('updateemailtemplates', updateEmailTemplates);
Parse.Cloud.define('triggerevent', triggerEvent);
Parse.Cloud.define('setwidgetpreferences', setWidgetPreferences);
Parse.Cloud.define('createdocumentfromapp', createDocumentFromApp);
@@ -1,7 +1,7 @@
async function DocumentAftersave(request) {
try {
if (!request.original) {
console.log('new entry is insert in contracts_Document');
console.log('new entry is insert in contracts_Document ', request?.object?.id);
const obj = request.object;
const objId = obj?.id;
const createdAt = obj?.get?.('createdAt');
@@ -26,6 +26,8 @@ export default async function GetTemplate(request) {
template.include('CreatedBy');
template.include('ExtUserPtr.TenantId');
template.include('Bcc');
template.include('Cc');
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('Email', userEmail);
extUserQuery.include('TeamIds');
@@ -62,6 +64,7 @@ export default async function GetTemplate(request) {
template.include('ExtUserPtr.TenantId');
template.include('Placeholders.signerPtr');
template.include('Bcc');
template.include('Cc');
}
}
const res = await template.first({ useMasterKey: true });
@@ -1,7 +1,7 @@
export default async function TemplateAfterSave(request) {
try {
if (!request.original) {
console.log('new entry is insert in contracts_Template');
console.log('new entry is insert in contracts_Template', request?.object?.id);
const obj = request.object;
const objId = obj?.id;
const ip = request?.headers?.['x-real-ip'] || '';
@@ -3,6 +3,7 @@ import { cloudServerUrl, mailTemplate, replaceMailVaribles, serverAppId } from '
import { setDocumentCount } from '../../utils/CountUtils.js';
import crypto from 'crypto';
import sendSystemMail from './sendSystemMail.js';
function chunkArray(arr, size) {
const out = [];
@@ -50,9 +51,6 @@ async function sendOwnerSummaryEmail({
failedList,
}) {
try {
const url = `${serverUrl}/functions/sendmailv3`;
const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId };
const subject = `Bulk send finished: ${failed} of ${total} failed to create`;
const failureHtml = failedList?.length
@@ -83,7 +81,7 @@ async function sendOwnerSummaryEmail({
html,
};
await axios.post(url, params, { headers });
await sendSystemMail({ params });
} catch (e) {
console.log('batchdoc Failed to send owner summary email:', e?.message || e);
}
@@ -92,7 +90,7 @@ async function sendOwnerSummaryEmail({
async function deductcount(docsCount, extUserId) {
try {
if (extUserId) {
setDocumentCount(extUserId);
setDocumentCount(extUserId, docsCount);
}
} catch (err) {
console.log('batchdoc deductcount error: ', err);
@@ -230,6 +228,7 @@ async function startBulkSendInBackground(userId, Documents, Ip, parseConfig, typ
Description: x.Description,
CreatedBy: x.CreatedBy,
SendinOrder: x.SendinOrder || true,
SendInOrderStrict: x.SendInOrderStrict || false,
ExtUserPtr: {
__type: 'Pointer',
className: x.ExtUserPtr.className,
@@ -272,6 +271,7 @@ async function startBulkSendInBackground(userId, Documents, Ip, parseConfig, typ
...(x?.SignatureType ? { SignatureType: x?.SignatureType } : {}),
...(x?.NotifyOnSignatures ? { NotifyOnSignatures: x?.NotifyOnSignatures } : {}),
...(x?.Bcc?.length > 0 ? { Bcc: x?.Bcc } : {}),
...(x?.Cc?.length > 0 ? { Cc: x?.Cc } : {}),
...(x?.RedirectUrl ? { RedirectUrl: x?.RedirectUrl } : {}),
...(mailBody ? { RequestBody: mailBody } : {}),
...(mailSubject ? { RequestSubject: mailSubject } : {}),
@@ -303,7 +303,6 @@ async function startBulkSendInBackground(userId, Documents, Ip, parseConfig, typ
createdAt: response.data[0]?.success?.createdAt,
};
deductcount(response.data.length, resExt.id);
console.log('here');
sendMail(updateDocuments, publicUrl); //sessionToken
return { total: 1, created: 1, failed: 0 };
}
@@ -0,0 +1,99 @@
import { setDocumentCount } from '../../utils/CountUtils.js';
export default async function createDocumentFromApp(request) {
const doc = request.params?.document;
if (!doc) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'Missing document payload.');
}
if (!request.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
}
const SentToOthers = doc.SentToOthers !== undefined ? doc.SentToOthers : false;
const SendinOrder = doc.SendinOrder !== undefined ? doc.SendinOrder : false;
const SendInOrderStrict = doc.SendInOrderStrict !== undefined ? !!doc.SendInOrderStrict : false;
const IsEnableOTP = doc?.IsEnableOTP !== undefined ? doc?.IsEnableOTP : false;
const IsTourEnabled = doc?.IsTourEnabled !== undefined ? doc?.IsTourEnabled : false;
const AllowModifications =
doc?.AllowModifications !== undefined ? doc?.AllowModifications : false;
const AutomaticReminders =
doc?.AutomaticReminders !== undefined ? doc?.AutomaticReminders : false;
const NotifyOnSignatures = doc.NotifyOnSignatures !== undefined ? doc.NotifyOnSignatures : false;
try {
const docCls = new Parse.Object('contracts_Document');
docCls.set('Name', doc?.Name || 'untitled document');
docCls.set('URL', doc?.URL);
docCls.set('ExtUserPtr', doc.ExtUserPtr);
docCls.set('CreatedBy', doc.CreatedBy);
if (doc.Description) {
docCls.set('Description', doc.Description);
}
if (doc.Note) {
docCls.set('Note', doc.Note);
}
if (doc.SignedUrl) {
docCls.set('SignedUrl', doc.SignedUrl);
}
docCls.set('SentToOthers', SentToOthers);
docCls.set('SendinOrder', SendinOrder);
docCls.set('SendInOrderStrict', SendInOrderStrict);
docCls.set('IsEnableOTP', IsEnableOTP);
docCls.set('IsTourEnabled', IsTourEnabled);
docCls.set('AllowModifications', AllowModifications);
docCls.set('AutomaticReminders', AutomaticReminders);
docCls.set('NotifyOnSignatures', NotifyOnSignatures);
if (doc.TimeToCompleteDays !== undefined) {
docCls.set('TimeToCompleteDays', Number(doc.TimeToCompleteDays));
}
if (doc.RemindOnceInEvery !== undefined) {
docCls.set('RemindOnceInEvery', Number(doc.RemindOnceInEvery));
}
if (doc?.DocSentAt?.iso) {
docCls.set('DocSentAt', new Date(doc?.DocSentAt?.iso));
}
if (doc.RedirectUrl) {
docCls.set('RedirectUrl', doc.RedirectUrl);
}
if (doc.TemplateId) {
docCls.set('TemplateId', doc.TemplateId);
}
if (Array.isArray(doc.Signers) && doc?.Signers?.length > 0) {
docCls.set('Signers', doc.Signers);
}
if (Array.isArray(doc.Placeholders) && doc?.Placeholders?.length > 0) {
docCls.set('Placeholders', doc.Placeholders);
}
if (Array.isArray(doc.SignatureType) && doc.SignatureType.length > 0) {
docCls.set('SignatureType', doc.SignatureType);
}
if (Array.isArray(doc.Bcc) && doc?.Bcc?.length > 0) {
docCls.set('Bcc', doc.Bcc);
}
if (Array.isArray(doc.Cc) && doc?.Cc?.length > 0) {
docCls.set('Cc', doc.Cc);
}
if (Array.isArray(doc?.PenColors) && doc?.PenColors?.length > 0) {
docCls.set('PenColors', doc.PenColors);
}
const docRes = await docCls.save(null, { useMasterKey: true });
// update documentCount in Users and tenant account
setDocumentCount(doc?.ExtUserPtr?.id);
return docRes;
} catch (error) {
console.log('error in create document from app: ', error);
throw error;
}
}
@@ -38,6 +38,7 @@ export default async function createDuplicate(request) {
newTemplate.set('SignedUrl', _templateRes.SignedUrl);
newTemplate.set('SentToOthers', _templateRes?.SentToOthers || false);
newTemplate.set('SendinOrder', _templateRes?.SendinOrder || false);
newTemplate.set('SendInOrderStrict', _templateRes?.SendInOrderStrict || false);
newTemplate.set('AutomaticReminders', _templateRes?.AutomaticReminders || false);
newTemplate.set('RemindOnceInEvery', _templateRes?.RemindOnceInEvery || 5);
newTemplate.set('IsEnableOTP', _templateRes?.IsEnableOTP || false);
@@ -77,6 +78,9 @@ export default async function createDuplicate(request) {
if (_templateRes?.Bcc?.length) {
newTemplate.set('Bcc', _templateRes?.Bcc);
}
if (_templateRes?.Cc?.length) {
newTemplate.set('Cc', _templateRes?.Cc);
}
const OriginIp = _templateRes?.OriginIp || request?.headers?.['x-real-ip'] || '';
if (OriginIp) {
@@ -1,13 +1,5 @@
import axios from 'axios';
import { appName, cloudServerUrl, serverAppId } from '../../Utils.js';
const serverUrl = cloudServerUrl;
const APPID = serverAppId;
const masterKEY = process.env.MASTER_KEY;
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
};
import { appName } from '../../Utils.js';
import sendSystemMail from './sendSystemMail.js';
async function sendDeclineMail(doc, publicUrl, userId, reason) {
try {
@@ -46,7 +38,7 @@ async function sendDeclineMail(doc, publicUrl, userId, reason) {
pdfName: pdfName,
html: body,
};
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
await sendSystemMail({ params });
} catch (err) {
console.log('err in sendnotifymail', err);
}
@@ -63,16 +55,21 @@ export default async function declinedocument(request) {
try {
const docCls = new Parse.Query('contracts_Document');
docCls.include('ExtUserPtr.TenantId,Placeholders.signerPtr,Signers');
docCls.notEqualTo('IsCompleted', true);
docCls.notEqualTo('IsArchive', true);
const updateDoc = await docCls.get(docId, { useMasterKey: true });
if (updateDoc) {
const _doc = JSON.parse(JSON.stringify(updateDoc));
const isEnableOTP = updateDoc?.get('IsEnableOTP') || false;
const isCreator = _doc?.CreatedBy?.objectId === userId;
if (!isEnableOTP) {
updateDoc.set('IsDeclined', true);
updateDoc.set('DeclineReason', reason);
updateDoc.set('DeclineBy', declineBy);
await updateDoc.save(null, { useMasterKey: true });
if (!isCreator) {
sendDeclineMail(_doc, publicUrl, userId, reason);
}
return 'document declined';
} else {
if (!request?.user) {
@@ -82,7 +79,10 @@ export default async function declinedocument(request) {
updateDoc.set('DeclineReason', reason);
updateDoc.set('DeclineBy', declineBy);
await updateDoc.save(null, { useMasterKey: true });
const isCreator = _doc?.CreatedBy?.objectId === request?.user?.id;
if (!isCreator) {
sendDeclineMail(_doc, publicUrl, userId, reason);
}
return 'document declined';
}
} else {
@@ -17,11 +17,6 @@ async function uploadFile(pdfName, filepath) {
const filedata = fs.readFileSync(filepath);
let fileUrl;
// const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
// await file.save({ useMasterKey: true });
// const fileRes = getSecureUrl(file.url());
// fileUrl = fileRes.url;
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
fileUrl = getSecureUrl(fileRes?.url)?.url;
return { imageUrl: fileUrl };
@@ -43,7 +43,7 @@ function makeS3Client() {
}
export default async function getPresignedUrl(url) {
if (url?.includes('files')) {
if (url?.includes('/files/')) {
return presignedlocalUrl(url);
} else {
const client = makeS3Client();
@@ -70,7 +70,7 @@ export async function getSignedUrl(request) {
if (docId || templateId) {
try {
if (url?.includes('files')) {
if (url?.includes('/files/')) {
return presignedlocalUrl(url);
} else if (useLocal !== 'true') {
const query = new Parse.Query(docId ? 'contracts_Document' : 'contracts_Template');
@@ -106,7 +106,7 @@ export async function getSignedUrl(request) {
if (!isAuth) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
} else {
if (url?.includes('files')) {
if (url?.includes('/files/')) {
return presignedlocalUrl(url);
} else if (useLocal !== 'true') {
const presignedUrl = await getPresignedUrl(url);
@@ -147,7 +147,7 @@ export function getSignedLocalUrl(fileUrl, expirationTimeInSeconds) {
}
export function presignedlocalUrl(signedUrl, expirationTimeInSeconds) {
if (signedUrl?.includes('files')) {
if (signedUrl?.includes('/files/')) {
const fileUrl = signedUrl.split('?')?.[0];
const secretKey = process.env.MASTER_KEY;
const exp = expirationTimeInSeconds || 200;
@@ -3,6 +3,13 @@ import fs from 'node:fs';
import fontkit from '@pdf-lib/fontkit';
import { formatDateTime } from '../../../Utils.js';
const formatDateStr = (dateStr, DateFormat, timezone, Is12Hr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
return formatDateTime(date, DateFormat, timezone, Is12Hr);
};
export default async function GenerateCertificate(docDetails) {
const timezone = docDetails?.ExtUserPtr?.Timezone || '';
const Is12Hr = docDetails?.ExtUserPtr?.Is12HourTime || false;
@@ -12,7 +19,9 @@ export default async function GenerateCertificate(docDetails) {
const fontBytes = fs.readFileSync('./font/times.ttf'); //
pdfDoc.registerFontkit(fontkit);
const timesRomanFont = await pdfDoc.embedFont(fontBytes, { subset: true });
const pngUrl = fs.readFileSync('./logo.png').buffer;
const pngUrl = fs.readFileSync('./images/logo.png').buffer;
const naSignUrl = fs.readFileSync('./images/na_sign.png').buffer;
const nasign = await pdfDoc.embedPng(naSignUrl);
const pngImage = await pdfDoc.embedPng(pngUrl);
const page = pdfDoc.addPage();
const { width, height } = page.getSize();
@@ -29,7 +38,7 @@ export default async function GenerateCertificate(docDetails) {
const textKeyColor = rgb(0.12, 0.12, 0.12);
const textValueColor = rgb(0.3, 0.3, 0.3);
const completedAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
const completedAtperTimezone = formatDateTime(completedAt, DateFormat, timezone, Is12Hr);
const completedAtperTimezone = formatDateStr(completedAt, DateFormat, timezone, Is12Hr);
const completedUTCtime = completedAtperTimezone;
const signersCount = docDetails?.Signers?.length || 1;
const generateAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
@@ -43,9 +52,19 @@ export default async function GenerateCertificate(docDetails) {
const company = docDetails?.ExtUserPtr?.Company || '';
const documentHash = docDetails?.DocumentHash || '';
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
const createdAtperTimezone = formatDateTime(createdAt, DateFormat, timezone, Is12Hr);
const createdAtperTimezone = formatDateStr(createdAt, DateFormat, timezone, Is12Hr);
const IsEnableOTP = docDetails?.IsEnableOTP || false;
const filteredaudit = docDetails?.AuditTrail?.filter(x => x?.UserPtr?.objectId);
const placeholders = Array.isArray(docDetails?.Placeholders) ? docDetails.Placeholders : [];
const filteredaudit = docDetails?.AuditTrail?.filter(x => {
if (!x?.UserPtr?.objectId) return false;
return true;
});
const toTs = v => {
if (!v) return 0;
if (typeof v === 'object' && v?.iso) return new Date(v.iso).getTime() || 0;
const t = new Date(v).getTime();
return Number.isFinite(t) ? t : 0;
};
const auditTrail =
docDetails?.Signers?.length > 0
? filteredaudit?.map(x => {
@@ -56,6 +75,7 @@ export default async function GenerateCertificate(docDetails) {
SignedOn: x?.SignedOn || generatedUTCTime,
ViewedOn: x?.ViewedOn || x?.SignedOn || generatedUTCTime,
Signature: x?.Signature || '',
_signedOnTs: toTs(x?.SignedOn),
};
})
: [
@@ -65,8 +85,12 @@ export default async function GenerateCertificate(docDetails) {
SignedOn: filteredaudit[0]?.SignedOn || generatedUTCTime,
ViewedOn: filteredaudit[0]?.ViewedOn || filteredaudit[0]?.SignedOn || generatedUTCTime,
Signature: filteredaudit[0]?.Signature || '',
_signedOnTs: toTs(filteredaudit[0]?.SignedOn),
},
];
if (Array.isArray(auditTrail)) {
auditTrail.sort((a, b) => (a?._signedOnTs || 0) - (b?._signedOnTs || 0));
}
const ownerName = docDetails?.SenderName || docDetails.ExtUserPtr?.Name || 'n/a';
const ownerEmail = docDetails?.SenderMail || docDetails.ExtUserPtr?.Email || 'n/a';
@@ -303,168 +327,19 @@ export default async function GenerateCertificate(docDetails) {
let yPosition7 = yPosition6 - 20;
let yPosition8 = yPosition7 - 35;
auditTrail.slice(0, 3).forEach(async (x, i) => {
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
page.drawText(`Signer ${i + 1}`, {
x: 30,
y: yPosition1,
size: subtitle,
font: timesRomanFont,
color: titleColor,
});
page.drawText('Name :', {
x: 30,
y: yPosition2,
size: signertext,
font: timesRomanFont,
color: textKeyColor,
});
// A signer/approver block spans from yPosition1 down to yPosition8 (the
// separator line at the bottom of the block). The signature image's
// bottom edge sits at yPosition7 - 30 and must remain inside the page
// border (whose bottom edge is at startY). Use the lowest of those two
// values when deciding whether the next block fits on the current page.
const minY = startY + 5;
const blockBottom = () => Math.min(yPosition7 - 30, yPosition8);
page.drawText(x?.Name, {
x: 75,
y: yPosition2,
size: signertext,
font: timesRomanFont,
color: textValueColor,
});
if (IsEnableOTP) {
page.drawText('Security level :', {
x: half + 120,
y: yPosition2,
size: timeText,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText('Email, OTP Auth', {
x: half + 190,
y: yPosition2,
size: timeText,
font: timesRomanFont,
color: textValueColor,
});
}
page.drawText('Email :', {
x: 30,
y: yPosition3,
size: signertext,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(x?.Email, {
x: 75,
y: yPosition3,
size: signertext,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Viewed on :', {
x: 30,
y: yPosition4,
size: signertext,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(`${formatDateTime(x.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
x: 97,
y: yPosition4,
size: signertext,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Signed on :', {
x: 30,
y: yPosition5,
size: signertext,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(`${formatDateTime(x.SignedOn, DateFormat, timezone, Is12Hr)}`, {
x: 95,
y: yPosition5,
size: signertext,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('IP address :', {
x: 30,
y: yPosition6,
size: signertext,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(x?.ipAddress, {
x: 95,
y: yPosition6,
size: signertext,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Signature :', {
x: 30,
y: yPosition7,
size: signertext,
font: timesRomanFont,
color: textKeyColor,
});
page.drawRectangle({
x: 98,
y: yPosition7 - 30,
width: 104,
height: 44,
borderColor: rgb(0.22, 0.18, 0.47),
borderWidth: 1,
});
if (embedPng) {
page.drawImage(embedPng, {
x: 100,
y: yPosition7 - 27,
width: 100,
height: 40,
});
}
page.drawLine({
start: { x: 30, y: yPosition8 },
end: { x: width - 30, y: yPosition8 },
color: rgb(0.12, 0.12, 0.12),
thickness: 0.5,
});
yPosition1 = yPosition8 - 20;
yPosition2 = yPosition1 - 20;
yPosition3 = yPosition2 - 20;
yPosition4 = yPosition3 - 20;
yPosition5 = yPosition4 - 20;
yPosition6 = yPosition5 - 20;
yPosition7 = yPosition6 - 20;
yPosition8 = yPosition8 - 174;
});
if (auditTrail.length > 3) {
let currentPageIndex = 1;
let currentPage = page;
auditTrail.slice(3).forEach(async (x, i) => {
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
// Calculate remaining space on current page
const remainingSpace = yPosition8;
// If there's not enough space for the next entry, create a new page
if (remainingSpace < 90) {
// Adjust the value as needed
currentPageIndex++;
currentPage = pdfDoc.addPage();
currentPage.drawRectangle({
// Helper that resets the y-positions to the top of a freshly added page so
// the next block starts cleanly under the border.
const startNewPage = () => {
const newPage = pdfDoc.addPage();
newPage.drawRectangle({
x: startX,
y: startY,
width: width - 2 * startX,
@@ -472,18 +347,29 @@ export default async function GenerateCertificate(docDetails) {
borderColor: borderColor,
borderWidth: 1,
});
yPosition1 = currentPage.getHeight() - 40;
yPosition1 = newPage.getHeight() - 40;
yPosition2 = yPosition1 - 20;
yPosition3 = yPosition2 - 20;
yPosition4 = yPosition3 - 20;
yPosition5 = yPosition4 - 20;
yPosition5 = yPosition4 - 20;
yPosition6 = yPosition5 - 20;
yPosition7 = yPosition6 - 20;
yPosition8 = currentPage.getHeight() - 190;
}
yPosition8 = yPosition7 - 35;
return newPage;
};
currentPage.drawText(`Signer ${4 + i}`, {
let currentPage = page;
for (let i = 0; i < auditTrail.length; i++) {
const x = auditTrail[i];
// If the next block would overflow the bottom border, move to a new page.
if (blockBottom() < minY) {
currentPage = startNewPage();
}
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : nasign;
const headerLabel = `${i + 1}. ${'Signer'}`;
const signedOnLabel = 'Signed on :';
currentPage.drawText(headerLabel, {
x: 30,
y: yPosition1,
size: subtitle,
@@ -497,8 +383,7 @@ export default async function GenerateCertificate(docDetails) {
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(x?.Name, {
currentPage.drawText(x?.Name || '', {
x: 75,
y: yPosition2,
size: signertext,
@@ -514,7 +399,7 @@ export default async function GenerateCertificate(docDetails) {
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(`Email, OTP Auth`, {
currentPage.drawText('Email, OTP Auth', {
x: half + 190,
y: yPosition2,
size: timeText,
@@ -530,8 +415,7 @@ export default async function GenerateCertificate(docDetails) {
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(x?.Email, {
currentPage.drawText(x?.Email || '', {
x: 75,
y: yPosition3,
size: signertext,
@@ -546,24 +430,24 @@ export default async function GenerateCertificate(docDetails) {
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(`${formatDateTime(x.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
currentPage.drawText(`${formatDateStr(x?.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
x: 97,
y: yPosition4,
size: signertext,
font: timesRomanFont,
color: textValueColor,
});
currentPage.drawText('Signed on :', {
currentPage.drawText(signedOnLabel, {
x: 30,
y: yPosition5,
size: signertext,
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(`${formatDateTime(x.SignedOn, DateFormat, timezone, Is12Hr)}`, {
x: 95,
const signedOnValueX = 30 + timesRomanFont.widthOfTextAtSize(signedOnLabel, signertext) + 5;
currentPage.drawText(`${formatDateStr(x?.SignedOn, DateFormat, timezone, Is12Hr)}`, {
x: signedOnValueX,
y: yPosition5,
size: signertext,
font: timesRomanFont,
@@ -577,9 +461,8 @@ export default async function GenerateCertificate(docDetails) {
font: timesRomanFont,
color: textKeyColor,
});
currentPage.drawText(x?.ipAddress, {
x: 100,
currentPage.drawText(x?.ipAddress || '', {
x: 95,
y: yPosition6,
size: signertext,
font: timesRomanFont,
@@ -595,7 +478,7 @@ export default async function GenerateCertificate(docDetails) {
});
currentPage.drawRectangle({
x: 98,
y: yPosition7 - 27,
y: yPosition7 - 30,
width: 104,
height: 44,
borderColor: rgb(0.22, 0.18, 0.47),
@@ -604,12 +487,11 @@ export default async function GenerateCertificate(docDetails) {
if (embedPng) {
currentPage.drawImage(embedPng, {
x: 100,
y: yPosition7 - 25,
y: yPosition7 - 27,
width: 100,
height: 40,
});
}
currentPage.drawLine({
start: { x: 30, y: yPosition8 },
end: { x: width - 30, y: yPosition8 },
@@ -617,7 +499,6 @@ export default async function GenerateCertificate(docDetails) {
thickness: 0.5,
});
// Update y positions for the next entry
yPosition1 = yPosition8 - 20;
yPosition2 = yPosition1 - 20;
yPosition3 = yPosition2 - 20;
@@ -626,7 +507,6 @@ export default async function GenerateCertificate(docDetails) {
yPosition6 = yPosition5 - 20;
yPosition7 = yPosition6 - 20;
yPosition8 = yPosition8 - 174;
});
}
const pdfBytes = await pdfDoc.save();
@@ -17,6 +17,22 @@ import { SignPdf } from '@signpdf/signpdf';
import { P12Signer } from '@signpdf/signer-p12';
import { buildDownloadFilename, parseUploadFile } from '../../../utils/fileUtils.js';
import sendMailWithAttachment from '../sendMailWithAttachment.js';
import sendSystemMail from '../sendSystemMail.js';
import {
COMPLETION_ACTIVITIES,
findPlaceholderIndex,
findPendingPriorSigner,
isCompletionRelevant,
} from '../../../utils/workflowUtils.js';
// Audit-trail activities that count toward document completion. The free
// build only counts 'Signed'; EE additionally counts 'Approved'.
// A placeholder participates in completion unless it is a prefill entry.
// EE additionally excludes viewers (who never act on the document).
// Strict-order gating: returns the signerObjId of the prior placeholder
// still pending, or null when the strict-order requirement is satisfied.
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
const APPID = serverAppId;
@@ -62,13 +78,24 @@ async function uploadFile(pdfName, filepath) {
}
// `updateDoc` is used to update signedUrl, AuditTrail, Iscompleted in document
async function updateDoc(docId, url, userId, ipAddress, data, className, sign, documentHash) {
async function updateDoc(
docId,
url,
userId,
ipAddress,
data,
className,
sign,
documentHash,
activity
) {
try {
const UserPtr = { __type: 'Pointer', className: className, objectId: userId };
const auditActivity = 'Signed';
const obj = {
UserPtr: UserPtr,
SignedUrl: url,
Activity: 'Signed',
Activity: auditActivity,
ipAddress: ipAddress,
SignedOn: new Date(),
Signature: sign,
@@ -88,13 +115,14 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign, d
updateAuditTrail = [obj];
}
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
// Count both Signed and Approved entries; only signer/approver
// placeholders count toward completion (viewers and prefill excluded).
const auditTrail = updateAuditTrail.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity));
let isCompleted = false;
if (data.Signers && data.Signers.length > 0) {
//'removePrefill' is used to remove prefill role from placeholders filed then compare length to change status of document
const removePrefill =
data.Placeholders.length > 0 && data.Placeholders.filter(x => x.Role !== 'prefill');
if (auditTrail.length === removePrefill?.length) {
const completionRelevant =
data.Placeholders?.length > 0 ? data.Placeholders.filter(isCompletionRelevant) : [];
if (auditTrail.length >= completionRelevant.length && completionRelevant.length > 0) {
isCompleted = true;
}
} else {
@@ -124,10 +152,11 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
const logo =
"<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' style='padding:20px'/>";
const auditTrailCount = doc?.AuditTrail?.filter(x => x.Activity === 'Signed')?.length || 0;
const removePrefill =
doc?.Placeholders?.length > 0 && doc?.Placeholders?.filter(x => x?.Role !== 'prefill');
const signersCount = removePrefill?.length;
const auditTrailCount =
doc?.AuditTrail?.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity))?.length || 0;
const completionRelevant =
doc?.Placeholders?.length > 0 ? doc.Placeholders.filter(isCompletionRelevant) : [];
const signersCount = completionRelevant?.length;
const remainingSign = signersCount - auditTrailCount;
if (remainingSign > 1 && doc?.NotifyOnSignatures) {
const sender = doc.ExtUserPtr;
@@ -154,7 +183,7 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
html: body,
mailProvider: mailProvider,
};
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
await sendSystemMail({ params });
}
} catch (err) {
console.log('err in sendnotifymail', err);
@@ -239,6 +268,7 @@ async function sendCompletedMail(obj) {
body = replaceVar.body;
}
const Bcc = doc?.Bcc?.length > 0 ? doc.Bcc.map(x => x.Email) : [];
const Cc = doc?.Cc?.length > 0 ? doc.Cc.map(x => x.Email) : [];
const updatedBcc = doc?.SenderMail ? [...Bcc, doc?.SenderMail] : Bcc;
const formatId = doc?.ExtUserPtr?.DownloadFilenameFormat;
const filename = pdfName?.length > 100 ? pdfName?.slice(0, 100) : pdfName;
@@ -258,6 +288,7 @@ async function sendCompletedMail(obj) {
html: body,
mailProvider: obj.mailProvider,
bcc: updatedBcc?.length > 0 ? updatedBcc : '',
cc: Cc?.length > 0 ? Cc : '',
certificatePath: `./exports/signed_certificate_${doc.objectId}.pdf`,
filename: docName,
};
@@ -360,11 +391,14 @@ async function PDF(req) {
const isCustomMail = req.params.isCustomCompletionMail || false;
const mailProvider = req.params.mailProvider || '';
const sign = req.params.signature || '';
const auditActivity = 'Signed';
const publicUrl = req.headers.public_url;
// below bode is used to get info of docId
const docQuery = new Parse.Query('contracts_Document');
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,CreatedBy');
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,Cc,CreatedBy');
docQuery.equalTo('objectId', docId);
docQuery.notEqualTo('IsDeclined', true);
docQuery.notEqualTo('IsArchive', true);
const resDoc = await docQuery.first({ useMasterKey: true });
if (!resDoc) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found.');
@@ -408,7 +442,7 @@ async function PDF(req) {
const P12Buffer = Buffer.from(pfxFile, 'base64');
fs.writeFileSync(pfxname, P12Buffer);
const UserPtr = { __type: 'Pointer', className: className, objectId: signUser.objectId };
const obj = { UserPtr: UserPtr, SignedUrl: '', Activity: 'Signed', ipAddress: userIP };
const obj = { UserPtr: UserPtr, SignedUrl: '', Activity: auditActivity, ipAddress: userIP };
let updateAuditTrail;
if (_resDoc.AuditTrail && _resDoc.AuditTrail.length > 0) {
updateAuditTrail = [..._resDoc.AuditTrail, obj];
@@ -416,14 +450,17 @@ async function PDF(req) {
updateAuditTrail = [obj];
}
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
// Both Signed and Approved entries count toward completion. Only
// signer/approver placeholders are counted; viewers and prefill are
// excluded.
const auditTrail = updateAuditTrail.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity));
let isCompleted = false;
if (_resDoc.Signers && _resDoc.Signers.length > 0) {
const removePrefill =
_resDoc?.Placeholders?.length > 0 &&
_resDoc?.Placeholders?.filter(x => x?.Role !== 'prefill');
if (auditTrail.length === removePrefill?.length) {
// if (auditTrail.length === _resDoc.Signers.length) {
const completionRelevant =
_resDoc?.Placeholders?.length > 0
? _resDoc.Placeholders.filter(isCompletionRelevant)
: [];
if (auditTrail.length >= completionRelevant.length && completionRelevant.length > 0) {
isCompleted = true;
}
} else {
@@ -476,7 +513,8 @@ async function PDF(req) {
_resDoc, // auditTrail, signers, etc data
className, // className based on flow
sign, // sign base64
isCompleted ? documentHash : undefined
isCompleted ? documentHash : undefined,
auditActivity
);
sendNotifyMail(_resDoc, signUser, mailProvider, publicUrl);
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
@@ -1,3 +1,5 @@
import { setDocumentCount } from '../../utils/CountUtils.js';
export default async function recreateDocument(request) {
const { docId } = request.params;
if (!docId) {
@@ -10,6 +12,16 @@ export default async function recreateDocument(request) {
try {
const docQuery = new Parse.Query('contracts_Document');
docQuery.equalTo('objectId', docId);
docQuery.notEqualTo('IsArchive', true);
docQuery.exclude([
'DocSentAt',
'SignedUrl',
'AuditTrail',
'DeclineBy',
'DeclineReason',
'DocumentHash',
'CertificateUrl',
]);
const doc = await docQuery.first({ useMasterKey: true });
if (!doc) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found');
@@ -18,11 +30,27 @@ export default async function recreateDocument(request) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Signyourself Document not allowed');
}
const _docRes = doc?.toJSON();
const { objectId, SignedUrl, AuditTrail, ACL, DeclineBy, DeclineReason, ...docRes } = _docRes;
const { ACL, objectId, ...docRes } = _docRes;
const createDoc = new Parse.Object('contracts_Document');
Object.entries(docRes).forEach(([key, value]) => {
if (key === 'IsDeclined' || key === 'IsCompleted') {
createDoc.set(key, false);
} else if (key === 'Placeholders') {
const placeHolders = value.map(signer => ({
...signer,
placeHolder: (signer.placeHolder || []).map(page => ({
...page,
pos: (page.pos || []).map(widget => {
const { SignUrl, ...rest } = widget;
return {
...rest,
type: widget.type === 'text' ? 'text input' : widget.type,
options: { ...widget.options, response: '', defaultValue: '' },
};
}),
})),
}));
createDoc.set(key, placeHolders);
} else {
createDoc.set(key, value);
}
@@ -31,6 +59,7 @@ export default async function recreateDocument(request) {
const createDocRes = await createDoc.save(null, { useMasterKey: true });
// console.log('createDocRes', createDocRes);
const newDoc = JSON.parse(JSON.stringify(createDocRes));
setDocumentCount(_docRes?.ExtUserPtr?.objectId);
return { objectId: newDoc.objectId, createdAt: newDoc.createdAt, updatedAt: newDoc.updatedAt };
} catch (err) {
console.log('err in recreate document', err);
@@ -1,4 +1,5 @@
const randomId = () => Math.floor(1000 + Math.random() * 9000);
import { randomId } from '../../Utils.js';
export default async function saveAsTemplate(request) {
const docId = request.params.docId;
const Ip = request?.headers?.['x-real-ip'] || '';
@@ -23,6 +24,7 @@ export default async function saveAsTemplate(request) {
templateCls.set('Description', _docRes?.Description);
templateCls.set('OriginIp', Ip);
templateCls.set('SendinOrder', _docRes?.SendinOrder || false);
templateCls.set('SendInOrderStrict', _docRes?.SendInOrderStrict || false);
templateCls.set('AutomaticReminders', _docRes?.AutomaticReminders || false);
templateCls.set('ExtUserPtr', _docRes?.ExtUserPtr);
templateCls.set('CreatedBy', _docRes?.CreatedBy);
@@ -91,18 +93,27 @@ export default async function saveAsTemplate(request) {
pos: (page.pos || []).map(widget => {
// if there is a defaultValue in options, zero it out
if (widget.options && widget.options.defaultValue !== undefined) {
const { SignUrl, ...rest } = widget;
return {
...widget,
signatureType: '',
...rest,
type: widget.type === 'text' ? 'text input' : widget.type,
...(widget.type === 'signature' ? { signatureType: '' } : {}),
options: {
...widget.options,
defaultValue: '',
response: '',
...(widget?.options?.isReadOnly ? { isReadOnly: false } : {}),
},
}; // reset only the value
}
// otherwise, return the widget unchanged
return widget;
const { SignUrl, ...rest } = widget;
return {
...rest,
type: widget.type === 'text' ? 'text input' : widget.type,
...(widget.type === 'signature' ? { signatureType: '' } : {}),
options: { ...widget.options, response: '' },
};
}),
})),
}));
@@ -116,6 +127,9 @@ export default async function saveAsTemplate(request) {
if (_docRes?.Bcc?.length > 0) {
templateCls.set('Bcc', _docRes?.Bcc);
}
if (_docRes?.Cc?.length > 0) {
templateCls.set('Cc', _docRes?.Cc);
}
if (_docRes?.PenColors?.length > 0) {
templateCls.set('PenColors', _docRes?.PenColors);
}
@@ -46,6 +46,7 @@ const makeEmail = async (
url,
pdfName,
bcc,
cc,
filename,
certificatePath,
replyto,
@@ -54,6 +55,7 @@ const makeEmail = async (
const htmlContent = html;
const boundary = 'boundary_' + Date.now().toString(16);
const bccHeader = bcc && bcc.length > 0 ? `BCC: ${bcc.join(',')}\n` : ''; // Construct BCC header if provided
const ccHeader = cc && cc.length > 0 ? `CC: ${cc.join(',')}\n` : ''; // Construct CC header if provided
const replyToHeader = replyto ? `Reply-To: ${replyto}\n` : ''; // Construct Reply-To header if provided
let str;
@@ -137,6 +139,7 @@ const makeEmail = async (
`To: ${to}\n`,
`From: ${from}\n`,
bccHeader,
ccHeader,
replyToHeader,
`Subject: ${subject}\n\n`,
'--' + boundary + '\n',
@@ -154,6 +157,7 @@ const makeEmail = async (
`To: ${to}\n`,
`From: ${from}\n`,
bccHeader,
ccHeader,
replyToHeader,
`Subject: ${subject}\n\n`,
'--' + boundary + '\n',
@@ -168,8 +172,19 @@ const makeEmail = async (
return encodedMail;
};
export default async function sendMailGmailProvider(_extRes, template) {
const { sender, receiver, subject, html, url, pdfName, bcc, filename, certificatePath, replyto } =
template;
const {
sender,
receiver,
subject,
html,
url,
pdfName,
bcc,
cc,
filename,
certificatePath,
replyto,
} = template;
if (_extRes) {
let refresh_token = '';
@@ -192,6 +207,7 @@ export default async function sendMailGmailProvider(_extRes, template) {
url,
pdfName,
bcc,
cc,
filename,
certificatePath,
replyto,
@@ -222,7 +238,8 @@ export default async function sendMailGmailProvider(_extRes, template) {
}
return { code: 200, message: 'Email sent successfully' };
} catch (error) {
console.error('Error sending email:', error);
const message = error?.response?.data || error?.message || 'Unknown error';
console.error('Error sending email:', message);
return { code: 500, message: 'Failed to send email ' + error };
}
}
@@ -21,8 +21,8 @@ async function sendMailProvider(params) {
const reportMsg = `<p style="font-size: 13px; color:grey; text-align: center;">If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href="mailto:complaints@opensignlabs.com?subject=Spam%20report%20for%20user%20ID%20${extUserId}&body=Hello%20Support%20Team%2C%0D%0A%0D%0AI%E2%80%99m%20reporting%20spam%20activity%20coming%20from%20a%20sender%20using%20your%20platform.%0D%0A%0D%0AThe%20messages%20I%20received%20appear%20unsolicited%20and%20suspicious.%20The%20user%20ID%20associated%20with%20the%20emails%20is%3A%20${extUserId}.%20Please%20investigate%20this%20account%20and%20take%20appropriate%20action%20to%20prevent%20further%20abuse.%0D%0A%0D%0AIf%20you%20need%20additional%20details%2C%20I%E2%80%99m%20happy%20to%20provide%20the%20original%20email%20headers%20or%20screenshots.%0D%0A%0D%0AThank%20you%20for%20looking%20into%20this.%0D%0A%0D%0ABest%20regards%2C%0D%0A%5BYour%20Name%5D">here</a>.</p>`;
const mailgunApiKey = process.env.MAILGUN_API_KEY;
try {
let transporterSMTP;
try {
let mailgunClient;
let mailgunDomain;
if (smtpenable) {
@@ -143,6 +143,7 @@ async function sendMailProvider(params) {
attachments: smtpenable ? attachment : undefined,
attachment: smtpenable ? undefined : attachment,
bcc: params.bcc ? params.bcc : undefined,
cc: params.cc ? params.cc : undefined,
replyTo: replyto ? replyto : undefined,
};
const cleanupPaths = [
@@ -193,6 +194,7 @@ async function sendMailProvider(params) {
text: params.text || 'mail',
html: params?.html ? params.html + reportMsg : '',
bcc: params.bcc ? params.bcc : undefined,
cc: params.cc ? params.cc : undefined,
replyTo: replyto ? replyto : undefined,
};
@@ -225,6 +227,10 @@ async function sendMailProvider(params) {
if (err) {
return { status: 'error' };
}
} finally {
if (transporterSMTP) {
transporterSMTP?.close?.();
}
}
}
@@ -1,18 +1,15 @@
import fs from 'node:fs';
import https from 'https';
import formData from 'form-data';
import Mailgun from 'mailgun.js';
import { appName, smtpenable, smtpsecure, updateMailCount } from '../../Utils.js';
import { createTransport } from 'nodemailer';
import axios from 'axios';
async function sendMailProvider(req) {
const app = appName;
const extUserId = req.params?.extUserId || '';
const reportMsg = `<p style="font-size: 13px; color:grey; text-align: center;">If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href="mailto:complaints@opensignlabs.com?subject=Spam%20report%20for%20user%20ID%20${extUserId}&body=Hello%20Support%20Team%2C%0D%0A%0D%0AI%E2%80%99m%20reporting%20spam%20activity%20coming%20from%20a%20sender%20using%20your%20platform.%0D%0A%0D%0AThe%20messages%20I%20received%20appear%20unsolicited%20and%20suspicious.%20The%20user%20ID%20associated%20with%20the%20emails%20is%3A%20${extUserId}.%20Please%20investigate%20this%20account%20and%20take%20appropriate%20action%20to%20prevent%20further%20abuse.%0D%0A%0D%0AIf%20you%20need%20additional%20details%2C%20I%E2%80%99m%20happy%20to%20provide%20the%20original%20email%20headers%20or%20screenshots.%0D%0A%0D%0AThank%20you%20for%20looking%20into%20this.%0D%0A%0D%0ABest%20regards%2C%0D%0A%5BYour%20Name%5D">here</a>.</p>`;
const mailgunApiKey = process.env.MAILGUN_API_KEY;
try {
let transporterSMTP;
try {
let mailgunClient;
let mailgunDomain;
if (smtpenable) {
@@ -51,6 +48,7 @@ async function sendMailProvider(req) {
text: req.params.text || 'mail',
html: req.params?.html ? req.params.html + reportMsg : '',
bcc: req.params.bcc ? req.params.bcc : undefined,
cc: req.params.cc ? req.params.cc : undefined,
replyTo: replyto ? replyto : undefined,
};
@@ -82,6 +80,10 @@ async function sendMailProvider(req) {
if (err) {
return { status: 'error' };
}
} finally {
if (transporterSMTP) {
transporterSMTP?.close?.();
}
}
}
@@ -0,0 +1,94 @@
import formData from 'form-data';
import Mailgun from 'mailgun.js';
import { appName, smtpenable, smtpsecure, updateMailCount } from '../../Utils.js';
import { createTransport } from 'nodemailer';
async function sendMailProvider(req) {
const app = appName;
const extUserId = req.params?.extUserId || '';
const reportMsg = `<p style="font-size: 13px; color:grey; text-align: center;">If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href="mailto:complaints@opensignlabs.com?subject=Spam%20report%20for%20user%20ID%20${extUserId}&body=Hello%20Support%20Team%2C%0D%0A%0D%0AI%E2%80%99m%20reporting%20spam%20activity%20coming%20from%20a%20sender%20using%20your%20platform.%0D%0A%0D%0AThe%20messages%20I%20received%20appear%20unsolicited%20and%20suspicious.%20The%20user%20ID%20associated%20with%20the%20emails%20is%3A%20${extUserId}.%20Please%20investigate%20this%20account%20and%20take%20appropriate%20action%20to%20prevent%20further%20abuse.%0D%0A%0D%0AIf%20you%20need%20additional%20details%2C%20I%E2%80%99m%20happy%20to%20provide%20the%20original%20email%20headers%20or%20screenshots.%0D%0A%0D%0AThank%20you%20for%20looking%20into%20this.%0D%0A%0D%0ABest%20regards%2C%0D%0A%5BYour%20Name%5D">here</a>.</p>`;
const mailgunApiKey = process.env.MAILGUN_API_KEY;
let transporterSMTP;
try {
let mailgunClient;
let mailgunDomain;
if (smtpenable) {
let transporterConfig = {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT || 465,
secure: smtpsecure,
};
// ✅ 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,
pass: smtpPass,
};
}
transporterSMTP = createTransport(transporterConfig);
} else {
if (mailgunApiKey) {
const mailgun = new Mailgun(formData);
mailgunClient = mailgun.client({ username: 'api', key: mailgunApiKey });
mailgunDomain = process.env.MAILGUN_DOMAIN;
}
}
const from = req.params.from || '';
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
const replyto = req.params?.replyto || '';
const messageParams = {
from: from + ' <' + mailsender + '>',
to: req.params.recipient,
subject: req.params.subject,
text: req.params.text || 'mail',
html: req.params?.html ? req.params.html + reportMsg : '',
bcc: req.params.bcc ? req.params.bcc : undefined,
replyTo: replyto ? replyto : undefined,
};
if (transporterSMTP) {
const res = await transporterSMTP.sendMail(messageParams);
console.log('smtp transporter res: ', res?.response);
if (!res.err) {
if (extUserId) {
await updateMailCount(extUserId);
}
return { status: 'success' };
}
} else {
if (mailgunApiKey) {
const res = await mailgunClient.messages.create(mailgunDomain, messageParams);
console.log('mailgun res: ', res?.status);
if (res.status === 200) {
if (extUserId) {
await updateMailCount(extUserId);
}
return { status: 'success' };
}
} else {
return { status: 'error' };
}
}
} catch (err) {
console.log(`sendSystemMail Error: ${err}`);
if (err) {
return { status: 'error' };
}
} finally {
if (transporterSMTP) {
transporterSMTP?.close?.();
}
}
}
async function sendSystemMail(req) {
const nonCustomMail = await sendMailProvider(req);
return nonCustomMail;
}
export default sendSystemMail;
@@ -5,6 +5,7 @@ const APPID = serverAppId;
const masterKEY = process.env.MASTER_KEY;
async function saveUser(userDetails) {
const normalizedEmail = normalizeEmail(userDetails.email.toLowerCase().replace(/\s/g, ''));
const userQuery = new Parse.Query(Parse.User);
userQuery.equalTo('username', userDetails.email);
const userRes = await userQuery.first({ useMasterKey: true });
@@ -31,6 +32,8 @@ async function saveUser(userDetails) {
user.set('username', userDetails.email);
user.set('password', userDetails.password);
user.set('email', userDetails?.email?.toLowerCase()?.replace(/\s/g, ''));
user.set('normalizedEmail', normalizedEmail);
if (userDetails?.phone) {
user.set('phone', userDetails.phone);
}
@@ -0,0 +1,27 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.addArray('Cc');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.addArray('Cc');
await templateSchema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.deleteField('Cc');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.deleteField('Cc');
await templateSchema.update();
};
@@ -0,0 +1,24 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const className = 'contracts_templateLinks';
const schema = new Parse.Schema(className);
schema.addString('Type');
schema.addPointer('TemplatePtr', 'contracts_Template');
schema.addArray('Placeholders');
return schema.save();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const className = 'contracts_templateLinks';
const schema = new Parse.Schema(className);
return schema.purge().then(() => schema.delete());
};
@@ -0,0 +1,19 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const schema = new Parse.Schema("_User");
schema.addString('normalizedEmail');
return schema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const schema = new Parse.Schema("_User");
schema.deleteField('normalizedEmail');
return schema.update();
};
@@ -0,0 +1,27 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.addBoolean('SendInOrderStrict');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.addBoolean('SendInOrderStrict');
await templateSchema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.deleteField('SendInOrderStrict');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.deleteField('SendInOrderStrict');
await templateSchema.update();
};

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+1 -1
View File
@@ -190,7 +190,7 @@ function getUserIP(request) {
}
app.use(async function (req, res, next) {
const isFilePath = req.path.includes('files') || false;
const isFilePath = req.path?.includes('/files/') || false;
if (isFilePath && req.method.toLowerCase() === 'get') {
const serverUrl = new URL(process.env.SERVER_URL);
const origin = serverUrl.pathname === '/api/app' ? serverUrl.origin + '/api' : serverUrl.origin;
@@ -0,0 +1,46 @@
import dotenv from 'dotenv';
import { MongoClient } from 'mongodb';
import { generateId } from '../Utils.js';
dotenv.config({ quiet: true });
export default async function createNormalizedEmailUnique() {
// Provide the complete MongoDB connection URL with the database name
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/dev'; // Replace with your MongoDB URI
const client = new MongoClient(uri);
try {
await client.connect();
const database = client.db();
const migrationCollection = database.collection('Migrationdb');
const migrationName = 'normalizedEmailUnique_1';
// Check if the migration has already been executed
const migrationExists = await migrationCollection.findOne({ name: migrationName });
if (migrationExists) {
console.log(' INFO The unqiue index for normalizedEmail is already present.');
return;
}
const collection = database.collection('_User');
// Create the unique index, but only on documents where NormalizedEmail exists
await collection.createIndex({ normalizedEmail: 1 }, { unique: true, sparse: true });
// Save the migration record in the migrationdb collection
await migrationCollection.insertOne({
_id: generateId(10),
name: migrationName,
_created_at: new Date(),
_updated_at: new Date(),
executedAt: new Date(),
details: 'Created unique index on NormalizedEmail',
});
console.log(' SUCCESS The unqiue index for normalizedEmail is already created.');
} catch (error) {
console.log(' ERROR Running unqiue index for normalizedEmail migration:', error);
} finally {
await client.close();
}
}
+2
View File
@@ -1,7 +1,9 @@
import createContactIndex from './createContactIndex.js';
import createDocumentIndex from './createDocumentIndex.js';
import createNormalizedEmailUnique from './createNormalizedEmailUnqiue.js';
export default async function runDbMigrations() {
await createContactIndex();
await createDocumentIndex();
await createNormalizedEmailUnique();
}
+1375 -1145
View File
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -18,36 +18,36 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1008.0",
"@aws-sdk/s3-request-presigner": "^3.1008.0",
"@aws-sdk/client-s3": "^3.1038.0",
"@aws-sdk/s3-request-presigner": "^3.1038.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/push-adapter": "^8.3.1",
"@parse/s3-files-adapter": "5.0.0",
"@parse/push-adapter": "^8.5.0",
"@parse/s3-files-adapter": "^5.3.1",
"@pdf-lib/fontkit": "^1.1.1",
"@signpdf/placeholder-pdf-lib": "^3.3.0",
"@signpdf/signer-p12": "^3.3.0",
"@signpdf/signpdf": "^3.3.0",
"axios": "^1.13.6",
"axios": "^1.15.2",
"coherentpdf": "^2.5.5",
"cors": "^2.8.6",
"date-fns-tz": "^3.2.0",
"dotenv": "^17.3.1",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"form-data": "^4.0.5",
"generate-api-key": "^1.0.2",
"googleapis": "^171.4.0",
"libreoffice-convert": "^1.8.1",
"mailgun.js": "^12.7.1",
"mailgun.js": "^13.0.0",
"moment": "^2.30.1",
"mongodb": "^7.1.0",
"mongodb": "^7.2.0",
"multer": "^2.1.1",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.3",
"nodemailer": "^8.0.2",
"nodemailer": "^8.0.7",
"p-limit": "^7.3.0",
"parse": "^8.1.0",
"parse-dbtool": "^1.2.0",
"parse-server": "^8.6.40",
"parse-server": "^8.6.76",
"parse-server-api-mail-adapter": "^5.0.5",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.21.2",
@@ -55,17 +55,17 @@
"rate-limiter-flexible": "^9.1.1",
"sharp": "^0.34.5",
"speakeasy": "^2.0.0",
"ws": "^8.19.0"
"ws": "^8.20.0"
},
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.28.6",
"eslint": "^9.39.4",
"jasmine": "^6.1.0",
"mongodb-runner": "^6.7.1",
"jasmine": "^6.2.0",
"mongodb-runner": "^6.7.8",
"nodemon": "^3.1.14",
"nyc": "^17.1.0",
"prettier": "^3.8.1"
"nyc": "^18.0.0",
"prettier": "^3.8.3"
},
"overrides": {
"ws": "$ws",
+8 -1
View File
@@ -1,4 +1,4 @@
export const setDocumentCount = async extUserId => {
export const setDocumentCount = async (extUserId, docsCount) => {
if (extUserId) {
try {
// Update count in contracts_Users class
@@ -6,7 +6,14 @@ export const setDocumentCount = async extUserId => {
extQuery.equalTo('objectId', extUserId);
const contractUser = await extQuery.first({ useMasterKey: true });
if (contractUser) {
if (docsCount) {
const count = contractUser.get('DocumentCount')
? contractUser.get('DocumentCount') + Number(docsCount)
: 0 + Number(docsCount);
contractUser.set('DocumentCount', count);
} else {
contractUser.increment('DocumentCount', 1);
}
await contractUser.save(null, { useMasterKey: true });
}
} catch (error) {
@@ -0,0 +1,48 @@
// Workflow helpers for placeholder/audit-trail processing. These helpers
// are intentionally agnostic of any role classification — they treat every
// non-prefill placeholder as a participant, and only the 'Signed' activity
// counts toward completion. Builds with role-aware behaviour layer extra
// filtering on top.
// Audit-trail activities that mark a placeholder as having fulfilled its
// completion duty.
export const COMPLETION_ACTIVITIES = ['Signed'];
// A placeholder participates in completion unless it is a prefill entry
// (which only pre-populates field values without acting on the document).
export function isParticipantBasic(placeholder) {
return placeholder?.Role !== 'prefill';
}
// A placeholder participates in completion unless it is a prefill entry or a viewer.
export function isCompletionRelevant(placeholder) {
if (!isParticipantBasic(placeholder)) return false;
return true;
}
// Locate a placeholder index by signerObjId. Prefill placeholders are
// excluded so caller indices stay aligned with the participant list.
export function findPlaceholderIndex(placeholders, signerObjId) {
if (!Array.isArray(placeholders) || !signerObjId) return -1;
return placeholders.findIndex(
p => (p?.signerObjId || p?.signerPtr?.objectId) === signerObjId && p?.Role !== 'prefill'
);
}
// Strict-order gating: returns the signerObjId of the prior placeholder
// still pending, or null when the strict-order requirement is satisfied.
export function findPendingPriorSigner(placeholders, idx, auditTrail) {
if (!Array.isArray(placeholders) || idx <= 0) return null;
const trail = Array.isArray(auditTrail) ? auditTrail : [];
for (let i = 0; i < idx; i++) {
const ph = placeholders[i];
if (!isCompletionRelevant(ph)) continue;
const signerObjId = ph?.signerObjId || ph?.signerPtr?.objectId;
if (!signerObjId) continue;
const acted = trail.some(
a => a?.UserPtr?.objectId === signerObjId && COMPLETION_ACTIVITIES.includes(a?.Activity)
);
if (!acted) return signerObjId;
}
return null;
}