mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-09-04 00:38:57 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2e535b70 | ||
|
|
336cfb3482 | ||
|
|
f1278d32a8 | ||
|
|
79d184cf88 | ||
|
|
cd2904b9f0 | ||
|
|
fce20a165b | ||
|
|
3a737ea1b7 | ||
|
|
e035592e94 | ||
|
|
0891cf55fc | ||
|
|
5d328b51c3 | ||
|
|
336114f6ec | ||
|
|
eb8e7b089b | ||
|
|
bce0e4971e | ||
|
|
4fb8aa9a16 |
+35
-36
@@ -1,58 +1,57 @@
|
||||
{
|
||||
"name": "open_sign",
|
||||
"version": "2.32.0",
|
||||
"version": "2.35.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.8",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.13",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.11.0",
|
||||
"axios": "^1.13.2",
|
||||
"@radix-ui/themes": "^3.3.0",
|
||||
"@reactour/tour": "^3.8.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"axios": "^1.13.4",
|
||||
"body-scroll-lock": "^4.0.0-beta.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.7.1",
|
||||
"i18next": "^25.8.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"parse": "^7.1.2",
|
||||
"parse": "^8.0.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pkijs": "^3.3.3",
|
||||
"print-js": "^1.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"quill-html-edit-button": "^3.0.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^18.3.1",
|
||||
"react": "^19.2.4",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-datepicker": "^8.10.0",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dnd-multi-backend": "^9.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-konva": "^18.2.12",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-quill-new": "^3.6.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-konva": "^19.2.1",
|
||||
"react-pdf": "^10.2.0",
|
||||
"react-quill-new": "^3.8.3",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.10.0",
|
||||
"react-router": "^7.13.0",
|
||||
"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-to-print": "^3.2.0",
|
||||
"react-tooltip": "^5.30.0",
|
||||
"reactour": "^1.19.4",
|
||||
"redux": "^5.0.1",
|
||||
"regex-parser": "^2.3.1",
|
||||
"serve": "^14.2.5",
|
||||
"styled-components": "^5.3.11",
|
||||
"web-vitals": "^5.1.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
@@ -94,37 +93,37 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/runtime-corejs2": "^7.28.3",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@babel/runtime-corejs2": "^7.28.6",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/ui": "^4.0.15",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"@types/react": "^19.2.10",
|
||||
"@vitejs/plugin-react": "^5.1.3",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"babel-loader": "^10.0.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-loader": "^7.1.3",
|
||||
"daisyui": "^4.12.24",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^27.2.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier": "^3.8.1",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"tmp": "^0.2.5",
|
||||
"vite": "^7.2.6",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.15"
|
||||
"vite-tsconfig-paths": "^6.0.5",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"overrides": {
|
||||
"tmp": "$tmp"
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"New Template": "Neue Vorlage",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "Datei ausgewählt",
|
||||
"files-selected": "Datei(en) ausgewählt",
|
||||
"template-title": "Vorlagentitel",
|
||||
"document-title": "Dokumenttitel",
|
||||
"title": "Titel",
|
||||
@@ -270,7 +270,7 @@
|
||||
"signature": "Unterschrift",
|
||||
"upload-image": "Bild hochladen",
|
||||
"clear": "Löschen",
|
||||
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen",
|
||||
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen/Stempel",
|
||||
"initials": "Initialen",
|
||||
"API": "API",
|
||||
"api-token": "API-Token",
|
||||
@@ -337,7 +337,7 @@
|
||||
"quick-send-alert-1": "Alle Rollen in diesem Dokument sind derzeit mit Kontakten verknüpft. Um Kopien dieser Vorlage schnell an mehrere Unterzeichner zu senden, stellen Sie sicher, dass mindestens eine Rolle keinem Kontakt zugeordnet ist.",
|
||||
"quick-send-alert-2": "Bitte stellen Sie sicher, dass für alle Empfänger mindestens ein Signatur-Widget hinzugefügt wurde.",
|
||||
"quick-send-alert-3": "Bitte fügen Sie diesem template mindestens eine Rolle hinzu.",
|
||||
"quick-send-alert-4": "Das Limit für Schnellsendungen wurde erreicht.",
|
||||
"quick-send-alert-4": "Limit für {{flow}} erreicht.",
|
||||
"copy-link": "Link kopieren",
|
||||
"copy": "Kopieren",
|
||||
"revoke-document": "Dokument widerrufen",
|
||||
@@ -393,7 +393,8 @@
|
||||
"radio button": "Radiobutton",
|
||||
"image": "Bild",
|
||||
"email": "E-Mail",
|
||||
"number": "Zahl"
|
||||
"number": "Zahl",
|
||||
"draw": "Zeichnen"
|
||||
},
|
||||
"fields": "Felder",
|
||||
"recipients": "Empfänger",
|
||||
@@ -516,14 +517,14 @@
|
||||
"placeholder-alert-4": "Sie haben erfolgreich E-Mails an alle Empfänger gesendet!",
|
||||
"placeholder-mail-alert": "Sie haben erfolgreich eine E-Mail an {{name}} gesendet. Die nachfolgenden Unterzeichner erhalten eine E-Mail, sobald {{name}} das Dokument unterzeichnet.",
|
||||
"placeholder-mail-alert-you": "Nachfolgende Unterzeichner erhalten E-Mails, sobald Sie das Dokument unterschreiben.",
|
||||
"placeholder-alert-5": "Möchten Sie die Dokumente jetzt unterschreiben?",
|
||||
"placeholder-alert-5": "Sie können jetzt unterschreiben oder es an Ihre E-Mail senden, um es später abzuschließen.",
|
||||
"placeholder-alert-6": "Bitte richten Sie den E-Mail-Adapter ein, um E-Mails zu senden!",
|
||||
"placeholder-alert-7": "Bitte wählen Sie einen Unterzeichner aus, um Platzhalter hinzuzufügen!",
|
||||
"email-subject": "E-Mail-Betreff",
|
||||
"email-body": "E-Mail-Inhalt",
|
||||
"email-placeholder": "E-Mail-Text eingeben",
|
||||
"reset-to-default": "Auf Standard zurücksetzen",
|
||||
"cutomize-email": "E-Mail anpassen",
|
||||
"customize-email": "E-Mail anpassen",
|
||||
"upgrade-to-customize-email": "Upgrade, um E-Mails anzupassen",
|
||||
"sign-url": "Signatur-URL",
|
||||
"mails-sent": "E-Mails gesendet",
|
||||
@@ -588,7 +589,7 @@
|
||||
"provide-your-details": "Bitte geben Sie Ihre Daten an",
|
||||
"provide-email": "Bitte E-Mail angeben.",
|
||||
"additional-info": "Zusätzliche Informationen",
|
||||
"signature-saved-alert": "Unterschrift erfolgreich gespeichert.",
|
||||
"signature-saved-alert": "erfolgreich gespeichert.",
|
||||
"saved-successfully": "Erfolgreich gespeichert.",
|
||||
"fill-field": "Bitte dieses Feld ausfüllen",
|
||||
"error-template": "Fehler: Vorlage nicht gefunden!",
|
||||
@@ -1243,6 +1244,7 @@
|
||||
},
|
||||
"read-only-date-error": "Das schreibgeschützte Datums-Widget muss ein Standarddatum haben.",
|
||||
"set-date": "Datum festlegen",
|
||||
"default-date": "Standarddatum",
|
||||
"set-today": "Unterzeichnungsdatum",
|
||||
"enter-name": "Name eingeben",
|
||||
"enter-email": "E-Mail eingeben",
|
||||
@@ -1308,5 +1310,55 @@
|
||||
"your-credits-usage": "Ihr Kreditverbrauch",
|
||||
"remaining": "verbleibend",
|
||||
"used": "verwendet",
|
||||
"your-credits-usage-help": "Dies ist die Anzahl der Credits, die mit Ihrem API-Schlüssel verbraucht wurden."
|
||||
"your-credits-usage-help": "Dies ist die Anzahl der Credits, die mit Ihrem API-Schlüssel verbraucht wurden.",
|
||||
"api-credits-updated": "API-Credits wurden erfolgreich zugewiesen.",
|
||||
"min-date": "Mindestdatum",
|
||||
"max-date": "Höchstdatum",
|
||||
"invalid-min-date": "Ungültiges Mindestdatum.",
|
||||
"min-less-max": "Das Mindestdatum muss kleiner oder gleich dem Höchstdatum sein.",
|
||||
"invalid-max-date": "Ungültiges Höchstdatum.",
|
||||
"max-less-min": "Das Höchstdatum muss größer oder gleich dem Mindestdatum sein.",
|
||||
"date-range": "Datumsbereich",
|
||||
"date-range-help": "Legen Sie ein Mindest- und/oder Höchstdatum fest, um die Auswahl einzuschränken. Das gewählte Datum muss innerhalb dieses Bereichs liegen.",
|
||||
"option-disabled-by-owner": "Der Dokumentersteller hat diese Option deaktiviert",
|
||||
"pen-colors": "Stiftfarben",
|
||||
"pen-colors-help": "Wählen Sie aus, welche Stiftfarben für Signatur- und Initialen-Widgets verfügbar sind. Aktivieren Sie ein Kontrollkästchen, um eine Farbe anzuzeigen, und deaktivieren Sie es, um sie auszublenden.",
|
||||
"security-key": "Sicherheitsschlüssel",
|
||||
"remove-limit": "Limit entfernen",
|
||||
"removed-api-credits": "Das API-Credit-Limit wurde entfernt.",
|
||||
"enable-authentication": "Authentifizierung aktivieren",
|
||||
"working": "In Bearbeitung",
|
||||
"test-security-key": "Sicherheitsschlüssel testen",
|
||||
"generate": "Generieren",
|
||||
"regenerate": "Neu generieren",
|
||||
"signing-date": "Signierdatum",
|
||||
"date-widget": "Datums-Widget",
|
||||
"mail-failed": "Beim Senden der E-Mail ist etwas schiefgelaufen. Bitte versuchen Sie es erneut.",
|
||||
"mail-status-head": "Nächster Schritt: Ihr Dokument unterschreiben?",
|
||||
"send-to-email": "Per E-Mail senden",
|
||||
"date-pref-help-sub-title": "Mit den Datums-Widget-Einstellungen können Sie die Standardkonfiguration für das Datums-Widget festlegen. Diese Standards werden automatisch angewendet, wenn Sie ein neues Datums-Widget zu einem Dokument oder einer Vorlage hinzufügen. Sie können jedes Datums-Widget jederzeit individuell anpassen, indem Sie auf das Zahnradsymbol klicken.",
|
||||
"date-pref-help-format": "Wählen Sie das standardmäßige Datumsformat für Datums-Widgets.",
|
||||
"date-pref-help-default-date": "Legen Sie einen Standard-Datumswert für Datums-Widgets fest.",
|
||||
"date-pref-help-signing-date": "Wenn aktiviert, verwendet das Datums-Widget automatisch das Datum, an dem der Unterzeichner das Dokument signiert.",
|
||||
"date-pref-help-read-only": "Wenn aktiviert, ist das Datums-Widget für den Unterzeichner schreibgeschützt.",
|
||||
"upload-csv": "CSV hochladen",
|
||||
"file-selected": "Datei ausgewählt",
|
||||
"bulk-send-csv-helper": "Verwenden Sie Header für Unterzeichnerrollen (Rollen-E-Mail/Name/Telefon) und Vorabfüllwerte (Vorabfüllung: Feldname). Erforderliche Vorabfüllfelder müssen ausgefüllt sein.",
|
||||
"bulk-send-csv-helper-note": "Hinweis: Für Vorabfüllfelder vom Typ Datum in der CSV muss der Wert in folgendem Format angegeben werden: {{value}} (Beispiel: {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} Zeilen importiert. {{invalid}} ungültige Zeilen wurden übersprungen.",
|
||||
"bulk-send-csv-missing-headers": "Erforderliche CSV-Header fehlen. Rollen: {{roles}}. Vorabfüllung: {{prefill}}.",
|
||||
"bulk-send-no-records": "Bitte fügen Sie vor dem Senden mindestens einen Empfänger hinzu.",
|
||||
"csv-only-support": "Es werden nur CSV-Dateien unterstützt.",
|
||||
"csv-no-records": "Keine Datensätze in der CSV gefunden.",
|
||||
"select-at-least-option": "Bitte wählen Sie mindestens eine Option aus.",
|
||||
"draw-required": "Bitte zeichnen Sie in das Feld.",
|
||||
"enter-smtp-username": "SMTP-Benutzernamen eingeben",
|
||||
"enter-webhook-url": "Webhook-URL eingeben",
|
||||
"enter-user-name": "Benutzernamen eingeben",
|
||||
"enter-tagline": "Slogan eingeben",
|
||||
"enter-password-or-click-autogenerate": "Passwort eingeben oder auf „Automatisch generieren“ klicken",
|
||||
"enter-daily-email-quota": "Tägliches E-Mail-Kontingent eingeben",
|
||||
"enter-custom-regular-expression": "Benutzerdefinierten regulären Ausdruck eingeben",
|
||||
"enter-value": "{{value}} eingeben",
|
||||
"are-you-sure-want-to-send-document": "Sind Sie sicher, dass Sie das Dokument senden möchten?"
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
"New Template": "New template",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "file(s) selected",
|
||||
"files-selected": "file(s) selected",
|
||||
"template-title": "Template title",
|
||||
"document-title": "Document title",
|
||||
"title": "Title",
|
||||
@@ -270,7 +270,7 @@
|
||||
"signature": "Signature",
|
||||
"upload-image": " Upload image",
|
||||
"clear": "Clear",
|
||||
"upload-signature/Image": "Please upload signature/Image",
|
||||
"upload-signature/Image": "Please upload signature/Image/stamp",
|
||||
"initials": "Initials",
|
||||
"API": "API",
|
||||
"api-token": "API token",
|
||||
@@ -337,7 +337,7 @@
|
||||
"quick-send-alert-1": "All roles in this document are currently linked to contacts. To quick send copies of this template to multiple signers, please ensure that at least one role is not linked to any contact.",
|
||||
"quick-send-alert-2": "Please ensure there's at least one signature widget added for all recipients.",
|
||||
"quick-send-alert-3": "Please add at least one role to this template.",
|
||||
"quick-send-alert-4": "Quick send reached limit.",
|
||||
"quick-send-alert-4": "Limit reached for {{flow}}.",
|
||||
"copy-link": "Copy link",
|
||||
"copy": "Copy",
|
||||
"revoke-document": "Revoke document",
|
||||
@@ -393,7 +393,8 @@
|
||||
"radio button": "radio button",
|
||||
"image": "image",
|
||||
"email": "email",
|
||||
"number": "number"
|
||||
"number": "number",
|
||||
"draw": "draw"
|
||||
},
|
||||
"fields": "Fields",
|
||||
"recipients": "Recipients",
|
||||
@@ -517,14 +518,14 @@
|
||||
"placeholder-alert-4": "You have successfully sent mails to all recipients!",
|
||||
"placeholder-mail-alert": "You have successfully sent email to {{name}}. Subsequent signers will get email(s) once {{name}} signs the document",
|
||||
"placeholder-mail-alert-you": "Subsequent signers will get email(s) once you sign the document.",
|
||||
"placeholder-alert-5": "Do you want to sign the document right now?",
|
||||
"placeholder-alert-5": "You can sign now or send it to your email to complete later.",
|
||||
"placeholder-alert-6": "Please setup mail adapter to send mail!",
|
||||
"placeholder-alert-7": "Please select signer for add placeholder!",
|
||||
"email-subject": "email subject",
|
||||
"email-body": "email body",
|
||||
"email-placeholder": "add body of email",
|
||||
"reset-to-default": "Reset to default",
|
||||
"cutomize-email": "Cutomize email",
|
||||
"customize-email": "Customize email",
|
||||
"upgrade-to-customize-email": "Upgrade to customize Email",
|
||||
"sign-url": "Sign url",
|
||||
"mails-sent": "Mails Sent",
|
||||
@@ -589,7 +590,7 @@
|
||||
"provide-your-details": "Provide your details",
|
||||
"provide-email": "Please provide email.",
|
||||
"additional-info": "Additional info",
|
||||
"signature-saved-alert": "Signature saved successfully.",
|
||||
"signature-saved-alert": "Saved successfully.",
|
||||
"saved-successfully": "Saved successfully.",
|
||||
"fill-field": "Please fill out this field",
|
||||
"error-template": "Error: Template not found!",
|
||||
@@ -1244,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "Read-only date widget must have a default date.",
|
||||
"set-date": "Set Date",
|
||||
"default-date": "Default Date",
|
||||
"set-today": "Signing Date",
|
||||
"enter-name": "Enter name",
|
||||
"enter-email": "Enter email",
|
||||
@@ -1309,5 +1311,55 @@
|
||||
"your-credits-usage": "Your credits usage",
|
||||
"remaining": "remaining",
|
||||
"used": "used",
|
||||
"your-credits-usage-help": "This is the number of credits consumed with your API key."
|
||||
"your-credits-usage-help": "This is the number of credits consumed with your API key.",
|
||||
"api-credits-updated": "API credits have been allocated successfully.",
|
||||
"min-date": "Min Date",
|
||||
"max-date": "Max Date",
|
||||
"invalid-min-date": "Invalid min date.",
|
||||
"min-less-max": "Min date must be less than or equal to max date.",
|
||||
"invalid-max-date": "Invalid max date.",
|
||||
"max-less-min": "Max date must be greater than or equal to min date.",
|
||||
"date-range": "Date range",
|
||||
"date-range-help": "Set Min and/or Max to restrict selection. Chosen date must fall within the range.",
|
||||
"option-disabled-by-owner": "Document creator has disabled this option",
|
||||
"pen-colors": "Pen colors",
|
||||
"pen-colors-help": "Choose which pen colors are available for Signature and Initials widgets. Check to show a color, uncheck to hide it.",
|
||||
"security-key": "Security key",
|
||||
"remove-limit": "Remove limit",
|
||||
"removed-api-credits": "The API credit limit has been removed.",
|
||||
"enable-authentication": "Enable authentication",
|
||||
"working": "Working",
|
||||
"test-security-key": "Test security key",
|
||||
"generate": "Generate",
|
||||
"regenerate": "Regenerate",
|
||||
"signing-date": "Signing Date",
|
||||
"date-widget": "Date Widget",
|
||||
"mail-failed": "Something went wrong while sending the email. Please try again.",
|
||||
"mail-status-head": "Next step: Sign your document ?",
|
||||
"send-to-email": "Send to Email",
|
||||
"date-pref-help-sub-title": "Date Widget Preferences let you define the default settings for the Date widget. These defaults are automatically applied whenever you add a new Date widget to a document or template. You can still customize any individual Date widget at any time by clicking the gear icon.",
|
||||
"date-pref-help-format": "Choose the default date format used for Date widgets.",
|
||||
"date-pref-help-default-date": "Set a default date value for Date widgets.",
|
||||
"date-pref-help-signing-date": "When enabled, the Date widget automatically uses the date the signer signs the document.",
|
||||
"date-pref-help-read-only": "When enabled, the Date widget is read-only for the signer.",
|
||||
"upload-csv": "Upload CSV",
|
||||
"file-selected": "file selected",
|
||||
"bulk-send-csv-helper": "Use headers for signer roles (Role Email/Name/Phone) and prefill values (Prefill: Field Name). Required prefill fields must be completed.",
|
||||
"bulk-send-csv-helper-note": "Note: For Date-type prefill fields in the CSV, the value must be provided in this format: {{value}} (example: {{example}}).",
|
||||
"bulk-send-csv-summary": "Imported {{total}} rows. {{invalid}} invalid rows were skipped.",
|
||||
"bulk-send-csv-missing-headers": "Missing required CSV headers. Roles: {{roles}}. Prefill: {{prefill}}.",
|
||||
"bulk-send-no-records": "Please add at least one recipient before sending.",
|
||||
"csv-only-support": "Only CSV files are supported.",
|
||||
"csv-no-records": "No records found in the CSV.",
|
||||
"select-at-least-option": "Please select at least one option.",
|
||||
"draw-required": "Please draw in the box.",
|
||||
"enter-smtp-username": "Enter your SMTP username",
|
||||
"enter-webhook-url": "Enter webhook url",
|
||||
"enter-user-name": "Enter user name",
|
||||
"enter-tagline": "Enter tagline",
|
||||
"enter-password-or-click-autogenerate": "Enter password or click Autogenerate",
|
||||
"enter-daily-email-quota": "Enter daily email quota",
|
||||
"enter-custom-regular-expression": "Enter custom regular expression",
|
||||
"enter-value": "Enter {{value}}",
|
||||
"are-you-sure-want-to-send-document": "Are you sure want to send document?"
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
"New Template": "Nueva plantilla",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "archivo seleccionado",
|
||||
"files-selected": "archivo(s) seleccionado(s)",
|
||||
"template-title": "Título de la plantilla",
|
||||
"document-title": "Título del documento",
|
||||
"title": "Título",
|
||||
@@ -270,7 +270,7 @@
|
||||
"signature": "Firma",
|
||||
"upload-image": " Subir imagen",
|
||||
"clear": "Limpiar",
|
||||
"upload-signature/Image": "Por favor, sube la firma/imagen",
|
||||
"upload-signature/Image": "Por favor, sube la firma/imagen/estampilla",
|
||||
"initials": "Iniciales",
|
||||
"API": "API",
|
||||
"api-token": "Token API",
|
||||
@@ -337,7 +337,7 @@
|
||||
"quick-send-alert-1": "Todos los roles de este documento están actualmente vinculados a contactos. Para enviar rápidamente copias de esta plantilla a varios firmantes, por favor, asegúrate de que al menos un rol no esté vinculado a ningún contacto.",
|
||||
"quick-send-alert-2": "Por favor, asegúrate de que hay al menos un widget de firma añadido para cada destinatario.",
|
||||
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
|
||||
"quick-send-alert-4": "El envío rápido ha alcanzado el límite.",
|
||||
"quick-send-alert-4": "Se alcanzó el límite para {{flow}}.",
|
||||
"copy-link": "Copiar enlace",
|
||||
"copy": "Copiar",
|
||||
"revoke-document": "Revocar documento",
|
||||
@@ -393,7 +393,8 @@
|
||||
"radio button": "botón de radio",
|
||||
"image": "imagen",
|
||||
"email": "correo",
|
||||
"number": "número"
|
||||
"number": "número",
|
||||
"draw": "Dibujar"
|
||||
},
|
||||
"fields": "Campos",
|
||||
"recipients": "Destinatarios",
|
||||
@@ -517,14 +518,14 @@
|
||||
"placeholder-alert-4": "¡Has enviado exitosamente correos a todos los destinatarios!",
|
||||
"placeholder-mail-alert": "Has enviado un correo electrónico con éxito a {{name}}. Los siguientes firmantes recibirán un correo electrónico una vez que {{name}} firme el documento.",
|
||||
"placeholder-mail-alert-you": "Los firmantes siguientes recibirán un correo electrónico una vez que usted firme el documento.",
|
||||
"placeholder-alert-5": "¿Quieres firmar documentos ahora mismo?",
|
||||
"placeholder-alert-5": "Puede firmar ahora o enviarlo a su correo electrónico para completarlo más tarde.",
|
||||
"placeholder-alert-6": "¡Por favor, configura el adaptador de correo para enviar correos!",
|
||||
"placeholder-alert-7": "¡Por favor, selecciona un firmante para agregar un marcador de posición!",
|
||||
"email-subject": "asunto del correo",
|
||||
"email-body": "cuerpo del correo",
|
||||
"email-placeholder": "agregar cuerpo del correo",
|
||||
"reset-to-default": "Restablecer a por defecto",
|
||||
"cutomize-email": "Personalizar correo",
|
||||
"customize-email": "Personalizar correo",
|
||||
"upgrade-to-customize-email": "Mejora para personalizar el correo",
|
||||
"sign-url": "URL de firma",
|
||||
"mails-sent": "Correos enviados",
|
||||
@@ -589,7 +590,7 @@
|
||||
"provide-your-details": "Proporciona tus detalles",
|
||||
"provide-email": "Por favor, proporciona un correo.",
|
||||
"additional-info": "Información adicional",
|
||||
"signature-saved-alert": "Firma guardada exitosamente.",
|
||||
"signature-saved-alert": "guardada exitosamente.",
|
||||
"saved-successfully": "Guardado con éxito.",
|
||||
"fill-field": "Por favor, rellena este campo",
|
||||
"error-template": "Error: ¡Plantilla no encontrada!",
|
||||
@@ -1244,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "El widget de fecha de solo lectura debe tener una fecha predeterminada.",
|
||||
"set-date": "Establecer fecha",
|
||||
"default-date": "Fecha predeterminada",
|
||||
"set-today": "Fecha de firma",
|
||||
"enter-name": "Ingrese nombre",
|
||||
"enter-email": "Ingrese correo electrónico",
|
||||
@@ -1309,5 +1311,55 @@
|
||||
"your-credits-usage": "Uso de sus créditos",
|
||||
"remaining": "restantes",
|
||||
"used": "usados",
|
||||
"your-credits-usage-help": "Esta es la cantidad de créditos consumidos con su clave API."
|
||||
"your-credits-usage-help": "Esta es la cantidad de créditos consumidos con su clave API.",
|
||||
"api-credits-updated": "Los créditos de API se han asignado correctamente.",
|
||||
"min-date": "Fecha mínima",
|
||||
"max-date": "Fecha máxima",
|
||||
"invalid-min-date": "Fecha mínima no válida.",
|
||||
"min-less-max": "La fecha mínima debe ser menor o igual a la fecha máxima.",
|
||||
"invalid-max-date": "Fecha máxima no válida.",
|
||||
"max-less-min": "La fecha máxima debe ser mayor o igual a la fecha mínima.",
|
||||
"date-range": "Rango de fechas",
|
||||
"date-range-help": "Establezca una fecha mínima y/o máxima para restringir la selección. La fecha elegida debe estar dentro del rango.",
|
||||
"option-disabled-by-owner": "El creador del documento ha deshabilitado esta opción",
|
||||
"pen-colors": "Colores del bolígrafo",
|
||||
"pen-colors-help": "Elija qué colores de bolígrafo están disponibles para los widgets de Firma e Iniciales. Marque una casilla para mostrar un color y desmárquela para ocultarlo.",
|
||||
"security-key": "Clave de seguridad",
|
||||
"remove-limit": "Eliminar límite",
|
||||
"removed-api-credits": "El límite de créditos API ha sido eliminado.",
|
||||
"enable-authentication": "Habilitar autenticación",
|
||||
"working": "En funcionamiento",
|
||||
"test-security-key": "Probar clave de seguridad",
|
||||
"generate": "Generar",
|
||||
"regenerate": "Regenerar",
|
||||
"signing-date": "Fecha de firma",
|
||||
"date-widget": "Widget de fecha",
|
||||
"mail-failed": "Algo salió mal al enviar el correo electrónico. Por favor, inténtelo de nuevo.",
|
||||
"mail-status-head": "Siguiente paso: ¿Firmar su documento?",
|
||||
"send-to-email": "Enviar por correo electrónico",
|
||||
"date-pref-help-sub-title": "Las preferencias del widget de fecha le permiten definir la configuración predeterminada del widget de fecha. Estos valores se aplican automáticamente cada vez que añade un nuevo widget de fecha a un documento o plantilla. Aún puede personalizar cualquier widget de fecha individual en cualquier momento haciendo clic en el icono de engranaje.",
|
||||
"date-pref-help-format": "Elija el formato de fecha predeterminado utilizado para los widgets de fecha.",
|
||||
"date-pref-help-default-date": "Establezca un valor de fecha predeterminado para los widgets de fecha.",
|
||||
"date-pref-help-signing-date": "Cuando está habilitado, el widget de fecha utiliza automáticamente la fecha en que el firmante firma el documento.",
|
||||
"date-pref-help-read-only": "Cuando está habilitado, el widget de fecha es de solo lectura para el firmante.",
|
||||
"upload-csv": "Subir CSV",
|
||||
"file-selected": "archivo seleccionado",
|
||||
"bulk-send-csv-helper": "Use encabezados para los roles de firmantes (Correo/Nombre/Teléfono del rol) y valores de prellenado (Prellenado: Nombre del campo). Los campos de prellenado obligatorios deben completarse.",
|
||||
"bulk-send-csv-helper-note": "Nota: Para los campos de prellenado de tipo fecha en el CSV, el valor debe proporcionarse en el siguiente formato: {{value}} (ejemplo: {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} filas importadas. {{invalid}} filas no válidas fueron omitidas.",
|
||||
"bulk-send-csv-missing-headers": "Faltan encabezados obligatorios del CSV. Roles: {{roles}}. Prellenado: {{prefill}}.",
|
||||
"bulk-send-no-records": "Agregue al menos un destinatario antes de enviar.",
|
||||
"csv-only-support": "Solo se admiten archivos CSV.",
|
||||
"csv-no-records": "No se encontraron registros en el CSV.",
|
||||
"select-at-least-option": "Por favor, seleccione al menos una opción.",
|
||||
"draw-required": "Por favor, dibuje en el campo.",
|
||||
"enter-smtp-username": "Ingrese su nombre de usuario SMTP",
|
||||
"enter-webhook-url": "Ingrese la URL del webhook",
|
||||
"enter-user-name": "Ingrese el nombre de usuario",
|
||||
"enter-tagline": "Ingrese el eslogan",
|
||||
"enter-password-or-click-autogenerate": "Ingrese la contraseña o haga clic en Autogenerar",
|
||||
"enter-daily-email-quota": "Ingrese la cuota diaria de correo electrónico",
|
||||
"enter-custom-regular-expression": "Ingrese una expresión regular personalizada",
|
||||
"enter-value": "Ingrese {{value}}",
|
||||
"are-you-sure-want-to-send-document": "¿Está seguro de que desea enviar el documento?"
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
"New Template": "Nouveau modèle",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "fichier sélectionné",
|
||||
"files-selected": "fichier(s) sélectionné(s)",
|
||||
"template-title": "Titre du modèle",
|
||||
"document-title": "Titre du document",
|
||||
"title": "Titre",
|
||||
@@ -270,7 +270,7 @@
|
||||
"signature": "Signature",
|
||||
"upload-image": "Télécharger une image",
|
||||
"clear": "Vider",
|
||||
"upload-signature/Image": "télécharger-signature/Image",
|
||||
"upload-signature/Image": "télécharger-signature/Image/timbre",
|
||||
"initials": "Initiales",
|
||||
"API": "API",
|
||||
"api-token": "Jeton API",
|
||||
@@ -337,7 +337,7 @@
|
||||
"quick-send-alert-1": "Tous les rôles dans ce document sont actuellement liés à des contacts. Pour envoyer rapidement des copies de ce modèle à plusieurs signataires, veuillez vous assurer qu'au moins un rôle n'est lié à aucun contact.",
|
||||
"quick-send-alert-2": "Veuillez vous assurer qu'au moins un widget de signature est ajouté pour tous les destinataires.",
|
||||
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
|
||||
"quick-send-alert-4": "L'envoi rapide a atteint la limite.",
|
||||
"quick-send-alert-4": "Limite atteint pour {{flow}}.",
|
||||
"copy-link": "Copier le lien",
|
||||
"copy": "Copier",
|
||||
"revoke-document": "Révoquer le document",
|
||||
@@ -393,7 +393,8 @@
|
||||
"radio button": "bouton radio",
|
||||
"image": "image",
|
||||
"email": "e-mail",
|
||||
"number": "nombre"
|
||||
"number": "nombre",
|
||||
"draw": "Dessiner"
|
||||
},
|
||||
"fields": "Des champs",
|
||||
"recipients": "Destinataires",
|
||||
@@ -516,14 +517,14 @@
|
||||
"placeholder-alert-4": "Vous avez envoyé avec succès des mails à tous les",
|
||||
"placeholder-mail-alert": "Vous avez envoyé un e-mail avec succès à {{name}}. Les signataires suivants recevront un e-mail une fois que {{name}} aura signé le document.",
|
||||
"placeholder-mail-alert-you": "Les signataires suivants recevront un e-mail une fois que vous aurez signé le document.",
|
||||
"placeholder-alert-5": "Voulez-vous signer des documents maintenant ?",
|
||||
"placeholder-alert-5": "Vous pouvez signer maintenant ou l’envoyer à votre e-mail pour le terminer plus tard.",
|
||||
"placeholder-alert-6": "Veuillez configurer l'adaptateur de messagerie pour envoyer du courrier !",
|
||||
"placeholder-alert-7": "Veuillez sélectionner le signataire pour ajouter un espace réservé !",
|
||||
"email-subject": "Sujet du courriel",
|
||||
"email-body": "corps de l'e-mail",
|
||||
"email-placeholder": "Ajouter le corps de l'e-mail",
|
||||
"reset-to-default": "Réinitialiser aux valeurs par défaut",
|
||||
"cutomize-email": "Personnaliser l'e-mail",
|
||||
"customize-email": "Personnaliser l'e-mail",
|
||||
"upgrade-to-customize-email": "Mise à niveau pour personnaliser l'e-mail",
|
||||
"sign-url": "Signer l'URL",
|
||||
"mails-sent": "Mails envoyés avec succès !",
|
||||
@@ -588,7 +589,7 @@
|
||||
"provide-your-details": "Fournissez vos coordonnées",
|
||||
"provide-email": "Veuillez fournir un e-mail.",
|
||||
"additional-info": "Information additionnelle",
|
||||
"signature-saved-alert": "Signature enregistrée avec succès.",
|
||||
"signature-saved-alert": "enregistrée avec succès.",
|
||||
"saved-successfully": "Enregistré avec succès.",
|
||||
"fill-field": "Veuillez remplir ce champ",
|
||||
"error-template": "Erreur : modèle introuvable ! ",
|
||||
@@ -1243,6 +1244,7 @@
|
||||
},
|
||||
"read-only-date-error": "Le widget de date en lecture seule doit avoir une date par défaut.",
|
||||
"set-date": "Définir la date",
|
||||
"default-date": "Date par défaut",
|
||||
"set-today": "Date de signature",
|
||||
"enter-name": "Saisir le nom",
|
||||
"enter-email": "Saisir l'e-mail",
|
||||
@@ -1308,5 +1310,55 @@
|
||||
"your-credits-usage": "Votre utilisation des crédits",
|
||||
"remaining": "restants",
|
||||
"used": "utilisés",
|
||||
"your-credits-usage-help": "Ceci est le nombre de crédits consommés avec votre clé API."
|
||||
"your-credits-usage-help": "Ceci est le nombre de crédits consommés avec votre clé API.",
|
||||
"api-credits-updated": "Les crédits API ont été attribués avec succès.",
|
||||
"min-date": "Date minimale",
|
||||
"max-date": "Date maximale",
|
||||
"invalid-min-date": "Date minimale invalide.",
|
||||
"min-less-max": "La date minimale doit être inférieure ou égale à la date maximale.",
|
||||
"invalid-max-date": "Date maximale invalide.",
|
||||
"max-less-min": "La date maximale doit être supérieure ou égale à la date minimale.",
|
||||
"date-range": "Plage de dates",
|
||||
"date-range-help": "Définissez une date minimale et/ou maximale pour restreindre la sélection. La date choisie doit se situer dans cette plage.",
|
||||
"option-disabled-by-owner": "Le créateur du document a désactivé cette option",
|
||||
"pen-colors": "Couleurs du stylo",
|
||||
"pen-colors-help": "Choisissez quelles couleurs de stylo sont disponibles pour les widgets Signature et Initiales. Cochez pour afficher une couleur, décochez pour la masquer.",
|
||||
"security-key": "Clé de sécurité",
|
||||
"remove-limit": "Supprimer la limite",
|
||||
"removed-api-credits": "La limite de crédits API a été supprimée.",
|
||||
"enable-authentication": "Activer l'authentification",
|
||||
"working": "En cours",
|
||||
"test-security-key": "Tester la clé de sécurité",
|
||||
"generate": "Générer",
|
||||
"regenerate": "Régénérer",
|
||||
"signing-date": "Date de signature",
|
||||
"date-widget": "Widget de date",
|
||||
"mail-failed": "Une erreur s'est produite lors de l'envoi de l'e-mail. Veuillez réessayer.",
|
||||
"mail-status-head": "Étape suivante : signer votre document ?",
|
||||
"send-to-email": "Envoyer par e-mail",
|
||||
"date-pref-help-sub-title": "Les préférences du widget de date vous permettent de définir les paramètres par défaut du widget de date. Ces paramètres sont automatiquement appliqués lorsque vous ajoutez un nouveau widget de date à un document ou à un modèle. Vous pouvez toujours personnaliser chaque widget de date individuellement en cliquant sur l'icône d'engrenage.",
|
||||
"date-pref-help-format": "Choisissez le format de date par défaut utilisé pour les widgets de date.",
|
||||
"date-pref-help-default-date": "Définissez une valeur de date par défaut pour les widgets de date.",
|
||||
"date-pref-help-signing-date": "Lorsqu'elle est activée, le widget de date utilise automatiquement la date à laquelle le signataire signe le document.",
|
||||
"date-pref-help-read-only": "Lorsqu'elle est activée, le widget de date est en lecture seule pour le signataire.",
|
||||
"upload-csv": "Téléverser un CSV",
|
||||
"file-selected": "fichier sélectionné",
|
||||
"bulk-send-csv-helper": "Utilisez des en-têtes pour les rôles des signataires (E-mail/Nom/Téléphone du rôle) et les valeurs de préremplissage (Préremplissage : Nom du champ). Les champs de préremplissage requis doivent être complétés.",
|
||||
"bulk-send-csv-helper-note": "Remarque : Pour les champs de préremplissage de type date dans le CSV, la valeur doit être fournie dans le format suivant : {{value}} (exemple : {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} lignes importées. {{invalid}} lignes non valides ont été ignorées.",
|
||||
"bulk-send-csv-missing-headers": "En-têtes CSV requis manquants. Rôles : {{roles}}. Préremplissage : {{prefill}}.",
|
||||
"bulk-send-no-records": "Veuillez ajouter au moins un destinataire avant l'envoi.",
|
||||
"csv-only-support": "Seuls les fichiers CSV sont pris en charge.",
|
||||
"csv-no-records": "Aucun enregistrement trouvé dans le CSV.",
|
||||
"select-at-least-option": "Veuillez sélectionner au moins une option.",
|
||||
"draw-required": "Veuillez dessiner dans le champ.",
|
||||
"enter-smtp-username": "Saisissez votre nom d'utilisateur SMTP",
|
||||
"enter-webhook-url": "Saisissez l'URL du webhook",
|
||||
"enter-user-name": "Saisissez le nom d'utilisateur",
|
||||
"enter-tagline": "Saisissez le slogan",
|
||||
"enter-password-or-click-autogenerate": "Saisissez le mot de passe ou cliquez sur Génération automatique",
|
||||
"enter-daily-email-quota": "Saisissez le quota quotidien d'e-mails",
|
||||
"enter-custom-regular-expression": "Saisissez une expression régulière personnalisée",
|
||||
"enter-value": "Saisissez {{value}}",
|
||||
"are-you-sure-want-to-send-document": "Êtes-vous sûr de vouloir envoyer le document ?"
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
"New Template": "नया टेम्पलेट",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "फ़ाइल चयनित",
|
||||
"files-selected": "फ़ाइलें चयनित",
|
||||
"template-title": "टेम्पलेट शीर्षक",
|
||||
"document-title": "दस्तावेज़ शीर्षक",
|
||||
"title": "शीर्षक",
|
||||
@@ -270,7 +270,7 @@
|
||||
"signature": "हस्ताक्षर",
|
||||
"upload-image": " छवि अपलोड करें",
|
||||
"clear": "स्पष्ट",
|
||||
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें",
|
||||
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें/मोहर",
|
||||
"initials": "हस्ताक्षर",
|
||||
"API": "एपीआई",
|
||||
"api-token": "एपीआई टोकन",
|
||||
@@ -337,7 +337,7 @@
|
||||
"quick-send-alert-1": "इस दस्तावेज़ में सभी भूमिकाएँ वर्तमान में संपर्कों से जुड़ी हुई हैं। इस टेम्पलेट की प्रतियां कई हस्ताक्षरकर्ताओं को त्वरित रूप से भेजने के लिए, कृपया सुनिश्चित करें कि कम से कम एक भूमिका किसी भी संपर्क से जुड़ी नहीं है।",
|
||||
"quick-send-alert-2": "कृपया सुनिश्चित करें कि सभी प्राप्तकर्ताओं के लिए कम से कम एक हस्ताक्षर विजेट जोड़ा गया है।",
|
||||
"quick-send-alert-3": "कृपया इस टेम्पलेट में कम से कम एक भूमिका जोड़ें।",
|
||||
"quick-send-alert-4": "त्वरित भेजें सीमा तक पहुँच गया।",
|
||||
"quick-send-alert-4": "{{flow}} के लिए सीमा पूरी हो गई है।",
|
||||
"copy-link": "लिंक कॉपी करें",
|
||||
"copy": "कॉपी करें",
|
||||
"revoke-document": "दस्तावेज़ रद्द करें",
|
||||
@@ -393,7 +393,8 @@
|
||||
"radio button": "रेडियो बटन",
|
||||
"image": "छवि",
|
||||
"email": "ईमेल",
|
||||
"number": "संख्या"
|
||||
"number": "संख्या",
|
||||
"draw": "आरेखित करें"
|
||||
},
|
||||
"fields": "क्षेत्र",
|
||||
"recipients": "प्राप्तकर्ता",
|
||||
@@ -516,14 +517,14 @@
|
||||
"placeholder-alert-4": "आपने सभी प्राप्तकर्ताओं को सफलतापूर्वक मेल भेज दिए हैं!",
|
||||
"placeholder-mail-alert": "आपने {{name}} को सफलतापूर्वक ईमेल भेज दिया है। {{name}} द्वारा दस्तावेज़ पर हस्ताक्षर करने के बाद बाद के हस्ताक्षरकर्ताओं को ईमेल प्राप्त होंगे",
|
||||
"placeholder-mail-alert-you": "आपके दस्तावेज़ पर हस्ताक्षर करने के बाद अगले हस्ताक्षरकर्ता को ईमेल प्राप्त होगा।",
|
||||
"placeholder-alert-5": "क्या आप अभी दस्तावेज़ पर हस्ताक्षर करना चाहते हैं?",
|
||||
"placeholder-alert-5": "आप अभी हस्ताक्षर कर सकते हैं या इसे बाद में पूरा करने के लिए अपने ईमेल पर भेज सकते हैं।",
|
||||
"placeholder-alert-6": "मेल भेजने के लिए कृपया मेल एडॉप्टर सेटअप करें!",
|
||||
"placeholder-alert-7": "प्लेसहोल्डर जोड़ने के लिए कृपया हस्ताक्षरकर्ता चुनें!",
|
||||
"email-subject": "ईमेल विषय",
|
||||
"email-body": "ईमेल मुख्य भाग",
|
||||
"email-placeholder": "ईमेल का मुख्य भाग जोड़ें",
|
||||
"reset-to-default": "डिफ़ॉल्ट पर रीसेट करें",
|
||||
"cutomize-email": "ईमेल अनुकूलित करें",
|
||||
"customize-email": "ईमेल अनुकूलित करें",
|
||||
"upgrade-to-customize-email": "ईमेल अनुकूलित करने के लिए अपग्रेड करें",
|
||||
"sign-url": "यूआरएल पर हस्ताक्षर करें",
|
||||
"mails-sent": "मेल भेजे गए",
|
||||
@@ -588,7 +589,7 @@
|
||||
"provide-your-details": "अपना विवरण प्रदान करें",
|
||||
"provide-email": "कृपया ईमेल प्रदान करें।",
|
||||
"additional-info": "अतिरिक्त जानकारी",
|
||||
"signature-saved-alert": "हस्ताक्षर सफलतापूर्वक सहेजा गया।",
|
||||
"signature-saved-alert": "सफलतापूर्वक सहेजा गया।",
|
||||
"saved-successfully": "सफलतापूर्वक सहेजा गया।",
|
||||
"fill-field": "कृपया इस फ़ील्ड को भरें",
|
||||
"error-template": "त्रुटि: टेम्पलेट नहीं मिला!",
|
||||
@@ -1243,6 +1244,7 @@
|
||||
},
|
||||
"read-only-date-error": "रीड-ओनली दिनांक विजेट में एक डिफ़ॉल्ट दिनांक होना आवश्यक है।",
|
||||
"set-date": "दिनांक सेट करें",
|
||||
"default-date": "डिफ़ॉल्ट तिथि",
|
||||
"set-today": "हस्ताक्षर की तिथि",
|
||||
"enter-name": "नाम दर्ज करें",
|
||||
"enter-email": "ईमेल दर्ज करें",
|
||||
@@ -1308,5 +1310,55 @@
|
||||
"your-credits-usage": "आपके क्रेडिट उपयोग",
|
||||
"remaining": "शेष",
|
||||
"used": "उपयोग किए गए",
|
||||
"your-credits-usage-help": "यह आपके API कुंजी द्वारा उपभोग किए गए क्रेडिट की संख्या है।"
|
||||
"your-credits-usage-help": "यह आपके API कुंजी द्वारा उपभोग किए गए क्रेडिट की संख्या है।",
|
||||
"api-credits-updated": "API क्रेडिट सफलतापूर्वक आवंटित कर दिए गए हैं।",
|
||||
"min-date": "न्यूनतम तिथि",
|
||||
"max-date": "अधिकतम तिथि",
|
||||
"invalid-min-date": "अमान्य न्यूनतम तिथि।",
|
||||
"min-less-max": "न्यूनतम तिथि अधिकतम तिथि से कम या उसके बराबर होनी चाहिए।",
|
||||
"invalid-max-date": "अमान्य अधिकतम तिथि।",
|
||||
"max-less-min": "अधिकतम तिथि न्यूनतम तिथि से अधिक या उसके बराबर होनी चाहिए।",
|
||||
"date-range": "तिथि सीमा",
|
||||
"date-range-help": "चयन को सीमित करने के लिए न्यूनतम और/या अधिकतम तिथि सेट करें। चयनित तिथि इस सीमा के भीतर होनी चाहिए।",
|
||||
"option-disabled-by-owner": "दस्तावेज़ निर्माता ने इस विकल्प को अक्षम कर दिया है",
|
||||
"pen-colors": "पेन के रंग",
|
||||
"pen-colors-help": "हस्ताक्षर और प्रारंभिक विजेट्स के लिए उपलब्ध पेन रंग चुनें। किसी रंग को दिखाने के लिए चेक करें और छिपाने के लिए अनचेक करें।",
|
||||
"security-key": "सुरक्षा कुंजी",
|
||||
"remove-limit": "सीमा हटाएँ",
|
||||
"removed-api-credits": "API क्रेडिट सीमा हटा दी गई है।",
|
||||
"enable-authentication": "प्रमाणीकरण सक्षम करें",
|
||||
"working": "कार्यरत",
|
||||
"test-security-key": "सुरक्षा कुंजी का परीक्षण करें",
|
||||
"generate": "जनरेट करें",
|
||||
"regenerate": "पुनः जनरेट करें",
|
||||
"signing-date": "हस्ताक्षर तिथि",
|
||||
"date-widget": "तिथि विजेट",
|
||||
"mail-failed": "ईमेल भेजते समय कुछ गलत हो गया। कृपया पुनः प्रयास करें।",
|
||||
"mail-status-head": "अगला कदम: अपने दस्तावेज़ पर हस्ताक्षर करें?",
|
||||
"send-to-email": "ईमेल पर भेजें",
|
||||
"date-pref-help-sub-title": "डेट विजेट प्राथमिकताएँ आपको डेट विजेट के लिए डिफ़ॉल्ट सेटिंग्स निर्धारित करने की अनुमति देती हैं। जब भी आप किसी दस्तावेज़ या टेम्पलेट में नया डेट विजेट जोड़ते हैं, ये डिफ़ॉल्ट सेटिंग्स स्वतः लागू हो जाती हैं। आप गियर आइकन पर क्लिक करके किसी भी डेट विजेट को कभी भी अनुकूलित कर सकते हैं।",
|
||||
"date-pref-help-format": "डेट विजेट के लिए उपयोग किए जाने वाले डिफ़ॉल्ट तिथि प्रारूप का चयन करें।",
|
||||
"date-pref-help-default-date": "डेट विजेट के लिए एक डिफ़ॉल्ट तिथि मान सेट करें।",
|
||||
"date-pref-help-signing-date": "सक्षम होने पर, डेट विजेट स्वतः उस तिथि का उपयोग करता है जब साइनर दस्तावेज़ पर हस्ताक्षर करता है।",
|
||||
"date-pref-help-read-only": "सक्षम होने पर, डेट विजेट साइनर के लिए केवल-पढ़ने योग्य होता है।",
|
||||
"upload-csv": "CSV अपलोड करें",
|
||||
"file-selected": "फ़ाइल चयनित",
|
||||
"bulk-send-csv-helper": "साइनर भूमिकाओं (भूमिका ईमेल/नाम/फ़ोन) और प्रीफ़िल मानों (प्रीफ़िल: फ़ील्ड नाम) के लिए हेडर का उपयोग करें। आवश्यक प्रीफ़िल फ़ील्ड भरना अनिवार्य है।",
|
||||
"bulk-send-csv-helper-note": "नोट: CSV में तिथि-प्रकार के प्रीफ़िल फ़ील्ड्स के लिए मान इस प्रारूप में प्रदान किया जाना चाहिए: {{value}} (उदाहरण: {{example}})।",
|
||||
"bulk-send-csv-summary": "{{total}} पंक्तियाँ आयात की गईं। {{invalid}} अमान्य पंक्तियाँ छोड़ दी गईं।",
|
||||
"bulk-send-csv-missing-headers": "आवश्यक CSV हेडर अनुपलब्ध हैं। भूमिकाएँ: {{roles}}। प्रीफ़िल: {{prefill}}।",
|
||||
"bulk-send-no-records": "भेजने से पहले कृपया कम से कम एक प्राप्तकर्ता जोड़ें।",
|
||||
"csv-only-support": "केवल CSV फ़ाइलें समर्थित हैं।",
|
||||
"csv-no-records": "CSV में कोई रिकॉर्ड नहीं मिला।",
|
||||
"select-at-least-option": "कृपया कम से कम एक विकल्प चुनें।",
|
||||
"draw-required": "कृपया बॉक्स में ड्रॉ करें।",
|
||||
"enter-smtp-username": "अपना SMTP उपयोगकर्ता नाम दर्ज करें",
|
||||
"enter-webhook-url": "Webhook URL दर्ज करें",
|
||||
"enter-user-name": "उपयोगकर्ता नाम दर्ज करें",
|
||||
"enter-tagline": "टैगलाइन दर्ज करें",
|
||||
"enter-password-or-click-autogenerate": "पासवर्ड दर्ज करें या ऑटो-जनरेट पर क्लिक करें",
|
||||
"enter-daily-email-quota": "दैनिक ईमेल कोटा दर्ज करें",
|
||||
"enter-custom-regular-expression": "कस्टम रेगुलर एक्सप्रेशन दर्ज करें",
|
||||
"enter-value": "{{value}} दर्ज करें",
|
||||
"are-you-sure-want-to-send-document": "क्या आप वाकई दस्तावेज़ भेजना चाहते हैं?"
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
"New Template": "Nuovo modello",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "file selezionato",
|
||||
"files-selected": "file selezionati",
|
||||
"template-title": "Titolo del modello",
|
||||
"document-title": "Titolo del documento",
|
||||
"title": "Titolo",
|
||||
@@ -270,7 +270,7 @@
|
||||
"signature": "Firma",
|
||||
"upload-image": "Carica immagine",
|
||||
"clear": "Cancella",
|
||||
"upload-signature/Image": "Carica firma/immagine",
|
||||
"upload-signature/Image": "Carica firma/immagine/timbro",
|
||||
"initials": "Iniziali",
|
||||
"API": "API",
|
||||
"api-token": "Token API",
|
||||
@@ -337,7 +337,7 @@
|
||||
"quick-send-alert-1": "Tutti i ruoli in questo documento sono attualmente collegati a contatti. Per inviare rapidamente copie di questo modello a più firmatari, assicurati che almeno un ruolo non sia collegato a nessun contatto.",
|
||||
"quick-send-alert-2": "Assicurati che ci sia almeno un widget firma aggiunto per tutti i destinatari.",
|
||||
"quick-send-alert-3": "Si prega di aggiungere almeno un ruolo a questo template.",
|
||||
"quick-send-alert-4": "Limite di invio rapido raggiunto.",
|
||||
"quick-send-alert-4": "Limite raggiunto per {{flow}}.",
|
||||
"copy-link": "Copia link",
|
||||
"copy": "Copia",
|
||||
"revoke-document": "Revoca documento",
|
||||
@@ -393,7 +393,8 @@
|
||||
"radio button": "pulsante di opzione",
|
||||
"image": "immagine",
|
||||
"email": "email",
|
||||
"number": "numero"
|
||||
"number": "numero",
|
||||
"draw": "Disegno"
|
||||
},
|
||||
"fields": "Campi",
|
||||
"recipients": "Destinatari",
|
||||
@@ -516,14 +517,14 @@
|
||||
"placeholder-alert-4": "Hai inviato con successo le mail a tutti i destinatari!",
|
||||
"placeholder-mail-alert": "Hai inviato con successo un'e-mail a {{name}}. I firmatari successivi riceveranno un'e-mail una volta che {{name}} avrà firmato il documento.",
|
||||
"placeholder-mail-alert-you": "I firmatari successivi riceveranno un'email dopo che avrai firmato il documento.",
|
||||
"placeholder-alert-5": "Vuoi firmare i documenti ora?",
|
||||
"placeholder-alert-5": "Puoi firmare ora oppure inviarlo alla tua email per completarlo in seguito.",
|
||||
"placeholder-alert-6": "Configura l'adattatore email per inviare mail!",
|
||||
"placeholder-alert-7": "Seleziona il firmatario per aggiungere un segnaposto!",
|
||||
"email-subject": "Oggetto email",
|
||||
"email-body": "Corpo email",
|
||||
"email-placeholder": "aggiungi corpo dell'email",
|
||||
"reset-to-default": "Reimposta predefinita",
|
||||
"cutomize-email": "Personalizza Email",
|
||||
"customize-email": "Personalizza Email",
|
||||
"upgrade-to-customize-email": "Aggiorna per personalizzare l'Email",
|
||||
"sign-url": "URL Firma",
|
||||
"mails-sent": "Mail inviate",
|
||||
@@ -588,7 +589,7 @@
|
||||
"provide-your-details": "Fornisci i tuoi dettagli",
|
||||
"provide-email": "Fornisci l'email.",
|
||||
"additional-info": "Informazioni aggiuntive",
|
||||
"signature-saved-alert": "Firma salvata con successo.",
|
||||
"signature-saved-alert": "salvata con successo.",
|
||||
"saved-successfully": "Salvato con successo.",
|
||||
"fill-field": "Compila questo campo",
|
||||
"error-template": "Errore: Modello non trovato!",
|
||||
@@ -1243,6 +1244,7 @@
|
||||
},
|
||||
"read-only-date-error": "Il widget data di sola lettura deve avere una data predefinita.",
|
||||
"set-date": "Imposta data",
|
||||
"default-date": "Data predefinita",
|
||||
"set-today": "Data di firma",
|
||||
"enter-name": "Inserisci nome",
|
||||
"enter-email": "Inserisci e-mail",
|
||||
@@ -1308,5 +1310,55 @@
|
||||
"your-credits-usage": "Utilizzo dei tuoi crediti",
|
||||
"remaining": "rimanenti",
|
||||
"used": "usati",
|
||||
"your-credits-usage-help": "Questo è il numero di crediti consumati con la tua chiave API."
|
||||
"your-credits-usage-help": "Questo è il numero di crediti consumati con la tua chiave API.",
|
||||
"api-credits-updated": "I crediti API sono stati assegnati con successo.",
|
||||
"min-date": "Data minima",
|
||||
"max-date": "Data massima",
|
||||
"invalid-min-date": "Data minima non valida.",
|
||||
"min-less-max": "La data minima deve essere minore o uguale alla data massima.",
|
||||
"invalid-max-date": "Data massima non valida.",
|
||||
"max-less-min": "La data massima deve essere maggiore o uguale alla data minima.",
|
||||
"date-range": "Intervallo di date",
|
||||
"date-range-help": "Imposta una data minima e/o massima per limitare la selezione. La data scelta deve rientrare nell'intervallo.",
|
||||
"option-disabled-by-owner": "Il creatore del documento ha disabilitato questa opzione.",
|
||||
"pen-colors": "Colori della penna",
|
||||
"pen-colors-help": "Scegli quali colori della penna sono disponibili per i widget Firma e Iniziali. Seleziona una casella per mostrare un colore e deselezionala per nasconderlo.",
|
||||
"security-key": "Chiave di sicurezza",
|
||||
"remove-limit": "Rimuovi limite",
|
||||
"removed-api-credits": "Il limite dei crediti API è stato rimosso.",
|
||||
"enable-authentication": "Abilita autenticazione",
|
||||
"working": "In funzione",
|
||||
"test-security-key": "Testa la chiave di sicurezza",
|
||||
"generate": "Genera",
|
||||
"regenerate": "Rigenera",
|
||||
"signing-date": "Data di firma",
|
||||
"date-widget": "Widget data",
|
||||
"mail-failed": "Si è verificato un errore durante l'invio dell'e-mail. Per favore riprova.",
|
||||
"mail-status-head": "Passaggio successivo: firmare il tuo documento?",
|
||||
"send-to-email": "Invia tramite email",
|
||||
"date-pref-help-sub-title": "Le preferenze del widget Data consentono di definire le impostazioni predefinite per il widget Data. Queste impostazioni vengono applicate automaticamente ogni volta che si aggiunge un nuovo widget Data a un documento o a un modello. È comunque possibile personalizzare qualsiasi widget Data in qualsiasi momento facendo clic sull'icona a forma di ingranaggio.",
|
||||
"date-pref-help-format": "Scegli il formato di data predefinito utilizzato per i widget Data.",
|
||||
"date-pref-help-default-date": "Imposta un valore di data predefinito per i widget Data.",
|
||||
"date-pref-help-signing-date": "Quando abilitato, il widget Data utilizza automaticamente la data in cui il firmatario firma il documento.",
|
||||
"date-pref-help-read-only": "Quando abilitato, il widget Data è in sola lettura per il firmatario.",
|
||||
"upload-csv": "Carica CSV",
|
||||
"file-selected": "file selezionato",
|
||||
"bulk-send-csv-helper": "Utilizza le intestazioni per i ruoli dei firmatari (Email/Nome/Telefono del ruolo) e i valori di precompilazione (Precompilazione: Nome campo). I campi di precompilazione obbligatori devono essere compilati.",
|
||||
"bulk-send-csv-helper-note": "Nota: Per i campi di precompilazione di tipo data nel CSV, il valore deve essere fornito in questo formato: {{value}} (esempio: {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} righe importate. {{invalid}} righe non valide sono state ignorate.",
|
||||
"bulk-send-csv-missing-headers": "Intestazioni CSV obbligatorie mancanti. Ruoli: {{roles}}. Precompilazione: {{prefill}}.",
|
||||
"bulk-send-no-records": "Aggiungi almeno un destinatario prima dell'invio.",
|
||||
"csv-only-support": "Sono supportati solo file CSV.",
|
||||
"csv-no-records": "Nessun record trovato nel CSV.",
|
||||
"select-at-least-option": "Seleziona almeno un'opzione.",
|
||||
"draw-required": "Per favore, disegna nel riquadro.",
|
||||
"enter-smtp-username": "Inserisci il nome utente SMTP",
|
||||
"enter-webhook-url": "Inserisci l'URL del webhook",
|
||||
"enter-user-name": "Inserisci il nome utente",
|
||||
"enter-tagline": "Inserisci lo slogan",
|
||||
"enter-password-or-click-autogenerate": "Inserisci la password o fai clic su Genera automaticamente",
|
||||
"enter-daily-email-quota": "Inserisci la quota giornaliera di e-mail",
|
||||
"enter-custom-regular-expression": "Inserisci un'espressione regolare personalizzata",
|
||||
"enter-value": "Inserisci {{value}}",
|
||||
"are-you-sure-want-to-send-document": "Sei sicuro di voler inviare il documento?"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -11,6 +11,8 @@ import {
|
||||
import {
|
||||
useTranslation
|
||||
} from "react-i18next";
|
||||
import { withSessionValidation } from "../utils";
|
||||
|
||||
function generatePassword(length) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
@@ -67,7 +69,7 @@ const AddUser = (props) => {
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!emailRegex.test(formdata.email)) {
|
||||
@@ -130,7 +132,7 @@ const AddUser = (props) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleReset = () => {
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import SuggestionInput from "./shared/fields/SuggestionInput";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../constant/const";
|
||||
const BulkSendUi = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [forms, setForms] = useState([]);
|
||||
const formRef = useRef(null);
|
||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
const [emails, setEmails] = useState([]);
|
||||
const [isPrefillExist, setIsPrefillExist] = useState(false);
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//function to check at least one signature field exist
|
||||
const signatureExist = async () => {
|
||||
const isPrefill =
|
||||
props?.Placeholders?.some((x) => x?.Role === "prefill") || false;
|
||||
if (isPrefill) {
|
||||
setIsPrefillExist(isPrefill);
|
||||
}
|
||||
setIsDisableBulkSend(false);
|
||||
const getPlaceholder = props?.Placeholders;
|
||||
const removePrefill = getPlaceholder?.filter(
|
||||
(x) => x?.Role !== "prefill"
|
||||
);
|
||||
const checkIsSignatureExistt = removePrefill?.every((placeholderObj) =>
|
||||
placeholderObj?.placeHolder?.some((holder) =>
|
||||
holder?.pos?.some((posItem) => posItem?.type === "signature")
|
||||
)
|
||||
);
|
||||
setIsSignatureExist(checkIsSignatureExistt);
|
||||
setIsLoader(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (scrollOnNextUpdate && formRef.current) {
|
||||
formRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
setScrollOnNextUpdate(false);
|
||||
}
|
||||
}, [forms, scrollOnNextUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
let emails = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
const signerEmail = element?.email || element?.signerPtr?.Email;
|
||||
|
||||
// only add when there's a non-empty signerEmail
|
||||
if (signerEmail) {
|
||||
emails = [...emails, signerEmail];
|
||||
}
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setEmails(emails);
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const handleInputChange = (index, signer, fieldIndex) => {
|
||||
const newForms = [...forms];
|
||||
newForms[index].fields[fieldIndex].email = signer?.Email
|
||||
? signer?.Email
|
||||
: signer || "";
|
||||
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
|
||||
const handleRemoveForm = (index) => {
|
||||
const updatedForms = forms.filter((_, i) => i !== index);
|
||||
setForms(updatedForms);
|
||||
};
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const item of data) {
|
||||
let email = "";
|
||||
for (const field of item.fields) {
|
||||
if (!emailRegex.test(field.email)) {
|
||||
alert(t("invalid-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSubmit(true);
|
||||
if (validateEmails(forms)) {
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.Placeholders];
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
// Loop through each form
|
||||
forms.forEach((form) => {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: field.email,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
});
|
||||
}
|
||||
});
|
||||
await batchQuery(Documents);
|
||||
} else {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const token =
|
||||
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
|
||||
const functionsUrl = `${localStorage.getItem(
|
||||
"baseUrl"
|
||||
)}functions/batchdocuments`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
...token,
|
||||
};
|
||||
const params = { Documents: JSON.stringify(Documents) };
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
// console.log("res ", res);
|
||||
if (res.data && res.data.result) {
|
||||
props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
props.handleClose("error", 0);
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isSubmit && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{isPrefillExist ? (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("prefill-bulk-error")}
|
||||
</div>
|
||||
) : !isDisableBulkSend ? (
|
||||
<>
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
<>
|
||||
{props.Placeholders?.some((x) => !x.signerObjId) ? (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-y-auto">
|
||||
{forms?.map((form, index) => (
|
||||
<div
|
||||
key={form.Id}
|
||||
className="p-3 op-card border-[1px] border-gray-400 mt-3 mx-4 mb-4 bg-base-200 text-base-content grid grid-cols-1 md:grid-cols-2 gap-2 relative"
|
||||
>
|
||||
{form?.fields?.map((field, fieldIndex) => (
|
||||
<div
|
||||
className="flex flex-col"
|
||||
key={field.fieldId}
|
||||
>
|
||||
<label className="block text-xs font-semibold">
|
||||
{field.label}
|
||||
</label>
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.value}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(
|
||||
index,
|
||||
signer,
|
||||
fieldIndex
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{forms?.length > 1 && (
|
||||
<button
|
||||
onClick={() => handleRemoveForm(index)}
|
||||
className="absolute right-3 top-1 text-[red] border-[1px] border-[red] rounded-lg w-[1.7rem] h-[1.7rem]"
|
||||
>
|
||||
<i className="fa-light fa-trash"></i>
|
||||
</button>
|
||||
)}
|
||||
<div ref={formRef}></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col mx-4 mb-4 gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-accent focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>{" "}
|
||||
<span>{t("send")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-1")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-2")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-3")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
|
||||
const selectedFormatIndex = dateFormatList?.findIndex(
|
||||
(item) => item.format === selectDate?.format
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:items-center md:flex-row gap-y-3 my-2">
|
||||
<span className="capitalize">{t("format")}: </span>
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-2"
|
||||
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
|
||||
onChange={(e) => handleChangeFormat(e)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("select-date-format")}
|
||||
</option>
|
||||
{dateFormatList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" value={ind} key={ind}>
|
||||
{data?.date ? data?.date : "nodata"}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span className="text-xs text-gray-400 ml-1 uppercase">
|
||||
{selectDate.format}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateFormat;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { forwardRef } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {
|
||||
getDefaultDate,
|
||||
getMonth,
|
||||
getYear,
|
||||
months,
|
||||
years
|
||||
} from "../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DatePicker = ({
|
||||
selectDate,
|
||||
format,
|
||||
minDate,
|
||||
maxDate,
|
||||
onChange,
|
||||
handleClear
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const CustomInput = 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-[15px]" : "w-20"} fa-light fa-calendar `}
|
||||
></i>
|
||||
</div>
|
||||
));
|
||||
CustomInput.displayName = "CustomInput";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>{t("default-date")}: </span>
|
||||
<ReactDatePicker
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<div className="flex justify-start md:ml-2">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) => changeYear(value)}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
closeOnScroll={true}
|
||||
selected={getDefaultDate(selectDate?.date, selectDate?.format)}
|
||||
popperPlacement="top-end"
|
||||
customInput={<CustomInput />}
|
||||
onChange={(date) => onChange(date)}
|
||||
dateFormat={
|
||||
selectDate ? selectDate?.format : format ? format : "MM/dd/yyyy"
|
||||
}
|
||||
portalId="root-portal"
|
||||
/>
|
||||
<span
|
||||
onClick={() => handleClear()}
|
||||
className="underline text-blue-500 cursor-pointer ml-2"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -197,7 +197,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
|
||||
className={`mt-4 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
|
||||
isOpen ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useLocation, matchPath } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
@@ -70,13 +69,13 @@ export default function Title() {
|
||||
[t, prefix, appName]
|
||||
);
|
||||
const manifestUrl = useManifestUrl(appName, logo);
|
||||
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={title} />
|
||||
{logo && <link rel="icon" type="image/png" href={logo} />}
|
||||
<link rel="manifest" href={manifestUrl} />
|
||||
</Helmet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from "react";
|
||||
import axios from "axios";
|
||||
import SuggestionInput from "../shared/fields/SuggestionInput";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../../constant/const";
|
||||
import {
|
||||
convertPdfArrayBuffer,
|
||||
getSignedUrl,
|
||||
isBase64,
|
||||
getBase64FromUrl,
|
||||
} from "../../constant/Utils";
|
||||
import {
|
||||
formatCSVDate,
|
||||
handleEmbedPrefillToDoc,
|
||||
normalizeKey
|
||||
} from "../../utils";
|
||||
import RenderWidgets from "./components/RenderWidgets";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setBulkLoader } from "../../redux/reducers/widgetSlice";
|
||||
|
||||
const EXCLUDED_PREFILL_TYPES = new Set(["image", "draw"]);
|
||||
const EXCLUDED_WIDGET_TYPES = new Set([
|
||||
"image",
|
||||
"signature",
|
||||
"stamp",
|
||||
"initials"
|
||||
]);
|
||||
const ALL_EXCLUDED_TYPES = new Set([
|
||||
...EXCLUDED_PREFILL_TYPES,
|
||||
...EXCLUDED_WIDGET_TYPES
|
||||
]);
|
||||
|
||||
const requiredAsteriskCls = (isRequired = false) => {
|
||||
return isRequired ? "after:content-['_*'] after:text-red-500" : "";
|
||||
};
|
||||
const BulkSendUi = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const dispatch = useDispatch();
|
||||
const { isBulkLoader } = useSelector((state) => state.widget);
|
||||
const [forms, setForms] = useState([]);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
const [emails, setEmails] = useState([]);
|
||||
const [isVacantRoles, setIsVacantRoles] = useState(false);
|
||||
const [fields, setFields] = useState([]);
|
||||
const [headers, setHeaders] = useState([]);
|
||||
|
||||
const tokenHeader = useMemo(() => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
|
||||
};
|
||||
return {
|
||||
...headers,
|
||||
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
||||
};
|
||||
}, [props?.jwttoken]);
|
||||
|
||||
// Initialize form state (fields, widgets/responses, emails, headers, forms, signers, allowedForm) from template placeholders with existing values.
|
||||
const initializeWidgetsWithValues = async () => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
const roles = props?.Placeholders?.filter((p) => !p.signerObjId) || [];
|
||||
const emails = [];
|
||||
const users = await Promise.all(
|
||||
roles.map(async (role) => {
|
||||
const isPrefill = role?.Role?.toLowerCase() === "prefill";
|
||||
const uniqueNames = new Set();
|
||||
const pages = role?.placeHolder ?? [];
|
||||
const widgets = await Promise.all(
|
||||
pages.flatMap((page) =>
|
||||
page.pos
|
||||
.filter((widget) => {
|
||||
const widgetName = widget?.options?.name;
|
||||
if (!widgetName) return false; //|| widget?.options?.isReadOnly
|
||||
// exclude certain widget types for non-prefill
|
||||
if (EXCLUDED_WIDGET_TYPES.has(widget.type) && !isPrefill)
|
||||
return false;
|
||||
// unique by widget name
|
||||
if (uniqueNames.has(widgetName)) return false;
|
||||
uniqueNames.add(widgetName);
|
||||
return true;
|
||||
})
|
||||
.map(async (widget) => {
|
||||
let response = "";
|
||||
if (ALL_EXCLUDED_TYPES.has(widget.type)) {
|
||||
const url =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
if (url && !isBase64(url)) {
|
||||
const signedUrl = await getSignedUrl(
|
||||
url,
|
||||
"", // docId
|
||||
props.item.objectId, // templateId
|
||||
);
|
||||
|
||||
response = await getBase64FromUrl(signedUrl, true);
|
||||
} else {
|
||||
response = "";
|
||||
}
|
||||
} else {
|
||||
if (widget.type === "date") {
|
||||
const value =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
response = value ? formatCSVDate(widget, value) : "";
|
||||
} else {
|
||||
response =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
}
|
||||
}
|
||||
const label = `${role.Role}::${widget.options?.name}`;
|
||||
|
||||
return {
|
||||
...widget,
|
||||
label: label,
|
||||
response,
|
||||
pageNumber: page.pageNumber
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
const signerEmail =
|
||||
normalizeKey(role?.email) || role?.signerPtr?.Email;
|
||||
if (signerEmail) emails.push(signerEmail);
|
||||
|
||||
return {
|
||||
fieldId: role.Id,
|
||||
email: "",
|
||||
label: role.Role,
|
||||
signer: {},
|
||||
widgets: widgets
|
||||
};
|
||||
})
|
||||
);
|
||||
setFields(users);
|
||||
setEmails(emails);
|
||||
|
||||
// Build headers with required ordering:
|
||||
// 1) all role emails first
|
||||
// 2) then all widgets
|
||||
const emailHeaders = [];
|
||||
const widgetHeaders = [];
|
||||
for (const role of users) {
|
||||
const isPrefill = role?.label?.toLowerCase() === "prefill";
|
||||
|
||||
if (!isPrefill) {
|
||||
emailHeaders.push({
|
||||
label: `${role.label}::Email`,
|
||||
role: role.label,
|
||||
type: "role",
|
||||
isRequired: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const widget of role?.widgets ?? []) {
|
||||
const isRequired =
|
||||
widget?.options?.status === "required" && isPrefill;
|
||||
|
||||
widgetHeaders.push({
|
||||
label: widget?.label,
|
||||
role: role?.label,
|
||||
type: widget?.type,
|
||||
isRequired
|
||||
});
|
||||
}
|
||||
}
|
||||
setHeaders([...emailHeaders, ...widgetHeaders]);
|
||||
setForms((prev) => [...prev, { Id: 1, fields: users }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const checkSignatureAndRoles = (placeholders = []) => {
|
||||
const filtered = placeholders.filter((x) => x?.Role !== "prefill");
|
||||
|
||||
const isSignExist = filtered.every((p) =>
|
||||
p?.placeHolder?.some((h) => h?.pos?.some((p) => p?.type === "signature"))
|
||||
);
|
||||
|
||||
setIsSignatureExist(isSignExist);
|
||||
setIsVacantRoles(placeholders.some((x) => !x.signerObjId));
|
||||
};
|
||||
|
||||
//function to check at least one signature field exist
|
||||
const signatureExist = async () => {
|
||||
setIsLoader(true);
|
||||
await initializeWidgetsWithValues();
|
||||
checkSignatureAndRoles(props?.Placeholders);
|
||||
setIsLoader(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (formIndex, signer, role) => {
|
||||
const newForms = [...forms];
|
||||
const email = signer?.Email ? signer?.Email : signer || "";
|
||||
const fieldId = newForms[formIndex].fields.findIndex(
|
||||
(f) => f.label === role
|
||||
);
|
||||
newForms[formIndex].fields[fieldId].email = normalizeKey(email);
|
||||
newForms[formIndex].fields[fieldId].signer = signer?.objectId ? signer : "";
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
const handleWidgetDetails = (
|
||||
value = "",
|
||||
formIndex,
|
||||
RoleIndex = 0,
|
||||
widgetIndex
|
||||
) => {
|
||||
const newForms = [...forms];
|
||||
newForms[formIndex].fields[RoleIndex].widgets[widgetIndex].response = value;
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const item of data) {
|
||||
let email = "";
|
||||
const filterFields = item.fields.filter((p) => p.label !== "prefill");
|
||||
for (const field of filterFields) {
|
||||
if (!emailRegex.test(field.email)) {
|
||||
alert(t("invalid-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// `updateWidgetValues` will update responses for all widget types
|
||||
const updateWidgetValues = (widgetValues = [], placeholders = []) => {
|
||||
if (!widgetValues?.length) return placeholders;
|
||||
const responseByName = new Map(
|
||||
widgetValues
|
||||
.map((w) => [w?.options?.name, w?.response])
|
||||
.filter(([name]) => !!name)
|
||||
);
|
||||
|
||||
return placeholders.map((ph) => {
|
||||
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
|
||||
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
|
||||
const name = widgetDetails?.options?.name;
|
||||
if (!name || !responseByName.has(name)) return widgetDetails;
|
||||
|
||||
const newResponse = responseByName.get(name);
|
||||
|
||||
return {
|
||||
...widgetDetails,
|
||||
options: {
|
||||
...widgetDetails.options,
|
||||
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
|
||||
response:
|
||||
newResponse ??
|
||||
widgetDetails.options?.response ??
|
||||
widgetDetails.options?.defaultValue
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { ...page, pos: updatedPos };
|
||||
});
|
||||
|
||||
return { ...ph, placeHolder: updatedPages };
|
||||
});
|
||||
};
|
||||
|
||||
// `removeWidgetValues` will clear out responses for draw/image/signature/stamp/initials widgets
|
||||
const removeWidgetValues = (widgetValues = [], placeholders = []) => {
|
||||
if (!widgetValues?.length) return placeholders;
|
||||
return placeholders.map((ph) => {
|
||||
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
|
||||
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
|
||||
const isImageType = ALL_EXCLUDED_TYPES.has(widgetDetails?.type);
|
||||
return {
|
||||
...widgetDetails,
|
||||
options: {
|
||||
...widgetDetails.options,
|
||||
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
|
||||
response: isImageType ? "" : widgetDetails.options?.response,
|
||||
defaultValue: isImageType
|
||||
? ""
|
||||
: widgetDetails.options?.defaultValue
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { ...page, pos: updatedPos };
|
||||
});
|
||||
|
||||
return { ...ph, placeHolder: updatedPages };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(setBulkLoader(true));
|
||||
if (!forms.length) {
|
||||
alert(t("bulk-send-no-records"));
|
||||
dispatch(setBulkLoader(false));
|
||||
return;
|
||||
}
|
||||
if (validateEmails(forms)) {
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.Placeholders];
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
let error = "";
|
||||
// Loop through each form
|
||||
for (const form of forms) {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: normalizeKey(field.email),
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
const widgetValues = form?.fields?.flatMap((f) => f.widgets);
|
||||
const updateWidgetPlaceholder = updateWidgetValues(
|
||||
widgetValues,
|
||||
updatedPlaceholders
|
||||
);
|
||||
const url = props.item?.URL || props.item?.SignedUrl;
|
||||
let signedUrl = await getSignedUrl(
|
||||
url,
|
||||
"", //docId
|
||||
props.item.objectId, // templateId
|
||||
);
|
||||
const prefillField = form?.fields?.find((f) => f.label === "prefill");
|
||||
const prefills = prefillField?.widgets ?? [];
|
||||
if (prefills?.length > 0) {
|
||||
const prefillDetails = updateWidgetPlaceholder?.find(
|
||||
(x) => x.Role === "prefill"
|
||||
);
|
||||
const pdfArrayBuffer = await convertPdfArrayBuffer(signedUrl);
|
||||
if (pdfArrayBuffer === "Error") {
|
||||
error = t("something-went-wrong-mssg");
|
||||
dispatch(setBulkLoader(false));
|
||||
break;
|
||||
}
|
||||
signedUrl = await handleEmbedPrefillToDoc(
|
||||
prefillDetails,
|
||||
1, // scale
|
||||
pdfArrayBuffer,
|
||||
[], // prefillImg,
|
||||
props.item?.ExtUserPtr?.UserId?.objectId // userId
|
||||
);
|
||||
if (signedUrl?.error) {
|
||||
error = signedUrl.error.includes("not compatible")
|
||||
? t("pdf-uncompatible", { appName: appName })
|
||||
: signedUrl?.error;
|
||||
dispatch(setBulkLoader(false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const placeholders = removeWidgetValues(
|
||||
widgetValues,
|
||||
updateWidgetPlaceholder
|
||||
);
|
||||
if (!signedUrl) {
|
||||
error = t("something-went-wrong-mssg");
|
||||
dispatch(setBulkLoader(false));
|
||||
break;
|
||||
}
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
URL: signedUrl,
|
||||
Placeholders: placeholders,
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
URL: signedUrl,
|
||||
Placeholders: placeholders,
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
alert(error);
|
||||
} else {
|
||||
await batchQuery(Documents);
|
||||
}
|
||||
} else {
|
||||
dispatch(setBulkLoader(false));
|
||||
}
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
|
||||
const headers = {
|
||||
...tokenHeader,
|
||||
};
|
||||
const params = { Documents: JSON.stringify(Documents) };
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
if (res.data && res.data.result) {
|
||||
props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
const message =
|
||||
err?.response?.data?.error || err?.message || "something went wrong.";
|
||||
console.error("Error sending documents:", message);
|
||||
props.handleClose("error", 0, message);
|
||||
} finally {
|
||||
dispatch(setBulkLoader(false));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isLoader) {
|
||||
return (
|
||||
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* {isBulkLoader && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)} */}
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
isVacantRoles ? (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-auto">
|
||||
{
|
||||
forms?.length > 0 && (
|
||||
<div className="mx-4">
|
||||
<table className="op-table border-collapse w-full">
|
||||
<thead className="text-[13px] text-center">
|
||||
<tr className="border-y-[1px]">
|
||||
{/* Roles + widgets */}
|
||||
{headers?.map((header) => (
|
||||
<th
|
||||
key={header.label}
|
||||
className={`${requiredAsteriskCls(header?.isRequired)} p-2`}
|
||||
>
|
||||
{header.label}
|
||||
</th>
|
||||
))}
|
||||
{forms?.length > 1 && (
|
||||
<th className="p-2">{t("action")}</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-[12px] text-base-content">
|
||||
{forms.map((form, formIndex) => {
|
||||
const fields = form?.fields ?? [];
|
||||
const emailFields = fields.filter(
|
||||
(f) => f.label !== "prefill"
|
||||
);
|
||||
const widgets = fields.flatMap(
|
||||
(f, fieldIndex) =>
|
||||
(f.widgets ?? []).map(
|
||||
(widget, widgetIndex) => ({
|
||||
widget,
|
||||
fieldIndex, // keep the fieldIndex that produced this widget
|
||||
widgetIndex
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<tr key={form.Id}>
|
||||
{/* Email cell (label + SuggestionInput) */}
|
||||
{emailFields?.map((field, fieldIndex) => (
|
||||
<td key={field.fieldId} className="p-2">
|
||||
<div className="flex flex-col min-w-max">
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.email ?? ""}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(
|
||||
formIndex,
|
||||
signer,
|
||||
field.label
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
|
||||
{widgets.map(
|
||||
({ widget, fieldIndex, widgetIndex }) => (
|
||||
<td key={widget.key} className="p-2">
|
||||
<RenderWidgets
|
||||
widget={widget}
|
||||
formIndex={formIndex}
|
||||
prefillIndex={widgetIndex}
|
||||
handleWidgetDetails={(value) =>
|
||||
handleWidgetDetails(
|
||||
value,
|
||||
formIndex,
|
||||
fieldIndex,
|
||||
widgetIndex
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap pb-3 pt-2 px-3 gap-3 justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-accent w-[150px] focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>
|
||||
<span>{t("send")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-1")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-2")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-3")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,582 @@
|
||||
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import moment from "moment";
|
||||
import {
|
||||
changeDateToMomentFormat,
|
||||
compressedFileSize,
|
||||
getMonth,
|
||||
getYear,
|
||||
months,
|
||||
radioButtonWidget,
|
||||
textInputWidget,
|
||||
textWidget,
|
||||
years
|
||||
} from "../../../constant/Utils";
|
||||
import PenColorComponent from "../../pdf/tab/PenColorComponent";
|
||||
import { getDatePickerDate, toHtmlPattern } from "../../../utils";
|
||||
import CellsInput from "../../shared/fields/CellsInput";
|
||||
import { emailRegex } from "../../../constant/const";
|
||||
|
||||
const inputOpt = new Set(["text", "email", "number"]);
|
||||
const PREFILL_PREFIX = "prefill::";
|
||||
|
||||
const inputTypes = {
|
||||
email: emailRegex,
|
||||
number: /^[0-9\s]*$/,
|
||||
ssn: /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/
|
||||
};
|
||||
const inputValidation = (pattern, type) => {
|
||||
if (pattern) return pattern;
|
||||
return inputTypes[type] || "";
|
||||
};
|
||||
const canavasTheme = `opensigncss:bg-white opensigndark:bg-[#121212] opensigndark:border-[#f6f3f4]/20 opensigncss:border-gray-300 opensigndark:hover:border-white opensigncss:hover:border-black border-[1px]`;
|
||||
const DateWidget = ({ widget, isRequired, onChange }) => {
|
||||
const format = widget?.options?.validation?.format || "MM/dd/yyyy";
|
||||
|
||||
const PrefillDateInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<div
|
||||
style={{ fontFamily: "Arial, sans-serif" }}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full"
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
{value ? value : format}
|
||||
<i className="fa-light fa-calendar ml-[5px]"></i>
|
||||
</div>
|
||||
));
|
||||
PrefillDateInput.displayName = "PrefillDateInput";
|
||||
|
||||
const handleDate = (widget) => {
|
||||
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
|
||||
try {
|
||||
return getDatePickerDate(widget?.response, format);
|
||||
} catch (err) {
|
||||
console.error("handleDate error ", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//function to set date with required date format onchange date
|
||||
const handleOnDateChange = (date) => {
|
||||
let updateDate = date;
|
||||
let newDate;
|
||||
const isSpecialDateFormat =
|
||||
format && ["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
|
||||
if (isSpecialDateFormat) {
|
||||
newDate = moment(updateDate).format(changeDateToMomentFormat(format));
|
||||
} else {
|
||||
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
|
||||
newDate = new Date(updateDate);
|
||||
newDate = moment(newDate.getTime()).format(
|
||||
changeDateToMomentFormat(format)
|
||||
);
|
||||
}
|
||||
onChange(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-max">
|
||||
<DatePicker
|
||||
portalId="datepicker-portal-root"
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<div className="flex justify-start ml-2">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) => changeYear(value)}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
closeOnScroll={true}
|
||||
selected={handleDate(widget)}
|
||||
onChange={(date) => handleOnDateChange(date, widget)}
|
||||
customInput={<PrefillDateInput />}
|
||||
dateFormat={widget?.options?.validation?.format || "MM/dd/yyyy"}
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const TextWidget = ({ widget, isRequired, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState([]);
|
||||
const inputType = widget?.options?.validation?.type || "";
|
||||
const serverRegex = widget?.options?.validation?.pattern;
|
||||
const isPredfineType = inputType ? inputOpt?.has(inputType) : "";
|
||||
const regExpression = inputValidation(serverRegex, inputType);
|
||||
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
|
||||
|
||||
useEffect(() => {
|
||||
const response =
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
|
||||
const isNumber = inputType === "number" && response;
|
||||
setValue(isNumber ? Number(response) : response);
|
||||
}, []);
|
||||
const handleInputChange = (e) => {
|
||||
const text = e.target.value || "";
|
||||
setValue(text);
|
||||
onChange(text);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-max">
|
||||
<input
|
||||
type={isPredfineType ? inputType : "text"}
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
value={value}
|
||||
placeholder={
|
||||
widget?.options?.hint ||
|
||||
t("enter-value", { value: widget?.options?.name })
|
||||
}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
onChange={(e) => handleInputChange(e)}
|
||||
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
|
||||
onInvalid={(e) => {
|
||||
const el = e.currentTarget;
|
||||
// ✅ Only override message if pattern exists AND the error is patternMismatch
|
||||
if (pattern && el.validity.patternMismatch) {
|
||||
el.setCustomValidity(t("validation-alert-1"));
|
||||
} else {
|
||||
el.setCustomValidity("");
|
||||
}
|
||||
}}
|
||||
onInput={(e) => {
|
||||
e.currentTarget.setCustomValidity("");
|
||||
}}
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const CheckboxWidget = ({ widget, isRequired, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedCheckbox(
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? []
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleCheckboxValue = (isChecked, ind) => {
|
||||
const prevArr = Array.isArray(selectedCheckbox) ? selectedCheckbox : [];
|
||||
|
||||
const updateResponse = isChecked
|
||||
? prevArr.includes(ind)
|
||||
? prevArr
|
||||
: [...prevArr, ind] // add once
|
||||
: prevArr.filter((v) => v !== ind); // remove
|
||||
|
||||
setSelectedCheckbox(updateResponse);
|
||||
onChange(updateResponse);
|
||||
};
|
||||
const noneSelected = selectedCheckbox?.length === 0;
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-y-1 min-w-max">
|
||||
{widget.options?.values?.map((value, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="select-none-cls flex items-center text-center gap-0.5"
|
||||
>
|
||||
<input
|
||||
id={`checkbox-${widget.key + ind}`}
|
||||
className="mt-[2px] op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
checked={selectedCheckbox.includes(ind)}
|
||||
required={isRequired && noneSelected && ind === 0} // ✅ key line
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("select-at-least-option"))
|
||||
}
|
||||
onChange={(e) => {
|
||||
e.target.setCustomValidity(""); // ✅ clear message as soon as user changes
|
||||
handleCheckboxValue(e.target.checked, ind);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`checkbox-${widget.key + ind}`}
|
||||
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
|
||||
>
|
||||
{value}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const DropdownWidget = ({ widget, isRequired, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? ""
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleDropdownChange = (value) => {
|
||||
setSelected(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-max">
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
|
||||
id={`widget-${widget?.options?.name}-${widget?.key}`}
|
||||
value={selected}
|
||||
onChange={(e) => handleDropdownChange(e.target.value)}
|
||||
required={isRequired}
|
||||
>
|
||||
{/* Default/Title option */}
|
||||
<option value="" disabled hidden>
|
||||
{t("choose-one")}
|
||||
</option>
|
||||
{widget?.options?.values?.map((option, ind) => (
|
||||
<option key={ind} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const RadioButtonWidget = ({ widget, isRequired, onChange, formIndex }) => {
|
||||
const [selected, setSelected] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(
|
||||
widget?.options?.response || widget?.options?.defaultValue || ""
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleRadioChange = (value) => {
|
||||
setSelected(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-y-1 min-w-max">
|
||||
{widget.options?.values?.map((data, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="select-none-cls flex items-center text-center gap-0.5"
|
||||
>
|
||||
<input
|
||||
id={`radio-${widget.key + ind}`}
|
||||
className="mt-[2px] op-radio op-radio-xs"
|
||||
name={`radio-group-${widget.key}-${formIndex}`} // ✅ same name => browser treats all radios as ONE group
|
||||
type="radio"
|
||||
required={isRequired && ind === 0} // ✅ set required ONCE (on first radio) to make the whole group mandatory
|
||||
checked={selected === data}
|
||||
onChange={() => handleRadioChange(data)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`radio-${widget.key + ind}`}
|
||||
className="text-xs mb-0 ml-[2px] cursor-pointer"
|
||||
>
|
||||
{data}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ImageWidget = ({ widget, isRequired, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const imageRef = useRef(null);
|
||||
const [img, setImg] = useState({ src: "", imgType: "" });
|
||||
const [defaultImg, setDefaultImg] = useState({ src: "", imgType: "" });
|
||||
|
||||
useEffect(() => {
|
||||
if (widget?.response) {
|
||||
setDefaultImg({ src: widget?.response, imgType: "" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onImageChange = (e) => {
|
||||
if (e.target.files && e.target.files?.[0]) {
|
||||
const file = e.target.files?.[0];
|
||||
compressedFileSize(file, ({ src, imgType }) => {
|
||||
onChange(src);
|
||||
setImg({ src, imgType });
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleClearImage = () => {
|
||||
setDefaultImg({ src: "", imgType: "" });
|
||||
setImg({ src: "", imgType: "" });
|
||||
onChange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col prefillCanvas">
|
||||
{defaultImg?.src || img?.src ? (
|
||||
<>
|
||||
<div
|
||||
className={`cursor-pointer rounded-box ${canavasTheme} flex flex-col w-full h-full justify-center items-center`}
|
||||
>
|
||||
<img
|
||||
alt={`image_${widget?.options?.name}`}
|
||||
src={img.src || defaultImg?.src}
|
||||
draggable="false"
|
||||
className="object-contain h-full w-full aspect-[5/2]"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
onClick={handleClearImage}
|
||||
className="flex justify-start text-blue-500 underline cursor-pointer ml-1"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={`cursor-pointer rounded-box ${canavasTheme} overflow-hidden w-full h-full aspect-[5/2] flex flex-col justify-center items-center`}
|
||||
onClick={() => imageRef.current?.click()}
|
||||
>
|
||||
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
|
||||
<div className="text-[10px] text-base-content">{t("upload")}</div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => onImageChange?.(e, widget)}
|
||||
className="filetype w-[1px] h-[1px] opacity-0"
|
||||
accept="image/png,image/jpeg"
|
||||
ref={imageRef} // Assign ref dynamically
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const DrawWidget = ({ widget, isRequired, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const canvasRef = useRef(null);
|
||||
const sigRequiredRef = useRef(null);
|
||||
const [hasDraw, setHasDraw] = useState("");
|
||||
const [penColor, setPenColor] = useState("blue");
|
||||
const [image, setImage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (widget?.response) {
|
||||
setImage(widget?.response);
|
||||
setHasDraw("signed");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSignatureChange = () => {
|
||||
const draw = canvasRef.current?.isEmpty?.() ? "" : "signed";
|
||||
setHasDraw(draw);
|
||||
onChange(canvasRef.current.toDataURL());
|
||||
|
||||
// clear custom error once user signs
|
||||
sigRequiredRef.current?.setCustomValidity("");
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setImage("");
|
||||
if (canvasRef?.current) {
|
||||
canvasRef.current.clear();
|
||||
onChange("");
|
||||
setHasDraw("");
|
||||
// clear custom error once user signs
|
||||
sigRequiredRef.current?.setCustomValidity("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="relative">
|
||||
{image ? (
|
||||
<div className={`prefillCanvas ${canavasTheme} rounded-[10px]`}>
|
||||
<img
|
||||
alt={`draw_${widget?.options?.name}`}
|
||||
src={image}
|
||||
className="prefillCanvas object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SignatureCanvas
|
||||
ref={canvasRef}
|
||||
penColor={penColor}
|
||||
canvasProps={{
|
||||
className: `prefillCanvas ${canavasTheme} rounded-[10px]`
|
||||
}}
|
||||
onEnd={() => handleSignatureChange()}
|
||||
dotSize={1}
|
||||
/>
|
||||
)}
|
||||
{/* ✅ Native form validation hook */}
|
||||
<input
|
||||
ref={sigRequiredRef}
|
||||
type="text"
|
||||
value={hasDraw}
|
||||
required={isRequired}
|
||||
tabIndex={-1}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("draw-required"))}
|
||||
onChange={() => {}}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between mt-[8px] w-[200px]">
|
||||
<PenColorComponent
|
||||
penColor={penColor}
|
||||
setPenColor={setPenColor}
|
||||
hideLabel
|
||||
penSize="sm"
|
||||
/>
|
||||
<span
|
||||
onClick={handleClear}
|
||||
className="flex justify-start text-blue-500 underline cursor-pointer"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CellsWidget = ({ widget, isRequired, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const count = widget?.options?.cellCount || 1;
|
||||
const [word, setWord] = useState("");
|
||||
const inputType = widget?.options?.validation?.type || "";
|
||||
const serverRegex = widget?.options?.validation?.pattern;
|
||||
const regExpression = inputValidation(serverRegex, inputType);
|
||||
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
|
||||
|
||||
useEffect(() => {
|
||||
const response =
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
|
||||
const isNumber = inputType === "number" && response;
|
||||
setWord(isNumber ? Number(response) : response);
|
||||
}, []);
|
||||
|
||||
const handleChange = (words) => {
|
||||
const value = words;
|
||||
setWord(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="relative">
|
||||
<CellsInput
|
||||
hint={
|
||||
widget?.options?.hint ||
|
||||
t("enter-value", { value: widget?.options?.name })
|
||||
}
|
||||
count={count}
|
||||
value={word}
|
||||
onChange={handleChange}
|
||||
only="all"
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
value={word}
|
||||
required={isRequired}
|
||||
tabIndex={-1}
|
||||
onChange={() => {}}
|
||||
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
|
||||
onInvalid={(e) => {
|
||||
const el = e.currentTarget;
|
||||
// ✅ Only override message if pattern exists AND the error is patternMismatch
|
||||
if (pattern && el.validity.patternMismatch) {
|
||||
el.setCustomValidity(t("validation-alert-1"));
|
||||
} else {
|
||||
el.setCustomValidity("");
|
||||
}
|
||||
}}
|
||||
onInput={(e) => {
|
||||
e.currentTarget.setCustomValidity("");
|
||||
}}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderWidgets = ({
|
||||
widget,
|
||||
formIndex,
|
||||
prefillIndex,
|
||||
handleWidgetDetails
|
||||
}) => {
|
||||
const label = widget?.label?.toLowerCase() ?? "";
|
||||
const isPrefill = label.includes(PREFILL_PREFIX);
|
||||
const commonProps = {
|
||||
widget,
|
||||
isRequired: widget.options?.status === "required" && isPrefill,
|
||||
formIndex
|
||||
};
|
||||
|
||||
const onChange = (value) => {
|
||||
handleWidgetDetails(value, formIndex, prefillIndex);
|
||||
};
|
||||
|
||||
switch (widget.type) {
|
||||
case "date":
|
||||
return <DateWidget {...commonProps} onChange={onChange} />;
|
||||
case textWidget:
|
||||
case textInputWidget:
|
||||
case "name":
|
||||
case "email":
|
||||
case "company":
|
||||
case "job title":
|
||||
case "number":
|
||||
return <TextWidget {...commonProps} onChange={onChange} />;
|
||||
case "checkbox":
|
||||
return <CheckboxWidget {...commonProps} onChange={onChange} />;
|
||||
case radioButtonWidget:
|
||||
return <RadioButtonWidget {...commonProps} onChange={onChange} />;
|
||||
case "dropdown":
|
||||
return <DropdownWidget {...commonProps} onChange={onChange} />;
|
||||
case "image":
|
||||
case "stamp":
|
||||
return <ImageWidget {...commonProps} onChange={onChange} />;
|
||||
case "initials":
|
||||
case "draw":
|
||||
return <DrawWidget {...commonProps} onChange={onChange} />;
|
||||
case "cells":
|
||||
return <CellsWidget {...commonProps} onChange={onChange} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default RenderWidgets;
|
||||
@@ -5,6 +5,7 @@ import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
||||
import { useNavigate } from "react-router";
|
||||
import Tooltip from "../../primitives/Tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const DashboardCard = (props) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -14,7 +15,7 @@ const DashboardCard = (props) => {
|
||||
const [response, setresponse] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const renderData = async () => {
|
||||
const renderData = withSessionValidation(async () => {
|
||||
if (props.Data.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -186,9 +187,9 @@ const DashboardCard = (props) => {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const filterRender = async () => {
|
||||
const filterRender = withSessionValidation(async () => {
|
||||
if (props.FilterData && props.FilterData.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -266,7 +267,7 @@ const DashboardCard = (props) => {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const setFormat = (val) => {
|
||||
switch (props.Format) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import reportJson from "../../json/ReportJson";
|
||||
import axios from "axios";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
function DashboardReport(props) {
|
||||
const { t } = useTranslation();
|
||||
const [List, setList] = useState([]);
|
||||
@@ -46,7 +47,7 @@ function DashboardReport(props) {
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
const handleSearchChange = async (e) => {
|
||||
const handleSearchChange = withSessionValidation(async (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
setSearchTerm(term);
|
||||
if (debounceTimer.current) {
|
||||
@@ -82,7 +83,7 @@ function DashboardReport(props) {
|
||||
}
|
||||
}, 300);
|
||||
setIsSearchResult(false);
|
||||
};
|
||||
});
|
||||
|
||||
const handleSearchPaste = (e) => {
|
||||
setTimeout(() => {
|
||||
@@ -98,96 +99,95 @@ function DashboardReport(props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getReportData = async (
|
||||
id,
|
||||
skipUserRecord = 0,
|
||||
limit = 20,
|
||||
term = searchTerm
|
||||
) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
setReportName(json.reportName);
|
||||
setHeading(json.heading);
|
||||
const currentUser = Parse.User.current().id;
|
||||
const getReportData = withSessionValidation(
|
||||
async (id, skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
setReportName(json.reportName);
|
||||
setHeading(json.heading);
|
||||
const currentUser = Parse.User.current().id;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
if (id === "5Go51Q7T8r") {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
if (id === "5Go51Q7T8r") {
|
||||
const listData = res.data?.result.filter(
|
||||
(x) => x.Signers.length > 0
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setIsNextRecord(false);
|
||||
if (!res.data.result.error) {
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setIsNextRecord(false);
|
||||
if (!res.data.result.error) {
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
};
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import FolderModal from "../shared/fields/FolderModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { handleDownloadPdf, isMobile } from "../../constant/Utils";
|
||||
import Parse from "parse";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
function DriveBody(props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -52,13 +53,11 @@ function DriveBody(props) {
|
||||
props.setSkip(0);
|
||||
};
|
||||
//function for change doc name and update doc name in _document class
|
||||
const handledRenameDoc = async (data) => {
|
||||
const handledRenameDoc = withSessionValidation(async (data) => {
|
||||
setRename("");
|
||||
const trimmedValue = renameValue.trim();
|
||||
if (trimmedValue.length > 0) {
|
||||
const updateName = {
|
||||
Name: renameValue
|
||||
};
|
||||
const updateName = { Name: renameValue };
|
||||
const docId = data.objectId;
|
||||
const docData = props.pdfData;
|
||||
const updatedData = docData.map((item) => {
|
||||
@@ -84,14 +83,14 @@ function DriveBody(props) {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
console.error("Error in rename doc", err);
|
||||
props.setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("something-went-wrong-mssg")
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
//function for navigate user to microapp-signature component
|
||||
const checkPdfStatus = async (data) => {
|
||||
@@ -157,12 +156,10 @@ function DriveBody(props) {
|
||||
}
|
||||
};
|
||||
//function for delete document
|
||||
const handleDeleteDocument = async (docData) => {
|
||||
const handleDeleteDocument = withSessionValidation(async (docData) => {
|
||||
setIsDeleteDoc({});
|
||||
const docId = docData.objectId;
|
||||
const data = {
|
||||
IsArchive: true
|
||||
};
|
||||
const data = { IsArchive: true };
|
||||
|
||||
await axios
|
||||
.put(
|
||||
@@ -184,19 +181,19 @@ function DriveBody(props) {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Err ", err);
|
||||
console.error("Err in delete doc", err);
|
||||
props.setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("something-went-wrong-mssg")
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
const handleMoveDocument = async (docData) => {
|
||||
setIsOpenMoveModal(true);
|
||||
setSelectDoc(docData);
|
||||
};
|
||||
//function for move document from one folder to another folder
|
||||
const handleMoveFolder = async (selectFolderData) => {
|
||||
const handleMoveFolder = withSessionValidation(async (selectFolderData) => {
|
||||
const selecFolderId = selectDoc?.Folder?.objectId;
|
||||
const moveFolderId = selectFolderData?.ObjectId;
|
||||
let updateDocId = selectDoc?.objectId;
|
||||
@@ -218,16 +215,12 @@ function DriveBody(props) {
|
||||
}
|
||||
};
|
||||
} else {
|
||||
updateData = {
|
||||
Folder: { __op: "Delete" }
|
||||
};
|
||||
updateData = { Folder: { __op: "Delete" } };
|
||||
}
|
||||
|
||||
await axios
|
||||
.put(
|
||||
`${localStorage.getItem(
|
||||
"baseUrl"
|
||||
)}classes/contracts_Document/${updateDocId}`,
|
||||
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${updateDocId}`,
|
||||
updateData,
|
||||
{
|
||||
headers: {
|
||||
@@ -237,7 +230,6 @@ function DriveBody(props) {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
.then((Listdata) => {
|
||||
const res = Listdata.data;
|
||||
if (res) {
|
||||
@@ -248,7 +240,7 @@ function DriveBody(props) {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("err", err);
|
||||
console.error("err in move folder", err);
|
||||
});
|
||||
|
||||
setIsOpenMoveModal(false);
|
||||
@@ -256,7 +248,7 @@ function DriveBody(props) {
|
||||
alert(t("folder-already-exist!"));
|
||||
setIsOpenMoveModal(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleEnterPress = (e, data) => {
|
||||
if (e.key === "Enter") {
|
||||
handledRenameDoc(data);
|
||||
@@ -296,7 +288,7 @@ function DriveBody(props) {
|
||||
props.setPdfData(updatedData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
console.error("Err in delete folder", err);
|
||||
props.setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("something-went-wrong-mssg")
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useNavigate } from "react-router";
|
||||
|
||||
function CustomizeMail(props) {
|
||||
const { t } = useTranslation();
|
||||
const editorRef = useRef();
|
||||
const navigate = useNavigate();
|
||||
const copyUrlRef = useRef(null);
|
||||
const [isCustomize, setIsCustomize] = useState(false);
|
||||
@@ -27,20 +26,17 @@ function CustomizeMail(props) {
|
||||
props?.setIsMailModal(false);
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
};
|
||||
const handleOnchangeRequest = () => {
|
||||
if (editorRef.current) {
|
||||
const html = editorRef.current.editor.root.innerHTML;
|
||||
props?.setCustomizeMail((prev) => ({
|
||||
...prev,
|
||||
body: html
|
||||
}));
|
||||
}
|
||||
const handleOnchangeRequest = (value) => {
|
||||
props?.setCustomizeMail((prev) => ({
|
||||
...prev,
|
||||
body: value
|
||||
}));
|
||||
};
|
||||
const handleEmailSendToSigners = async () => {
|
||||
setIsLoader(true);
|
||||
const documentData = await contractDocument(props?.documentId);
|
||||
if (documentData && documentData?.length > 0) {
|
||||
props?.setDocumentDetails(documentData[0]);
|
||||
props?.setDocumentDetails && props?.setDocumentDetails(documentData[0]);
|
||||
if (
|
||||
documentData?.[0]?.SendinOrder &&
|
||||
documentData?.[0]?.SendinOrder === true
|
||||
@@ -49,7 +45,7 @@ function CustomizeMail(props) {
|
||||
const ownerDetails = documentData[0].Signers.find(
|
||||
(x) => x.Email === ownerEmail
|
||||
);
|
||||
props?.setCurrUserId(ownerDetails?.objectId);
|
||||
props?.setCurrUserId && props?.setCurrUserId(ownerDetails?.objectId);
|
||||
}
|
||||
//function is used to send email to signers for sign the document
|
||||
const mailRes = await sendEmailToSigners(
|
||||
@@ -94,9 +90,8 @@ function CustomizeMail(props) {
|
||||
isCustomize && (
|
||||
<>
|
||||
<EmailBody
|
||||
editorRef={editorRef}
|
||||
requestBody={props?.customizeMail.body}
|
||||
requestSubject={props?.customizeMail.subject}
|
||||
requestBody={props?.customizeMail?.body}
|
||||
requestSubject={props?.customizeMail?.subject}
|
||||
handleOnchangeRequest={handleOnchangeRequest}
|
||||
setCustomizeMail={props?.setCustomizeMail}
|
||||
/>
|
||||
@@ -135,9 +130,9 @@ function CustomizeMail(props) {
|
||||
!isCustomize && (
|
||||
<span
|
||||
className="op-link op-link-accent text-sm"
|
||||
onClick={() => setIsCustomize(!isCustomize)}
|
||||
onClick={() => setIsCustomize(true)}
|
||||
>
|
||||
{t("cutomize-email")}
|
||||
{t("customize-email")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
function DefaultSignature(props) {
|
||||
@@ -105,9 +105,7 @@ function DefaultSignature(props) {
|
||||
</button>
|
||||
{!props.isDefault && (
|
||||
<div className="absolute bg-black/70 text-white w-full h-full flex items-center justify-center text-[11px] cursor-default">
|
||||
<span className="-rotate-45">
|
||||
Document creator has disabled this option
|
||||
</span>
|
||||
<span className="-rotate-45">{t("option-disabled-by-owner")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,17 @@ import { useEffect, useRef } from "react";
|
||||
function DragGuideLinesLayer(props) {
|
||||
const { showGuidelines } = props;
|
||||
const prevDragState = useRef({ isDragging: false, x: 0, y: 0 });
|
||||
const { isDraggingWidgets, offset, itemType } = useDragLayer((monitor) => ({
|
||||
isDraggingWidgets: monitor.isDragging(),
|
||||
const { isDraggingWidget, offset, itemType } = useDragLayer((monitor) => ({
|
||||
isDraggingWidget: monitor.isDragging(),
|
||||
offset: monitor.getClientOffset(),
|
||||
itemType: monitor.getItem()
|
||||
}));
|
||||
useEffect(() => {
|
||||
// Hide guidelines when dragging stops
|
||||
if (!isDraggingWidget) {
|
||||
showGuidelines(false);
|
||||
return;
|
||||
}
|
||||
if (!offset) return;
|
||||
//getting container by id
|
||||
const container = document.getElementById("container");
|
||||
@@ -28,15 +33,14 @@ function DragGuideLinesLayer(props) {
|
||||
// Check if there’s any change in the dragging state or the dragged element’s position
|
||||
// Compare the current drag state with the previously stored one
|
||||
const hasStateChanged =
|
||||
prevDragState.current.isDragging !== isDraggingWidgets || // Drag started or stopped
|
||||
prevDragState.current.isDragging !== isDraggingWidget || // Drag started or stopped
|
||||
prevDragState.current.x !== x || // X position changed
|
||||
prevDragState.current.y !== y; // Y position changed
|
||||
|
||||
if (hasStateChanged) {
|
||||
// Update the previous drag state with the latest values
|
||||
prevDragState.current = { isDragging: isDraggingWidgets, x, y };
|
||||
|
||||
if (isDraggingWidgets) {
|
||||
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
|
||||
@@ -64,13 +68,10 @@ function DragGuideLinesLayer(props) {
|
||||
|
||||
// Display the alignment guidelines based on the current drag position and widget size
|
||||
showGuidelines(true, getXPosition, getYPosition, getWidth, getHeight);
|
||||
} else {
|
||||
// Hide guidelines when dragging stops
|
||||
showGuidelines(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isDraggingWidgets, offset, itemType, showGuidelines]);
|
||||
}, [isDraggingWidget, offset, itemType, showGuidelines]);
|
||||
}
|
||||
|
||||
export default DragGuideLinesLayer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { radioButtonWidget } from "../../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -111,16 +111,15 @@ function DropdownWidgetOption(props) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const defaultData =
|
||||
defaultCheckbox && defaultCheckbox.length > 0
|
||||
? defaultCheckbox
|
||||
: defaultValue;
|
||||
|
||||
const isDropdownOrRadio =
|
||||
props?.type === "dropdown" || props?.type === radioButtonWidget;
|
||||
const isCheckbox = props?.type === "checkbox";
|
||||
|
||||
const defaultData = isCheckbox ? defaultCheckbox : defaultValue;
|
||||
|
||||
const readOnlyWithoutValue =
|
||||
isReadOnly && !defaultValue && status !== "optional";
|
||||
const isCheckbox = props?.type === "checkbox";
|
||||
const WidgetLayout = ["checkbox", radioButtonWidget].includes(props.type)
|
||||
? layout
|
||||
: null;
|
||||
@@ -157,13 +156,15 @@ function DropdownWidgetOption(props) {
|
||||
};
|
||||
|
||||
|
||||
const handleDefaultCheck = (index) => {
|
||||
const getDefaultCheck = defaultCheckbox.includes(index);
|
||||
if (getDefaultCheck) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const handleSelectDefaultCheckbox = (e, index) => {
|
||||
const checked = e.target.checked;
|
||||
setDefaultCheckbox((prev) =>
|
||||
checked
|
||||
? prev.includes(index)
|
||||
? prev
|
||||
: [...prev, index]
|
||||
: prev.filter((i) => i !== index)
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
|
||||
@@ -201,21 +202,8 @@ function DropdownWidgetOption(props) {
|
||||
{props.type === "checkbox" && props.isShowAdvanceFeature && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={handleDefaultCheck(index)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
const getDefaultCheck =
|
||||
defaultCheckbox?.includes(index);
|
||||
if (!getDefaultCheck) {
|
||||
setDefaultCheckbox((prev) => [...prev, index]);
|
||||
}
|
||||
} else {
|
||||
const removeOption = defaultCheckbox.filter(
|
||||
(data) => data !== index
|
||||
);
|
||||
setDefaultCheckbox(removeOption);
|
||||
}
|
||||
}}
|
||||
checked={defaultCheckbox?.includes(index)}
|
||||
onChange={(e) => handleSelectDefaultCheckbox(e, index)}
|
||||
className="op-checkbox focus:outline-none hover:border-base-content mr-[5px]"
|
||||
/>
|
||||
)}
|
||||
@@ -270,7 +258,8 @@ function DropdownWidgetOption(props) {
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{props.type !== "checkbox" && !isPrefillExist && (
|
||||
{((props.type !== "checkbox" && !isPrefillExist) ||
|
||||
isPrefillExist) && (
|
||||
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
|
||||
{statusArr.map((data, ind) => (
|
||||
<div
|
||||
|
||||
@@ -54,6 +54,10 @@ const EditTemplate = ({
|
||||
AllowModifications: template?.AllowModifications || false,
|
||||
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
|
||||
});
|
||||
const pensList = ["blue", "red", "black"];
|
||||
const [selectedColors, setSelectedColors] = useState(
|
||||
template?.PenColors || pensList
|
||||
);
|
||||
const [isUpdate, setIsUpdate] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [uploadPdf, setUploadPdf] = useState({
|
||||
@@ -211,6 +215,7 @@ const EditTemplate = ({
|
||||
IsEnableOTP: IsEnableOTP,
|
||||
IsTourEnabled: isTourEnabled,
|
||||
AllowModifications: allowModify,
|
||||
PenColors: selectedColors,
|
||||
...reminderDate
|
||||
};
|
||||
onSuccess(data);
|
||||
@@ -245,6 +250,19 @@ const EditTemplate = ({
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleColorsChange = (color) => {
|
||||
setSelectedColors((prev) => {
|
||||
// If user tries to uncheck the last remaining color → block it
|
||||
if (prev.length === 1 && prev.includes(color)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Normal toggle behavior
|
||||
return prev.includes(color)
|
||||
? prev.filter((c) => c !== color) // remove
|
||||
: [...prev, color]; // add
|
||||
});
|
||||
};
|
||||
return (
|
||||
<ModalUi
|
||||
isOpen
|
||||
@@ -485,6 +503,41 @@ const EditTemplate = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3 mb-4">
|
||||
<label htmlFor="penColors">
|
||||
{t("pen-colors")}
|
||||
<a data-tooltip-id="pen-colors-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<Tooltip id="pen-colors-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("pen-colors")}</p>
|
||||
<div>{t("pen-colors-help")}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
|
||||
{pensList.map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
<input
|
||||
className="mr-[2px] op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
name="penColors"
|
||||
checked={selectedColors.includes(color)}
|
||||
onChange={() => handleColorsChange(color)}
|
||||
/>
|
||||
<div className="hover:underline underline-offset-2 cursor-default capitalize">
|
||||
{color}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<SignersInput
|
||||
label={t("Bcc")}
|
||||
@@ -524,6 +577,7 @@ const EditTemplate = ({
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Quill } from "react-quill-new";
|
||||
// Use a third-party plugin so editors can toggle raw HTML view.
|
||||
import htmlEditButton from "quill-html-edit-button";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import Tooltip from "../../primitives/Tooltip";
|
||||
import ReactQuill from "react-quill-new";
|
||||
import "../../styles/quill.css";
|
||||
@@ -19,7 +18,6 @@ export function EmailBody(props) {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
value={props.requestSubject}
|
||||
onChange={(e) => {
|
||||
props?.setRequestSubject(e.target.value);
|
||||
props?.setCustomizeMail((prev) => ({
|
||||
...prev,
|
||||
subject: e.target.value
|
||||
@@ -37,7 +35,6 @@ export function EmailBody(props) {
|
||||
theme="snow"
|
||||
value={props.requestBody}
|
||||
placeholder={t("email-placeholder")}
|
||||
ref={props.editorRef}
|
||||
modules={module1}
|
||||
formats={formats}
|
||||
onChange={props.handleOnchangeRequest}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
handleToPrint,
|
||||
} from "../../constant/Utils";
|
||||
|
||||
@@ -25,13 +25,14 @@ import {
|
||||
import PlaceholderType from "./PlaceholderType";
|
||||
import moment from "moment";
|
||||
import "../../styles/opensigndrive.css";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setIsShowModal } from "../../redux/reducers/widgetSlice";
|
||||
import { themeColor } from "../../constant/const";
|
||||
import { useGuidelinesContext } from "../../context/GuidelinesContext";
|
||||
import DatePicker from "react-datepicker";
|
||||
import DateWidgetModal from "../../primitives/DateWidgetModal";
|
||||
import { dateFormat } from "../../utils";
|
||||
|
||||
function Placeholder(props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -47,11 +48,18 @@ function Placeholder(props) {
|
||||
const [clickonWidget, setClickonWidget] = useState({});
|
||||
const [formdata, setFormdata] = useState({
|
||||
status: props?.pos?.options?.status || "required",
|
||||
isReadOnly: props?.pos?.options?.isReadOnly || false
|
||||
isReadOnly: props?.pos?.options?.isReadOnly || false,
|
||||
name: props?.pos?.options?.name
|
||||
});
|
||||
const [isToday, setIsToday] = useState(
|
||||
props.pos?.options?.response === "today" ? true : false
|
||||
);
|
||||
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
|
||||
? getDefaultDate(
|
||||
props?.pos?.options?.response,
|
||||
@@ -65,20 +73,6 @@ function Placeholder(props) {
|
||||
setContainerScale(props.containerWH.width / getPdfPageWidth.width);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.containerWH.width, props.pdfOriginalWH]);
|
||||
const dateFormatArr = [
|
||||
"L",
|
||||
"DD-MM-YYYY",
|
||||
"YYYY-MM-DD",
|
||||
"MM.DD.YYYY",
|
||||
"MM-DD-YYYY",
|
||||
"MMM DD, YYYY",
|
||||
"LL",
|
||||
"DD MMM, YYYY",
|
||||
"DD MMMM, YYYY",
|
||||
"DD.MM.YYYY",
|
||||
"DD/MM/YYYY"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (props?.pos?.type === "date") {
|
||||
const isDateChange = true;
|
||||
@@ -91,24 +85,30 @@ function Placeholder(props) {
|
||||
}, [widgetData]);
|
||||
|
||||
//function is used to convert date according to selected format
|
||||
const handleGetFormatDate = (data, isDateChange) => {
|
||||
const handleGetFormatDate = (data, isDateChange, isNewDate) => {
|
||||
const format =
|
||||
data?.format ||
|
||||
selectDate?.format ||
|
||||
props.pos?.options?.validation?.format;
|
||||
const isSpecialDateFormat =
|
||||
data?.format &&
|
||||
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
|
||||
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
|
||||
let updateDate = data.date;
|
||||
let date = "";
|
||||
if (isSpecialDateFormat && updateDate) {
|
||||
date = isDateChange
|
||||
? moment(updateDate).format(changeDateToMomentFormat(data.format))
|
||||
? moment(updateDate).format(changeDateToMomentFormat(format))
|
||||
: updateDate;
|
||||
} else if (updateDate) {
|
||||
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
|
||||
const newDate = new Date(updateDate);
|
||||
date = moment(newDate.getTime()).format(
|
||||
changeDateToMomentFormat(data?.format)
|
||||
);
|
||||
date = moment(newDate.getTime()).format(changeDateToMomentFormat(format));
|
||||
} else if (isNewDate) {
|
||||
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
|
||||
const newDate = new Date();
|
||||
date = moment(newDate.getTime()).format(changeDateToMomentFormat(format));
|
||||
}
|
||||
setSelectDate({ date: date, format: data?.format });
|
||||
setSelectDate({ date: date, format: format });
|
||||
return date;
|
||||
};
|
||||
const saveDateSetting = (data) => {
|
||||
@@ -123,8 +123,15 @@ function Placeholder(props) {
|
||||
};
|
||||
//function to save date and format on local array onchange date and onclick format
|
||||
const handleSaveDate = (data, isDateChange) => {
|
||||
const date = isToday ? "today" : handleGetFormatDate(data, isDateChange);
|
||||
const date =
|
||||
props.isPlaceholder && isToday
|
||||
? "today"
|
||||
: handleGetFormatDate(data, isDateChange);
|
||||
//`onChangeInput` is used to save data related to date in a placeholder field
|
||||
const format =
|
||||
data?.format ||
|
||||
selectDate?.format ||
|
||||
props.pos?.options?.validation?.format;
|
||||
onChangeInput(
|
||||
date,
|
||||
props.pos,
|
||||
@@ -133,13 +140,12 @@ function Placeholder(props) {
|
||||
props.setXyPosition,
|
||||
props.data && props.data.Id,
|
||||
false,
|
||||
data?.format,
|
||||
format,
|
||||
props.fontSize || props.pos?.options?.fontSize || 12,
|
||||
props.fontColor || props.pos?.options?.fontColor || "black",
|
||||
formdata
|
||||
formdata,
|
||||
);
|
||||
};
|
||||
|
||||
//Date-picker- accept date - Thu Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
|
||||
//if date have in this format - "30/12/2025"
|
||||
//then first need to be convert in this format - Tue Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
|
||||
@@ -150,7 +156,7 @@ function Placeholder(props) {
|
||||
//function change format array list with selected date and format
|
||||
const changeDateFormat = () => {
|
||||
const updateDate = [];
|
||||
dateFormatArr.map((data) => {
|
||||
dateFormat.map((data) => {
|
||||
let date = selectDate?.date || new Date();
|
||||
const isString = typeof date === "string";
|
||||
if (selectDate.format === "dd-MM-yyyy" && isString) {
|
||||
@@ -174,7 +180,7 @@ function Placeholder(props) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.isPlaceholder || props.isSignYourself || props.isSelfSign) {
|
||||
if (props.isPlaceholder || isSelfSign) {
|
||||
selectDate && changeDateFormat();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -221,7 +227,7 @@ function Placeholder(props) {
|
||||
if (props?.currWidgetsDetails?.key === props.pos?.key) {
|
||||
props.handleLinkUser(props.data.Id);
|
||||
}
|
||||
props.setUniqueId(props.data.Id);
|
||||
props.setUniqueId && props.setUniqueId(props.data.Id);
|
||||
const checkIndex = props.xyPosition.findIndex(
|
||||
(data) => data.Id === props.data.Id
|
||||
);
|
||||
@@ -244,10 +250,7 @@ function Placeholder(props) {
|
||||
props?.setIsCheckbox(true);
|
||||
}
|
||||
// cells widget settings in sign yourself flow
|
||||
else if (
|
||||
props.pos.type === cellsWidget &&
|
||||
(props.isSignYourself || props.isSelfSign)
|
||||
) {
|
||||
else if (props.pos.type === cellsWidget && isSelfSign) {
|
||||
props.handleCellSettingModal && props.handleCellSettingModal();
|
||||
}
|
||||
//condition to handle setting icon for signyour-self flow for all type text widgets
|
||||
@@ -260,7 +263,7 @@ function Placeholder(props) {
|
||||
"job title",
|
||||
"email"
|
||||
].includes(props.pos.type) &&
|
||||
(props.isSignYourself || props.isSelfSign)
|
||||
isSelfSign
|
||||
) {
|
||||
props.handleTextSettingModal(true);
|
||||
}
|
||||
@@ -373,8 +376,7 @@ function Placeholder(props) {
|
||||
"job title",
|
||||
"email",
|
||||
...isSettingForCells
|
||||
].includes(props.pos.type) &&
|
||||
(props.isSignYourself || props.isSelfSign) ? (
|
||||
].includes(props.pos.type) && isSelfSign ? (
|
||||
<i
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -496,6 +498,7 @@ function Placeholder(props) {
|
||||
className="fa-light fa-circle-xmark icon text-[#188ae2] -right-[8px] -top-[18px] "
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showGuidelines(false);
|
||||
//condition for template and placeholder flow
|
||||
if (props.data) {
|
||||
props.handleDeleteWidget(props.pos.key, props.data.Id);
|
||||
@@ -508,6 +511,7 @@ function Placeholder(props) {
|
||||
//for mobile and tablet touch event
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
showGuidelines(false);
|
||||
//condition for template and placeholder flow
|
||||
if (props.data) {
|
||||
props.handleDeleteWidget(props.pos.key, props.data?.Id);
|
||||
@@ -618,17 +622,19 @@ function Placeholder(props) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<div
|
||||
className="border-gray-400 rounded-[50px] border-[1px] px-3 md:ml-2 text-xs py-2 focus:outline-none hover:border-base-content "
|
||||
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="fa-light fa-calendar ml-[5px]"></i>
|
||||
<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();
|
||||
@@ -642,6 +648,17 @@ function Placeholder(props) {
|
||||
format: dateFormatList[selectedIndex]?.format
|
||||
}));
|
||||
} else {
|
||||
if (
|
||||
props?.data?.Role === "prefill" &&
|
||||
(formdata.status === "optional" || props.pos?.status === "optional")
|
||||
) {
|
||||
setSelectDate({
|
||||
date: "",
|
||||
format: dateFormatList[selectedIndex]?.format
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectDate(dateFormatList[selectedIndex]);
|
||||
}
|
||||
};
|
||||
@@ -649,18 +666,40 @@ function Placeholder(props) {
|
||||
setIsToday(false);
|
||||
setSelectDate((prev) => ({ ...prev, date: "" }));
|
||||
};
|
||||
const handleCloseDateModal = () => {
|
||||
setIsDateModal(false);
|
||||
};
|
||||
const handleDateStatus = (data) => {
|
||||
if (data === "Optional" && props?.data?.Role === "prefill") {
|
||||
setSelectDate((prev) => ({ ...prev, date: "" }));
|
||||
} else if (
|
||||
props?.data?.Role === "prefill" &&
|
||||
data === "Required" &&
|
||||
props?.pos?.options?.status === "optional"
|
||||
) {
|
||||
const isNewDate = true;
|
||||
handleGetFormatDate(selectDate, null, isNewDate);
|
||||
}
|
||||
setFormdata({
|
||||
...formdata,
|
||||
status: data.toLowerCase()
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Check if a text widget (prefill type) exists. Once the user enters a value and clicks outside or the widget becomes non-selectable, it should appear as plain text (just like embedded text in a document). When the user clicks on the text again, it should become editable. */}
|
||||
{props.pos?.options?.response &&
|
||||
props.pos.key !== props?.currWidgetsDetails?.key &&
|
||||
props.pos.type === textWidget ? (
|
||||
<span
|
||||
<textarea
|
||||
readOnly
|
||||
onClick={() => {
|
||||
if (props?.ispublicTemplate) return;
|
||||
props.setCurrWidgetsDetails &&
|
||||
props.setCurrWidgetsDetails(props.pos);
|
||||
props?.setRoleName("prefill");
|
||||
props.setUniqueId(props?.data?.Id);
|
||||
props?.setRoleName && props?.setRoleName("prefill");
|
||||
props.setUniqueId && props.setUniqueId(props?.data?.Id);
|
||||
}}
|
||||
style={{
|
||||
fontFamily: "Arial, sans-serif",
|
||||
@@ -671,13 +710,21 @@ function Placeholder(props) {
|
||||
...(props.pos?.Height ? { height: props.pos?.Height } : {}),
|
||||
fontSize: fontSize,
|
||||
color: fontColor,
|
||||
zIndex: 99
|
||||
zIndex: 99,
|
||||
cursor: getCursor(),
|
||||
resize: "none",
|
||||
background: "transparent",
|
||||
width: props.posWidth(props.pos, props.isSignYourself),
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
outline: "none",
|
||||
border: "none"
|
||||
}}
|
||||
>
|
||||
{
|
||||
props.pos?.options?.response
|
||||
}
|
||||
</span>
|
||||
</textarea>
|
||||
) : (
|
||||
<Rnd
|
||||
id={props.pos.key}
|
||||
@@ -737,7 +784,7 @@ function Placeholder(props) {
|
||||
props?.data,
|
||||
props?.isNeedSign,
|
||||
props?.uniqueId
|
||||
) //handle block color of widget for
|
||||
)
|
||||
}}
|
||||
onDrag={(_, d) => {
|
||||
props?.handleTabDrag?.(props.pos.key);
|
||||
@@ -829,7 +876,7 @@ function Placeholder(props) {
|
||||
top={-11}
|
||||
pos={props.pos}
|
||||
posHeight={props.posHeight}
|
||||
isSignYourself={props.isSignYourself || props.isSelfSign}
|
||||
isSignYourself={isSelfSign}
|
||||
/>
|
||||
) : props.data &&
|
||||
props.isNeedSign &&
|
||||
@@ -907,110 +954,147 @@ function Placeholder(props) {
|
||||
</div>
|
||||
</Rnd>
|
||||
)}
|
||||
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
|
||||
<DateWidgetModal isOpen={isDateModal} title={t("widget-info")}>
|
||||
<div className="text-base-content h-[100%] p-[20px]">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col md:items-center md:flex-row gap-y-3">
|
||||
<span className="capitalize">{t("format")} :</span>
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-4"
|
||||
defaultValue={""}
|
||||
onChange={(e) => handleChangeFormat(e)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("select-date-format")}
|
||||
</option>
|
||||
{dateFormatList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" value={ind} key={ind}>
|
||||
{data?.date ? data?.date : "nodata"}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
{selectDate.format}
|
||||
</span>
|
||||
</div>
|
||||
{props?.data?.Role !== "prefill" && props?.isPlaceholder && (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-2">
|
||||
<span>{t("set-date")} :</span>
|
||||
<DatePicker
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<div className="flex justify-start md:ml-2 ">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeYear(value)
|
||||
}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
closeOnScroll={true}
|
||||
selected={getDefaultDate(
|
||||
selectDate?.date,
|
||||
selectDate?.format
|
||||
)}
|
||||
popperPlacement="top-end"
|
||||
customInput={<ExampleCustomInput />}
|
||||
onChange={(date) => {
|
||||
setIsToday(false);
|
||||
const isDateChange = true;
|
||||
const dateObj = {
|
||||
date: date,
|
||||
format: selectDate.format
|
||||
};
|
||||
handleGetFormatDate(dateObj, isDateChange);
|
||||
}}
|
||||
dateFormat={
|
||||
selectDate
|
||||
? selectDate?.format
|
||||
: props.pos?.options?.validation?.format
|
||||
? props.pos?.options?.validation?.format
|
||||
: "MM/dd/yyyy"
|
||||
}
|
||||
portalId="root-portal"
|
||||
/>
|
||||
<label className=" flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
checked={isToday}
|
||||
type="checkbox"
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
onClick={() => {
|
||||
setSelectDate((prev) => ({ ...prev, date: "" }));
|
||||
setIsToday(!isToday);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-[2px]">{t("set-today")}</span>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveDateSetting(selectDate);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{!isSelfSign && (
|
||||
<div className="mb-[0.75rem] text-[13px] md:w-[60%] w-full">
|
||||
<label htmlFor="name">
|
||||
{t("name")}
|
||||
<span className="text-[red]"> *</span>
|
||||
</label>
|
||||
<span
|
||||
onClick={() => handleClearDate()}
|
||||
className="underline text-blue-500 cursor-pointer md:ml-2"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
<input
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
name="name"
|
||||
value={formdata.name}
|
||||
onChange={(e) =>
|
||||
setFormdata({
|
||||
...formdata,
|
||||
name: e.target.value
|
||||
})
|
||||
}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col md:items-center md:flex-row gap-y-3">
|
||||
<span className="capitalize">{t("format")} :</span>
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-4"
|
||||
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
|
||||
onChange={(e) => handleChangeFormat(e)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("select-date-format")}
|
||||
</option>
|
||||
{dateFormatList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" value={ind} key={ind}>
|
||||
{data?.date ? data?.date : "nodata"}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span className="text-xs text-gray-400 ml-1 uppercase">
|
||||
{selectDate.format || props.pos?.options?.validation?.format}
|
||||
</span>
|
||||
</div>
|
||||
{props?.data?.Role !== "prefill" && props?.isPlaceholder && (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-2">
|
||||
<span>{t("default-date")} :</span>
|
||||
<DatePicker
|
||||
renderCustomHeader={({
|
||||
date,
|
||||
changeYear,
|
||||
changeMonth
|
||||
}) => (
|
||||
<div className="flex justify-start md:ml-2">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeYear(value)
|
||||
}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
closeOnScroll={true}
|
||||
selected={getDefaultDate(
|
||||
selectDate?.date,
|
||||
selectDate?.format
|
||||
)}
|
||||
popperPlacement="top-end"
|
||||
customInput={<ExampleCustomInput />}
|
||||
onChange={(date) => {
|
||||
setIsToday(false);
|
||||
const isDateChange = true;
|
||||
const dateObj = {
|
||||
date: date,
|
||||
format: selectDate.format
|
||||
};
|
||||
handleGetFormatDate(dateObj, isDateChange);
|
||||
}}
|
||||
dateFormat={
|
||||
selectDate
|
||||
? selectDate?.format
|
||||
: props.pos?.options?.validation?.format
|
||||
? props.pos?.options?.validation?.format
|
||||
: "MM/dd/yyyy"
|
||||
}
|
||||
portalId="root-portal"
|
||||
/>
|
||||
<label className="flex items-center gap-1 cursor-pointer mb-0">
|
||||
<input
|
||||
checked={isToday}
|
||||
type="checkbox"
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
onClick={() => {
|
||||
setSelectDate((prev) => ({ ...prev, date: "" }));
|
||||
setIsToday(!isToday);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-[2px]">{t("set-today")}</span>
|
||||
</label>
|
||||
<span
|
||||
onClick={() => handleClearDate()}
|
||||
className="underline text-blue-500 cursor-pointer ml-2"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isSelfSign && (
|
||||
<div className="flex flex-row items-center gap-[10px]">
|
||||
{statusArr.map((data, ind) => {
|
||||
return (
|
||||
@@ -1022,12 +1106,7 @@ function Placeholder(props) {
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
name="status"
|
||||
onChange={() =>
|
||||
setFormdata({
|
||||
...formdata,
|
||||
status: data.toLowerCase()
|
||||
})
|
||||
}
|
||||
onChange={() => handleDateStatus(data)}
|
||||
checked={
|
||||
formdata.status.toLowerCase() === data.toLowerCase()
|
||||
}
|
||||
@@ -1039,80 +1118,92 @@ function Placeholder(props) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2">
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="capitalize">{t("font-size")} :</span>
|
||||
<select
|
||||
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
value={
|
||||
props.fontSize || clickonWidget.options?.fontSize || 12
|
||||
}
|
||||
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
|
||||
>
|
||||
{fontsizeArr.map((size, ind) => (
|
||||
<option className="text-[13px]" value={size} key={ind}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<span className="capitalize">{t("color")} :</span>
|
||||
<select
|
||||
value={
|
||||
props.fontColor ||
|
||||
clickonWidget.options?.fontColor ||
|
||||
"black"
|
||||
}
|
||||
onChange={(e) => props.setFontColor(e.target.value)}
|
||||
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
>
|
||||
{fontColorArr.map((color, ind) => (
|
||||
<option value={color} key={ind}>
|
||||
{t(`color-type.${color}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
props.fontColor || props.pos.options?.fontColor || "black"
|
||||
}}
|
||||
className="w-5 h-[19px] ml-1"
|
||||
></span>
|
||||
)}
|
||||
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2">
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="capitalize">{t("font-size")} :</span>
|
||||
<select
|
||||
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
value={
|
||||
props.fontSize || clickonWidget.options?.fontSize || 12
|
||||
}
|
||||
onChange={(e) =>
|
||||
props.setFontSize(parseInt(e.target.value))
|
||||
}
|
||||
>
|
||||
{fontsizeArr.map((size, ind) => (
|
||||
<option className="text-[13px]" value={size} key={ind}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<span className="capitalize">{t("color")} :</span>
|
||||
<select
|
||||
value={
|
||||
props.fontColor ||
|
||||
clickonWidget.options?.fontColor ||
|
||||
"black"
|
||||
}
|
||||
onChange={(e) => props.setFontColor(e.target.value)}
|
||||
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
>
|
||||
{fontColorArr.map((color, ind) => (
|
||||
<option value={color} key={ind}>
|
||||
{t(`color-type.${color}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
props.fontColor ||
|
||||
props.pos.options?.fontColor ||
|
||||
"black"
|
||||
}}
|
||||
className="w-5 h-[19px] ml-1"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
{props?.isPlaceholder && props?.data?.Role !== "prefill" && (
|
||||
<div className="flex items-center gap-1 w-max">
|
||||
<input
|
||||
name="isReadOnly"
|
||||
id="isReadOnly"
|
||||
type="checkbox"
|
||||
checked={formdata?.isReadOnly}
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
onChange={() =>
|
||||
setFormdata({
|
||||
...formdata,
|
||||
isReadOnly: !formdata?.isReadOnly
|
||||
})
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="isReadOnly"
|
||||
className="capitalize ml-[2px] mb-0 cursor-pointer"
|
||||
>
|
||||
{t("read-only")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{props?.isPlaceholder && props?.data?.Role !== "prefill" && (
|
||||
<label className="flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
name="isReadOnly"
|
||||
type="checkbox"
|
||||
checked={formdata?.isReadOnly}
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
onChange={() =>
|
||||
setFormdata({
|
||||
...formdata,
|
||||
isReadOnly: !formdata?.isReadOnly
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="capitalize ml-[2px]">{t("read-only")}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary"
|
||||
onClick={() => saveDateSetting(selectDate)}
|
||||
>
|
||||
{t("save")}
|
||||
</button>
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<button className="op-btn op-btn-primary" type="submit">
|
||||
{t("save")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content ml-1"
|
||||
onClick={() => handleCloseDateModal()}
|
||||
>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</ModalUi>
|
||||
</DateWidgetModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { handleCopyNextToWidget, randomId } from "../../constant/Utils";
|
||||
import {
|
||||
drawWidget,
|
||||
handleCopyNextToWidget,
|
||||
randomId
|
||||
} from "../../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { setPrefillImg } from "../../redux/reducers/widgetSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
function PlaceholderCopy(props) {
|
||||
const { prefillImg } = useSelector((state) => state.widget);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const copyType = [
|
||||
@@ -151,12 +156,17 @@ function PlaceholderCopy(props) {
|
||||
if (
|
||||
filterSignerPosition[0]?.Role === "prefill" &&
|
||||
currentPlaceholder?.options?.response &&
|
||||
currentPlaceholder?.type === "image"
|
||||
(currentPlaceholder?.type === "image" ||
|
||||
currentPlaceholder?.type === drawWidget)
|
||||
) {
|
||||
const getPrefillImg = prefillImg?.find(
|
||||
(x) => x.id === currentPlaceholder?.key
|
||||
);
|
||||
const base64Img = getPrefillImg?.base64;
|
||||
dispatch(
|
||||
setPrefillImg({
|
||||
id: newId,
|
||||
base64: currentPlaceholder?.options?.response
|
||||
base64: base64Img
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -254,15 +264,20 @@ function PlaceholderCopy(props) {
|
||||
props.setXyPosition,
|
||||
props?.Id
|
||||
);
|
||||
if (
|
||||
if (
|
||||
filterSignerPosition[0]?.Role === "prefill" &&
|
||||
currentXYposition?.options?.response &&
|
||||
currentXYposition?.type === "image"
|
||||
(currentXYposition?.type === "image" ||
|
||||
currentXYposition?.type === drawWidget)
|
||||
) {
|
||||
const getPrefillImg = prefillImg?.find(
|
||||
(x) => x.id === currentXYposition?.key
|
||||
);
|
||||
const base64Img = getPrefillImg?.base64;
|
||||
dispatch(
|
||||
setPrefillImg({
|
||||
id: newId,
|
||||
base64: currentXYposition?.options?.response
|
||||
base64: base64Img
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { useEffect, useMemo, useState, forwardRef } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
useRef
|
||||
} from "react";
|
||||
import {
|
||||
onChangeInput,
|
||||
getMonth,
|
||||
getYear,
|
||||
radioButtonWidget,
|
||||
@@ -9,9 +15,8 @@ import {
|
||||
months,
|
||||
years,
|
||||
selectCheckbox,
|
||||
checkRegularExpress,
|
||||
isBase64,
|
||||
getSignerPages
|
||||
drawWidget
|
||||
} from "../../constant/Utils";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
@@ -28,6 +33,7 @@ const widgetCls =
|
||||
function PlaceholderType(props) {
|
||||
const selectWidgetCls = `w-full h-full absolute left-0 top-0 focus:outline-none text-base-content`;
|
||||
const { t } = useTranslation();
|
||||
const textRef = useRef();
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const prefillImgLoad = useSelector((state) => state.widget.prefillImgLoad);
|
||||
const type = props?.pos?.type;
|
||||
@@ -42,10 +48,8 @@ function PlaceholderType(props) {
|
||||
// prefer the latest response value over any default value
|
||||
const widgetData =
|
||||
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
|
||||
// const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
|
||||
const [widgetValue, setwidgetValue] = useState();
|
||||
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
|
||||
// const [hint, setHint] = useState("");
|
||||
const [imgUrl, setImgUrl] = useState("");
|
||||
const fontSize = props.calculateFont(props.pos.options?.fontSize);
|
||||
const fontColor = props.pos.options?.fontColor || "black";
|
||||
@@ -72,11 +76,6 @@ function PlaceholderType(props) {
|
||||
// keep displayed value in sync with the stored response
|
||||
setwidgetValue(widgetData);
|
||||
}
|
||||
// if (props.pos?.options?.hint) {
|
||||
// setHint(props.pos?.options.hint);
|
||||
// } else if (props.pos?.options?.validation?.type) {
|
||||
// checkRegularExpress(props.pos?.options?.validation?.type, setHint);
|
||||
// }
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.pos, widgetData, type]);
|
||||
@@ -100,14 +99,8 @@ function PlaceholderType(props) {
|
||||
ExampleCustomInput.displayName = "ExampleCustomInput";
|
||||
|
||||
const handleRadioCheck = (data) => {
|
||||
const defaultData = props.pos.options?.defaultValue;
|
||||
if (widgetValue === data) {
|
||||
return true;
|
||||
} else if (defaultData === data) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const defaultData = widgetValue || props.pos.options?.defaultValue;
|
||||
return defaultData === data;
|
||||
};
|
||||
//function is used to get prefill image's signedUrl after expired
|
||||
useEffect(() => {
|
||||
@@ -138,6 +131,39 @@ function PlaceholderType(props) {
|
||||
props?.pos?.type === name[0] ? `${name[0]}${lastWord}` : widgetName;
|
||||
return props?.pos?.options?.hint || title;
|
||||
};
|
||||
//useEffect is used to increase auto height of text/textInput widget when user using multiline option and enter value in next line
|
||||
useEffect(() => {
|
||||
if (!textRef.current) return;
|
||||
if (
|
||||
(type === "text" || type === textInputWidget) &&
|
||||
textRef.current &&
|
||||
widgetValue
|
||||
) {
|
||||
const el = textRef.current;
|
||||
// Count actual number of lines
|
||||
const lines = widgetValue?.split("\n")?.length || props.pos?.Height;
|
||||
// Get line height from computed style
|
||||
const lineHeight = parseInt(window.getComputedStyle(el).lineHeight);
|
||||
|
||||
const textWidgetHeight = lines * lineHeight;
|
||||
// Set widget box height (your logic)
|
||||
onChangeInput(
|
||||
null,
|
||||
props.pos,
|
||||
props?.xyPosition,
|
||||
props.index,
|
||||
props?.setXyPosition,
|
||||
props.data && props.data.Id,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
textWidgetHeight
|
||||
);
|
||||
}
|
||||
}, [widgetValue]);
|
||||
|
||||
switch (type) {
|
||||
case "signature":
|
||||
return props.pos.SignUrl ? (
|
||||
@@ -230,17 +256,19 @@ function PlaceholderType(props) {
|
||||
return props.isSignYourself || iswidgetEnable ? (
|
||||
<textarea
|
||||
placeholder={formatWidgetName()}
|
||||
rows={1}
|
||||
ref={textRef}
|
||||
rows={50}
|
||||
value={
|
||||
widgetValue
|
||||
}
|
||||
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
|
||||
className={`w-full resize-none overflow-hidden text-base-content item-center outline-none ${isReadOnly ? "select-none" : ""}`}
|
||||
style={{
|
||||
fontSize: fontSize,
|
||||
color: fontColor,
|
||||
background: isReadOnly ? props.data?.blockColor : "white",
|
||||
pointerEvents: "none"
|
||||
}}
|
||||
name="text"
|
||||
readOnly
|
||||
disabled={props.isNeedSign && isReadOnly}
|
||||
cols="50"
|
||||
@@ -430,7 +458,7 @@ function PlaceholderType(props) {
|
||||
) : (
|
||||
<div
|
||||
style={textWidgetStyle}
|
||||
className="select-none-cls overflow-hidden"
|
||||
className="select-none-cls overflow-hidden uppercase"
|
||||
>
|
||||
<span>
|
||||
{props?.selectDate?.format ||
|
||||
@@ -530,22 +558,52 @@ function PlaceholderType(props) {
|
||||
case textWidget:
|
||||
return (
|
||||
<textarea
|
||||
name="text"
|
||||
ref={textRef}
|
||||
readOnly
|
||||
placeholder={t("widgets-name.text")}
|
||||
rows={1}
|
||||
value={
|
||||
rows={50}
|
||||
cols={50}
|
||||
defaultValue={
|
||||
widgetValue
|
||||
}
|
||||
className={textWidgetCls}
|
||||
style={{
|
||||
fontFamily: "Arial, sans-serif",
|
||||
fontSize: fontSize,
|
||||
color: fontColor,
|
||||
background: "white"
|
||||
}}
|
||||
cols="50"
|
||||
className="w-full resize-none overflow-hidden text-base-content item-center outline-none"
|
||||
/>
|
||||
);
|
||||
case drawWidget:
|
||||
return prefillImgLoad[props.pos?.key] ? (
|
||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
|
||||
<Loader />
|
||||
</div>
|
||||
) : widgetValue ? (
|
||||
<img
|
||||
alt="signature"
|
||||
draggable="false"
|
||||
src={widgetValue}
|
||||
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
|
||||
/>
|
||||
) : (
|
||||
<div className={widgetCls}>
|
||||
{props.pos.type && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: props.pos
|
||||
? props.calculateFontsize(props.pos)
|
||||
: "11px"
|
||||
}}
|
||||
className="font-medium"
|
||||
>
|
||||
{formatWidgetName()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return props.pos.SignUrl ? (
|
||||
<div className="pointer-events-none">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import {
|
||||
getMonth,
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
changeDateToMomentFormat,
|
||||
convertBase64ToFile,
|
||||
generatePdfName,
|
||||
drawWidget,
|
||||
getBase64MimeType,
|
||||
isBase64
|
||||
} from "../../constant/Utils";
|
||||
import DatePicker from "react-datepicker";
|
||||
@@ -27,9 +29,13 @@ import {
|
||||
setPrefillImg
|
||||
} from "../../redux/reducers/widgetSlice";
|
||||
import * as utils from "../../utils";
|
||||
import Draw from "./tab/Draw";
|
||||
import PenColorComponent from "./tab/PenColorComponent";
|
||||
|
||||
const widgetTitle = "font-medium";
|
||||
|
||||
const widgetLabelCss = (isRequired = false) => {
|
||||
return `${isRequired ? "after:content-['_*'] after:text-red-500" : ""} block text-xs font-semibold`;
|
||||
};
|
||||
const ShowTextWidget = ({ position, handleWidgetDetails }) => {
|
||||
const inputRef = useRef(null);
|
||||
const [inputValue, setInputValue] = useState(position.options.response || "");
|
||||
@@ -47,7 +53,6 @@ const ShowTextWidget = ({ position, handleWidgetDetails }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageComponent = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
@@ -66,13 +71,17 @@ const ImageComponent = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={widgetTitle}>{props?.position.options?.name}</span>
|
||||
<span
|
||||
className={`${widgetLabelCss(props?.position?.options?.status === "required")} ${widgetTitle}`}
|
||||
>
|
||||
{props?.position.options?.name}
|
||||
</span>
|
||||
{imgUrl ? (
|
||||
<>
|
||||
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
|
||||
<img
|
||||
alt="print img"
|
||||
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamicallys
|
||||
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamicallys
|
||||
src={imgUrl}
|
||||
draggable="false"
|
||||
className="object-contain h-full w-full aspect-[5/2]"
|
||||
@@ -90,19 +99,14 @@ const ImageComponent = (props) => {
|
||||
) : (
|
||||
<div
|
||||
className="cursor-pointer op-card border-[1px] op-border-hover flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center"
|
||||
onClick={() => imageRefs.current[props?.position.key]?.click()}
|
||||
onClick={() => imageRefs.current[props?.id]?.click()}
|
||||
>
|
||||
{props?.imageLoaders[props?.position?.key] && (
|
||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => props?.onImageChange?.(e, props?.position)}
|
||||
className="filetype"
|
||||
accept="image/png,image/jpeg"
|
||||
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamically
|
||||
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamically
|
||||
hidden
|
||||
/>
|
||||
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
|
||||
@@ -116,20 +120,18 @@ const ImageComponent = (props) => {
|
||||
function PrefillWidgetModal(props) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const canvasRefs = useRef({});
|
||||
const [penColor, setPenColor] = useState("blue");
|
||||
// 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
|
||||
const [image, setImage] = useState(null);
|
||||
const [imageLoaders, setImageLoaders] = useState({});
|
||||
const [currentWidget, setCurrentWidget] = useState("");
|
||||
const [userList, setUserList] = useState([]);
|
||||
const [totalImages, setTotalImages] = useState(0);
|
||||
const [loadedImages, setLoadedImages] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const years = range(1950, getYear(new Date()) + 16, 1);
|
||||
const isAnyLoaderActive = Object.values(imageLoaders).some(
|
||||
(val) => val === true
|
||||
);
|
||||
|
||||
// useMemo to memoize the calculation of unique widgets
|
||||
const uniqueWidget = useMemo(() => {
|
||||
@@ -171,15 +173,15 @@ function PrefillWidgetModal(props) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [props?.isPrefillModal]);
|
||||
|
||||
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)) {
|
||||
setLoading(true);
|
||||
prefillImg.forEach((img) => dispatch(setPrefillImg(img)));
|
||||
}
|
||||
setImageLoaders({});
|
||||
setLoading(false);
|
||||
};
|
||||
savePrefillImg();
|
||||
}, [props.xyPosition]);
|
||||
@@ -211,25 +213,7 @@ function PrefillWidgetModal(props) {
|
||||
}
|
||||
}
|
||||
}, [props?.isPrefillModal, uniqueWidget]);
|
||||
|
||||
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
|
||||
const getDatePickerDate = (selectedDate, format = "dd-MM-yyyy") => {
|
||||
let date;
|
||||
if (format && format === "dd-MM-yyyy") {
|
||||
const [day, month, year] = selectedDate?.split("-");
|
||||
date = new Date(`${year}-${month}-${day}`);
|
||||
} else if (format && format === "dd.MM.yyyy") {
|
||||
const [day, month, year] = selectedDate?.split(".");
|
||||
date = new Date(`${year}.${month}.${day}`);
|
||||
} else if (format && format === "dd/MM/yyyy") {
|
||||
const [day, month, year] = selectedDate?.split("/");
|
||||
date = new Date(`${year}/${month}/${day}`);
|
||||
} else {
|
||||
date = new Date(selectedDate);
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
|
||||
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<div
|
||||
style={{ fontFamily: "Arial, sans-serif" }}
|
||||
@@ -245,8 +229,8 @@ function PrefillWidgetModal(props) {
|
||||
|
||||
const handleDate = (position) => {
|
||||
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
|
||||
const getDate = getDatePickerDate(
|
||||
position?.options.response || new Date(),
|
||||
const getDate = utils.getDatePickerDate(
|
||||
position?.options.response,
|
||||
position?.options?.validation?.format
|
||||
);
|
||||
return getDate;
|
||||
@@ -270,14 +254,15 @@ function PrefillWidgetModal(props) {
|
||||
handleWidgetDetails(position, newDate);
|
||||
};
|
||||
|
||||
const handleSavePrefillImg = async (widgetDetails) => {
|
||||
setImageLoaders((prev) => ({ ...prev, [widgetDetails?.key]: true }));
|
||||
const handleSavePrefillImg = async (base64) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const imageName = generatePdfName(16);
|
||||
const imgType = image?.imgType || getBase64MimeType(base64);
|
||||
const imageUrl = await convertBase64ToFile(
|
||||
imageName,
|
||||
image.src,
|
||||
image.imgType
|
||||
base64 || image.src,
|
||||
imgType
|
||||
);
|
||||
if (imageUrl) {
|
||||
return imageUrl;
|
||||
@@ -293,7 +278,7 @@ function PrefillWidgetModal(props) {
|
||||
const getPlaceholder = getPrefill?.placeHolder;
|
||||
let imgUrl;
|
||||
if (widgetDetails?.type === "image") {
|
||||
imgUrl = await handleSavePrefillImg(widgetDetails);
|
||||
imgUrl = await handleSavePrefillImg(response);
|
||||
}
|
||||
const updatedData = getPlaceholder.map((page) => ({
|
||||
...page,
|
||||
@@ -391,7 +376,7 @@ function PrefillWidgetModal(props) {
|
||||
if (item.options.name === position.options.name) {
|
||||
return {
|
||||
...item,
|
||||
SignUrl: "",
|
||||
...(item.SignUrl !== undefined && { SignUrl: "" }),
|
||||
options: {
|
||||
...position.options,
|
||||
response: ""
|
||||
@@ -419,14 +404,35 @@ function PrefillWidgetModal(props) {
|
||||
// (tracks how many images have finished loading)
|
||||
setLoadedImages((prev) => prev + 1);
|
||||
}
|
||||
setImageLoaders({});
|
||||
setLoading(false);
|
||||
};
|
||||
const handleWidgetType = (position) => {
|
||||
//function for set signature url
|
||||
const handleSignatureChange = (drawImage, position) => {
|
||||
handleWidgetDetails(position, drawImage);
|
||||
};
|
||||
const getCanvasRef = (widgetId) => {
|
||||
if (!canvasRefs.current[widgetId]) {
|
||||
canvasRefs.current[widgetId] = React.createRef();
|
||||
}
|
||||
return canvasRefs.current[widgetId];
|
||||
};
|
||||
const clearCanvasById = (widgetId) => {
|
||||
const canvasRef = canvasRefs.current?.[widgetId];
|
||||
|
||||
if (canvasRef?.current) {
|
||||
canvasRef.current.clear();
|
||||
}
|
||||
};
|
||||
const handleWidgetType = (position, id) => {
|
||||
switch (position?.type) {
|
||||
case "checkbox":
|
||||
return (
|
||||
<>
|
||||
<span className={widgetTitle}>{position.options?.name}</span>
|
||||
<span
|
||||
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
|
||||
>
|
||||
{position.options?.name}
|
||||
</span>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
{position.options?.values?.map((data, ind) => (
|
||||
<div
|
||||
@@ -456,7 +462,11 @@ function PrefillWidgetModal(props) {
|
||||
case textWidget:
|
||||
return (
|
||||
<>
|
||||
<span className={widgetTitle}>{position.options?.name}</span>
|
||||
<span
|
||||
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
|
||||
>
|
||||
{position.options?.name}
|
||||
</span>
|
||||
<ShowTextWidget
|
||||
position={position}
|
||||
handleWidgetDetails={handleWidgetDetails}
|
||||
@@ -466,7 +476,11 @@ function PrefillWidgetModal(props) {
|
||||
case "dropdown":
|
||||
return (
|
||||
<>
|
||||
<span className={widgetTitle}>{position.options?.name}</span>
|
||||
<span
|
||||
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
|
||||
>
|
||||
{position.options?.name}
|
||||
</span>
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
|
||||
id="myDropdown"
|
||||
@@ -490,7 +504,11 @@ function PrefillWidgetModal(props) {
|
||||
case "date":
|
||||
return (
|
||||
<>
|
||||
<span className={widgetTitle}>{position.options?.name}</span>
|
||||
<span
|
||||
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
|
||||
>
|
||||
{position.options?.name}
|
||||
</span>
|
||||
<DatePicker
|
||||
portalId="datepicker-portal-root"
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
@@ -533,16 +551,20 @@ function PrefillWidgetModal(props) {
|
||||
return (
|
||||
<ImageComponent
|
||||
position={position}
|
||||
imageLoaders={imageLoaders}
|
||||
onImageChange={onImageChange}
|
||||
handleImageLoaded={handleImageLoaded}
|
||||
handleClearImage={handleClearImage}
|
||||
id={id}
|
||||
/>
|
||||
);
|
||||
case radioButtonWidget:
|
||||
return (
|
||||
<>
|
||||
<span className={widgetTitle}>{position.options?.name}</span>
|
||||
<span
|
||||
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
|
||||
>
|
||||
{position.options?.name}
|
||||
</span>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
{position.options?.values.map((data, ind) => (
|
||||
<div
|
||||
@@ -567,6 +589,38 @@ function PrefillWidgetModal(props) {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case drawWidget:
|
||||
return (
|
||||
<div>
|
||||
<span
|
||||
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
|
||||
>
|
||||
{position.options?.name}
|
||||
</span>
|
||||
<Draw
|
||||
penColor={penColor}
|
||||
canvasRef={getCanvasRef(position.key)}
|
||||
currWidgetsDetails={position}
|
||||
handleSignatureChange={handleSignatureChange}
|
||||
prefillCls={"prefillCanvas"}
|
||||
/>
|
||||
<div className="flex flex-row justify-between mt-[10px]">
|
||||
<PenColorComponent
|
||||
penColor={penColor}
|
||||
setPenColor={setPenColor}
|
||||
/>
|
||||
<span
|
||||
onClick={() => {
|
||||
clearCanvasById(position?.key);
|
||||
handleClearImage(position);
|
||||
}}
|
||||
className="flex justify-start text-blue-500 underline cursor-pointer"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return position?.SignUrl ? (
|
||||
<div className="pointer-events-none">
|
||||
@@ -586,6 +640,66 @@ function PrefillWidgetModal(props) {
|
||||
};
|
||||
|
||||
const handleEmbedPrefill = async (item) => {
|
||||
//checking is there any draw widget response have base64 url then generate that base64 to url and save it
|
||||
const prefillWidgets = props.xyPosition?.find((x) => x.Role === "prefill");
|
||||
let updatedXyPosition = [];
|
||||
if (prefillWidgets) {
|
||||
const isDrawWidget = (prefillWidgets.placeHolder ?? []).some((ph) =>
|
||||
(ph?.pos ?? []).some((p) => p?.type === drawWidget)
|
||||
);
|
||||
if (isDrawWidget) {
|
||||
const getPrefill = props.xyPosition.find((x) => x?.Role === "prefill");
|
||||
const getPlaceholder = getPrefill?.placeHolder ?? [];
|
||||
const updatedData = await Promise.all(
|
||||
getPlaceholder.map(async (page) => ({
|
||||
...page,
|
||||
pos: await Promise.all(
|
||||
page.pos.map(async (item) => {
|
||||
if (item?.type === drawWidget) {
|
||||
const widgetResponse = item?.options?.response;
|
||||
|
||||
// Skip if no response
|
||||
if (!widgetResponse) return item;
|
||||
|
||||
// If already URL, do not re-upload
|
||||
if (
|
||||
typeof widgetResponse === "string" &&
|
||||
widgetResponse.startsWith("http")
|
||||
) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// Convert THIS widget's base64 → URL
|
||||
const drawUrl = await handleSavePrefillImg(widgetResponse);
|
||||
return {
|
||||
...item,
|
||||
options: {
|
||||
...item.options,
|
||||
response: drawUrl
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
)
|
||||
}))
|
||||
);
|
||||
|
||||
updatedXyPosition = props.xyPosition.map((obj) => {
|
||||
if (obj.Role === "prefill") {
|
||||
return {
|
||||
...obj,
|
||||
placeHolder: updatedData
|
||||
};
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
|
||||
props.setXyPosition(updatedXyPosition);
|
||||
}
|
||||
}
|
||||
|
||||
await props.handleCreateDocument();
|
||||
};
|
||||
//`loadOptions` function to use show all list of signer in dropdown
|
||||
@@ -593,9 +707,9 @@ function PrefillWidgetModal(props) {
|
||||
try {
|
||||
const baseURL = localStorage.getItem("baseUrl");
|
||||
const url = `${baseURL}functions/getsigners`;
|
||||
const token = {
|
||||
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
||||
};
|
||||
|
||||
const token =
|
||||
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
@@ -658,7 +772,7 @@ function PrefillWidgetModal(props) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
|
||||
{uniqueWidget.map((x, id) => (
|
||||
<div key={id} className="flex flex-col gap-2 w-full">
|
||||
{handleWidgetType(x.widget)}
|
||||
{handleWidgetType(x.widget, id)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -673,7 +787,7 @@ function PrefillWidgetModal(props) {
|
||||
{t("recipients")}
|
||||
</h1>
|
||||
)}
|
||||
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 md:mx-3 text-base-content flex flex-col relative">
|
||||
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 text-base-content flex flex-col relative">
|
||||
{props.forms?.map((field, id) => {
|
||||
return (
|
||||
<div className="flex flex-col" key={field?.value}>
|
||||
@@ -727,7 +841,7 @@ function PrefillWidgetModal(props) {
|
||||
</div>
|
||||
<div className="flex gap-2 mx-4 mb-3">
|
||||
<button
|
||||
disabled={isAnyLoaderActive || props?.isSubmit}
|
||||
disabled={props?.isSubmit}
|
||||
className="op-btn op-btn-primary op-btn-sm w-[80px]"
|
||||
onClick={() => handleEmbedPrefill(props?.item)}
|
||||
>
|
||||
|
||||
@@ -48,7 +48,7 @@ function RenderAllPdfPage(props) {
|
||||
};
|
||||
|
||||
// Use setTimeout to wait for the transition to complete
|
||||
const timer = setTimeout(updateSize, 100); // match the transition duration
|
||||
const timer = setTimeout(updateSize, 150); // match the transition duration
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSidebar, pageContainer, props?.containerWH]);
|
||||
|
||||
@@ -29,12 +29,15 @@ import WidgetsDragPreview from "./WidgetsDragPreview";
|
||||
function RenderPdf(props) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { guideline, showGuidelines } = useGuidelinesContext();
|
||||
const [, drop] = useDrop({
|
||||
accept: "BOX",
|
||||
drop: (item, monitor) => props?.addPositionOfSignature(item, monitor)
|
||||
drop: (item, monitor) => {
|
||||
props?.addPositionOfSignature(item, monitor);
|
||||
showGuidelines(false);
|
||||
}
|
||||
});
|
||||
const [scaledHeight, setScaledHeight] = useState();
|
||||
const { guideline, showGuidelines } = useGuidelinesContext();
|
||||
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
|
||||
const isGuestSigner = localStorage.getItem("isGuestSigner");
|
||||
const scrollTriggerId = useSelector((state) => state.widget.scrollTriggerId);
|
||||
@@ -511,9 +514,10 @@ function RenderPdf(props) {
|
||||
props.setPdfLoad(true);
|
||||
props.pageDetails(pdf);
|
||||
}}
|
||||
onClick={() =>
|
||||
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({})
|
||||
}
|
||||
onClick={() => {
|
||||
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({});
|
||||
showGuidelines(false);
|
||||
}}
|
||||
file={pdfDataBase64}
|
||||
>
|
||||
<Page
|
||||
|
||||
@@ -10,7 +10,8 @@ function SelectLanguage(props) {
|
||||
{ value: "fr", text: "Français" }, //french
|
||||
{ value: "it", text: "Italiano" }, //italian
|
||||
{ value: "de", text: "Deutsch" }, //german
|
||||
{ value: "hi", text: "हिन्दी" } //hindi
|
||||
{ value: "hi", text: "हिन्दी" }, //hindi
|
||||
{ value: "kr", text: "한국어" } //korean
|
||||
];
|
||||
const defaultLanguage = i18next.language || "en";
|
||||
const [lang, setLang] = useState(defaultLanguage);
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
textInputWidget,
|
||||
cellsWidget,
|
||||
textWidget,
|
||||
widgets
|
||||
widgets,
|
||||
drawWidget
|
||||
} from "../../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useWidgetDrag } from "../../hook/useWidgetDrag";
|
||||
@@ -34,7 +35,8 @@ function WidgetComponent(props) {
|
||||
{ id: 13, text: "checkbox" },
|
||||
{ id: 14, text: "dropdown" },
|
||||
{ id: 15, text: radioButtonWidget },
|
||||
{ id: 16, text: "image" }
|
||||
{ id: 16, text: "image" },
|
||||
{ id: 17, text: drawWidget }
|
||||
];
|
||||
|
||||
// Create all drag refs in one go
|
||||
@@ -65,7 +67,8 @@ function WidgetComponent(props) {
|
||||
textInputWidget,
|
||||
"date",
|
||||
"image",
|
||||
"checkbox"
|
||||
"checkbox",
|
||||
drawWidget
|
||||
].includes(data.type)
|
||||
);
|
||||
// allow only (signature, stamp, initials, text, cells) widget when isAllowModification true and user does not have session token
|
||||
@@ -81,18 +84,19 @@ function WidgetComponent(props) {
|
||||
"name",
|
||||
"email",
|
||||
"job title",
|
||||
"company"
|
||||
"company",
|
||||
drawWidget
|
||||
].includes(data.type)
|
||||
);
|
||||
const selfSignWidgets = widgetList.filter(
|
||||
(data) =>
|
||||
!["dropdown", radioButtonWidget, textInputWidget].includes(data.type)
|
||||
!["dropdown", radioButtonWidget, textInputWidget,drawWidget].includes(data.type)
|
||||
);
|
||||
//if user select prefill role then allow only date,image,text,checkbox,radio,dropdownAdd commentMore actions
|
||||
//dropdown widget should only be show in template flow
|
||||
const prefillAllowWidgets = widgetList.filter((data) =>
|
||||
(props.isPrefillDropdown ? ["dropdown"] : [])
|
||||
.concat([radioButtonWidget, textWidget, "date", "image", "checkbox"])
|
||||
.concat([radioButtonWidget, textWidget, "date", "image", "checkbox",drawWidget])
|
||||
.includes(data.type)
|
||||
);
|
||||
//function to show widget on the base of conditionAdd commentMore actions
|
||||
@@ -108,7 +112,7 @@ function WidgetComponent(props) {
|
||||
return unlogedInUserWidgets;
|
||||
}
|
||||
} else if (props?.roleName !== "prefill") {
|
||||
return widgetList.filter((data) => ![textWidget].includes(data.type));
|
||||
return widgetList.filter((data) => ![textWidget,drawWidget].includes(data.type));
|
||||
}
|
||||
};
|
||||
const handleSelectRecipient = () => {
|
||||
|
||||
@@ -222,6 +222,7 @@ const WidgetNameModal = (props) => {
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi
|
||||
isOpen={props.isOpen}
|
||||
@@ -308,72 +309,70 @@ const WidgetNameModal = (props) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!props?.isSelfSign &&
|
||||
!isSignOrInitials &&
|
||||
props?.roleName !== "prefill" && (
|
||||
<div className={showFontControls ? "mb-[0.5rem]" : "mb-[0.75rem]"}>
|
||||
<div className="flex flex-row gap-[10px] mb-[0.5rem]">
|
||||
{statusArr.map((data, ind) => {
|
||||
return (
|
||||
<div
|
||||
key={ind}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
name="status"
|
||||
onChange={() =>
|
||||
setFormdata({
|
||||
...formdata,
|
||||
status: data.toLowerCase()
|
||||
})
|
||||
}
|
||||
checked={
|
||||
formdata.status.toLowerCase() === data.toLowerCase()
|
||||
}
|
||||
/>
|
||||
<div className="text-[13px] font-medium">
|
||||
{t(`widget-status.${data}`)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{[
|
||||
textInputWidget,
|
||||
cellsWidget,
|
||||
].includes(props.defaultdata?.type) && (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="isReadOnly"
|
||||
name="isReadOnly"
|
||||
type="checkbox"
|
||||
checked={formdata.isReadOnly}
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
onChange={() =>
|
||||
setFormdata((prev) => ({
|
||||
...formdata,
|
||||
isReadOnly: !prev.isReadOnly
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className="ml-1.5 mb-0 capitalize text-[13px]"
|
||||
htmlFor="isreadonly"
|
||||
{!props?.isSelfSign && !isSignOrInitials && (
|
||||
<div className={showFontControls ? "mb-[0.5rem]" : "mb-[0.75rem]"}>
|
||||
<div className="flex flex-row gap-[10px] mb-[0.5rem]">
|
||||
{statusArr.map((data, ind) => {
|
||||
return (
|
||||
<div
|
||||
key={ind}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
{t("read-only")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
name="status"
|
||||
onChange={() =>
|
||||
setFormdata({
|
||||
...formdata,
|
||||
status: data.toLowerCase()
|
||||
})
|
||||
}
|
||||
checked={
|
||||
formdata.status.toLowerCase() === data.toLowerCase()
|
||||
}
|
||||
/>
|
||||
<div className="text-[13px] font-medium">
|
||||
{t(`widget-status.${data}`)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
textInputWidget,
|
||||
cellsWidget,
|
||||
].includes(props.defaultdata?.type) && (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="isReadOnly"
|
||||
name="isReadOnly"
|
||||
type="checkbox"
|
||||
checked={formdata.isReadOnly}
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
onChange={() =>
|
||||
setFormdata((prev) => ({
|
||||
...formdata,
|
||||
isReadOnly: !prev.isReadOnly
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className="ml-1.5 mb-0 capitalize text-[13px]"
|
||||
htmlFor="isreadonly"
|
||||
>
|
||||
{t("read-only")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isSignOrInitials && (
|
||||
<div className="mb-[0.75rem]">
|
||||
<label htmlFor="signaturetype" className="text-[14px] mb-[0.7rem]">
|
||||
{t("allowed-signature-types")}
|
||||
</label>
|
||||
<div className=" ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
|
||||
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
|
||||
{signatureType.map((type, i) => {
|
||||
return (
|
||||
<div key={i} className="flex flex-row gap-[5px] items-center">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
function DefaultSignature(props) {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} border-[1.3px] border-gray-300 rounded-[4px]`}
|
||||
>
|
||||
<img
|
||||
src={props?.defaultSignImg}
|
||||
draggable="false"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DefaultSignature;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect } from "react";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { drawWidget } from "../../../constant/Utils";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
//this component is used for signature widget "Draw tab" and prefill user Draw widget
|
||||
function Draw(props) {
|
||||
const { prefillImg } = useSelector((state) => state.widget);
|
||||
useEffect(() => {
|
||||
handleDrawResponse();
|
||||
}, [prefillImg]);
|
||||
|
||||
//function is used to show draw widget response
|
||||
const handleDrawResponse = async () => {
|
||||
if (
|
||||
props?.currWidgetsDetails?.type === drawWidget &&
|
||||
props?.canvasRef.current
|
||||
) {
|
||||
const getPrefillImg = prefillImg?.find(
|
||||
(x) => x.id === props?.currWidgetsDetails?.key
|
||||
);
|
||||
const base64 = getPrefillImg?.base64;
|
||||
props?.canvasRef.current.fromDataURL(base64);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<SignatureCanvas
|
||||
ref={props?.canvasRef}
|
||||
penColor={props?.penColor}
|
||||
canvasProps={{
|
||||
className: `${
|
||||
props?.prefillCls
|
||||
? "prefillCanvas"
|
||||
: props?.currWidgetsDetails?.type === "initials"
|
||||
? "intialSignatureCanvas"
|
||||
: "signatureCanvas"
|
||||
} border-[1.3px] border-gray-300 rounded-[10px]`
|
||||
}}
|
||||
onEnd={() =>
|
||||
props?.handleSignatureChange(
|
||||
props?.canvasRef.current?.toDataURL(),
|
||||
props?.currWidgetsDetails
|
||||
)
|
||||
}
|
||||
dotSize={1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Draw;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ALLOWED_COLORS = ["blue", "red", "black"];
|
||||
|
||||
const COLOR_CLASS = {
|
||||
blue: "text-blue-600",
|
||||
red: "text-red-600",
|
||||
black: "text-black",
|
||||
white: "text-white"
|
||||
};
|
||||
|
||||
function PenColorComponent({
|
||||
providedColors,
|
||||
penColor,
|
||||
convertToImg,
|
||||
fontSelect,
|
||||
typedSignature,
|
||||
setPenColor,
|
||||
hideLabel = false,
|
||||
penSize
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pensList = useMemo(() => {
|
||||
const allowed = new Set(ALLOWED_COLORS);
|
||||
const filtered = Array.isArray(providedColors)
|
||||
? providedColors
|
||||
.map((c) => String(c).toLowerCase())
|
||||
.filter((c) => allowed.has(c))
|
||||
: [];
|
||||
|
||||
// remove duplicates while preserving order
|
||||
return filtered.length ? [...new Set(filtered)] : ALLOWED_COLORS;
|
||||
}, [providedColors]);
|
||||
|
||||
const PEN_SIZE = penSize === "sm" ? "text-[14px]" : "text-[16px]";
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-row items-center m-[5px] ${penSize === "sm" ? "gap-2" : "gap-3"}`}
|
||||
>
|
||||
{!hideLabel && <span className="text-base-content">{t("options")}</span>}
|
||||
{pensList.map((color) => {
|
||||
const selected = penColor === color;
|
||||
return (
|
||||
<i
|
||||
key={color}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Select ${color} pen`}
|
||||
onClick={() => {
|
||||
setPenColor?.(color);
|
||||
convertToImg && convertToImg?.(fontSelect, typedSignature, color);
|
||||
}}
|
||||
className={`${COLOR_CLASS[color] || "text-base-content"} ${selected ? "border-current" : "border-white"} border-b-[2px] pb-0.5 cursor-pointer ${PEN_SIZE} fa-light fa-pen-nib`}
|
||||
></i>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default PenColorComponent;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function TypeSignature(props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center tabWidth rounded-[4px]">
|
||||
<span className="ml-[5px] text-[12px] text-base-content">
|
||||
{props?.currWidgetsDetails?.type === "initials"
|
||||
? t("initial-teb")
|
||||
: t("signature-tab")}
|
||||
:
|
||||
</span>
|
||||
<input
|
||||
maxLength={props?.currWidgetsDetails?.type === "initials" ? 3 : 30}
|
||||
style={{ fontFamily: props?.fontSelect, color: props?.penColor }}
|
||||
type="text"
|
||||
className="bg-transparent ml-1 op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-[20px]"
|
||||
placeholder={
|
||||
props?.currWidgetsDetails?.type === "initials"
|
||||
? t("initial-type")
|
||||
: t("signature-type")
|
||||
}
|
||||
value={props?.typedSignature}
|
||||
onChange={(e) => {
|
||||
props?.setTypedSignature(e.target.value);
|
||||
if (e.target.value?.trim()?.length > 0) {
|
||||
props?.convertToImg(props?.fontSelect, e.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-[1px] border-[#d6d3d3] mt-[10px] rounded-[4px] tabWidth">
|
||||
{props?.fontOptions.map((font, ind) => {
|
||||
return (
|
||||
<div
|
||||
key={ind}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
fontFamily: font.value,
|
||||
backgroundColor:
|
||||
props?.fontSelect === font.value && "rgb(206 225 247)"
|
||||
}}
|
||||
onClick={() => {
|
||||
props?.setFontSelect(font.value);
|
||||
props?.convertToImg(font.value, props?.typedSignature);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="py-[5px] px-[10px] text-[20px]"
|
||||
style={{ color: props?.penColor }}
|
||||
>
|
||||
{props?.typedSignature
|
||||
? props?.typedSignature
|
||||
: t("Your-Signature")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TypeSignature;
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function UploadImage(props) {
|
||||
const { t } = useTranslation();
|
||||
const imageRefs = useRef({});
|
||||
|
||||
const getImageRef = (key) => {
|
||||
if (!imageRefs.current[key]) {
|
||||
imageRefs.current[key] = React.createRef();
|
||||
}
|
||||
return imageRefs.current[key];
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(props?.isImageSelect || props?.isStampOrImage) && !props?.image ? (
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-gray-300 flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
|
||||
onClick={() =>
|
||||
getImageRef(props?.currWidgetsDetails?.key).current.click()
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
onChange={props?.onImageChange}
|
||||
className="filetype"
|
||||
accept="image/png,image/jpeg"
|
||||
ref={getImageRef(props?.currWidgetsDetails?.key)}
|
||||
hidden
|
||||
/>
|
||||
<i className="fa-light fa-cloud-upload-alt uploadImgLogo text-base-content"></i>
|
||||
<div className="text-[10px] text-base-content">{t("upload")}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-gray-300 mb-[6px] overflow-hidden`}
|
||||
>
|
||||
<img
|
||||
alt="print img"
|
||||
ref={getImageRef(props?.currWidgetsDetails?.key)}
|
||||
src={props?.image?.src}
|
||||
draggable="false"
|
||||
className="object-contain h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UploadImage;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
//This file is for both Text and textInput widgets
|
||||
export default function TextInput(props) {
|
||||
const [isMultiline, setIsMultiline] = useState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (!isMultiline && e.key === "Enter") {
|
||||
e.preventDefault(); // disable new line in single-line mode
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (props?.widgetValue) {
|
||||
const isNextLine = props?.widgetValue.includes("\n");
|
||||
if (isNextLine) {
|
||||
setIsMultiline(true);
|
||||
}
|
||||
}
|
||||
}, [props?.widgetValue]);
|
||||
return (
|
||||
<div style={{ position: "relative", width: "100%" }}>
|
||||
{isMultiline ? (
|
||||
<textarea
|
||||
onBlur={props?.handleInputBlur}
|
||||
ref={props?.widgetRef}
|
||||
value={props?.widgetValue ?? ""} //use an empty string "" as a default
|
||||
onChange={(e) => props?.handleOnchangeTextBox(e)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={3}
|
||||
placeholder={t("widgets-name.text")}
|
||||
// className={`${textInputcls} resize-none outline-none max-h-[100px] `}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px 40px 12px 12px",
|
||||
borderRadius: "20px",
|
||||
border: "1px solid #dcdcdc",
|
||||
fontSize: "16px",
|
||||
resize: "none",
|
||||
outline: "none"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
name="text"
|
||||
ref={props?.widgetRef}
|
||||
type="text"
|
||||
placeholder={t("widgets-name.text")}
|
||||
value={props?.widgetValue ?? ""} //use an empty string "" as a default
|
||||
onChange={(e) => props?.handleOnchangeTextBox(e)}
|
||||
className={`${props?.textInputcls} pr-4`}
|
||||
onBlur={props?.handleInputBlur}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isMultiline && (
|
||||
<>
|
||||
<i
|
||||
data-tooltip-id="toggle-multiline"
|
||||
className="fa-light fa-file-lines"
|
||||
size={20}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "12px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
color: "#444",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={() => setIsMultiline((prev) => !prev)}
|
||||
></i>
|
||||
|
||||
<Tooltip id={"toggle-multiline"}>
|
||||
<span>Toggle Multiline Text</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,16 +10,16 @@ const DateFormatSelector = (props) => {
|
||||
|
||||
const dateFormats = [
|
||||
"MM/DD/YYYY",
|
||||
"MMMM DD, YYYY",
|
||||
"DD MMMM, YYYY",
|
||||
"DD-MM-YYYY",
|
||||
"DD MMM, YYYY",
|
||||
"YYYY-MM-DD",
|
||||
"MM-DD-YYYY",
|
||||
"MM.DD.YYYY",
|
||||
"MMM DD, YYYY",
|
||||
"DD/MM/YYYY",
|
||||
"DD-MM-YYYY",
|
||||
"DD.MM.YYYY",
|
||||
"DD/MM/YYYY"
|
||||
"YYYY-MM-DD",
|
||||
"MMM DD, YYYY",
|
||||
"MMMM DD, YYYY",
|
||||
"DD MMM, YYYY",
|
||||
"DD MMMM, YYYY"
|
||||
];
|
||||
|
||||
// Handle format change
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Parse from "parse";
|
||||
import ReactQuill from "react-quill-new";
|
||||
@@ -7,6 +7,7 @@ import EditorToolbar, { module1, module2, formats } from "../pdf/EditorToolbar";
|
||||
import Tooltip from "../../primitives/Tooltip";
|
||||
import Alert from "../../primitives/Alert";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const MailTemplateEditor = ({
|
||||
info,
|
||||
@@ -14,8 +15,6 @@ const MailTemplateEditor = ({
|
||||
}) => {
|
||||
const appName = localStorage.getItem("appname") || "OpenSign™";
|
||||
const { t } = useTranslation();
|
||||
const editorRef = useRef();
|
||||
const editorRefCom = useRef();
|
||||
const [requestBody, setRequestBody] = useState("");
|
||||
const [requestSubject, setRequestSubject] = useState("");
|
||||
const [completionBody, setCompletionBody] = useState("");
|
||||
@@ -79,7 +78,7 @@ const MailTemplateEditor = ({
|
||||
}
|
||||
};
|
||||
//function to save completion email template
|
||||
const handleSaveCompletionEmail = async (e) => {
|
||||
const handleSaveCompletionEmail = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const replacedHtmlBody = completionBody.replace(/"/g, "'");
|
||||
@@ -103,9 +102,9 @@ const MailTemplateEditor = ({
|
||||
setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") });
|
||||
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
|
||||
}
|
||||
};
|
||||
});
|
||||
//function to save request email template
|
||||
const handleSaveRequestEmail = async (e) => {
|
||||
const handleSaveRequestEmail = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const replacedHtmlBody = requestBody.replace(/"/g, "'");
|
||||
@@ -135,10 +134,10 @@ const MailTemplateEditor = ({
|
||||
setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") });
|
||||
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
//function to use reset form
|
||||
const handleReset = async (request, completion) => {
|
||||
const handleReset = withSessionValidation(async (request, completion) => {
|
||||
let extUser =
|
||||
localStorage.getItem("Extand_Class") &&
|
||||
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
@@ -185,17 +184,15 @@ const MailTemplateEditor = ({
|
||||
setIsMailLoader((p) => ({ ...p, completion: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
//function for handle ontext change and save again text in delta
|
||||
const handleOnchangeRequest = () => {
|
||||
if (editorRef.current) {
|
||||
const html = editorRef.current.editor.root.innerHTML;
|
||||
const handleOnchangeRequest = (html) => {
|
||||
if (html) {
|
||||
setRequestBody(html);
|
||||
}
|
||||
};
|
||||
const handleOnchangeCompletion = () => {
|
||||
if (editorRefCom.current) {
|
||||
const html = editorRefCom.current.editor.root.innerHTML;
|
||||
const handleOnchangeCompletion = (html) => {
|
||||
if (html) {
|
||||
setCompletionBody(html);
|
||||
}
|
||||
};
|
||||
@@ -258,10 +255,9 @@ const MailTemplateEditor = ({
|
||||
theme="snow"
|
||||
value={requestBody}
|
||||
placeholder="add body of email"
|
||||
ref={editorRef}
|
||||
modules={module1}
|
||||
formats={formats}
|
||||
onChange={handleOnchangeRequest}
|
||||
onChange={(value) => handleOnchangeRequest(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center mt-3 gap-2">
|
||||
@@ -336,10 +332,9 @@ const MailTemplateEditor = ({
|
||||
theme="snow"
|
||||
value={completionBody}
|
||||
placeholder="add body of email"
|
||||
ref={editorRefCom}
|
||||
modules={module2}
|
||||
formats={formats}
|
||||
onChange={handleOnchangeCompletion}
|
||||
onChange={(value) => handleOnchangeCompletion(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center mt-3 gap-2">
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import MailTemplateEditor from "../MailTemplateEditor";
|
||||
|
||||
const EmailTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
tenantInfo
|
||||
} = useSelector((state) => state.user);
|
||||
return (
|
||||
<div className="flex flex-col mb-4">
|
||||
<MailTemplateEditor
|
||||
info={
|
||||
tenantInfo
|
||||
}
|
||||
tenantId={tenantInfo?.objectId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailTab;
|
||||
@@ -0,0 +1,268 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { dateFormat, formatDate, withSessionValidation } from "../../../utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
setAlertInfo,
|
||||
setLoader,
|
||||
setUserInfo
|
||||
} from "../../../redux/reducers/userReducer";
|
||||
import DatePicker from "../../DatePicker";
|
||||
import DateFormat from "../../DateFormat";
|
||||
import {
|
||||
changeDateToMomentFormat,
|
||||
selectFormat
|
||||
} from "../../../constant/Utils";
|
||||
import moment from "moment";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
|
||||
|
||||
const WidgetsTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { userInfo } = useSelector((state) => state.user);
|
||||
const [selectDate, setSelectDate] = useState({
|
||||
date: "",
|
||||
format: "MM/dd/yyyy"
|
||||
});
|
||||
const [dateFormatList, setDateFormatList] = useState([]);
|
||||
const [dateWidget, setDateWidget] = useState({
|
||||
isSigningDate: false,
|
||||
isReadOnly: false,
|
||||
date: "",
|
||||
format: "MM/dd/yyyy"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateStates();
|
||||
}, [userInfo]);
|
||||
|
||||
const updateStates = () => {
|
||||
const widgetPreferences = userInfo?.WidgetPreferences || [];
|
||||
const dateWidgetPref = widgetPreferences.find((w) => w.type === "date") || {
|
||||
isSigningDate: false,
|
||||
isReadOnly: false
|
||||
};
|
||||
setDateWidget({
|
||||
isSigningDate: !!dateWidgetPref.isSigningDate,
|
||||
isReadOnly: !!dateWidgetPref.isReadOnly,
|
||||
date: dateWidgetPref.date,
|
||||
format: dateWidgetPref.format
|
||||
});
|
||||
setSelectDate({ date: dateWidgetPref.date, format: dateWidgetPref.format });
|
||||
const formatted = moment(
|
||||
dateWidgetPref.date,
|
||||
changeDateToMomentFormat(dateWidgetPref.format),
|
||||
true
|
||||
).toISOString();
|
||||
formatDateList(formatted);
|
||||
};
|
||||
|
||||
const handleSigningDateChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectDate({ date: "", format: selectDate.format });
|
||||
}
|
||||
setDateWidget((prev) => ({
|
||||
...prev,
|
||||
date: "",
|
||||
isSigningDate: e.target.checked
|
||||
}));
|
||||
};
|
||||
|
||||
const handleReadOnlyChange = (e) => {
|
||||
setDateWidget({ ...dateWidget, isReadOnly: e.target.checked });
|
||||
};
|
||||
|
||||
const showAlert = (type, msg) => {
|
||||
dispatch(setAlertInfo({ type, msg }));
|
||||
setTimeout(() => {
|
||||
dispatch(setAlertInfo({ type: "success", msg: "" }));
|
||||
}, 2000);
|
||||
};
|
||||
// `handleSave` is used save updated value signature type
|
||||
const handleSave = withSessionValidation(async () => {
|
||||
dispatch(setLoader(true));
|
||||
try {
|
||||
if (
|
||||
dateWidget?.isReadOnly &&
|
||||
!selectDate.date &&
|
||||
!dateWidget.isSigningDate
|
||||
) {
|
||||
alert(t("read-only-date-error"));
|
||||
return;
|
||||
}
|
||||
const res = await Parse.Cloud.run("setwidgetpreferences", {
|
||||
dateWidget: dateWidget
|
||||
});
|
||||
if (res) {
|
||||
const widgetPreferences = userInfo?.WidgetPreferences || [];
|
||||
// Normalize booleans (simple + safe)
|
||||
const dateOpt = { type: "date", ...dateWidget };
|
||||
// Upsert: replace if exists, otherwise append
|
||||
const updatedWidgetPreferences =
|
||||
widgetPreferences?.length > 0
|
||||
? widgetPreferences.map((w) => (w.type === "date" ? dateOpt : w))
|
||||
: [...widgetPreferences, dateOpt];
|
||||
const userdata = { ...userInfo };
|
||||
userdata.WidgetPreferences = updatedWidgetPreferences;
|
||||
dispatch(setUserInfo(userdata));
|
||||
showAlert("success", t("saved-successfully"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Widgets preferences error: ", error);
|
||||
showAlert("danger", error.message);
|
||||
} finally {
|
||||
dispatch(setLoader(false));
|
||||
}
|
||||
});
|
||||
const handleDateChange = (date) => {
|
||||
//function to save date and format in local array
|
||||
const formattedDate = formatDate({
|
||||
date: date,
|
||||
format: selectDate.format
|
||||
});
|
||||
setDateWidget({
|
||||
...dateWidget,
|
||||
isSigningDate: false,
|
||||
date: formattedDate,
|
||||
format: selectDate.format
|
||||
});
|
||||
setSelectDate({ date: formattedDate, format: selectDate.format });
|
||||
formatDateList(date);
|
||||
};
|
||||
|
||||
const formatDateList = (selecteddate) => {
|
||||
let date = selecteddate || new Date();
|
||||
const list = dateFormat.map((dateFormat) => {
|
||||
const format = selectFormat(dateFormat);
|
||||
return { date: formatDate({ date, format }), format: format };
|
||||
});
|
||||
setDateFormatList(list);
|
||||
};
|
||||
const handleChangeFormat = (e) => {
|
||||
e.stopPropagation();
|
||||
const selectedIndex = Number(e.target.value);
|
||||
const format = dateFormatList[selectedIndex]?.format;
|
||||
const dateObj =
|
||||
selectDate.date && !dateWidget?.isSigningDate
|
||||
? { date: dateFormatList[selectedIndex]?.date, format: format }
|
||||
: { format: format };
|
||||
setDateWidget((prev) => ({ ...prev, ...dateObj }));
|
||||
setSelectDate((prev) => ({ ...prev, ...dateObj }));
|
||||
};
|
||||
const handleClear = () => {
|
||||
setSelectDate((prev) => ({ ...prev, date: "" }));
|
||||
setDateWidget((prev) => ({ ...prev, date: "" }));
|
||||
};
|
||||
return (
|
||||
<div id="panel-widgets">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 md:gap-x-8 gap-y-6">
|
||||
{/* Left Column - Signature Settings */}
|
||||
<div className="md:col-span-6 flex flex-col">
|
||||
<div>
|
||||
<label className="inline-flex text-[14px] mb-0 font-medium">
|
||||
{t("date-widget")}
|
||||
<a data-tooltip-id="date-widget-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<ReactTooltip id="date-widget-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold"> {t("date-widget")}</p>
|
||||
<p>{t("date-pref-help-sub-title")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold capitalize">
|
||||
{t("format")}:{" "}
|
||||
</span>
|
||||
<span>{t("date-pref-help-format")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold capitalize">
|
||||
{t("default-date")}:{" "}
|
||||
</span>
|
||||
<span>{t("date-pref-help-default-date")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold capitalize">
|
||||
{t("signing-date")}:{" "}
|
||||
</span>
|
||||
<span>{t("date-pref-help-signing-date")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold capitalize">
|
||||
{t("read-only")}:{" "}
|
||||
</span>
|
||||
<span>{t("date-pref-help-read-only")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
</label>
|
||||
<DateFormat
|
||||
selectDate={selectDate}
|
||||
dateFormatList={dateFormatList}
|
||||
handleChangeFormat={handleChangeFormat}
|
||||
/>
|
||||
<DatePicker
|
||||
selectDate={selectDate}
|
||||
onChange={handleDateChange}
|
||||
handleClear={handleClear}
|
||||
/>
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
id="date-widget-signingdate"
|
||||
name="isSigningDate"
|
||||
onChange={handleSigningDateChange}
|
||||
checked={dateWidget.isSigningDate}
|
||||
/>
|
||||
<label
|
||||
htmlFor="date-widget-signingdate"
|
||||
className="text-sm font-medium text-base-content hover:underline underline-offset-2 cursor-pointer capitalize mb-0"
|
||||
title={t("signing-date")}
|
||||
>
|
||||
{t("signing-date")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
id="date-widget-readonly"
|
||||
name="isReadOnly"
|
||||
onChange={handleReadOnlyChange}
|
||||
checked={dateWidget.isReadOnly}
|
||||
/>
|
||||
<label
|
||||
htmlFor="date-widget-readonly"
|
||||
className="text-sm font-medium text-base-content hover:underline underline-offset-2 cursor-pointer capitalize mb-0"
|
||||
title={t("read-only")}
|
||||
>
|
||||
{t("read-only")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button - Full Width */}
|
||||
<div className="md:col-span-12 flex justify-start mt-3">
|
||||
<button
|
||||
className="op-btn op-btn-primary w-[110px]"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t("save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetsTab;
|
||||
@@ -0,0 +1,4 @@
|
||||
import EmailTab from "./Email";
|
||||
import WidgetsTab from "./Widgets";
|
||||
|
||||
export { EmailTab, WidgetsTab };
|
||||
@@ -0,0 +1,214 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* CellsInput (cells for a single word/value)
|
||||
*
|
||||
* Props:
|
||||
* - count: number (how many cells)
|
||||
* - value: string (controlled)
|
||||
* - onChange: (nextValue: string) => void
|
||||
* - hint
|
||||
* - textSize: number (px) default 12
|
||||
* - textColor: string default "#000000"
|
||||
* - only: "all" | "digits" | "letters" | "alphanumeric"
|
||||
* - autoFocus: boolean
|
||||
* - disabled: boolean
|
||||
*/
|
||||
export default function CellsInput({
|
||||
count = 1,
|
||||
value,
|
||||
onChange,
|
||||
hint = "",
|
||||
textSize = 12,
|
||||
textColor = "#000000",
|
||||
only = "all",
|
||||
autoFocus = false,
|
||||
disabled = false,
|
||||
className = ""
|
||||
}) {
|
||||
const isControlled = value !== undefined && typeof onChange === "function";
|
||||
const [internal, setInternal] = useState("");
|
||||
|
||||
const currentValue = isControlled ? value : internal;
|
||||
|
||||
const safeCount = useMemo(() => clampInt(count, 1, 64, 6), [count]);
|
||||
|
||||
// refs for focusing next/prev
|
||||
const inputsRef = useRef([]);
|
||||
|
||||
// normalize value length
|
||||
useEffect(() => {
|
||||
const normalized = normalizeValue(currentValue, safeCount, only);
|
||||
if (normalized !== currentValue) emit(normalized);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [safeCount, only]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus && !disabled) {
|
||||
queueMicrotask(() => inputsRef.current?.[0]?.focus?.());
|
||||
}
|
||||
}, [autoFocus, disabled]);
|
||||
|
||||
function emit(next) {
|
||||
if (isControlled) onChange(next);
|
||||
else setInternal(next);
|
||||
}
|
||||
|
||||
const chars = useMemo(() => {
|
||||
const v = normalizeValue(currentValue, safeCount, only);
|
||||
const arr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
|
||||
return arr;
|
||||
}, [currentValue, safeCount, only]);
|
||||
|
||||
const inputStyle = {
|
||||
fontSize: `${Number(textSize || 12)}px`,
|
||||
color: textColor && String(textColor).trim() ? textColor : "#000000"
|
||||
};
|
||||
|
||||
function focusIndex(i) {
|
||||
const el = inputsRef.current?.[i];
|
||||
if (el) {
|
||||
el.focus();
|
||||
el.select?.();
|
||||
}
|
||||
}
|
||||
|
||||
function setCharAt(index, ch) {
|
||||
const v = normalizeValue(currentValue, safeCount, only);
|
||||
const nextArr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
|
||||
nextArr[index] = ch;
|
||||
emit(nextArr.join(""));
|
||||
}
|
||||
|
||||
function handleChange(index, raw) {
|
||||
if (disabled) return;
|
||||
|
||||
// user typed multiple chars (mobile suggestions/paste) -> distribute
|
||||
const cleaned = filterString(raw, only);
|
||||
if (!cleaned) {
|
||||
setCharAt(index, "");
|
||||
return;
|
||||
}
|
||||
|
||||
const v = normalizeValue(currentValue, safeCount, only);
|
||||
const nextArr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
|
||||
|
||||
let writeAt = index;
|
||||
for (const ch of cleaned) {
|
||||
if (writeAt >= safeCount) break;
|
||||
nextArr[writeAt] = ch;
|
||||
writeAt++;
|
||||
}
|
||||
emit(nextArr.join(""));
|
||||
|
||||
// move focus
|
||||
const nextFocus = Math.min(writeAt, safeCount - 1);
|
||||
if (nextFocus !== index) focusIndex(nextFocus);
|
||||
}
|
||||
|
||||
function handleKeyDown(index, e) {
|
||||
if (disabled) return;
|
||||
|
||||
const key = e.key;
|
||||
|
||||
if (key === "Backspace") {
|
||||
e.preventDefault();
|
||||
const v = normalizeValue(currentValue, safeCount, only);
|
||||
if (v[index]) {
|
||||
setCharAt(index, "");
|
||||
return;
|
||||
}
|
||||
// if empty, go previous
|
||||
if (index > 0) {
|
||||
focusIndex(index - 1);
|
||||
setCharAt(index - 1, "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
if (index > 0) focusIndex(index - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
if (index < safeCount - 1) focusIndex(index + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === " " && only !== "all") {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste(index, e) {
|
||||
if (disabled) return;
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData?.getData("text") || "";
|
||||
const cleaned = filterString(text, only);
|
||||
if (!cleaned) return;
|
||||
|
||||
const v = normalizeValue(currentValue, safeCount, only);
|
||||
const nextArr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
|
||||
|
||||
let writeAt = index;
|
||||
for (const ch of cleaned) {
|
||||
if (writeAt >= safeCount) break;
|
||||
nextArr[writeAt] = ch;
|
||||
writeAt++;
|
||||
}
|
||||
emit(nextArr.join(""));
|
||||
|
||||
const nextFocus = Math.min(writeAt, safeCount - 1);
|
||||
focusIndex(nextFocus);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-full ${className}`}>
|
||||
<div className="flex items-center justify-center gap-0">
|
||||
{chars.map((ch, i) => (
|
||||
<input
|
||||
key={i}
|
||||
ref={(el) => (inputsRef.current[i] = el)}
|
||||
type="text"
|
||||
inputMode={only === "digits" ? "numeric" : "text"}
|
||||
autoComplete="one-time-code"
|
||||
className="op-input op-input-bordered op-input-sm w-9 text-center rounded-sm focus:outline-none hover:border-base-content disabled:opacity-60"
|
||||
style={inputStyle}
|
||||
value={ch}
|
||||
// maxLength={safeCount} // allows multi-char suggestions; we still distribute
|
||||
disabled={disabled}
|
||||
onChange={(e) => handleChange(i, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(i, e)}
|
||||
onPaste={(e) => handlePaste(i, e)}
|
||||
placeholder={hint[i]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- helpers ---------------- */
|
||||
|
||||
function clampInt(v, min, max, fallback) {
|
||||
const n = parseInt(String(v), 10);
|
||||
if (Number.isNaN(n)) return fallback;
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
function filterString(str, only) {
|
||||
const s = String(str ?? "");
|
||||
if (only === "digits") return s.replace(/[^0-9]/g, "");
|
||||
if (only === "letters") return s.replace(/[^a-zA-Z]/g, "");
|
||||
if (only === "alphanumeric") return s.replace(/[^a-zA-Z0-9]/g, "");
|
||||
return s.replace(/\s/g, ""); // for "all" we still drop spaces for OTP-like behavior
|
||||
}
|
||||
|
||||
function normalizeValue(v, count, only) {
|
||||
const cleaned = filterString(v || "", only);
|
||||
return cleaned.slice(0, count);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import Parse from "parse";
|
||||
import Alert from "../../../primitives/Alert";
|
||||
import Loader from "../../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withSessionValidation } from "../../../utils";
|
||||
|
||||
const CreateFolder = ({ parentFolderId, onSuccess, folderCls, onBack }) => {
|
||||
const folderPtr = {
|
||||
@@ -18,7 +19,7 @@ const CreateFolder = ({ parentFolderId, onSuccess, folderCls, onBack }) => {
|
||||
setAlert({ type: type, message: msg });
|
||||
setTimeout(() => setAlert({ type: type, message: "" }), 1000);
|
||||
};
|
||||
const handleCreateFolder = async (event) => {
|
||||
const handleCreateFolder = withSessionValidation(async (event) => {
|
||||
event.preventDefault();
|
||||
handleLoader(true);
|
||||
if (name) {
|
||||
@@ -41,6 +42,12 @@ const CreateFolder = ({ parentFolderId, onSuccess, folderCls, onBack }) => {
|
||||
template.set("Folder", folderPtr);
|
||||
}
|
||||
template.set("CreatedBy", Parse.User.createWithoutData(currentUser.id));
|
||||
const ExtCls = JSON.parse(localStorage.getItem("Extand_Class"));
|
||||
template.set("ExtUserPtr", {
|
||||
__type: "Pointer",
|
||||
className: "contracts_Users",
|
||||
objectId: ExtCls[0].objectId
|
||||
});
|
||||
const res = await template.save();
|
||||
if (res) {
|
||||
handleLoader(false);
|
||||
@@ -52,7 +59,7 @@ const CreateFolder = ({ parentFolderId, onSuccess, folderCls, onBack }) => {
|
||||
handleLoader(false);
|
||||
showToast("info", t("fill-folder-name"));
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleLoader = (status) => setIsLoader(status);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,23 +1,98 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useState, useLayoutEffect, useEffect, useRef } from "react";
|
||||
import { findContact } from "../../../constant/Utils";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function findScrollParent(el) {
|
||||
let parent = el?.parentElement;
|
||||
while (parent) {
|
||||
const style = window.getComputedStyle(parent);
|
||||
const overflowY = style.overflowY;
|
||||
if (overflowY === "auto" || overflowY === "scroll") return parent;
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return window; // fallback
|
||||
}
|
||||
|
||||
const SuggestionInput = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [inputValue, setInputValue] = useState(props?.value || "");
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const ref = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const dropdownRef = useRef(null);
|
||||
const [dropdownPos, setDropdownPos] = useState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
placement: "bottom" // bottom | top
|
||||
});
|
||||
|
||||
const updateDropdownPosition = () => {
|
||||
const inputEl = inputRef.current;
|
||||
if (!inputEl) return;
|
||||
|
||||
const rect = inputEl.getBoundingClientRect();
|
||||
const desiredHeight = 120; // roughly max-h (adjust if you want)
|
||||
const gap = 6;
|
||||
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const openUpwards = spaceBelow < desiredHeight + gap;
|
||||
|
||||
const top = openUpwards
|
||||
? Math.max(gap, rect.top - desiredHeight - gap)
|
||||
: rect.bottom + gap;
|
||||
|
||||
setDropdownPos({
|
||||
top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
placement: openUpwards ? "top" : "bottom"
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!showSuggestions) return;
|
||||
updateDropdownPosition();
|
||||
}, [showSuggestions, inputValue, suggestions.length]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", Clickout);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", Clickout);
|
||||
const Clickout = (e) => {
|
||||
if (
|
||||
inputRef.current?.contains(e.target) ||
|
||||
dropdownRef.current?.contains(e.target)
|
||||
)
|
||||
return;
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
document.addEventListener("mousedown", Clickout);
|
||||
return () => document.removeEventListener("mousedown", Clickout);
|
||||
}, []);
|
||||
|
||||
const Clickout = (event) => {
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!showSuggestions) return;
|
||||
|
||||
const inputEl = inputRef.current;
|
||||
if (!inputEl) return;
|
||||
|
||||
const scrollParent = findScrollParent(inputEl);
|
||||
|
||||
const onScroll = () => updateDropdownPosition();
|
||||
const onResize = () => updateDropdownPosition();
|
||||
|
||||
// capture scroll on window + scroll parent
|
||||
window.addEventListener("scroll", onScroll, true);
|
||||
window.addEventListener("resize", onResize);
|
||||
if (scrollParent !== window)
|
||||
scrollParent.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll, true);
|
||||
window.removeEventListener("resize", onResize);
|
||||
if (scrollParent !== window)
|
||||
scrollParent.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [showSuggestions]);
|
||||
// create debounce to avoid unnecessay api calls
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
@@ -63,32 +138,55 @@ const SuggestionInput = (props) => {
|
||||
props.onChange(suggestion);
|
||||
}
|
||||
};
|
||||
|
||||
const dropdown =
|
||||
showSuggestions && suggestions.length > 0
|
||||
? createPortal(
|
||||
<ul
|
||||
ref={dropdownRef}
|
||||
className="fixed z-[1002] max-h-[250px] overflow-y-auto bg-base-200 border border-gray-300 rounded shadow-md text-base-content"
|
||||
style={{
|
||||
top: dropdownPos?.top,
|
||||
left: dropdownPos?.left,
|
||||
maxWidth: dropdownPos?.width > 200 ? dropdownPos?.width : 200
|
||||
}}
|
||||
>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="block py-2 px-2 w-full text-sm cursor-pointer hover:bg-base-300"
|
||||
// use onMouseDown so click registers BEFORE input blur
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleSuggestionClick(suggestion);
|
||||
}}
|
||||
>
|
||||
{suggestion.Name} {"<" + suggestion.Email + ">"}
|
||||
</li>
|
||||
))}
|
||||
</ul>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center relative text-base-content">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type={props?.type || "text"}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Email..."
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
placeholder={`${t("enter-value", { value: t("Email") })}...`}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase placeholder:capitalize"
|
||||
required={props.required}
|
||||
onFocus={() => {
|
||||
if (suggestions.length) setShowSuggestions(true);
|
||||
}}
|
||||
/>
|
||||
{showSuggestions && (
|
||||
<ul
|
||||
ref={ref}
|
||||
className="absolute z-50 left-0 top-[2.55rem] w-full max-h-[100px] overflow-y-auto bg-base-200 border border-gray-300 rounded shadow-md"
|
||||
>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="py-2 px-2 w-full text-sm cursor-pointer hover:bg-base-300"
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
>
|
||||
{suggestion.Name} {"<" + suggestion.Email + ">"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{dropdown}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
const Menu = ({ item, isOpen, closeSidebar }) => {
|
||||
@@ -7,6 +7,8 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
|
||||
"OpenSign™";
|
||||
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
|
||||
const { t } = useTranslation();
|
||||
const { selectedMenu } = useSelector((state) => state.sidebar);
|
||||
|
||||
return (
|
||||
<li key={item.title} role="none" className="my-0.5">
|
||||
<NavLink
|
||||
@@ -16,11 +18,9 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
|
||||
: `/${item.objectId}`
|
||||
}
|
||||
className={({ isActive }) =>
|
||||
`${
|
||||
isActive ? " bg-base-300 text-base-content" : ""
|
||||
} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
`${isActive && selectedMenu ? "bg-base-300 text-base-content" : ""} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
}
|
||||
onClick={closeSidebar}
|
||||
onClick={() => closeSidebar(item.title)}
|
||||
tabIndex={isOpen ? 0 : -1}
|
||||
role="menuitem"
|
||||
>
|
||||
|
||||
@@ -7,7 +7,10 @@ import sidebarList, { subSetting } from "../../json/menuJson";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useWindowSize } from "../../hook/useWindowSize";
|
||||
import { toggleSidebar } from "../../redux/reducers/sidebarReducer";
|
||||
import {
|
||||
setSelectedMenu,
|
||||
toggleSidebar
|
||||
} from "../../redux/reducers/sidebarReducer";
|
||||
|
||||
const Sidebar = () => {
|
||||
const { width } = useWindowSize();
|
||||
@@ -29,6 +32,7 @@ const Sidebar = () => {
|
||||
}, []);
|
||||
|
||||
const closeSidebar = () => {
|
||||
dispatch(setSelectedMenu(true));
|
||||
if (width <= 1023) {
|
||||
dispatch(toggleSidebar(false));
|
||||
}
|
||||
@@ -58,10 +62,12 @@ const Sidebar = () => {
|
||||
};
|
||||
|
||||
const toggleSubmenu = (title) => {
|
||||
dispatch(setSelectedMenu(false));
|
||||
setSubmenuOpen({ [title]: !submenuOpen[title] });
|
||||
};
|
||||
|
||||
const handleMenuItem = () => {
|
||||
dispatch(setSelectedMenu(true));
|
||||
closeSidebar();
|
||||
setSubmenuOpen({});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
|
||||
@@ -8,6 +8,8 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
|
||||
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
|
||||
const { t } = useTranslation();
|
||||
const { title, icon, children } = item;
|
||||
const { selectedMenu } = useSelector((state) => state.sidebar);
|
||||
|
||||
return (
|
||||
<li role="none" className="my-0.5">
|
||||
<button
|
||||
@@ -45,11 +47,9 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
|
||||
: `/${childItem.objectId}`
|
||||
}
|
||||
className={({ isActive }) =>
|
||||
`${
|
||||
isActive ? "bg-base-300 text-base-content " : ""
|
||||
} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
`${isActive && selectedMenu ? "bg-base-300 text-base-content" : ""} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
|
||||
}
|
||||
onClick={closeSidebar}
|
||||
onClick={() => closeSidebar(childItem.title)}
|
||||
role="menuitem"
|
||||
tabIndex={submenuOpen ? 0 : -1}
|
||||
>
|
||||
|
||||
+369
-266
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDrag } from "react-dnd";
|
||||
import { getEmptyImage } from "react-dnd-html5-backend";
|
||||
import { useGuidelinesContext } from "../context/GuidelinesContext";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export const useWidgetDrag = (item) => {
|
||||
const { showGuidelines } = useGuidelinesContext();
|
||||
const [, dragRef, preview] = useDrag({
|
||||
type: "BOX",
|
||||
item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
}),
|
||||
end: () => {
|
||||
showGuidelines(false);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Hide browser default drag preview
|
||||
|
||||
@@ -25,7 +25,7 @@ i18n
|
||||
interpolation: {
|
||||
escapeValue: false // Not needed for react as it escapes by default
|
||||
},
|
||||
whitelist: ["en", "es", "fr", "it", "de", "hi"] // List of allowed languages
|
||||
whitelist: ["en", "es", "fr", "it", "de", "hi", "kr"] // List of allowed languages
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
+11
-56
@@ -22,7 +22,7 @@ body {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 766px) {
|
||||
.reactour__close {
|
||||
.reactour__close-button {
|
||||
width: 17px !important;
|
||||
padding-left: 3px !important;
|
||||
}
|
||||
@@ -119,47 +119,24 @@ body {
|
||||
}
|
||||
|
||||
/* React-tour modal styling */
|
||||
.reactour__helper {
|
||||
.reactour__popover {
|
||||
background-color: #1f2937 !important;
|
||||
color: #e5e7eb !important;
|
||||
border: 1px solid #374151 !important;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.reactour__close {
|
||||
.reactour__close-button {
|
||||
color: #e5e7eb !important;
|
||||
background-color: #374151 !important;
|
||||
border: 1px solid #4b5563 !important;
|
||||
}
|
||||
|
||||
.reactour__close:hover {
|
||||
.reactour__close-button:hover {
|
||||
background-color: #4b5563 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* React-tour navigation buttons */
|
||||
.reactour__controls {
|
||||
background-color: #1f2937 !important;
|
||||
border-top: 1px solid #374151 !important;
|
||||
}
|
||||
|
||||
.reactour__controls button {
|
||||
background-color: #374151 !important;
|
||||
color: #e5e7eb !important;
|
||||
border: 1px solid #4b5563 !important;
|
||||
}
|
||||
|
||||
.reactour__controls button:hover {
|
||||
background-color: #4b5563 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.reactour__controls button:disabled {
|
||||
background-color: #3c3c3c !important;
|
||||
color: #858585 !important;
|
||||
border-color: #565656 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
/* ReactTooltip styling */
|
||||
.react-tooltip {
|
||||
@@ -187,41 +164,19 @@ body {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.reactour__helper h1,
|
||||
.reactour__helper h2,
|
||||
.reactour__helper h3,
|
||||
.reactour__helper h4,
|
||||
.reactour__helper h5,
|
||||
.reactour__helper h6 {
|
||||
.reactour__popover h1,
|
||||
.reactour__popover h2,
|
||||
.reactour__popover h3,
|
||||
.reactour__popover h4,
|
||||
.reactour__popover h5,
|
||||
.reactour__popover h6 {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.reactour__helper p {
|
||||
.reactour__popover p {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
/* VS Code-style tour buttons */
|
||||
.reactour__controls .op-btn {
|
||||
background-color: #007acc !important;
|
||||
color: #ffffff !important;
|
||||
border: 1px solid #007acc !important;
|
||||
}
|
||||
|
||||
.reactour__controls .op-btn:hover {
|
||||
background-color: #0086d1 !important;
|
||||
border-color: #0086d1 !important;
|
||||
}
|
||||
|
||||
.reactour__controls .op-btn-secondary {
|
||||
background-color: #374151 !important;
|
||||
color: #e5e7eb !important;
|
||||
border: 1px solid #4b5563 !important;
|
||||
}
|
||||
|
||||
.reactour__controls .op-btn-secondary:hover {
|
||||
background-color: #4b5563 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* React-datepicker dark mode styling */
|
||||
.react-datepicker {
|
||||
|
||||
@@ -5,19 +5,19 @@ export const templateReportTour = [
|
||||
selector: "[data-tut=reactourFirst]",
|
||||
content: <Trans i18nKey="tour-mssg.report-1" />,
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "11px" }
|
||||
},
|
||||
{
|
||||
selector: "[data-tut=reactourSecond]",
|
||||
content: <Trans i18nKey="tour-mssg.redirect" />,
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: "[data-tut=tourbulksend]",
|
||||
content: <Trans i18nKey="tour-mssg.bulksend" />,
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: "[data-tut=reactourThird]",
|
||||
@@ -42,6 +42,6 @@ export const templateReportTour = [
|
||||
</Trans>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
nonPresentMaskCss
|
||||
} from "../constant/Utils";
|
||||
import Header from "../components/Header";
|
||||
import Footer from "../components/Footer";
|
||||
import Sidebar from "../components/sidebar/Sidebar";
|
||||
@@ -6,7 +9,9 @@ import Tour from "../primitives/Tour";
|
||||
import axios from "axios";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import Parse from "parse";
|
||||
import { useNavigate, Outlet } from "react-router";
|
||||
import {
|
||||
Outlet
|
||||
} from "react-router";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||
@@ -16,9 +21,8 @@ const HomeLayout = () => {
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const arr = useSelector((state) => state.TourSteps);
|
||||
const tourArr = useSelector((state) => state.TourSteps);
|
||||
const isValidSession = useSelector((state) => state.user.isValidSession);
|
||||
const [isLoader, setIsLoader] = useState(true);
|
||||
const [isCloseBtn, setIsCloseBtn] = useState(true);
|
||||
@@ -55,7 +59,7 @@ const HomeLayout = () => {
|
||||
dispatch(sessionStatus(true));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("catch");
|
||||
console.error("error in authentication:", error?.message);
|
||||
// Session token is invalid or there was an error
|
||||
dispatch(sessionStatus(false));
|
||||
}
|
||||
@@ -68,40 +72,36 @@ const HomeLayout = () => {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (arr && arr.length > 0) {
|
||||
if (tourArr && tourArr.length > 0) {
|
||||
handleDynamicSteps();
|
||||
} else {
|
||||
setIsTour(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [arr]);
|
||||
}, [tourArr]);
|
||||
|
||||
const handleDynamicSteps = () => {
|
||||
const github = "https://github.com/OpenSignLabs/OpenSign";
|
||||
if (arr && arr.length > 0) {
|
||||
// const resArr = arr;
|
||||
const resArr = arr.map((obj, index) => {
|
||||
if (arr.length - 1 === index) {
|
||||
return { ...obj };
|
||||
if (tourArr && tourArr.length > 0) {
|
||||
const resArr = tourArr.map((obj, index) => {
|
||||
if (tourArr.length - 1 === index) {
|
||||
return obj;
|
||||
} else {
|
||||
return {
|
||||
...obj,
|
||||
actions: () => {
|
||||
setIsCloseBtn(false);
|
||||
}
|
||||
};
|
||||
return { ...obj, actions: () => setIsCloseBtn(false) };
|
||||
}
|
||||
});
|
||||
setTourConfigs([
|
||||
{
|
||||
selector: '[data-tut="nonpresentmask"]',
|
||||
content: t("tour-mssg.home-layout-1"),
|
||||
position: "center"
|
||||
position: "center",
|
||||
styles: { fontSize: "13px", maskArea: nonPresentMaskCss }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="tourbutton"]',
|
||||
content: t("tour-mssg.home-layout-2"),
|
||||
position: "top"
|
||||
position: "top",
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
...resArr,
|
||||
{
|
||||
@@ -122,20 +122,19 @@ const HomeLayout = () => {
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
position: "center"
|
||||
position: "center",
|
||||
styles: { fontSize: "13px", maskArea: nonPresentMaskCss }
|
||||
}
|
||||
]);
|
||||
checkTourStatus();
|
||||
}
|
||||
};
|
||||
const closeTour = async () => {
|
||||
// console.log("closeTour");
|
||||
setIsTour(false);
|
||||
const serverUrl = localStorage.getItem("baseUrl");
|
||||
const appId = localStorage.getItem("parseAppId");
|
||||
const json = JSON.parse(localStorage.getItem("Extand_Class"));
|
||||
const extUserId = json && json.length > 0 && json[0].objectId;
|
||||
// console.log("extUserId ", extUserId)
|
||||
|
||||
let updatedTourStatus = [];
|
||||
if (tourStatusArr.length > 0) {
|
||||
@@ -209,15 +208,15 @@ const HomeLayout = () => {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<Tour
|
||||
onRequestClose={closeTour}
|
||||
steps={tourConfigs}
|
||||
isOpen={isTour}
|
||||
disableKeyboardNavigation={["esc"]}
|
||||
// disableInteraction={true}
|
||||
scrollOffset={-100}
|
||||
showCloseButton={isCloseBtn}
|
||||
/>
|
||||
{isTour && (
|
||||
<Tour
|
||||
onRequestClose={closeTour}
|
||||
steps={tourConfigs}
|
||||
isOpen={isTour}
|
||||
// scrollOffset={-100}
|
||||
showCloseButton={isCloseBtn}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,8 @@ const Dashboard = () => {
|
||||
return {
|
||||
selector: `[data-tut=${col.widget.data.tourSection}]`,
|
||||
content: t(`tour-mssg.${col.widget.label}`),
|
||||
position: "top"
|
||||
position: "top",
|
||||
styles: { fontSize: "13px" }
|
||||
// style: { backgroundColor: "#abd4d2" },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||
import { withSessionValidation } from "../utils";
|
||||
|
||||
// `Form` render all type of Form on this basis of their provided in path
|
||||
function Form() {
|
||||
@@ -84,6 +85,8 @@ const Forms = (props) => {
|
||||
const [isCorrectPass, setIsCorrectPass] = useState(true);
|
||||
const [isAdvanceOpt, setIsAdvanceOpt] = useState(false);
|
||||
const [bcc, setBcc] = useState([]);
|
||||
const pensList = ["blue", "red", "black"];
|
||||
const [selectedColors, setSelectedColors] = useState(pensList);
|
||||
|
||||
const handleStrInput = (e) => {
|
||||
setIsCorrectPass(true);
|
||||
@@ -130,7 +133,7 @@ const Forms = (props) => {
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
const handleFileInput = async (e) => {
|
||||
const handleFileInput = withSessionValidation(async (e) => {
|
||||
setpercentage(0);
|
||||
try {
|
||||
const files = Array.from(e.target.files);
|
||||
@@ -351,7 +354,7 @@ const Forms = (props) => {
|
||||
setSelectedFiles([]);
|
||||
removeFile(e);
|
||||
}
|
||||
};
|
||||
});
|
||||
// `isValidURL` is used to check valid webhook url
|
||||
function isValidURL(value) {
|
||||
try {
|
||||
@@ -361,7 +364,7 @@ const Forms = (props) => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (fileupload) {
|
||||
@@ -417,6 +420,8 @@ const Forms = (props) => {
|
||||
object.set("RemindOnceInEvery", remindOnceInEvery);
|
||||
object.set("IsTourEnabled", isTourEnabled);
|
||||
object.set("TimeToCompleteDays", TimeToCompleteDays);
|
||||
object.set("PenColors", selectedColors);
|
||||
|
||||
object.set("AllowModifications", false);
|
||||
object.set("IsEnableOTP", false);
|
||||
if (formData.NotifyOnSignatures !== undefined) {
|
||||
@@ -456,6 +461,7 @@ const Forms = (props) => {
|
||||
if (res) {
|
||||
setSigners([]);
|
||||
setBcc([]);
|
||||
setSelectedColors(pensList);
|
||||
setFolder({ ObjectId: "", Name: "" });
|
||||
const notifySign =
|
||||
extUserData?.NotifyOnSignatures !== undefined
|
||||
@@ -507,7 +513,7 @@ const Forms = (props) => {
|
||||
} else {
|
||||
alert(t("file-alert-3"));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const handleFolder = (data) => {
|
||||
setFolder(data);
|
||||
@@ -538,6 +544,7 @@ const Forms = (props) => {
|
||||
setIsReset(true);
|
||||
setSigners([]);
|
||||
setBcc([]);
|
||||
setSelectedColors(pensList);
|
||||
setFolder({ ObjectId: "", Name: "" });
|
||||
const notifySign =
|
||||
extUserData?.NotifyOnSignatures !== undefined
|
||||
@@ -651,6 +658,20 @@ const Forms = (props) => {
|
||||
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
|
||||
};
|
||||
|
||||
const handleColorsChange = (color) => {
|
||||
setSelectedColors((prev) => {
|
||||
// If user tries to uncheck the last remaining color → block it
|
||||
if (prev.length === 1 && prev.includes(color)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Normal toggle behavior
|
||||
return prev.includes(color)
|
||||
? prev.filter((c) => c !== color) // remove
|
||||
: [...prev, color]; // add
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${isSubmit ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
|
||||
@@ -678,7 +699,7 @@ const Forms = (props) => {
|
||||
value={formData.password}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs"
|
||||
placeholder="Enter pdf password"
|
||||
placeholder={t("enter-pdf-password")}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
@@ -748,16 +769,16 @@ const Forms = (props) => {
|
||||
<div className="flex gap-1 justify-center items-center">
|
||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm w-full h-full text-[13px]">
|
||||
<div className="break-all cursor-default">
|
||||
{t("file-selected")}: {selectedFiles.join(", ")}
|
||||
{t("files-selected")}: {selectedFiles.join(", ")}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setFileUpload("");
|
||||
setSelectedFiles([]);
|
||||
}}
|
||||
className="cursor-pointer px-[10px] text-[20px] font-bold text-red-500"
|
||||
className="cursor-pointer ml-[10px] text-[20px] font-bold"
|
||||
>
|
||||
<i className="fa-light fa-xmark"></i>
|
||||
<i className="fa-light fa-xmark inline-flex h-7 w-7 items-center justify-center rounded-full hover:bg-red-100 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -939,6 +960,7 @@ const Forms = (props) => {
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -1038,6 +1060,7 @@ const Forms = (props) => {
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -1088,6 +1111,7 @@ const Forms = (props) => {
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -1214,6 +1238,46 @@ const Forms = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
</div>
|
||||
{props.title !== "Sign Yourself" && (
|
||||
<div className="text-xs mt-3">
|
||||
<label htmlFor="penColors">
|
||||
{t("pen-colors")}
|
||||
<a
|
||||
data-tooltip-id="pen-colors-tooltip"
|
||||
className="ml-1"
|
||||
>
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>
|
||||
<Tooltip id="pen-colors-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("pen-colors")}</p>
|
||||
<div>{t("pen-colors-help")}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
|
||||
{pensList.map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
<input
|
||||
className="mr-[2px] op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
name="penColors"
|
||||
checked={selectedColors.includes(color)}
|
||||
onChange={() => handleColorsChange(color)}
|
||||
/>
|
||||
<div className="hover:underline underline-offset-2 cursor-default capitalize">
|
||||
{color}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,15 @@ import { SaveFileSize } from "../constant/saveFileSize";
|
||||
import Alert from "../primitives/Alert";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { sanitizeFileName } from "../utils";
|
||||
import { sanitizeFileName, withSessionValidation } from "../utils";
|
||||
|
||||
const COLOR_CLASS = {
|
||||
blue: "text-blue-600",
|
||||
red: "text-red-600",
|
||||
black: "text-black",
|
||||
white: "text-white"
|
||||
};
|
||||
|
||||
const ManageSign = () => {
|
||||
const { t } = useTranslation();
|
||||
const [penColor, setPenColor] = useState("blue");
|
||||
@@ -29,10 +37,12 @@ const ManageSign = () => {
|
||||
const [isInitials, setIsInitials] = useState(false);
|
||||
const [id, setId] = useState("");
|
||||
const [imgInitials, setImgInitials] = useState("");
|
||||
const [stamp, setStamp] = useState();
|
||||
const canvasRef = useRef(null);
|
||||
const imageRef = useRef(null);
|
||||
const initailsRef = useRef(null);
|
||||
const imgInitialsRef = useRef(null);
|
||||
const stampRef = useRef(null);
|
||||
useEffect(() => {
|
||||
fetchUserSign();
|
||||
// eslint-disable-next-line
|
||||
@@ -63,6 +73,9 @@ const ManageSign = () => {
|
||||
setIsInitials(true);
|
||||
setImgInitials(res?.Initials);
|
||||
}
|
||||
if (res && res?.Stamp) {
|
||||
setStamp(res?.Stamp);
|
||||
}
|
||||
} else {
|
||||
if (User?.get("name")) {
|
||||
const sanitizename = generateTitleFromFilename(User?.get("name"));
|
||||
@@ -127,7 +140,7 @@ const ManageSign = () => {
|
||||
setIsValue(false);
|
||||
}
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
const isUrl = image?.includes("https") || image?.includes("http");
|
||||
if (!isvalue) {
|
||||
@@ -171,15 +184,28 @@ const ManageSign = () => {
|
||||
} else {
|
||||
initialsUrl = imgInitials;
|
||||
}
|
||||
const isStampUrl = stamp?.includes("https") || stamp?.includes("http");
|
||||
|
||||
let stampFile;
|
||||
if (stamp && !isStampUrl) {
|
||||
stampFile = base64StringtoFile(stamp, `${replaceSpace}_stamp`);
|
||||
}
|
||||
let stampUrl;
|
||||
if (stampFile && !isStampUrl) {
|
||||
stampUrl = await uploadFile(stampFile);
|
||||
} else {
|
||||
stampUrl = stamp;
|
||||
}
|
||||
if (imgUrl) {
|
||||
await saveEntry({
|
||||
name: signName,
|
||||
url: imgUrl,
|
||||
initialsUrl: initialsUrl
|
||||
initialsUrl: initialsUrl,
|
||||
stampUrl: stampUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
function base64StringtoFile(base64String, filename) {
|
||||
let arr = base64String.split(","),
|
||||
// type of uploaded image
|
||||
@@ -232,7 +258,8 @@ const ManageSign = () => {
|
||||
userId: User,
|
||||
initials: obj.initialsUrl,
|
||||
id: id,
|
||||
title: obj.name
|
||||
title: obj.name,
|
||||
stamp: obj?.stampUrl
|
||||
});
|
||||
setIsAlert({ type: "success", message: t("signature-saved-alert") });
|
||||
return res;
|
||||
@@ -274,6 +301,28 @@ const ManageSign = () => {
|
||||
setIsValue(false);
|
||||
}
|
||||
};
|
||||
const handleClearStamp = () => {
|
||||
if (stampRef.current) {
|
||||
stampRef.current.clear();
|
||||
}
|
||||
if (stampRef.current) {
|
||||
stampRef.current.value = "";
|
||||
}
|
||||
setStamp("");
|
||||
if (image) {
|
||||
setIsValue(true);
|
||||
}
|
||||
};
|
||||
const onStampChange = async (event) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const file = event.target.files[0];
|
||||
const base64Img = await toDataUrl(file);
|
||||
setStamp(base64Img);
|
||||
setIsValue(true);
|
||||
} else {
|
||||
setStamp("");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="relative h-full bg-base-100 text-base-content flex shadow-md rounded-box overflow-auto">
|
||||
{isLoader && (
|
||||
@@ -287,9 +336,9 @@ const ManageSign = () => {
|
||||
<div className="text-[20px] font-semibold m-[10px] md:m-0 mb-2">
|
||||
{t("my-signature")}
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-0 md:gap-[12px]">
|
||||
<div className="flex flex-col md:flex-row gap-2 md:gap-5">
|
||||
<div className="relative">
|
||||
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
|
||||
<span className="font-medium select-none flex mb-[10px]">
|
||||
{t("signature")}
|
||||
</span>
|
||||
<input
|
||||
@@ -300,7 +349,7 @@ const ManageSign = () => {
|
||||
ref={imageRef}
|
||||
hidden
|
||||
/>
|
||||
<div className="relative">
|
||||
<div>
|
||||
{image ? (
|
||||
<div className="mysignatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
|
||||
<img
|
||||
@@ -323,23 +372,24 @@ const ManageSign = () => {
|
||||
dotSize={1}
|
||||
/>
|
||||
)}
|
||||
<div className="penContainerDefault flex flex-row justify-between">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div>
|
||||
{!image && (
|
||||
<div className="flex flex-row gap-1.5 m-[5px]">
|
||||
{allColor.map((data, key) => {
|
||||
<div className="flex flex-row gap-1 m-[5px]">
|
||||
{allColor.map((color, key) => {
|
||||
const selected = penColor === color;
|
||||
return (
|
||||
<i
|
||||
key={key}
|
||||
onClick={() => setPenColor(allColor[key])}
|
||||
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
|
||||
onClick={() => setPenColor(color)}
|
||||
className={`${COLOR_CLASS[color] ?? ""} ${selected ? "border-current" : "border-white"} cursor-pointer border-b-[2px] pb-[2px] text-[16px] fa-light fa-pen-nib`}
|
||||
></i>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 text-sm md:text-base mr-1">
|
||||
<div className="flex flex-row gap-2 text-sm pr-2">
|
||||
<div
|
||||
type="button"
|
||||
className="op-link"
|
||||
@@ -364,7 +414,8 @@ const ManageSign = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
|
||||
<div>
|
||||
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
|
||||
{t("initials")}
|
||||
</span>
|
||||
@@ -396,26 +447,29 @@ const ManageSign = () => {
|
||||
dotSize={1}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row justify-between w-[183px]">
|
||||
<div
|
||||
className={`${isInitials ? "justify-between" : ""} flex flex-row gap-1`}
|
||||
>
|
||||
<div>
|
||||
{!isInitials && (
|
||||
<div className="flex flex-row gap-1.5 m-[5px]">
|
||||
{allColor.map((data, key) => {
|
||||
<div className="flex flex-row gap-1 m-[5px]">
|
||||
{allColor.map((color, key) => {
|
||||
const selected = initialPen === color;
|
||||
return (
|
||||
<i
|
||||
key={key}
|
||||
onClick={() => setInitialPen(allColor[key])}
|
||||
className={`border-b-[2px] ${key === 0 && initialPen === "blue" ? "border-blue-600" : key === 1 && initialPen === "red" ? "border-red-500" : key === 2 && initialPen === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
|
||||
onClick={() => setInitialPen(color)}
|
||||
className={`${COLOR_CLASS[color] ?? ""} ${selected ? "border-current" : "border-white"} cursor-pointer border-b-[2px] pb-[2px] text-[16px] fa-light fa-pen-nib`}
|
||||
></i>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1">
|
||||
<div className="flex flex-row gap-1.5 text-sm">
|
||||
<div
|
||||
type="button"
|
||||
className="op-link text-sm md:text-base mr-1"
|
||||
className="op-link"
|
||||
onClick={() => handleUploadInitials()}
|
||||
>
|
||||
{t("upload")}
|
||||
@@ -423,7 +477,7 @@ const ManageSign = () => {
|
||||
<div>
|
||||
<div
|
||||
type="button"
|
||||
className="op-link text-sm md:text-base mr-1"
|
||||
className="op-link"
|
||||
onClick={() => handleClearInitials()}
|
||||
>
|
||||
{t("clear")}
|
||||
@@ -433,6 +487,48 @@ const ManageSign = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
|
||||
Stamp
|
||||
</span>
|
||||
|
||||
<div
|
||||
onClick={() => stampRef.current.click()}
|
||||
className="mystampCanvas opensigndark:bg-[#1f2937] cursor-pointer border-[2px] opensigncss:border-[#888] opensigndark:border-[#4b5563] rounded-box flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center mr-2"
|
||||
>
|
||||
{stamp ? (
|
||||
<img
|
||||
alt="signature"
|
||||
src={stamp}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
onChange={onStampChange}
|
||||
type="file"
|
||||
className="filetype"
|
||||
accept="image/png,image/jpeg"
|
||||
hidden
|
||||
ref={stampRef}
|
||||
/>
|
||||
<i className="fa-light text-base-content fa-cloud-upload-alt text-[28px]"></i>
|
||||
<div className="text-[15px] text-base-content">
|
||||
{t("upload")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end pr-3">
|
||||
<div
|
||||
type="button"
|
||||
className="op-link text-sm"
|
||||
onClick={() => handleClearStamp()}
|
||||
>
|
||||
{t("clear")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3 ml-2 md:ml-0">
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { lazyWithRetry } from "../utils";
|
||||
import { lazyWithRetry, withSessionValidation } from "../utils";
|
||||
import "../styles/opensigndrive.css";
|
||||
import {
|
||||
getThemeIconColor,
|
||||
@@ -20,7 +20,7 @@ const DriveBody = lazyWithRetry(
|
||||
() => import("../components/opensigndrive/DriveBody")
|
||||
);
|
||||
const dropdowncss =
|
||||
"absolute right-0 py-2 text-[1rem] text-left bg-white border-[1px] border-gray-300 min-w-1 hover:bg-gray-300 rounded-[0.25rem] z-[800]";
|
||||
"absolute right-0 py-2 text-[1rem] text-left bg-base-100 border-[1px] border-gray-300 min-w-1 rounded-[0.25rem] z-[800]";
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
<div className="h-[100vh] flex justify-center items-center">
|
||||
@@ -98,7 +98,7 @@ function Opensigndrive() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourSecond"]',
|
||||
@@ -109,7 +109,7 @@ function Opensigndrive() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourThird"]',
|
||||
@@ -120,7 +120,7 @@ function Opensigndrive() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourForth"]',
|
||||
@@ -131,7 +131,7 @@ function Opensigndrive() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
const getDetails = async () => {
|
||||
@@ -161,7 +161,7 @@ function Opensigndrive() {
|
||||
/>
|
||||
),
|
||||
position: "bottom",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourSixth"]',
|
||||
@@ -172,7 +172,7 @@ function Opensigndrive() {
|
||||
/>
|
||||
),
|
||||
position: "bottom",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourSeventh"]',
|
||||
@@ -183,7 +183,7 @@ function Opensigndrive() {
|
||||
/>
|
||||
),
|
||||
position: "bottom",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
let newTour = [...tourConfigs, ...addMoreTour];
|
||||
@@ -279,7 +279,7 @@ function Opensigndrive() {
|
||||
setNewFolderName(value);
|
||||
};
|
||||
//function for create folder
|
||||
const handleAddFolder = async (e) => {
|
||||
const handleAddFolder = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
if (newFolderName) {
|
||||
setIsFolderLoader(true);
|
||||
@@ -313,7 +313,12 @@ function Opensigndrive() {
|
||||
const template = new Parse.Object(foldercls);
|
||||
template.set("Name", newFolderName);
|
||||
template.set("Type", "Folder");
|
||||
|
||||
const ExtCls = JSON.parse(localStorage.getItem("Extand_Class"));
|
||||
template.set("ExtUserPtr", {
|
||||
__type: "Pointer",
|
||||
className: "contracts_Users",
|
||||
objectId: ExtCls[0].objectId
|
||||
});
|
||||
if (parentId) {
|
||||
template.set("Folder", folderPtr);
|
||||
}
|
||||
@@ -338,7 +343,7 @@ function Opensigndrive() {
|
||||
} else {
|
||||
setError(t("fill-field"));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
//function to use sorting document list according to type and order
|
||||
const sortedBy = (appInfo, type, order) => {
|
||||
@@ -688,7 +693,7 @@ function Opensigndrive() {
|
||||
</div>
|
||||
{/* Mobile search toggle */}
|
||||
<button
|
||||
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-gray-200 text-[18px]"
|
||||
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-base-300 text-[18px]"
|
||||
aria-label="Search"
|
||||
onClick={() => setMobileSearchOpen((open) => !open)}
|
||||
>
|
||||
@@ -699,7 +704,7 @@ function Opensigndrive() {
|
||||
</button>
|
||||
<div
|
||||
id="folder-menu"
|
||||
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block cursor-pointer hover:bg-gray-200 p-2 rounded-md`}
|
||||
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block cursor-pointer hover:bg-base-300 p-2 rounded-md`}
|
||||
onClick={handleFolderOptions}
|
||||
>
|
||||
<div data-tut="reactourSecond">
|
||||
@@ -749,7 +754,7 @@ function Opensigndrive() {
|
||||
>
|
||||
<div
|
||||
data-tut="reactourThird"
|
||||
className="cursor-pointer flex flex-row items-center justify-center p-2 hover:bg-gray-200 rounded-md"
|
||||
className="cursor-pointer flex flex-row items-center justify-center p-2 hover:bg-base-300 rounded-md"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<i
|
||||
@@ -767,9 +772,7 @@ function Opensigndrive() {
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
isShowSort ? "dropdown-menu show" : "dropdown-menu"
|
||||
}
|
||||
className={`${isShowSort ? "dropdown-menu show" : "dropdown-menu"} bg-base-100 border-[1px] border-gray-300`}
|
||||
aria-labelledby="dropdownMenuButton"
|
||||
aria-expanded={isShowSort ? "true" : "false"}
|
||||
>
|
||||
@@ -817,7 +820,7 @@ function Opensigndrive() {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer p-2 hover:bg-gray-200 rounded-md flex justify-center items-center"
|
||||
className="cursor-pointer p-2 hover:bg-base-300 rounded-md flex justify-center items-center"
|
||||
data-tut="reactourForth"
|
||||
onClick={handleViewOption}
|
||||
>
|
||||
@@ -833,7 +836,7 @@ function Opensigndrive() {
|
||||
onClick={() => setIsOptions(!isOptions)}
|
||||
>
|
||||
<div
|
||||
className="p-3 flex items-center justify-center cursor-pointer rounded-md hover:bg-gray-200"
|
||||
className="p-3 flex items-center justify-center cursor-pointer rounded-md hover:bg-base-300"
|
||||
data-tut="reactourSecond"
|
||||
>
|
||||
<i
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
setDefaultSignImg,
|
||||
resetWidgetState,
|
||||
setPrefillImg,
|
||||
setTypedSignFont
|
||||
setTypedSignFont,
|
||||
setMyStamp
|
||||
} from "../redux/reducers/widgetSlice";
|
||||
import {
|
||||
contractDocument,
|
||||
@@ -70,11 +71,13 @@ import PlaceholderCopy from "../components/pdf/PlaceholderCopy";
|
||||
import TextFontSetting from "../components/pdf/TextFontSetting";
|
||||
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
|
||||
import * as utils from "../utils";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
|
||||
function PdfRequestFiles(
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const windowSize = useWindowSize();
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const isShowModal = useSelector((state) => state.widget.isShowModal);
|
||||
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
|
||||
@@ -155,12 +158,10 @@ function PdfRequestFiles(
|
||||
const [assignedWidgetId, setAssignedWidgetId] = useState([]);
|
||||
const [showSignPagenumber, setShowSignPagenumber] = useState([]);
|
||||
const [owner, setOwner] = useState({});
|
||||
const [tenant, setTenant] = useState({});
|
||||
const [tenantMailTemplate, setTenantMailTemplate] = useState({
|
||||
body: "",
|
||||
subject: ""
|
||||
});
|
||||
const [isOptionalDetails, setIsOptionalDetails] = useState(false);
|
||||
const isMobile = window.innerWidth < 767;
|
||||
let isGuestSignFlow = false;
|
||||
let sendmail;
|
||||
@@ -209,10 +210,10 @@ function PdfRequestFiles(
|
||||
};
|
||||
|
||||
// Use setTimeout to wait for the transition to complete
|
||||
const timer = setTimeout(updateSize, 100); // match the transition duration
|
||||
const timer = setTimeout(updateSize, 150); // match the transition duration
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [divRef.current, isSidebar]);
|
||||
}, [divRef.current, isSidebar, windowSize?.width]);
|
||||
const redirectUrl = pdfDetails?.[0]?.RedirectUrl || "";
|
||||
useEffect(() => {
|
||||
if (isredirectCanceled) return; // Stop the redirect timer if canceled
|
||||
@@ -245,7 +246,6 @@ function PdfRequestFiles(
|
||||
const filterSignTypes = signatureType?.filter(
|
||||
(x) => x.enabled === true
|
||||
);
|
||||
setTenant(tenantDetails || {});
|
||||
if (tenantDetails?.RequestBody) {
|
||||
setTenantMailTemplate({
|
||||
body: tenantDetails?.RequestBody,
|
||||
@@ -532,6 +532,7 @@ function PdfRequestFiles(
|
||||
const initials = defaultSignRes?.res?.defaultInitial || "";
|
||||
dispatch(setDefaultSignImg(sign));
|
||||
dispatch(setMyInitial(initials));
|
||||
dispatch(setMyStamp(defaultSignRes?.res?.defaultStamp));
|
||||
} else {
|
||||
dispatch(setSaveSignCheckbox({ isVisible: true }));
|
||||
}
|
||||
@@ -1103,7 +1104,7 @@ function PdfRequestFiles(
|
||||
? t("signature-validate-alert", { minRequiredCount })
|
||||
: t("signature-validate-alert-2"),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
//function for get pdf page details
|
||||
@@ -1260,7 +1261,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourFirst"]',
|
||||
@@ -1272,7 +1273,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="pdfArea"]',
|
||||
@@ -1284,7 +1285,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="pdftools"]',
|
||||
@@ -1296,7 +1297,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourFifth"]',
|
||||
@@ -1308,7 +1309,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
const signedByStep = {
|
||||
@@ -1321,7 +1322,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
};
|
||||
//checking if signed by user component exist then add signed by tour step
|
||||
const signedBy =
|
||||
@@ -1340,7 +1341,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
};
|
||||
//checking if AllowModifications is true then add allow widgets panel tour step
|
||||
const allModifyWidgets = {
|
||||
@@ -1353,7 +1354,7 @@ function PdfRequestFiles(
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
};
|
||||
|
||||
//handle signed by panel index if signed by exist then 2 else 1 to add tour step
|
||||
@@ -1395,10 +1396,10 @@ function PdfRequestFiles(
|
||||
};
|
||||
|
||||
const clickOnZoomIn = () => {
|
||||
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
|
||||
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
|
||||
};
|
||||
const clickOnZoomOut = () => {
|
||||
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
|
||||
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
|
||||
};
|
||||
const handleDownloadBtn = async () => {
|
||||
const url = pdfDetails?.[0]?.SignedUrl || pdfDetails?.[0]?.URL;
|
||||
@@ -1823,6 +1824,7 @@ function PdfRequestFiles(
|
||||
!alreadySign &&
|
||||
requestSignTourFunction()}
|
||||
<Tour
|
||||
key={`tour-min-required-${minRequiredCount || 0}`}
|
||||
showNumber={false}
|
||||
showNavigation={false}
|
||||
showNavigationNumber={false}
|
||||
@@ -2224,6 +2226,7 @@ function PdfRequestFiles(
|
||||
setUniqueId={setUniqueId}
|
||||
signatureTypes={signatureType}
|
||||
allowCellResize={false}
|
||||
penColors={pdfDetails?.[0]?.PenColors}
|
||||
/>
|
||||
)}
|
||||
<DownloadPdfZip
|
||||
|
||||
@@ -11,7 +11,6 @@ import SignerListPlace from "../components/pdf/SignerListPlace";
|
||||
import Header from "../components/pdf/PdfHeader";
|
||||
import ShareButton from "../primitives/ShareButton";
|
||||
import {
|
||||
replaceMailVaribles,
|
||||
pdfNewWidthFun,
|
||||
contractDocument,
|
||||
contractUsers,
|
||||
@@ -39,7 +38,6 @@ import {
|
||||
handleSignatureType,
|
||||
getBase64FromUrl,
|
||||
generatePdfName,
|
||||
mailTemplate,
|
||||
getOriginalWH,
|
||||
defaultMailBody,
|
||||
defaultMailSubject
|
||||
@@ -60,31 +58,27 @@ import TourContentWithBtn from "../primitives/TourContentWithBtn";
|
||||
import HandleError from "../primitives/HandleError";
|
||||
import LoaderWithMsg from "../primitives/LoaderWithMsg";
|
||||
import LinkUserModal from "../primitives/LinkUserModal";
|
||||
import { EmailBody } from "../components/pdf/EmailBody";
|
||||
import LottieWithLoader from "../primitives/DotLottieReact";
|
||||
import Alert from "../primitives/Alert";
|
||||
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
|
||||
import * as utils from "../utils";
|
||||
import { resetWidgetState, setPrefillImg } from "../redux/reducers/widgetSlice";
|
||||
import EditDocument from "../components/pdf/EditTemplate";
|
||||
import CustomizeMail from "../components/pdf/CustomizeMail";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
|
||||
function PlaceHolderSign() {
|
||||
const { t } = useTranslation();
|
||||
const copyUrlRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
const windowSize = useWindowSize();
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const isShowModal = useSelector((state) => state.widget.isShowModal);
|
||||
const editorRef = useRef();
|
||||
const { state } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [defaultBody, setDefaultBody] = useState("");
|
||||
const [defaultSubject, setDefaultSubject] = useState("");
|
||||
const [requestSubject, setRequestSubject] = useState("");
|
||||
const [requestBody, setRequestBody] = useState("");
|
||||
const [tenantMailTemplate, setTenantMailTemplate] = useState({
|
||||
body: "",
|
||||
subject: ""
|
||||
});
|
||||
const [isMailModal, setIsMailModal] = useState(false);
|
||||
const [customizeMail, setCustomizeMail] = useState({ body: "", subject: "" });
|
||||
const [defaultMail, setDefaultMail] = useState({ body: "", subject: "" });
|
||||
const [pdfDetails, setPdfDetails] = useState([]);
|
||||
const [isMailSend, setIsMailSend] = useState(false);
|
||||
const [allPages, setAllPages] = useState(null);
|
||||
@@ -138,7 +132,7 @@ function PlaceHolderSign() {
|
||||
const [isCheckbox, setIsCheckbox] = useState(false);
|
||||
const [isNameModal, setIsNameModal] = useState(false);
|
||||
const [mailStatus, setMailStatus] = useState("");
|
||||
const [isCurrUser, setIsCurrUser] = useState(false);
|
||||
const [isCurrUser, setIsCurrUser] = useState("");
|
||||
const [pdfArrayBuffer, setPdfArrayBuffer] = useState("");
|
||||
const isSidebar = useSelector((state) => state.sidebar.isOpen);
|
||||
const [showRotateAlert, setShowRotateAlert] = useState({
|
||||
@@ -149,7 +143,6 @@ function PlaceHolderSign() {
|
||||
status: false,
|
||||
message: ""
|
||||
});
|
||||
const [isCustomize, setIsCustomize] = useState(false);
|
||||
const [zoomPercent, setZoomPercent] = useState(0);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [pdfBase64Url, setPdfBase64Url] = useState("");
|
||||
@@ -185,12 +178,6 @@ function PlaceHolderSign() {
|
||||
);
|
||||
if (user) {
|
||||
try {
|
||||
const defaultRequestBody = defaultMailBody;
|
||||
const defaultSubject = defaultMailSubject;
|
||||
setDefaultBody(defaultRequestBody);
|
||||
setDefaultSubject(defaultSubject);
|
||||
setRequestBody(defaultRequestBody);
|
||||
setRequestSubject(defaultSubject);
|
||||
const tenantDetails = await getTenantDetails(user?.objectId);
|
||||
if (tenantDetails && tenantDetails === "user does not exist!") {
|
||||
alert(t("user-not-exist"));
|
||||
@@ -199,14 +186,22 @@ function PlaceHolderSign() {
|
||||
const filterSignTypes = signatureType?.filter(
|
||||
(x) => x.enabled === true
|
||||
);
|
||||
if (tenantDetails?.RequestBody) {
|
||||
setRequestBody(tenantDetails?.RequestBody);
|
||||
setRequestSubject(tenantDetails?.RequestSubject);
|
||||
setTenantMailTemplate({
|
||||
body: tenantDetails?.RequestBody,
|
||||
subject: tenantDetails?.RequestSubject
|
||||
});
|
||||
}
|
||||
const extUser =
|
||||
localStorage.getItem("Extand_Class") &&
|
||||
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
const subject = tenantDetails?.RequestSubject ?? "";
|
||||
const body = tenantDetails?.RequestBody ?? "";
|
||||
//customize mail state is handle to when user want to customize already set tenant email format then use that format
|
||||
const userSubject =
|
||||
subject;
|
||||
const userBody =
|
||||
body;
|
||||
|
||||
setCustomizeMail({
|
||||
subject: userSubject ?? defaultMailSubject,
|
||||
body: userBody ?? defaultMailBody
|
||||
});
|
||||
setDefaultMail({ subject: userSubject, body: userBody });
|
||||
return filterSignTypes;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -231,10 +226,10 @@ function PlaceHolderSign() {
|
||||
}
|
||||
};
|
||||
// Use setTimeout to wait for the transition to complete
|
||||
const timer = setTimeout(updateSize, 100); // match the transition duration
|
||||
const timer = setTimeout(updateSize, 150); // match the transition duration
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [divRef.current, isSidebar]);
|
||||
}, [divRef.current, isSidebar, windowSize?.width]);
|
||||
//function for get document details
|
||||
const getDocumentDetails = async () => {
|
||||
const tenantSignTypes = await fetchTenantDetails();
|
||||
@@ -320,9 +315,9 @@ function PlaceHolderSign() {
|
||||
updatedPdfDetails[0].SignatureType = updatedSignatureType;
|
||||
setPdfDetails(updatedPdfDetails);
|
||||
if (documentData?.[0]?.RequestBody && documentData?.[0]?.RequestSubject) {
|
||||
setTenantMailTemplate({
|
||||
body: documentData?.[0]?.RequestBody,
|
||||
subject: documentData?.[0]?.RequestSubject
|
||||
setDefaultMail({
|
||||
subject: documentData?.[0]?.RequestSubject,
|
||||
body: documentData?.[0]?.RequestBody
|
||||
});
|
||||
}
|
||||
if (
|
||||
@@ -873,7 +868,6 @@ function PlaceHolderSign() {
|
||||
});
|
||||
const isSignYourSelfFlow = false;
|
||||
try {
|
||||
const docId = pdfDetails[0]?.objectId;
|
||||
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
|
||||
const pdfBase64 = await embedWidgetsToDoc(
|
||||
placeholder,
|
||||
@@ -925,12 +919,16 @@ function PlaceHolderSign() {
|
||||
//if empty widget exist and user try to send document then throw tour message
|
||||
for (const placeholder of getPlaceholder || []) {
|
||||
const requiredWidgets = placeholder.pos.filter(
|
||||
(position) => position.type !== "checkbox"
|
||||
(position) => position?.options?.status === "required"
|
||||
);
|
||||
for (const widget of requiredWidgets) {
|
||||
const hasResponse = widget?.options?.response;
|
||||
const hasDefault = widget?.options?.defaultValue;
|
||||
if (!hasResponse && !hasDefault && !isPrefillEmpty) {
|
||||
const response =
|
||||
widget?.options?.response || widget?.options?.defaultValue;
|
||||
const isEmptyResponse =
|
||||
!response ||
|
||||
(typeof response === "string" && response.trim() === "") ||
|
||||
(Array.isArray(response) && response.length === 0);
|
||||
if (isEmptyResponse && !isPrefillEmpty) {
|
||||
isPrefillEmpty = true;
|
||||
unfilledTextWidgetId = widget.key;
|
||||
setPageNumber(placeholder.pageNumber);
|
||||
@@ -1035,7 +1033,7 @@ function PlaceHolderSign() {
|
||||
}
|
||||
};
|
||||
//function to use save placeholder details in contracts_document
|
||||
const saveDocumentDetails = async () => {
|
||||
const saveDocumentDetails = utils.withSessionValidation(async () => {
|
||||
setIsUiLoading(true);
|
||||
let signerMail = signersdata.slice();
|
||||
// For "Send in order", only consider the first signer
|
||||
@@ -1059,16 +1057,6 @@ function PlaceHolderSign() {
|
||||
: 15;
|
||||
const currentUser = signersdata.find((x) => x.Email === currentId);
|
||||
setCurrentId(currentUser?.objectId);
|
||||
if (
|
||||
pdfDetails?.[0]?.SendinOrder &&
|
||||
pdfDetails?.[0]?.SendinOrder === true
|
||||
) {
|
||||
const currentUserMail = Parse.User.current()?.getEmail();
|
||||
const isCurrentUser = signerMail?.[0]?.Email === currentUserMail;
|
||||
setIsCurrUser(isCurrentUser);
|
||||
} else {
|
||||
setIsCurrUser(currentUser?.objectId ? true : false);
|
||||
}
|
||||
// Compute expiry date with extra days
|
||||
let updateExpiryDate = new Date();
|
||||
updateExpiryDate.setDate(updateExpiryDate.getDate() + addExtraDays);
|
||||
@@ -1094,7 +1082,6 @@ function PlaceHolderSign() {
|
||||
}
|
||||
}
|
||||
);
|
||||
setIsMailSend(true);
|
||||
setIsLoading({ isLoad: false });
|
||||
setIsUiLoading(false);
|
||||
setIsSendAlert({ mssg: "confirm", alert: true });
|
||||
@@ -1103,6 +1090,21 @@ function PlaceHolderSign() {
|
||||
updatedPdfDetails[0].Name = docTitle;
|
||||
setPdfDetails(updatedPdfDetails);
|
||||
}
|
||||
const ownerId = pdfDetails[0].ExtUserPtr?.UserId?.objectId;
|
||||
const firstSigner = signersdata[0];
|
||||
const isOwner = firstSigner?.UserId?.objectId === ownerId;
|
||||
if (pdfDetails[0]?.SendinOrder && isOwner) {
|
||||
setIsSend(true);
|
||||
setIsCurrUser(currentUser?.objectId);
|
||||
} else {
|
||||
const currentSigner = signersdata?.find(
|
||||
(x) => x?.UserId?.objectId === ownerId
|
||||
);
|
||||
if (currentSigner) {
|
||||
setIsCurrUser(currentSigner?.objectId);
|
||||
}
|
||||
setIsMailModal(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
@@ -1110,7 +1112,7 @@ function PlaceHolderSign() {
|
||||
} else {
|
||||
setIsUiLoading(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const copytoclipboard = (text) => {
|
||||
copytoData(text);
|
||||
@@ -1152,215 +1154,24 @@ function PlaceHolderSign() {
|
||||
className="flex flex-row items-center op-link op-link-primary"
|
||||
>
|
||||
<i className="fa-light fa-copy" />
|
||||
<span className=" hidden md:block ml-1 ">{t("copy-link")}</span>
|
||||
<span className="hidden md:block ml-1 ">{t("copy-link")}</span>
|
||||
</button>
|
||||
<ShareButton
|
||||
title={t("sign-url")}
|
||||
text={t("sign-url")}
|
||||
url={data.url}
|
||||
>
|
||||
<i className="fa-light fa-share-from-square op-link data-[theme=opensigndark]:op-link-primary data-[theme=opensigncss]:op-link-secondary no-underline"></i>
|
||||
<i className="fa-light fa-share-from-square op-link opensigncss:op-link-secondary opensigndark:op-link-primary no-underline"></i>
|
||||
</ShareButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
const sendEmailToSigners = async () => {
|
||||
let htmlReqBody;
|
||||
setIsUiLoading(true);
|
||||
setIsSendAlert({});
|
||||
let sendMail;
|
||||
const expireDate = pdfDetails?.[0].ExpiryDate.iso;
|
||||
const newDate = new Date(expireDate);
|
||||
const localExpireDate = newDate.toLocaleDateString("en-US", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric"
|
||||
});
|
||||
|
||||
let senderEmail =
|
||||
pdfDetails?.[0]?.ExtUserPtr?.Email;
|
||||
let senderPhone = pdfDetails?.[0]?.ExtUserPtr?.Phone;
|
||||
let signerMail = signersdata.slice();
|
||||
if (pdfDetails?.[0]?.SendinOrder && pdfDetails?.[0]?.SendinOrder === true) {
|
||||
signerMail.splice(1);
|
||||
}
|
||||
|
||||
for (let i = 0; i < signerMail.length; i++) {
|
||||
try {
|
||||
let url = `${localStorage.getItem("baseUrl")}functions/sendmailv3`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessionToken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
const objectId = signerMail[i].objectId;
|
||||
const hostUrl = window.location.origin;
|
||||
//encode this url value `${pdfDetails?.[0].objectId}/${signerMail[i].Email}/${objectId}` to base64 using `btoa` function
|
||||
const encodeBase64 = btoa(
|
||||
`${pdfDetails?.[0].objectId}/${signerMail[i].Email}/${objectId}`
|
||||
);
|
||||
let signPdf = `${hostUrl}/login/${encodeBase64}`;
|
||||
const orgName = pdfDetails[0]?.ExtUserPtr.Company
|
||||
? pdfDetails[0].ExtUserPtr.Company
|
||||
: "";
|
||||
const senderName =
|
||||
pdfDetails?.[0].ExtUserPtr.Name;
|
||||
const documentName = `${pdfDetails?.[0].Name}`;
|
||||
let replaceVar;
|
||||
|
||||
if (
|
||||
requestBody &&
|
||||
requestSubject &&
|
||||
isCustomize
|
||||
) {
|
||||
const replacedRequestBody = requestBody.replace(/"/g, "'");
|
||||
htmlReqBody =
|
||||
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" +
|
||||
replacedRequestBody +
|
||||
"</body> </html>";
|
||||
|
||||
const variables = {
|
||||
document_title: documentName,
|
||||
note: pdfDetails?.[0]?.Note,
|
||||
sender_name: senderName,
|
||||
sender_mail: senderEmail,
|
||||
sender_phone: senderPhone || "",
|
||||
receiver_name: signerMail[i]?.Name || "",
|
||||
receiver_email: signerMail[i].Email,
|
||||
receiver_phone: signerMail[i]?.Phone || "",
|
||||
expiry_date: localExpireDate,
|
||||
company_name: orgName,
|
||||
signing_url: signPdf
|
||||
};
|
||||
replaceVar = replaceMailVaribles(
|
||||
requestSubject,
|
||||
htmlReqBody,
|
||||
variables
|
||||
);
|
||||
} else if (
|
||||
tenantMailTemplate?.body &&
|
||||
tenantMailTemplate?.subject
|
||||
) {
|
||||
const mailBody = tenantMailTemplate?.body;
|
||||
const mailSubject = tenantMailTemplate?.subject;
|
||||
const replacedRequestBody = mailBody.replace(/"/g, "'");
|
||||
const htmlReqBody =
|
||||
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" +
|
||||
replacedRequestBody +
|
||||
"</body> </html>";
|
||||
const variables = {
|
||||
document_title: documentName,
|
||||
note: pdfDetails?.[0]?.Note,
|
||||
sender_name: senderName,
|
||||
sender_mail: senderEmail,
|
||||
sender_phone: senderPhone || "",
|
||||
receiver_name: signerMail[i]?.Name || "",
|
||||
receiver_email: signerMail[i].Email,
|
||||
receiver_phone: signerMail[i]?.Phone || "",
|
||||
expiry_date: localExpireDate,
|
||||
company_name: orgName,
|
||||
signing_url: signPdf
|
||||
};
|
||||
replaceVar = replaceMailVaribles(mailSubject, htmlReqBody, variables);
|
||||
}
|
||||
const mailparam = {
|
||||
senderName: senderName,
|
||||
note: pdfDetails?.[0]?.Note || "",
|
||||
senderMail: senderEmail,
|
||||
title: documentName,
|
||||
organization: orgName,
|
||||
localExpireDate: localExpireDate,
|
||||
signingUrl: signPdf
|
||||
};
|
||||
let params = {
|
||||
extUserId: owner?.objectId,
|
||||
recipient: signerMail[i].Email,
|
||||
subject: replaceVar?.subject
|
||||
? replaceVar?.subject
|
||||
: mailTemplate(mailparam).subject,
|
||||
replyto: senderEmail,
|
||||
from:
|
||||
senderEmail,
|
||||
html: replaceVar?.body
|
||||
? replaceVar?.body
|
||||
: mailTemplate(mailparam).body
|
||||
};
|
||||
|
||||
sendMail = await axios.post(url, params, { headers: headers });
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
}
|
||||
if (sendMail?.data?.result?.status === "success") {
|
||||
setMailStatus("success");
|
||||
try {
|
||||
let data;
|
||||
if (
|
||||
requestBody &&
|
||||
requestSubject &&
|
||||
isCustomize
|
||||
) {
|
||||
data = {
|
||||
RequestBody: htmlReqBody,
|
||||
RequestSubject: requestSubject,
|
||||
SendMail: true
|
||||
};
|
||||
} else if (
|
||||
tenantMailTemplate?.body &&
|
||||
tenantMailTemplate?.subject
|
||||
) {
|
||||
data = {
|
||||
RequestBody: tenantMailTemplate?.body,
|
||||
RequestSubject: tenantMailTemplate?.subject,
|
||||
SendMail: true
|
||||
};
|
||||
} else {
|
||||
data = { SendMail: true };
|
||||
}
|
||||
try {
|
||||
await axios.put(
|
||||
`${localStorage.getItem(
|
||||
"baseUrl"
|
||||
)}classes/contracts_Document/${documentId}`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("axois err ", err);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
}
|
||||
setIsSend(true);
|
||||
setIsMailSend(true);
|
||||
setIsLoading({ isLoad: false });
|
||||
setIsUiLoading(false);
|
||||
} else if (sendMail?.data?.result?.status === "quota-reached") {
|
||||
setMailStatus("quotareached");
|
||||
setIsSend(true);
|
||||
setIsMailSend(true);
|
||||
setIsUiLoading(false);
|
||||
}
|
||||
else {
|
||||
setMailStatus("failed");
|
||||
setIsSend(true);
|
||||
setIsMailSend(true);
|
||||
setIsUiLoading(false);
|
||||
}
|
||||
};
|
||||
const handleDontShow = (isChecked) => {
|
||||
setIsDontShow(isChecked);
|
||||
};
|
||||
//here you can add your messages in content and selector is key of particular steps
|
||||
|
||||
const tourConfig = [
|
||||
{
|
||||
selector: '[data-tut="prefillTour"]',
|
||||
@@ -1372,7 +1183,7 @@ function PlaceHolderSign() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="recipientArea"]',
|
||||
@@ -1384,7 +1195,7 @@ function PlaceHolderSign() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="addRecipient"]',
|
||||
@@ -1396,7 +1207,7 @@ function PlaceHolderSign() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="addWidgets"]',
|
||||
@@ -1408,7 +1219,7 @@ function PlaceHolderSign() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="pdfArea"]',
|
||||
@@ -1420,7 +1231,7 @@ function PlaceHolderSign() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="pdftools"]',
|
||||
@@ -1432,7 +1243,7 @@ function PlaceHolderSign() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="headerArea"]',
|
||||
@@ -1444,7 +1255,7 @@ function PlaceHolderSign() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1461,6 +1272,9 @@ function PlaceHolderSign() {
|
||||
isHideLabel,
|
||||
layout
|
||||
) => {
|
||||
const isPrefill = signerPos.some(
|
||||
(x) => x?.Role === "prefill" && x.Id === uniqueId
|
||||
);
|
||||
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
|
||||
if (filterSignerPos.length > 0) {
|
||||
const getPlaceHolder = filterSignerPos[0].placeHolder;
|
||||
@@ -1526,16 +1340,21 @@ function PlaceHolderSign() {
|
||||
...position.options,
|
||||
name: dropdownName,
|
||||
values: dropdownOptions,
|
||||
validation: {
|
||||
minRequiredCount: minCount,
|
||||
maxRequiredCount: maxCount
|
||||
},
|
||||
...(!isPrefill
|
||||
? {
|
||||
validation: {
|
||||
minRequiredCount: minCount,
|
||||
maxRequiredCount: maxCount
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
defaultValue: defaultValue,
|
||||
layout: layout,
|
||||
isReadOnly: isReadOnly || false,
|
||||
isHideLabel: isHideLabel || false,
|
||||
fontSize: textSize || 12,
|
||||
fontColor: textColor || "black"
|
||||
fontColor: textColor || "black",
|
||||
...(isPrefill ? { status } : {})
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1641,13 +1460,16 @@ function PlaceHolderSign() {
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (["signature"].includes(position.type)) {
|
||||
else if (["signature", "initials"].includes(position.type)) {
|
||||
return {
|
||||
...position,
|
||||
options: {
|
||||
...position.options,
|
||||
name: defaultdata.name,
|
||||
hint: defaultdata?.hint || ""
|
||||
hint: defaultdata?.hint || "",
|
||||
...(defaultdata?.penColors?.length > 0 && {
|
||||
penColors: defaultdata?.penColors
|
||||
})
|
||||
}
|
||||
};
|
||||
} else {
|
||||
@@ -1844,7 +1666,7 @@ function PlaceHolderSign() {
|
||||
selector: '[data-tut="assignSigner"]',
|
||||
content: t("attach-signer-tour"),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
const prefillWidgetTour = [
|
||||
@@ -1852,7 +1674,7 @@ function PlaceHolderSign() {
|
||||
selector: '[data-tut="IsSigned"]',
|
||||
content: t("text-field-tour"),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
const signatureWidgetTour = [
|
||||
@@ -1860,7 +1682,7 @@ function PlaceHolderSign() {
|
||||
selector: '[data-tut="isSignatureWidget"]',
|
||||
content: t("signature-field-widget", { signersName }),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1901,10 +1723,10 @@ function PlaceHolderSign() {
|
||||
}
|
||||
};
|
||||
const clickOnZoomIn = () => {
|
||||
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
|
||||
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
|
||||
};
|
||||
const clickOnZoomOut = () => {
|
||||
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
|
||||
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
|
||||
};
|
||||
//`handleRotationFun` function is used to roatate pdf particular page
|
||||
const handleRotationFun = async (rotateDegree) => {
|
||||
@@ -1963,6 +1785,10 @@ function PlaceHolderSign() {
|
||||
const RedirectUrl = updateDocument?.[0]?.RedirectUrl
|
||||
? { RedirectUrl: updateDocument?.[0]?.RedirectUrl }
|
||||
: {};
|
||||
const penColors =
|
||||
updateDocument?.[0]?.PenColors?.length > 0
|
||||
? { PenColors: updateDocument?.[0]?.PenColors }
|
||||
: {};
|
||||
const data = {
|
||||
...(updateDocument?.[0]?.URL ? { URL: updateDocument?.[0]?.URL } : {}),
|
||||
Name: updateDocument?.[0]?.Name || "",
|
||||
@@ -1979,6 +1805,7 @@ function PlaceHolderSign() {
|
||||
: false,
|
||||
TimeToCompleteDays:
|
||||
parseInt(updateDocument?.[0]?.TimeToCompleteDays) || 15,
|
||||
...penColors,
|
||||
...Bcc,
|
||||
...RedirectUrl
|
||||
};
|
||||
@@ -2017,8 +1844,6 @@ function PlaceHolderSign() {
|
||||
onRequestClose={closeTour}
|
||||
steps={tourConfig}
|
||||
isOpen={placeholderTour}
|
||||
rounded={5}
|
||||
closeWithMask={false}
|
||||
/>
|
||||
<Tour
|
||||
onRequestClose={() => setSignerExistModal(false)}
|
||||
@@ -2071,90 +1896,17 @@ function PlaceHolderSign() {
|
||||
pdfDetails={pdfDetails}
|
||||
/>
|
||||
<div className=" w-full md:w-[95%] ">
|
||||
{/* this modal is used show alert set placeholder for all signers before send mail */}
|
||||
<ModalUi
|
||||
isOpen={isSendAlert.alert && isSendAlert.mssg === "confirm"}
|
||||
title={isSendAlert.mssg === "confirm" && t("send-mail")}
|
||||
handleClose={() => handleCloseSendmailModal()}
|
||||
>
|
||||
<div className="max-h-96 overflow-y-scroll scroll-hide p-[20px] text-base-content">
|
||||
{isSendAlert.mssg === "confirm" && (
|
||||
<>
|
||||
{!isCustomize && <span>{t("placeholder-alert-3")}</span>}
|
||||
{
|
||||
isCustomize && (
|
||||
<>
|
||||
<EmailBody
|
||||
editorRef={editorRef}
|
||||
requestBody={requestBody}
|
||||
requestSubject={requestSubject}
|
||||
handleOnchangeRequest={handleOnchangeRequest}
|
||||
setRequestSubject={setRequestSubject}
|
||||
/>
|
||||
<div
|
||||
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
|
||||
onClick={() => {
|
||||
setRequestBody(defaultBody);
|
||||
setRequestSubject(defaultSubject);
|
||||
}}
|
||||
>
|
||||
<span>{t("reset-to-default")}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className="flex flex-row items-center gap-2 md:gap-6 mt-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<button
|
||||
onClick={() => sendEmailToSigners()}
|
||||
className="op-btn op-btn-primary font-[500] text-sm shadow"
|
||||
>
|
||||
{t("send")}
|
||||
</button>
|
||||
{isCustomize && (
|
||||
<button
|
||||
onClick={() => setIsCustomize(false)}
|
||||
className="op-btn op-btn-ghost font-[500] text-sm"
|
||||
>
|
||||
{t("close")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
!isCustomize && (
|
||||
<span
|
||||
className="op-link op-link-accent text-sm"
|
||||
onClick={() => setIsCustomize(!isCustomize)}
|
||||
>
|
||||
{t("cutomize-email")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isSendAlert.mssg === "confirm" && (
|
||||
<>
|
||||
<div className="flex justify-center items-center mt-3">
|
||||
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
|
||||
<span className="ml-[5px] mr-[5px]">{t("or")}</span>
|
||||
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
|
||||
</div>
|
||||
<div className="my-3">{handleShareList()}</div>
|
||||
<p id="copyUrl" ref={copyUrlRef} className="hidden"></p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalUi>
|
||||
{/* this modal is used show send mail message and after send mail success message */}
|
||||
<ModalUi
|
||||
isOpen={isSend}
|
||||
title={
|
||||
mailStatus === "success"
|
||||
? t("mails-sent")
|
||||
: mailStatus === "quotareached"
|
||||
? t("quota-mail-head")
|
||||
: t("mail-not-delivered")
|
||||
!pdfDetails[0]?.SendinOrder
|
||||
? mailStatus === "success"
|
||||
? t("mails-sent")
|
||||
: mailStatus === "quotareached"
|
||||
? t("quota-mail-head")
|
||||
: t("mail-not-delivered")
|
||||
: t("mail-status-head")
|
||||
}
|
||||
handleClose={() => {
|
||||
setIsSend(false);
|
||||
@@ -2163,70 +1915,104 @@ function PlaceHolderSign() {
|
||||
}}
|
||||
>
|
||||
<div className="h-[100%] p-[20px] text-base-content">
|
||||
{mailStatus === "success" ? (
|
||||
<div className="text-center mb-[10px]">
|
||||
<LottieWithLoader />
|
||||
{pdfDetails[0].SendinOrder ? (
|
||||
<p>
|
||||
{isCurrUser
|
||||
? t("placeholder-mail-alert-you")
|
||||
: t("placeholder-mail-alert", {
|
||||
name: signersdata[0]?.Name
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-5">
|
||||
<div>
|
||||
{mailStatus === "success" ? (
|
||||
<div className="text-center mb-[10px]">
|
||||
<LottieWithLoader />
|
||||
{pdfDetails[0].SendinOrder ? (
|
||||
<p>
|
||||
{isCurrUser
|
||||
? t("placeholder-mail-alert-you")
|
||||
: t("placeholder-mail-alert", {
|
||||
name: signersdata[0]?.Name
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-4")}</p>
|
||||
)}
|
||||
</div>
|
||||
) : mailStatus === "quotareached" ? (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="my-3">{handleShareList()}</div>
|
||||
</div>
|
||||
) : mailStatus === "failed" ? (
|
||||
<p>{t("mail-failed")} </p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-4")}</p>
|
||||
<div className="mb-[10px]">
|
||||
{!pdfDetails[0]?.SendinOrder &&
|
||||
(mailStatus === "dailyquotareached" ? (
|
||||
<p>{t("daily-quota-reached")}</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-6")}</p>
|
||||
))}
|
||||
{isCurrUser && (
|
||||
<span className="mt-1">
|
||||
{t("placeholder-alert-5")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isCurrUser && <p>{t("placeholder-alert-5")}</p>}
|
||||
|
||||
{mailStatus !== "quotareached" &&
|
||||
mailStatus !== "failed" && (
|
||||
<div
|
||||
className={
|
||||
mailStatus === "success"
|
||||
? "flex justify-center mt-1"
|
||||
: "flex items-center justify-center mt-7"
|
||||
}
|
||||
>
|
||||
{isCurrUser && (
|
||||
<button
|
||||
onClick={() => handleRecipientSign()}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary mr-1"
|
||||
>
|
||||
{t("sign-now")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsSend(false);
|
||||
setSignerPos([]);
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
}}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
>
|
||||
{isCurrUser ? t("no") : t("close")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : mailStatus === "quotareached" ? (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="my-3">{handleShareList()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-[10px]">
|
||||
{mailStatus === "dailyquotareached" ? (
|
||||
<p>{t("daily-quota-reached")}</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-6")}</p>
|
||||
{mailStatus !== "success" &&
|
||||
isCurrUser &&
|
||||
pdfDetails[0]?.SendinOrder && (
|
||||
<div className="op-divider text-base-content mx-[0%] my-1 font-medium">
|
||||
{t("or")}
|
||||
</div>
|
||||
)}
|
||||
{isCurrUser && (
|
||||
<p className="mt-1">{t("placeholder-alert-5")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!mailStatus && (
|
||||
<div className="w-full h-[1px] bg-[#9f9f9f] my-[15px]"></div>
|
||||
)}
|
||||
{mailStatus !== "quotareached" && (
|
||||
<div
|
||||
className={
|
||||
mailStatus === "success"
|
||||
? "flex justify-center mt-1"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{isCurrUser && (
|
||||
<button
|
||||
onClick={() => handleRecipientSign()}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary mr-1"
|
||||
{mailStatus !== "success" &&
|
||||
pdfDetails[0]?.SendinOrder &&
|
||||
isCurrUser && (
|
||||
<div
|
||||
className="op-btn op-btn-outline w-[50%] md:w-[35%] mt-1"
|
||||
onClick={() => {
|
||||
setIsSend(false);
|
||||
setIsMailModal(true);
|
||||
}}
|
||||
>
|
||||
{t("yes")}
|
||||
</button>
|
||||
<i
|
||||
className="fa-regular fa-envelope"
|
||||
style={{ color: "#002864", fontSize: "19px" }}
|
||||
></i>{" "}
|
||||
<span>{t("send-to-email")}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsSend(false);
|
||||
setSignerPos([]);
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
}}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
>
|
||||
{isCurrUser ? t("no") : t("close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!mailStatus && (
|
||||
<div className="op-divider text-base-content mx-[0%] mt-3"></div>
|
||||
)}
|
||||
</div>
|
||||
</ModalUi>
|
||||
@@ -2473,6 +2259,19 @@ function PlaceHolderSign() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CustomizeMail
|
||||
setIsMailModal={setIsMailModal}
|
||||
setCustomizeMail={setCustomizeMail}
|
||||
documentId={documentId}
|
||||
signerList={signersdata}
|
||||
setIsSend={setIsSend}
|
||||
setMailStatus={setMailStatus}
|
||||
customizeMail={customizeMail}
|
||||
defaultMail={defaultMail}
|
||||
isMailModal={isMailModal}
|
||||
handleShareList={handleShareList}
|
||||
setCurrUserId={setIsCurrUser}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isShowModal[currWidgetsDetails?.key] && (
|
||||
@@ -2489,6 +2288,8 @@ function PlaceHolderSign() {
|
||||
isSave={true}
|
||||
setUniqueId={setUniqueId}
|
||||
signatureTypes={signatureType}
|
||||
penColors={pdfDetails?.[0]?.PenColors}
|
||||
role={roleName}
|
||||
/>
|
||||
)}
|
||||
<ModalUi
|
||||
|
||||
@@ -6,7 +6,6 @@ import Tooltip from "../primitives/Tooltip";
|
||||
import {
|
||||
getTenantDetails,
|
||||
handleSignatureType,
|
||||
openInNewTab,
|
||||
signatureTypes,
|
||||
usertimezone
|
||||
} from "../constant/Utils";
|
||||
@@ -16,17 +15,25 @@ import TimezoneSelector from "../components/preferences/TimezoneSelector";
|
||||
import DateFormatSelector from "../components/preferences/DateFormatSelector";
|
||||
import FilenameFormatSelector from "../components/preferences/FilenameFormatSelector";
|
||||
import axios from "axios";
|
||||
import MailTemplateEditor from "../components/preferences/MailTemplateEditor";
|
||||
import { withSessionValidation } from "../utils";
|
||||
import { WidgetsTab, EmailTab } from "../components/preferences/tabs";
|
||||
import {
|
||||
setUserInfo,
|
||||
setTenantInfo,
|
||||
setLoader,
|
||||
setTopLoader,
|
||||
setAlertInfo
|
||||
} from "../redux/reducers/userReducer";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
const Preferences = () => {
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const { t } = useTranslation();
|
||||
const Extand_Class = localStorage.getItem("Extand_Class");
|
||||
const extClass = Extand_Class && JSON.parse(Extand_Class);
|
||||
const [isalert, setIsAlert] = useState({ type: "success", msg: "" });
|
||||
const [isTopLoader, setIsTopLoader] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const { isLoader, isTopLoader, alertInfo } = useSelector(
|
||||
(state) => state.user
|
||||
);
|
||||
const [signatureType, setSignatureType] = useState([]);
|
||||
const [errMsg, setErrMsg] = useState("");
|
||||
const [isNotifyOnSignatures, setIsNotifyOnSignatures] = useState();
|
||||
@@ -44,22 +51,28 @@ const Preferences = () => {
|
||||
const [is12HourTime, setIs12HourTime] = useState(false);
|
||||
const [isLTVEnabled, setIsLTVEnabled] = useState(false);
|
||||
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
|
||||
const [userInfo, setUserInfo] = useState({});
|
||||
const [tenantInfo, setTenantInfo] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSignType();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const fetchSignType = async () => {
|
||||
setIsTopLoader(true);
|
||||
const showAlert = (type, msg) => {
|
||||
dispatch(setAlertInfo({ type, msg }));
|
||||
setTimeout(() => {
|
||||
dispatch(setAlertInfo({ type: "success", msg: "" }));
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const fetchSignType = withSessionValidation(async () => {
|
||||
dispatch(setTopLoader(true));
|
||||
const EmailTab = [
|
||||
{ name: "email", title: t("email"), icon: "fa-light fa-envelope" }
|
||||
];
|
||||
|
||||
const arr = [
|
||||
generaltab,
|
||||
{ name: "widgets", title: t("widgets"), icon: "fa-light fa-list" },
|
||||
...EmailTab,
|
||||
];
|
||||
setTab(arr);
|
||||
@@ -70,7 +83,7 @@ const Preferences = () => {
|
||||
)
|
||||
);
|
||||
const tenantDetails = await getTenantDetails(user?.objectId);
|
||||
setTenantInfo(tenantDetails);
|
||||
dispatch(setTenantInfo(tenantDetails));
|
||||
const signatureType = tenantDetails?.SignatureType || [];
|
||||
const tenantSignTypes = signatureType?.filter((x) => x.enabled === true);
|
||||
const extUser = await axios.post(
|
||||
@@ -84,69 +97,74 @@ const Preferences = () => {
|
||||
}
|
||||
}
|
||||
);
|
||||
const getUser = extUser.data?.result;
|
||||
if (getUser) {
|
||||
const _getUser = JSON.parse(JSON.stringify(getUser));
|
||||
setUserInfo(_getUser);
|
||||
setIsNotifyOnSignatures(
|
||||
_getUser?.NotifyOnSignatures !== undefined
|
||||
? _getUser?.NotifyOnSignatures
|
||||
: true
|
||||
);
|
||||
setTimezone(_getUser?.Timezone || usertimezone);
|
||||
if (tenantSignTypes?.length > 0) {
|
||||
const signatureType = _getUser?.SignatureType || signatureTypes;
|
||||
const updatedSignatureType = await handleSignatureType(
|
||||
tenantSignTypes,
|
||||
signatureType
|
||||
);
|
||||
setSignatureType(updatedSignatureType);
|
||||
} else {
|
||||
const SignatureType = _getUser?.SignatureType || signatureTypes;
|
||||
setSignatureType(SignatureType);
|
||||
}
|
||||
setSendinOrder(
|
||||
_getUser?.SendinOrder !== undefined ? _getUser?.SendinOrder : true
|
||||
);
|
||||
setIsTourEnabled(
|
||||
_getUser?.IsTourEnabled !== undefined ? _getUser?.IsTourEnabled : true
|
||||
);
|
||||
setDateFormat(
|
||||
_getUser?.DateFormat !== undefined
|
||||
? _getUser?.DateFormat
|
||||
: "MM/DD/YYYY"
|
||||
);
|
||||
setIs12HourTime(
|
||||
_getUser?.Is12HourTime !== undefined ? _getUser?.Is12HourTime : false
|
||||
);
|
||||
setIsLTVEnabled(
|
||||
_getUser?.IsLTVEnabled !== undefined ? _getUser?.IsLTVEnabled : false
|
||||
);
|
||||
const downloadFilenameFormat =
|
||||
_getUser?.DownloadFilenameFormat || "DOCNAME";
|
||||
setFileNameFormat(downloadFilenameFormat);
|
||||
const getUser = extUser?.data?.result;
|
||||
if (!getUser) {
|
||||
setErrMsg(t("something-went-wrong-mssg"));
|
||||
return;
|
||||
}
|
||||
const _getUser = JSON.parse(JSON.stringify(getUser));
|
||||
dispatch(setUserInfo(_getUser));
|
||||
setIsNotifyOnSignatures(
|
||||
_getUser?.NotifyOnSignatures !== undefined
|
||||
? _getUser?.NotifyOnSignatures
|
||||
: true
|
||||
);
|
||||
setTimezone(_getUser?.Timezone || usertimezone);
|
||||
if (tenantSignTypes?.length > 0) {
|
||||
const signatureType = _getUser?.SignatureType || signatureTypes;
|
||||
const updatedSignatureType = await handleSignatureType(
|
||||
tenantSignTypes,
|
||||
signatureType
|
||||
);
|
||||
setSignatureType(updatedSignatureType);
|
||||
} else {
|
||||
setSignatureType(_getUser?.SignatureType || signatureTypes);
|
||||
}
|
||||
setSendinOrder(
|
||||
_getUser?.SendinOrder !== undefined ? _getUser?.SendinOrder : true
|
||||
);
|
||||
setIsTourEnabled(
|
||||
_getUser?.IsTourEnabled !== undefined ? _getUser?.IsTourEnabled : true
|
||||
);
|
||||
setDateFormat(
|
||||
_getUser?.DateFormat !== undefined ? _getUser?.DateFormat : "MM/DD/YYYY"
|
||||
);
|
||||
setIs12HourTime(
|
||||
_getUser?.Is12HourTime !== undefined ? _getUser?.Is12HourTime : false
|
||||
);
|
||||
setIsLTVEnabled(
|
||||
_getUser?.IsLTVEnabled !== undefined ? _getUser?.IsLTVEnabled : false
|
||||
);
|
||||
const downloadFilenameFormat =
|
||||
_getUser?.DownloadFilenameFormat || "DOCNAME";
|
||||
setFileNameFormat(downloadFilenameFormat);
|
||||
} catch (err) {
|
||||
console.error("Error while getting user details: ", err);
|
||||
setErrMsg(t("something-went-wrong-mssg"));
|
||||
} finally {
|
||||
setIsTopLoader(false);
|
||||
dispatch(setTopLoader(false));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// `handleCheckboxChange` is trigger when user enable/disable checkbox of respective type
|
||||
const handleCheckboxChange = (index) => {
|
||||
// Create a copy of the signatureType array
|
||||
const updatedSignatureType = [...signatureType];
|
||||
// Toggle the enabled value for the clicked item
|
||||
updatedSignatureType[index].enabled = !updatedSignatureType[index].enabled;
|
||||
// Update the state with the modified array
|
||||
setSignatureType(updatedSignatureType);
|
||||
// // Create a copy of the signatureType array
|
||||
// const updatedSignatureType = [...signatureType];
|
||||
// // Toggle the enabled value for the clicked item
|
||||
// updatedSignatureType[index].enabled = !updatedSignatureType[index].enabled;
|
||||
// // Update the state with the modified array
|
||||
// setSignatureType(updatedSignatureType);
|
||||
|
||||
setSignatureType((prev) =>
|
||||
prev.map((item, i) =>
|
||||
i === index ? { ...item, enabled: !item.enabled } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// `handleSave` is used save updated value signature type
|
||||
const handleSave = async () => {
|
||||
setIsLoader(true);
|
||||
const handleSave = withSessionValidation(async () => {
|
||||
dispatch(setLoader(true));
|
||||
const Timezone = timezone || usertimezone;
|
||||
if (
|
||||
signatureType.length > 0 ||
|
||||
@@ -160,20 +178,12 @@ const Preferences = () => {
|
||||
enabledSignTypes?.length === 1 &&
|
||||
enabledSignTypes[0]?.name === "default";
|
||||
if (enabledSignTypes.length === 0) {
|
||||
setIsAlert({
|
||||
type: "danger",
|
||||
msg: t("at-least-one-signature-type")
|
||||
});
|
||||
setIsLoader(false);
|
||||
setTimeout(() => setIsAlert({ type: "success", msg: "" }), 2000);
|
||||
showAlert("danger", t("at-least-one-signature-type"));
|
||||
dispatch(setLoader(false));
|
||||
return;
|
||||
} else if (isDefaultSignTypeOnly) {
|
||||
setIsAlert({
|
||||
type: "danger",
|
||||
msg: t("expect-default-one-signature-type")
|
||||
});
|
||||
setIsLoader(false);
|
||||
setTimeout(() => setIsAlert({ type: "success", msg: "" }), 2000);
|
||||
showAlert("danger", t("expect-default-one-signature-type"));
|
||||
dispatch(setLoader(false));
|
||||
return;
|
||||
} else {
|
||||
params = { ...params, SignatureType: signatureType };
|
||||
@@ -194,7 +204,7 @@ const Preferences = () => {
|
||||
};
|
||||
const updateRes = await Parse.Cloud.run("updatepreferences", params);
|
||||
if (updateRes) {
|
||||
setIsAlert({ type: "success", msg: t("saved-successfully") });
|
||||
showAlert("success", t("saved-successfully"));
|
||||
let extUser =
|
||||
localStorage.getItem("Extand_Class") &&
|
||||
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
@@ -211,13 +221,11 @@ const Preferences = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error updating signature type: ", err);
|
||||
setIsAlert({ type: "danger", msg: err.message });
|
||||
showAlert("danger", err.message);
|
||||
}
|
||||
|
||||
setTimeout(() => setIsAlert({ type: "success", msg: "" }), 1500);
|
||||
setIsLoader(false);
|
||||
dispatch(setLoader(false));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
|
||||
const handleNotifySignChange = (value) => {
|
||||
@@ -231,7 +239,7 @@ const Preferences = () => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isalert.msg && <Alert type={isalert.type}>{isalert.msg}</Alert>}
|
||||
{alertInfo.msg && <Alert type={alertInfo.type}>{alertInfo.msg}</Alert>}
|
||||
{isTopLoader ? (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
<Loader />
|
||||
@@ -265,20 +273,25 @@ const Preferences = () => {
|
||||
onClick={() => setactiveTab(ind)}
|
||||
key={ind}
|
||||
role="tab"
|
||||
className={`${activeTab === ind ? "op-tab-active" : ""} op-tab text-xs md:text-base pb-2 md:pb-0 transition-all `}
|
||||
className={` op-tab text-xs md:text-base pb-2 md:pb-0 transition-all`}
|
||||
aria-selected={activeTab === ind}
|
||||
aria-controls={`panel-${tabData.title}`}
|
||||
>
|
||||
<i className={tabData.icon}></i>
|
||||
<span className="ml-1 md:ml-2">{tabData.title}</span>
|
||||
<span
|
||||
className={`${activeTab === ind ? "block" : "hidden"} md:block ml-1`}
|
||||
title={tabData?.title}
|
||||
>
|
||||
{tabData.title}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="panel-general"
|
||||
id={`panel-${activeTab}`}
|
||||
className="px-6 pt-4 pb-6"
|
||||
aria-labelledby="tab-general"
|
||||
aria-labelledby={`tab-${activeTab}`}
|
||||
role="tabpanel"
|
||||
>
|
||||
{tabName(activeTab) === "general" && (
|
||||
@@ -312,7 +325,9 @@ const Preferences = () => {
|
||||
<div className="p-[5px] ml-2">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">Draw: </span>
|
||||
<span className="font-bold">
|
||||
{t("draw")}:{" "}
|
||||
</span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l1")}
|
||||
</span>
|
||||
@@ -324,7 +339,9 @@ const Preferences = () => {
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">Upload: </span>
|
||||
<span className="font-bold">
|
||||
{t("upload")}:{" "}
|
||||
</span>
|
||||
<span>
|
||||
{t("allowed-signature-types-help.l3")}
|
||||
</span>
|
||||
@@ -620,20 +637,8 @@ const Preferences = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tabName(activeTab) === "email" && (
|
||||
<div className="flex flex-col mb-4">
|
||||
<MailTemplateEditor
|
||||
info={
|
||||
tenantInfo
|
||||
}
|
||||
tenantId={tenantInfo?.objectId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{tabName(activeTab) === "security" && (
|
||||
<>
|
||||
</>
|
||||
)}
|
||||
{tabName(activeTab) === "widgets" && <WidgetsTab />}
|
||||
{tabName(activeTab) === "email" && <EmailTab />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+128
-100
@@ -10,6 +10,7 @@ import ColumnSelector from "../components/ColumnSelector";
|
||||
import TemplatesReport from "../reports/template/TemplatesReport";
|
||||
import DocumentsReport from "../reports/document/DocumentsReport";
|
||||
import { templateReportTour } from "../json/ReportTour";
|
||||
import { withSessionValidation } from "../utils";
|
||||
|
||||
const Report = () => {
|
||||
const { id } = useParams();
|
||||
@@ -31,7 +32,9 @@ const Report = () => {
|
||||
const [columnLabels, setColumnLabels] = useState({});
|
||||
const [defaultColumns, setDefaultColumns] = useState([]);
|
||||
const [isColumnModal, setIsColumnModal] = useState(false);
|
||||
const [searchLoader, setSearchLoader] = useState(false);
|
||||
const debounceTimer = useRef(null);
|
||||
const searchAbortRef = useRef(null); // holds AbortController
|
||||
// Number of documents to display per page (should always be half of docLimit for proper pagination)
|
||||
const docPerPage = 10;
|
||||
// Number of documents to fetch per API call
|
||||
@@ -61,6 +64,10 @@ const Report = () => {
|
||||
setIsNextRecord(false);
|
||||
// Here it'll abort the fetch
|
||||
abortController.abort();
|
||||
if (searchAbortRef.current) {
|
||||
searchAbortRef.current.abort();
|
||||
searchAbortRef.current = null;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [id]);
|
||||
@@ -73,13 +80,23 @@ const Report = () => {
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
const handleSearchChange = async (e) => {
|
||||
const handleSearchChange = withSessionValidation(async (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
setSearchTerm(term);
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
// 2) abort any in-flight request triggered by older debounce
|
||||
if (searchAbortRef.current) {
|
||||
searchAbortRef.current.abort();
|
||||
searchAbortRef.current = null;
|
||||
}
|
||||
debounceTimer.current = setTimeout(async () => {
|
||||
// create controller for THIS request
|
||||
const controller = new AbortController();
|
||||
searchAbortRef.current = controller;
|
||||
|
||||
setSearchLoader(true);
|
||||
try {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -90,21 +107,31 @@ const Report = () => {
|
||||
const res = await axios.post(
|
||||
url,
|
||||
{ reportId: id, searchTerm: term, skip: 0, limit: docPerPage },
|
||||
{ headers }
|
||||
{ headers: headers, signal: controller.signal } // ✅ axios abort
|
||||
);
|
||||
// if you want to be extra safe (ignore late responses)
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
const data = res.data?.result || [];
|
||||
if (!data.error) {
|
||||
setList(data);
|
||||
setIsMoreDocs(data.length >= docPerPage);
|
||||
setIsNextRecord(false);
|
||||
setIsSearchResult(true);
|
||||
setSearchLoader(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Search error:", err);
|
||||
// ✅ ignore abort errors
|
||||
const isAbort =
|
||||
err?.name === "CanceledError" ||
|
||||
err?.code === "ERR_CANCELED" ||
|
||||
err?.message?.toLowerCase?.().includes("canceled");
|
||||
if (!isAbort) console.error("Search error:", err);
|
||||
setSearchLoader(false);
|
||||
}
|
||||
}, 300);
|
||||
setIsSearchResult(false);
|
||||
};
|
||||
});
|
||||
|
||||
const handleSearchPaste = (e) => {
|
||||
setTimeout(() => {
|
||||
@@ -119,113 +146,113 @@ const Report = () => {
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
const getReportData = async (
|
||||
skipUserRecord = 0,
|
||||
limit = 20,
|
||||
term = searchTerm
|
||||
) => {
|
||||
// setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
const savedCols = JSON.parse(
|
||||
localStorage.getItem("reportColumns") || "{}"
|
||||
);
|
||||
const visible = savedCols[id]?.visible || json.heading;
|
||||
const labels = savedCols[id]?.labels || {};
|
||||
if (!savedCols[id] || id === "contacts") {
|
||||
savedCols[id] = { visible: json.heading, labels: {} };
|
||||
localStorage.setItem("reportColumns", JSON.stringify(savedCols));
|
||||
}
|
||||
setVisibleColumns(visible);
|
||||
setColumnLabels(labels);
|
||||
setHeading(visible);
|
||||
setDefaultColumns(json.heading);
|
||||
setReportName(json.reportName);
|
||||
setReportHelp(json?.helpMsg);
|
||||
const currentUser = Parse.User.current().id;
|
||||
const getReportData = withSessionValidation(
|
||||
async (skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||
// setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
const savedCols = JSON.parse(
|
||||
localStorage.getItem("reportColumns") || "{}"
|
||||
);
|
||||
const visible = savedCols[id]?.visible || json.heading;
|
||||
const labels = savedCols[id]?.labels || {};
|
||||
if (!savedCols[id] || id === "contacts") {
|
||||
savedCols[id] = { visible: json.heading, labels: {} };
|
||||
localStorage.setItem("reportColumns", JSON.stringify(savedCols));
|
||||
}
|
||||
setVisibleColumns(visible);
|
||||
setColumnLabels(labels);
|
||||
setHeading(visible);
|
||||
setDefaultColumns(json.heading);
|
||||
setReportName(json.reportName);
|
||||
setReportHelp(json?.helpMsg);
|
||||
const currentUser = Parse.User.current().id;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "4Hhwbp482K" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "4Hhwbp482K" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
const extraHeads =
|
||||
id === "4Hhwbp482K" ? [...extraCols, "Expiry Date"] : extraCols;
|
||||
setAllColumns(Array.from(new Set([...json.heading, ...extraHeads])));
|
||||
if (id === "6TeaPr321t") {
|
||||
setTourData(templateReportTour);
|
||||
}
|
||||
if (id === "4Hhwbp482K") {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj?.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "4Hhwbp482K" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "4Hhwbp482K" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
const extraHeads =
|
||||
id === "4Hhwbp482K" ? [...extraCols, "Expiry Date"] : extraCols;
|
||||
setAllColumns(Array.from(new Set([...json.heading, ...extraHeads])));
|
||||
if (id === "6TeaPr321t") {
|
||||
setTourData(templateReportTour);
|
||||
}
|
||||
if (id === "4Hhwbp482K") {
|
||||
const listData = res.data?.result.filter(
|
||||
(x) => x.Signers.length > 0
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj?.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
if (!res.data.result.error) {
|
||||
setIsNextRecord(false);
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
if (!res.data.result.error) {
|
||||
setIsNextRecord(false);
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.error("getreport error", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
const commonProps = {
|
||||
ReportName: reportName,
|
||||
@@ -243,6 +270,7 @@ const Report = () => {
|
||||
handleSearchPaste,
|
||||
isSearchResult,
|
||||
columnLabels,
|
||||
searchLoader,
|
||||
openColumnModal: () => setIsColumnModal(true)
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -63,11 +63,13 @@ import {
|
||||
setDefaultSignImg,
|
||||
setIsShowModal,
|
||||
resetWidgetState,
|
||||
setTypedSignFont
|
||||
setTypedSignFont,
|
||||
setMyStamp
|
||||
} from "../redux/reducers/widgetSlice";
|
||||
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
|
||||
import WidgetNameModal from "../components/pdf/WidgetNameModal";
|
||||
import CellsSettingModal from "../components/pdf/CellsSettingModal";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
import {
|
||||
applyNumberFormulasToPages,
|
||||
} from "../utils";
|
||||
@@ -76,6 +78,7 @@ function SignYourSelf() {
|
||||
const { t } = useTranslation();
|
||||
const { docId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const windowSize = useWindowSize();
|
||||
const isShowModal = useSelector((state) => state.widget.isShowModal);
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
@@ -188,11 +191,10 @@ function SignYourSelf() {
|
||||
};
|
||||
|
||||
// Use setTimeout to wait for the transition to complete
|
||||
const timer = setTimeout(updateSize, 100); // match the transition duration
|
||||
const timer = setTimeout(updateSize, 150); // match the transition duration
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [divRef.current, isSidebar]);
|
||||
|
||||
}, [divRef.current, isSidebar, windowSize?.width]);
|
||||
//function for get document details for perticular signer with signer'object id
|
||||
const getDocumentDetails = async (showComplete) => {
|
||||
try {
|
||||
@@ -267,6 +269,7 @@ function SignYourSelf() {
|
||||
);
|
||||
dispatch(setDefaultSignImg(defaultSignRes?.res?.defaultSignature));
|
||||
dispatch(setMyInitial(defaultSignRes?.res?.defaultInitial));
|
||||
dispatch(setMyStamp(defaultSignRes?.res?.defaultStamp));
|
||||
} else {
|
||||
dispatch(setSaveSignCheckbox({ isVisible: true }));
|
||||
}
|
||||
@@ -380,7 +383,8 @@ function SignYourSelf() {
|
||||
dragTypeValue,
|
||||
getWidgetValue,
|
||||
owner,
|
||||
xyPosition
|
||||
xyPosition,
|
||||
true
|
||||
)
|
||||
};
|
||||
dropData.push(dropObj);
|
||||
@@ -433,7 +437,9 @@ function SignYourSelf() {
|
||||
dragTypeValue,
|
||||
getWidgetValue,
|
||||
owner,
|
||||
xyPosition
|
||||
xyPosition,
|
||||
pdfDetails?.[0],
|
||||
true
|
||||
),
|
||||
scale: containerScale
|
||||
};
|
||||
@@ -892,7 +898,7 @@ function SignYourSelf() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourSecond"]',
|
||||
@@ -904,7 +910,7 @@ function SignYourSelf() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="pdftools"]',
|
||||
@@ -916,7 +922,7 @@ function SignYourSelf() {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1160,10 +1166,10 @@ function SignYourSelf() {
|
||||
setCurrWidgetsDetails({});
|
||||
};
|
||||
const clickOnZoomIn = () => {
|
||||
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
|
||||
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
|
||||
};
|
||||
const clickOnZoomOut = () => {
|
||||
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
|
||||
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
|
||||
};
|
||||
//`handleRotationFun` function is used to roatate pdf particular page
|
||||
const handleRotationFun = async (rotateDegree) => {
|
||||
@@ -1215,7 +1221,7 @@ function SignYourSelf() {
|
||||
selector: '[data-tut="IsSigned"]',
|
||||
content: t("signature-validate-alert-2"),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,8 @@ import {
|
||||
getOriginalWH,
|
||||
defaultMailBody,
|
||||
defaultMailSubject,
|
||||
handleDeleteWidget
|
||||
handleDeleteWidget,
|
||||
nonPresentMaskCss
|
||||
} from "../constant/Utils";
|
||||
import RenderPdf from "../components/pdf/RenderPdf";
|
||||
import "../styles/AddUser.css";
|
||||
@@ -63,18 +64,16 @@ import Alert from "../primitives/Alert";
|
||||
import LottieWithLoader from "../primitives/DotLottieReact";
|
||||
import * as utils from "../utils";
|
||||
import CustomizeMail from "../components/pdf/CustomizeMail";
|
||||
import {
|
||||
resetWidgetState,
|
||||
setPrefillImg
|
||||
} from "../redux/reducers/widgetSlice.js";
|
||||
import { resetWidgetState, setPrefillImg } from "../redux/reducers/widgetSlice";
|
||||
import ShareButton from "../primitives/ShareButton";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
|
||||
const TemplatePlaceholder = () => {
|
||||
const { t } = useTranslation();
|
||||
const journey = "Use Template";
|
||||
const { templateId } = useParams();
|
||||
const windowSize = useWindowSize();
|
||||
const dispatch = useDispatch();
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const { prefillImg, isBulkLoader } = useSelector((state) => state.widget);
|
||||
const divRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
const numPages = 1;
|
||||
@@ -186,11 +185,11 @@ const TemplatePlaceholder = () => {
|
||||
};
|
||||
|
||||
// Use setTimeout to wait for the transition to complete
|
||||
const timer = setTimeout(updateSize, 100); // match the transition duration
|
||||
const timer = setTimeout(updateSize, 150); // match the transition duration
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [divRef.current, isSidebar]);
|
||||
}, [divRef.current, isSidebar, windowSize?.width]);
|
||||
|
||||
|
||||
//function to fetch tenant Details
|
||||
@@ -235,7 +234,7 @@ const TemplatePlaceholder = () => {
|
||||
}
|
||||
};
|
||||
// `fetchTemplate` function in used to get Template from server and setPlaceholder ,setSigner if present
|
||||
const fetchTemplate = async () => {
|
||||
const fetchTemplate = utils.withSessionValidation(async () => {
|
||||
try {
|
||||
const tenantSignTypes = await fetchTenantDetails();
|
||||
const params = { templateId: templateId };
|
||||
@@ -419,7 +418,7 @@ const TemplatePlaceholder = () => {
|
||||
setHandleError(t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
//function for setting position after drop signature button over pdf
|
||||
const addPositionOfSignature = (item, monitor) => {
|
||||
@@ -629,7 +628,7 @@ const TemplatePlaceholder = () => {
|
||||
selector: '[data-tut="reactourAddbtn"]',
|
||||
content: t("add-user-template"),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -958,30 +957,38 @@ const TemplatePlaceholder = () => {
|
||||
);
|
||||
if (res?.status === "unfilled") {
|
||||
setIsUiLoading(false);
|
||||
const emptyWidget = res?.emptyResponseObjects.map((item, index) => (
|
||||
<span className="font-medium" key={index}>
|
||||
{item.options.name}
|
||||
</span>
|
||||
));
|
||||
const emptyWidget = res?.emptyResponseObjects
|
||||
?.map((item) => item.options.name)
|
||||
?.join(", ");
|
||||
const timeInMiliSec = 6000;
|
||||
showAlert(
|
||||
"danger",
|
||||
<>
|
||||
The following required field(s) cannot be left empty:{" "}
|
||||
{emptyWidget.map((item, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && ", "}
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
. Please fill them out to proceed.
|
||||
</>,
|
||||
6000
|
||||
t("prefill-unfilled-widget", {
|
||||
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
|
||||
}),
|
||||
timeInMiliSec
|
||||
);
|
||||
} else if (res?.status === "unattach signer") {
|
||||
setIsUiLoading(false);
|
||||
showAlert("danger", "please attach all role to signer");
|
||||
} else if (res?.status === "success") {
|
||||
setDocumentId(res.id);
|
||||
const ownerId = pdfDetails[0].ExtUserPtr?.UserId?.objectId;
|
||||
const firstSigner = signersdata[0];
|
||||
const isOwner = firstSigner?.UserId?.objectId === ownerId;
|
||||
if (pdfDetails[0]?.SendinOrder && isOwner) {
|
||||
setCurrUserId(firstSigner?.objectId);
|
||||
setIsSend(true);
|
||||
return;
|
||||
} else {
|
||||
const currentSigner = signersdata?.find(
|
||||
(x) => x?.UserId?.objectId === ownerId
|
||||
);
|
||||
if (currentSigner) {
|
||||
setCurrUserId(currentSigner?.objectId);
|
||||
}
|
||||
}
|
||||
|
||||
setIsMailModal(true);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -996,7 +1003,7 @@ const TemplatePlaceholder = () => {
|
||||
pdfDetails,
|
||||
signerPos,
|
||||
signersdata,
|
||||
updatedPdfUrl
|
||||
updatedPdfUrl,
|
||||
);
|
||||
if (res.status === "success") {
|
||||
navigate(`/placeHolderSign/${res.id}`, {
|
||||
@@ -1025,7 +1032,7 @@ const TemplatePlaceholder = () => {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="reactourAddbtn"]',
|
||||
@@ -1038,19 +1045,31 @@ const TemplatePlaceholder = () => {
|
||||
),
|
||||
position: "top",
|
||||
observe: '[data-tut="reactourAddbtn--observe"]',
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
// {
|
||||
// selector: '[data-tut="addWidgets"]',
|
||||
// content: () => (
|
||||
// <TourContentWithBtn
|
||||
// message={t("tour-mssg.template-placeholder-2")}
|
||||
// isDontShowCheckbox={!checkTourStatus}
|
||||
// isChecked={handleDontShow}
|
||||
// />
|
||||
// ),
|
||||
// position: "top",
|
||||
// styles: { fontSize: "13px" }
|
||||
// },
|
||||
{
|
||||
selector: '[data-tut="addWidgets"]',
|
||||
selector: '[data-tut="nonpresentmask"]',
|
||||
content: () => (
|
||||
<TourContentWithBtn
|
||||
message={t("tour-mssg.template-placeholder-2")}
|
||||
isDontShowCheckbox={!checkTourStatus}
|
||||
isChecked={handleDontShow}
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
position: "center",
|
||||
styles: { fontSize: "13px", maskArea: nonPresentMaskCss },
|
||||
action: () => handleCloseRoleModal()
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="pdftools"]',
|
||||
@@ -1062,7 +1081,7 @@ const TemplatePlaceholder = () => {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
},
|
||||
{
|
||||
selector: '[data-tut="headerArea"]',
|
||||
@@ -1074,7 +1093,7 @@ const TemplatePlaceholder = () => {
|
||||
/>
|
||||
),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1277,6 +1296,10 @@ const TemplatePlaceholder = () => {
|
||||
const RedirectUrl = updateTemplate?.[0]?.RedirectUrl
|
||||
? { RedirectUrl: updateTemplate?.[0]?.RedirectUrl }
|
||||
: {};
|
||||
const penColors =
|
||||
updateTemplate?.[0]?.PenColors?.length > 0
|
||||
? { PenColors: updateTemplate?.[0]?.PenColors }
|
||||
: {};
|
||||
const data = {
|
||||
...(updateTemplate?.[0]?.URL ? { URL: updateTemplate?.[0]?.URL } : {}),
|
||||
Name: updateTemplate?.[0]?.Name || "",
|
||||
@@ -1293,6 +1316,7 @@ const TemplatePlaceholder = () => {
|
||||
: false,
|
||||
TimeToCompleteDays:
|
||||
parseInt(updateTemplate?.[0]?.TimeToCompleteDays) || 15,
|
||||
...penColors,
|
||||
...Bcc,
|
||||
...RedirectUrl
|
||||
};
|
||||
@@ -1324,6 +1348,9 @@ const TemplatePlaceholder = () => {
|
||||
isHideLabel,
|
||||
layout
|
||||
) => {
|
||||
const isPrefill = signerPos.some(
|
||||
(x) => x?.Role === "prefill" && x.Id === uniqueId
|
||||
);
|
||||
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
|
||||
if (filterSignerPos.length > 0) {
|
||||
const getPlaceHolder = filterSignerPos[0].placeHolder;
|
||||
@@ -1391,16 +1418,21 @@ const TemplatePlaceholder = () => {
|
||||
...position.options,
|
||||
name: dropdownName,
|
||||
values: dropdownOptions,
|
||||
validation: {
|
||||
minRequiredCount: minCount,
|
||||
maxRequiredCount: maxCount
|
||||
},
|
||||
...(!isPrefill
|
||||
? {
|
||||
validation: {
|
||||
minRequiredCount: minCount,
|
||||
maxRequiredCount: maxCount
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
layout: layout,
|
||||
isReadOnly: isReadOnly || false,
|
||||
defaultValue: defaultValue,
|
||||
isHideLabel: isHideLabel || false,
|
||||
fontSize: textSize || 12,
|
||||
fontColor: textColor || "black"
|
||||
fontColor: textColor || "black",
|
||||
...(isPrefill ? { status } : {})
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1507,13 +1539,16 @@ const TemplatePlaceholder = () => {
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (["signature"].includes(position.type)) {
|
||||
else if (["signature", "initials"].includes(position.type)) {
|
||||
return {
|
||||
...position,
|
||||
options: {
|
||||
...position.options,
|
||||
name: defaultdata.name,
|
||||
hint: defaultdata?.hint || ""
|
||||
hint: defaultdata?.hint || "",
|
||||
...(defaultdata?.penColors?.length > 0 && {
|
||||
penColors: defaultdata?.penColors
|
||||
})
|
||||
}
|
||||
};
|
||||
} else {
|
||||
@@ -1580,10 +1615,10 @@ const TemplatePlaceholder = () => {
|
||||
};
|
||||
|
||||
const clickOnZoomIn = () => {
|
||||
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
|
||||
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
|
||||
};
|
||||
const clickOnZoomOut = () => {
|
||||
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
|
||||
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
|
||||
};
|
||||
//`handleRotationFun` function is used to roatate pdf particular page
|
||||
const handleRotationFun = async (rotateDegree) => {
|
||||
@@ -1621,7 +1656,7 @@ const TemplatePlaceholder = () => {
|
||||
selector: '[data-tut="isSignatureWidget"]',
|
||||
content: t("signature-field-widget", { signersName }),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
const showAlert = (type, message, duration = 1500) => {
|
||||
@@ -1633,7 +1668,7 @@ const TemplatePlaceholder = () => {
|
||||
selector: '[data-tut="IsSigned"]',
|
||||
content: t("text-field-tour"),
|
||||
position: "top",
|
||||
style: { fontSize: "13px" }
|
||||
styles: { fontSize: "13px" }
|
||||
}
|
||||
];
|
||||
const copytoclipboard = (text) => {
|
||||
@@ -1692,7 +1727,6 @@ const TemplatePlaceholder = () => {
|
||||
setIsCreateDocModal(true);
|
||||
setIsUiLoading(false);
|
||||
};
|
||||
|
||||
const handleUseButton = async () => {
|
||||
setIsCreateDocModal(false);
|
||||
//this function is used to open modal to show signers list
|
||||
@@ -1701,8 +1735,9 @@ const TemplatePlaceholder = () => {
|
||||
};
|
||||
|
||||
const handleRecipientSign = (docId, currUserId) => {
|
||||
const docObjId = docId || documentId;
|
||||
if (currUserId) {
|
||||
navigate(`/recipientSignPdf/${docId}/${currUserId}`);
|
||||
navigate(`/recipientSignPdf/${docObjId}/${currUserId}`);
|
||||
} else {
|
||||
navigate(`/recipientSignPdf/${docId}`);
|
||||
}
|
||||
@@ -2150,6 +2185,8 @@ const TemplatePlaceholder = () => {
|
||||
isSave={true}
|
||||
setUniqueId={setUniqueId}
|
||||
signatureTypes={signatureType}
|
||||
penColors={pdfDetails?.[0]?.PenColors}
|
||||
role={roleName}
|
||||
/>
|
||||
)}
|
||||
<RotateAlert
|
||||
@@ -2174,11 +2211,13 @@ const TemplatePlaceholder = () => {
|
||||
<ModalUi
|
||||
isOpen={isSend}
|
||||
title={
|
||||
mailStatus === "success"
|
||||
? t("mails-sent")
|
||||
: mailStatus === "quotareached"
|
||||
? t("quota-mail-head")
|
||||
: t("mail-not-delivered")
|
||||
!pdfDetails[0]?.SendinOrder
|
||||
? mailStatus === "success"
|
||||
? t("mails-sent")
|
||||
: mailStatus === "quotareached"
|
||||
? t("quota-mail-head")
|
||||
: t("mail-not-delivered")
|
||||
: t("mail-status-head")
|
||||
}
|
||||
handleClose={() => {
|
||||
setIsSend(false);
|
||||
@@ -2186,66 +2225,103 @@ const TemplatePlaceholder = () => {
|
||||
}}
|
||||
>
|
||||
<div className="h-[100%] p-[20px] text-base-content">
|
||||
{mailStatus === "success" ? (
|
||||
<div className="text-center mb-[10px]">
|
||||
<LottieWithLoader />
|
||||
{documentDetails.SendinOrder ? (
|
||||
<p>
|
||||
{currUserId
|
||||
? t("placeholder-mail-alert-you")
|
||||
: t("placeholder-mail-alert", {
|
||||
name: signersdata[0]?.Name
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-5">
|
||||
<div>
|
||||
{mailStatus === "success" ? (
|
||||
<div className="text-center mb-[10px]">
|
||||
<LottieWithLoader />
|
||||
{documentDetails.SendinOrder ? (
|
||||
<p>
|
||||
{currUserId
|
||||
? t("placeholder-mail-alert-you")
|
||||
: t("placeholder-mail-alert", {
|
||||
name: signersdata[0]?.Name
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-4")}</p>
|
||||
)}
|
||||
</div>
|
||||
) : mailStatus === "quotareached" ? (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="my-3">{handleShareList()}</div>
|
||||
</div>
|
||||
) : mailStatus === "failed" ? (
|
||||
<p>{t("mail-failed")} </p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-4")}</p>
|
||||
<div className="mb-[10px]">
|
||||
{!pdfDetails[0]?.SendinOrder &&
|
||||
(mailStatus === "dailyquotareached" ? (
|
||||
<p>{t("daily-quota-reached")}</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-6")}</p>
|
||||
))}
|
||||
{currUserId && (
|
||||
<span className="mt-1">{t("placeholder-alert-5")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currUserId && <p>{t("placeholder-alert-5")}</p>}
|
||||
</div>
|
||||
) : mailStatus === "quotareached" ? (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="my-3">{handleShareList()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-[10px]">
|
||||
{mailStatus === "dailyquotareached" ? (
|
||||
<p>{t("daily-quota-reached")}</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-6")}</p>
|
||||
)}
|
||||
{currUserId && <p className="mt-1">{t("placeholder-alert-5")}</p>}
|
||||
</div>
|
||||
)}
|
||||
{!mailStatus && (
|
||||
<div className="w-full h-[1px] bg-[#9f9f9f] my-[15px]"></div>
|
||||
)}
|
||||
{mailStatus !== "quotareached" && (
|
||||
<div
|
||||
className={
|
||||
mailStatus === "success" ? "flex justify-center mt-1" : ""
|
||||
}
|
||||
>
|
||||
{currUserId && (
|
||||
<button
|
||||
onClick={() =>
|
||||
handleRecipientSign(documentDetails?.objectId, currUserId)
|
||||
{mailStatus !== "quotareached" && (
|
||||
<div
|
||||
className={
|
||||
mailStatus === "success"
|
||||
? "flex justify-center mt-1"
|
||||
: "flex items-center justify-center mt-7"
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary mr-1"
|
||||
>
|
||||
{t("yes")}
|
||||
</button>
|
||||
{currUserId && (
|
||||
<button
|
||||
onClick={() =>
|
||||
handleRecipientSign(
|
||||
documentDetails?.objectId,
|
||||
currUserId
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary mr-1"
|
||||
>
|
||||
{t("sign-now")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
}}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
>
|
||||
{currUserId ? t("no") : t("close")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
}}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
>
|
||||
{currUserId ? t("no") : t("close")}
|
||||
</button>
|
||||
</div>
|
||||
{mailStatus !== "success" &&
|
||||
currUserId &&
|
||||
pdfDetails[0]?.SendinOrder && (
|
||||
<div className="op-divider text-base-content mx-[0%] my-1 font-medium">
|
||||
{t("or")}
|
||||
</div>
|
||||
)}
|
||||
{mailStatus !== "success" &&
|
||||
currUserId &&
|
||||
pdfDetails[0]?.SendinOrder && (
|
||||
<div
|
||||
className="op-btn op-btn-outline w-[50%] md:w-[35%] mt-1"
|
||||
onClick={() => {
|
||||
setIsSend(false);
|
||||
setIsMailModal(true);
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="fa-regular fa-envelope"
|
||||
style={{ color: "#002864", fontSize: "19px" }}
|
||||
></i>{" "}
|
||||
<span>{t("send-to-email")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!mailStatus && (
|
||||
<div className="op-divider text-base-content mx-[0%] mt-3"></div>
|
||||
)}
|
||||
</div>
|
||||
</ModalUi>
|
||||
|
||||
@@ -14,6 +14,7 @@ import DeleteUserModal from "../primitives/DeleteUserModal";
|
||||
import axios from "axios";
|
||||
import PasswordResetModal from "../primitives/PasswordResetModal";
|
||||
import { usersActions } from "../json/ReportJson";
|
||||
import { withSessionValidation } from "../utils";
|
||||
|
||||
const heading = ["Sr.No", "Name", "Email", "Phone", "Role", "Team", "Active"];
|
||||
const UserList = () => {
|
||||
@@ -168,7 +169,7 @@ const UserList = () => {
|
||||
};
|
||||
const handleClose = () => setIsActiveModal({});
|
||||
|
||||
const handleToggleSubmit = async (user) => {
|
||||
const handleToggleSubmit = withSessionValidation(async (user) => {
|
||||
const index = userList.findIndex((obj) => obj.objectId === user.objectId);
|
||||
if (index !== -1) {
|
||||
setIsActiveModal({});
|
||||
@@ -193,7 +194,7 @@ const UserList = () => {
|
||||
setIsActLoader({});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleToggleBtn = (user) => {
|
||||
setIsActiveModal({ [user.objectId]: true });
|
||||
};
|
||||
@@ -204,7 +205,7 @@ const UserList = () => {
|
||||
setTimeout(() => setIsAlert({ type: "success", msg: "" }), timer);
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async (item) => {
|
||||
const handleDeleteAccount = withSessionValidation(async (item) => {
|
||||
setDeleting(true);
|
||||
if (item?.UserId?.objectId) {
|
||||
const url = localStorage.getItem("baseUrl")?.replace(/\/app\/?$/, "/");
|
||||
@@ -230,16 +231,16 @@ const UserList = () => {
|
||||
setDeleteUserRes(t("something-went-wrong-mssg"));
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleCloseModal = () => {
|
||||
setIsActModal({});
|
||||
setDeleteUserRes("");
|
||||
setDeleting(false);
|
||||
};
|
||||
|
||||
const handleActionBtn = async (act, item) => {
|
||||
const handleActionBtn = withSessionValidation(async (act, item) => {
|
||||
setIsActModal({ [`${act.action}_${item.objectId}`]: true });
|
||||
};
|
||||
});
|
||||
const handleBtnVisibility = (act, item) => {
|
||||
if (act.restrictAdmin) {
|
||||
if (item?.UserRole === "contracts_Admin") {
|
||||
@@ -264,7 +265,7 @@ const UserList = () => {
|
||||
}
|
||||
};
|
||||
|
||||
async function submitPassword(userId, password) {
|
||||
const submitPassword = withSessionValidation(async (userId, password) => {
|
||||
setIsLoader(true);
|
||||
setIsActModal({});
|
||||
try {
|
||||
@@ -277,7 +278,7 @@ const UserList = () => {
|
||||
} finally {
|
||||
setIsLoader(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="relative">
|
||||
{isLoader && (
|
||||
|
||||
@@ -6,12 +6,12 @@ import { Navigate, useNavigate } from "react-router";
|
||||
import Parse from "parse";
|
||||
import { SaveFileSize } from "../constant/saveFileSize";
|
||||
import dp from "../assets/images/dp.png";
|
||||
import { sanitizeFileName } from "../utils";
|
||||
import { sanitizeFileName, withSessionValidation } from "../utils";
|
||||
import axios from "axios";
|
||||
import Tooltip from "../primitives/Tooltip";
|
||||
import {
|
||||
getSecureUrl,
|
||||
handleSendOTP,
|
||||
handleSendOTP
|
||||
} from "../constant/Utils";
|
||||
import ModalUi from "../primitives/ModalUi";
|
||||
import Loader from "../primitives/Loader";
|
||||
@@ -124,7 +124,7 @@ function UserProfile() {
|
||||
};
|
||||
|
||||
// `updateExtUser` is used to update user details in extended class
|
||||
const updateExtUser = async (obj) => {
|
||||
const updateExtUser = withSessionValidation(async (obj) => {
|
||||
try {
|
||||
const extData = JSON.parse(localStorage.getItem("Extand_Class"));
|
||||
const ExtUserId = extData?.[0]?.objectId;
|
||||
@@ -152,10 +152,10 @@ function UserProfile() {
|
||||
const json = JSON.parse(JSON.stringify([res]));
|
||||
const extRes = JSON.stringify(json);
|
||||
localStorage.setItem("Extand_Class", extRes);
|
||||
} catch (e) {
|
||||
console.log("error in save data in contracts_Users class");
|
||||
} catch (err) {
|
||||
console.log("error in save data in contracts_Users class", err);
|
||||
}
|
||||
};
|
||||
});
|
||||
// file upload function
|
||||
const fileUpload = async (file) => {
|
||||
if (file) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// below code is used to handle undefined Promise.withResolvers for safari
|
||||
// below code is used to handle undefined Promise.withResolvers for safari for react-pdf
|
||||
if (typeof Promise.withResolvers === "undefined") {
|
||||
Promise.withResolvers = function () {
|
||||
let resolve, reject;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import Loader from "./Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import axios from "axios";
|
||||
import { getTenantDetails } from "../constant/Utils";
|
||||
import { emailRegex } from "../constant/const";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||
|
||||
const AddContact = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const [name, setName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
@@ -108,6 +112,7 @@ const AddContact = (props) => {
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
dispatch(sessionStatus(false));
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
@@ -139,7 +144,9 @@ const AddContact = (props) => {
|
||||
)}
|
||||
<div className="w-full mx-auto p-[8px]">
|
||||
{!props?.isDisableTitle && (
|
||||
<div className="text-[14px] font-[700] text-base-content mb-1">{t("add-contact")}</div>
|
||||
<div className="text-[14px] font-[700] text-base-content mb-1">
|
||||
{t("add-contact")}
|
||||
</div>
|
||||
)}
|
||||
{isUserExist && props?.isAddYourSelfCheckbox && (
|
||||
<div className="mb-[0.75rem] flex items-center mt-1">
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import "../styles/signature.css";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
const DateWidgetModal = ({
|
||||
children,
|
||||
title,
|
||||
isOpen
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return createPortal(
|
||||
<div id="dateWidgetModal" className="items-center op-modal op-modal-open">
|
||||
<div className="md:min-w-[500px] op-modal-box p-0 max-h-90 overflow-y-auto hide-scrollbar text-sm">
|
||||
{title && (
|
||||
<h3 className="text-base-content text-left font-bold text-lg pt-[15px] px-[20px]">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default DateWidgetModal;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import "../styles/signature.css";
|
||||
import Loader from "./Loader";
|
||||
|
||||
const ModalUi = ({
|
||||
children,
|
||||
@@ -10,11 +10,14 @@ const ModalUi = ({
|
||||
showClose = true,
|
||||
reduceWidth,
|
||||
position,
|
||||
crossColor
|
||||
crossColor,
|
||||
showScrollBar = false,
|
||||
isLoader = false
|
||||
}) => {
|
||||
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" : "";
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
@@ -26,10 +29,13 @@ const ModalUi = ({
|
||||
content: { zIndex: 1001, overflow: "visible" } // Ensure modal doesn’t clip content
|
||||
}}
|
||||
>
|
||||
{isLoader && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
width || "md:min-w-[500px]"
|
||||
} op-modal-box p-0 max-h-90 overflow-y-auto hide-scrollbar text-sm`}
|
||||
className={`${width || "md:min-w-[500px]"} op-modal-box p-0 max-h-90 overflow-y-auto ${hideScrollBar} text-sm`}
|
||||
>
|
||||
{showHeader && (
|
||||
<>
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function PasswordResetModal({
|
||||
<input
|
||||
type="text"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder="Enter password or click Autogenerate"
|
||||
placeholder={t("enter-password-or-click-autogenerate")}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoFocus
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDate } from "../constant/Utils";
|
||||
import { formatDateToDdMmmYyyy } from "../constant/Utils";
|
||||
import SignerCell from "./SignerCell";
|
||||
|
||||
function isValidDateString(str) {
|
||||
@@ -14,12 +14,12 @@ const formatRow = (row) => {
|
||||
if (Array.isArray(row)) {
|
||||
return row.map((x) => x.Name).join(", ");
|
||||
} else if (typeof row === "object" && row !== null) {
|
||||
return row?.iso ? formatDate(row?.iso) : row?.Name || "-";
|
||||
return row?.iso ? formatDateToDdMmmYyyy(row?.iso) : row?.Name || "-";
|
||||
} else if (typeof row === "boolean" && row !== null) {
|
||||
return row ? row?.toString() : "false";
|
||||
} else if (isValidDateString(row) && row !== null) {
|
||||
// handle createdAt and updatedAt
|
||||
return formatDate(row) || "-";
|
||||
return formatDateToDdMmmYyyy(row) || "-";
|
||||
} else {
|
||||
return row || "-";
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export const RenderReportCell = ({
|
||||
<div className="font-semibold break-words">{rowData?.Name}</div>
|
||||
{rowData?.ExpiryDate?.iso && (
|
||||
<div className="text-gray-500">
|
||||
{t("expires")} {formatDate(rowData?.ExpiryDate?.iso)}
|
||||
{t("expires")} {formatDateToDdMmmYyyy(rowData?.ExpiryDate?.iso)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
@@ -133,25 +133,29 @@ export const RenderReportCell = ({
|
||||
case "Created Date":
|
||||
return (
|
||||
<td key={col} className="p-2 text-center">
|
||||
{rowData?.createdAt ? formatDate(rowData?.createdAt) : "-"}
|
||||
{rowData?.createdAt ? formatDateToDdMmmYyyy(rowData?.createdAt) : "-"}
|
||||
</td>
|
||||
);
|
||||
case "Updated Date":
|
||||
return (
|
||||
<td key={col} className="p-2 text-center">
|
||||
{rowData?.updatedAt ? formatDate(rowData?.updatedAt) : "-"}
|
||||
{rowData?.updatedAt ? formatDateToDdMmmYyyy(rowData?.updatedAt) : "-"}
|
||||
</td>
|
||||
);
|
||||
case "Expiry Date":
|
||||
return (
|
||||
<td key={col} className="p-2 text-center">
|
||||
{rowData?.ExpiryDate ? formatDate(rowData?.ExpiryDate?.iso) : "-"}
|
||||
{rowData?.ExpiryDate
|
||||
? formatDateToDdMmmYyyy(rowData?.ExpiryDate?.iso)
|
||||
: "-"}
|
||||
</td>
|
||||
);
|
||||
case "Sent Date":
|
||||
return (
|
||||
<td key={col} className="p-2 text-center">
|
||||
{rowData?.DocSentAt ? formatDate(rowData?.DocSentAt?.iso) : "-"}
|
||||
{rowData?.DocSentAt
|
||||
? formatDateToDdMmmYyyy(rowData?.DocSentAt?.iso)
|
||||
: "-"}
|
||||
</td>
|
||||
);
|
||||
case "Signers":
|
||||
|
||||
@@ -1,33 +1,84 @@
|
||||
import ReactTour from "reactour";
|
||||
import { TourProvider, useTour } from "@reactour/tour";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
clearAllBodyScrollLocks,
|
||||
disableBodyScroll,
|
||||
enableBodyScroll
|
||||
} from "body-scroll-lock";
|
||||
|
||||
function TourStateSync({ isOpen, onRequestClose }) {
|
||||
const { isOpen: contextIsOpen, setIsOpen } = useTour();
|
||||
const previousIsOpen = useRef(contextIsOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(isOpen);
|
||||
}, [isOpen, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousIsOpen.current && !contextIsOpen && onRequestClose) {
|
||||
onRequestClose();
|
||||
}
|
||||
previousIsOpen.current = contextIsOpen;
|
||||
}, [contextIsOpen, onRequestClose]);
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Tour({
|
||||
steps,
|
||||
isOpen,
|
||||
rounded,
|
||||
rounded = 5,
|
||||
className,
|
||||
showNumber,
|
||||
closeWithMask = false,
|
||||
onRequestClose,
|
||||
showNavigation,
|
||||
showCloseButton,
|
||||
showNavigationNumber,
|
||||
disableKeyboardNavigation
|
||||
showNavigationNumber
|
||||
}) {
|
||||
const radius = rounded || 5;
|
||||
function disableBody() {
|
||||
if (typeof window !== "undefined") {
|
||||
disableBodyScroll(document.body);
|
||||
}
|
||||
}
|
||||
|
||||
function enableBody() {
|
||||
if (typeof window !== "undefined") {
|
||||
enableBodyScroll(document.body);
|
||||
clearAllBodyScrollLocks();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ReactTour
|
||||
<TourProvider
|
||||
className={className}
|
||||
steps={steps}
|
||||
isOpen={isOpen}
|
||||
rounded={radius}
|
||||
scrollOffset={-100}
|
||||
closeWithMask={closeWithMask}
|
||||
disableKeyboardNavigation={disableKeyboardNavigation}
|
||||
showCloseButton={showCloseButton}
|
||||
onRequestClose={onRequestClose}
|
||||
showNumber={showNumber}
|
||||
defaultOpen={isOpen}
|
||||
// scrollOffset={-100} // available only for v1
|
||||
inViewThreshold={{ y: 150 }}
|
||||
onClickMask={() => {}}
|
||||
disableKeyboardNavigation={["esc"]}
|
||||
showBadge={showNumber}
|
||||
showNavigation={showNavigation}
|
||||
showNavigationNumber={showNavigationNumber}
|
||||
/>
|
||||
showPrevNextButtons={showNavigation}
|
||||
showDots={showNavigationNumber ?? showNavigation}
|
||||
showCloseButton={showCloseButton}
|
||||
padding={rounded}
|
||||
onClickClose={(clickProps) => {
|
||||
clickProps.setIsOpen(false);
|
||||
}}
|
||||
styles={{
|
||||
popover: (base) => ({
|
||||
...base,
|
||||
borderRadius: rounded,
|
||||
padding: "18px",
|
||||
fontSize: "13px"
|
||||
}),
|
||||
maskArea: (base) => ({ ...base, rx: rounded }),
|
||||
close: (base) => ({ ...base, right: 10, top: 10, outline: "none" })
|
||||
}}
|
||||
afterOpen={disableBody}
|
||||
beforeClose={enableBody}
|
||||
scrollSmooth
|
||||
>
|
||||
<TourStateSync isOpen={isOpen} onRequestClose={onRequestClose} />
|
||||
</TourProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const sidebarSlice = createSlice({
|
||||
name: "sidebar",
|
||||
initialState: { isOpen: true },
|
||||
initialState: { isOpen: true, selectedMenu: true },
|
||||
reducers: {
|
||||
toggleSidebar: (state, action) => {
|
||||
if (typeof action.payload === "undefined") {
|
||||
@@ -12,9 +12,12 @@ const sidebarSlice = createSlice({
|
||||
// payload provided → use it directly
|
||||
state.isOpen = action.payload;
|
||||
}
|
||||
},
|
||||
setSelectedMenu: (state, action) => {
|
||||
state.selectedMenu = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { toggleSidebar } = sidebarSlice.actions;
|
||||
export const { toggleSidebar, setSelectedMenu } = sidebarSlice.actions;
|
||||
export default sidebarSlice.reducer;
|
||||
|
||||
@@ -1,15 +1,47 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
const initialState = { isValidSession: true };
|
||||
const initialState = {
|
||||
userInfo: {},
|
||||
isValidSession: true,
|
||||
isLoader: false,
|
||||
isTopLoader: false,
|
||||
tenantInfo: {},
|
||||
alertInfo: { type: "success", msg: "" }
|
||||
};
|
||||
const userReducer = createSlice({
|
||||
name: "user",
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
setUserInfo: (state, action) => {
|
||||
state.userInfo = action.payload;
|
||||
},
|
||||
sessionStatus: (state, action) => {
|
||||
state.isValidSession = action.payload;
|
||||
},
|
||||
setTenantInfo: (state, action) => {
|
||||
state.tenantInfo = action.payload;
|
||||
},
|
||||
setLoader: (state, action) => {
|
||||
state.isLoader = action.payload;
|
||||
},
|
||||
setTopLoader: (state, action) => {
|
||||
state.isTopLoader = action.payload;
|
||||
},
|
||||
setAlertInfo: (state, action) => {
|
||||
state.alertInfo = {
|
||||
type: action.payload?.type,
|
||||
msg: action.payload?.msg
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { sessionStatus } = userReducer.actions;
|
||||
export const {
|
||||
setUserInfo,
|
||||
sessionStatus,
|
||||
setTenantInfo,
|
||||
setLoader,
|
||||
setTopLoader,
|
||||
setAlertInfo
|
||||
} = userReducer.actions;
|
||||
|
||||
export default userReducer.reducer;
|
||||
|
||||
@@ -10,11 +10,14 @@ const initialState = {
|
||||
signatureTypes: null,
|
||||
defaultSignImg: null,
|
||||
myInitial: null,
|
||||
myStamp: null,
|
||||
lastIndex: "",
|
||||
scrollTriggerId: "",
|
||||
prefillImg: [],
|
||||
prefillImgLoad: {},
|
||||
typedSignFont: "Fasthand"
|
||||
typedSignFont: "Fasthand",
|
||||
signatureResponse: [],
|
||||
isBulkLoader: false
|
||||
};
|
||||
|
||||
const widgetSlice = createSlice({
|
||||
@@ -63,6 +66,31 @@ const widgetSlice = createSlice({
|
||||
setPrefillImgLoad: (state, action) => {
|
||||
state.prefillImgLoad = action.payload;
|
||||
},
|
||||
setMyStamp: (state, action) => {
|
||||
state.myStamp = action.payload;
|
||||
},
|
||||
setSignatureRes: (state, action) => {
|
||||
const payload = action.payload;
|
||||
const { type } = payload;
|
||||
const index = state.signatureResponse.findIndex(
|
||||
(item) => item.type === type
|
||||
);
|
||||
if (index !== -1) {
|
||||
// 🔁 Update existing object by type
|
||||
state.signatureResponse[index] = {
|
||||
...state.signatureResponse[index],
|
||||
...payload
|
||||
};
|
||||
return;
|
||||
}
|
||||
// ➕ Add only if less than 2 objects
|
||||
if (state.signatureResponse.length < 3) {
|
||||
state.signatureResponse.push(payload);
|
||||
}
|
||||
},
|
||||
setBulkLoader: (state, action) => {
|
||||
state.isBulkLoader = action.payload;
|
||||
},
|
||||
resetWidgetState: () => initialState
|
||||
}
|
||||
});
|
||||
@@ -78,7 +106,10 @@ export const {
|
||||
setScrollTriggerId,
|
||||
setPrefillImg,
|
||||
setPrefillImgLoad,
|
||||
setTypedSignFont
|
||||
setTypedSignFont,
|
||||
setMyStamp,
|
||||
setSignatureRes,
|
||||
setBulkLoader
|
||||
} = widgetSlice.actions;
|
||||
|
||||
export default widgetSlice.reducer;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { serverUrl_fn } from "../../constant/appinfo";
|
||||
import { useElSize } from "../../hook/useElSize";
|
||||
import ImportContact from "./ImportContact";
|
||||
import AddContact from "../../primitives/AddContact";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const Contactbook = (props) => {
|
||||
const titleRef = useRef(null);
|
||||
@@ -110,7 +111,7 @@ const Contactbook = (props) => {
|
||||
}
|
||||
}, [isMoreDocs, pageNumbers, currentPage, setIsNextRecord]);
|
||||
|
||||
const handleActionBtn = async (act, item) => {
|
||||
const handleActionBtn = withSessionValidation(async (act, item) => {
|
||||
if (act.action === "delete") {
|
||||
setIsDeleteModal({ [item.objectId]: true });
|
||||
} else if (act.action === "option") {
|
||||
@@ -119,7 +120,7 @@ const Contactbook = (props) => {
|
||||
setContact(item);
|
||||
setIsModal({ [`edit_${item.objectId}`]: true });
|
||||
}
|
||||
};
|
||||
});
|
||||
// Get current list
|
||||
const indexOfLastDoc = currentPage * props.docPerPage;
|
||||
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
|
||||
@@ -163,7 +164,7 @@ const Contactbook = (props) => {
|
||||
props.setList((prevData) => [data, ...prevData]);
|
||||
};
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
const handleDelete = withSessionValidation(async (item) => {
|
||||
setIsDeleteModal({});
|
||||
setActLoader({ [`${item.objectId}`]: true });
|
||||
try {
|
||||
@@ -191,7 +192,7 @@ const Contactbook = (props) => {
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleClose = () => {
|
||||
setIsDeleteModal({});
|
||||
};
|
||||
@@ -336,6 +337,7 @@ const Contactbook = (props) => {
|
||||
</thead>
|
||||
<tbody className="text-[12px]">
|
||||
{props.List?.length > 0 &&
|
||||
!props.searchLoader &&
|
||||
currentList.map((item, index) => (
|
||||
<tr className="last:border-none border-y-[1px]" key={index}>
|
||||
{props.heading.includes("Sr.No") && (
|
||||
@@ -409,18 +411,27 @@ const Contactbook = (props) => {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{props.List?.length <= 0 && (
|
||||
{(props.searchLoader || props.List?.length <= 0) && (
|
||||
<div className="flex flex-col items-center justify-center w-ful bg-base-100 text-base-content rounded-xl py-4">
|
||||
<div className="w-[60px] h-[60px] overflow-hidden">
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={pad}
|
||||
alt={t("no-data-avaliable")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
{t("no-data-avaliable")}
|
||||
</div>
|
||||
{props.searchLoader ? (
|
||||
<>
|
||||
<Loader />
|
||||
<div className="text-sm ">{t("loading-mssg")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-[60px] h-[60px] overflow-hidden">
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={pad}
|
||||
alt={t("no-data-avaliable")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
{t("no-data-avaliable")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useState } from "react";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Parse from "parse";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const EditContactForm = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
@@ -27,9 +29,10 @@ const EditContactForm = (props) => {
|
||||
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
}
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (props.handleEditContact) {
|
||||
try {
|
||||
setIsLoader(true);
|
||||
@@ -64,7 +67,7 @@ const EditContactForm = (props) => {
|
||||
props.handleClose && props.handleClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
return (
|
||||
<div className="h-full p-[20px]">
|
||||
{isLoader && (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as XLSX from "xlsx";
|
||||
import Parse from "parse";
|
||||
import { emailRegex } from "../../constant/const";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const ImportContact = ({ setLoader, onImport, showAlert }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -178,7 +179,7 @@ const ImportContact = ({ setLoader, onImport, showAlert }) => {
|
||||
}
|
||||
};
|
||||
// `handleImportData` is used to create batch in contact
|
||||
const handleImportData = async (e) => {
|
||||
const handleImportData = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setLoader(true);
|
||||
@@ -213,10 +214,10 @@ const ImportContact = ({ setLoader, onImport, showAlert }) => {
|
||||
setImportedData([]);
|
||||
setInvalidRecords(0);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleImportData} className="p-[20px] h-full ">
|
||||
<form onSubmit={handleImportData} className="p-[20px] h-full">
|
||||
<div className="text-xs">
|
||||
<label className="block ml-2">
|
||||
{t("contacts-file")}
|
||||
|
||||
@@ -11,7 +11,6 @@ import Parse from "parse";
|
||||
import {
|
||||
copytoData,
|
||||
fetchUrl,
|
||||
formatDate,
|
||||
getSignedUrl,
|
||||
getTenantDetails,
|
||||
handleSignatureType,
|
||||
@@ -29,7 +28,7 @@ import EditorToolbar, {
|
||||
} from "../../components/pdf/EditorToolbar";
|
||||
import ReactQuill from "react-quill-new";
|
||||
import "../../styles/quill.css";
|
||||
import BulkSendUi from "../../components/BulkSendUi";
|
||||
import BulkSendUi from "../../components/bulksend/BulkSendUi";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { serverUrl_fn } from "../../constant/appinfo";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -46,13 +45,12 @@ const DocumentsReport = (props) => {
|
||||
const copyUrlRef = useRef(null);
|
||||
const titleRef = useRef(null);
|
||||
const titleElement = useElSize(titleRef);
|
||||
const journey = "Use Template";
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const { prefillImg, isBulkLoader } = useSelector((state) => state.widget);
|
||||
const isDashboard =
|
||||
location?.pathname === "/dashboard/35KBoSgoAK" ? true : false;
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -187,7 +185,7 @@ const DocumentsReport = (props) => {
|
||||
}, [isMoreDocs, pageNumbers, currentPage, setIsNextRecord]);
|
||||
|
||||
//function to fetch tenant Details
|
||||
const fetchTenantDetails = async () => {
|
||||
const fetchTenantDetails = utils.withSessionValidation(async () => {
|
||||
const user = JSON.parse(
|
||||
localStorage.getItem(
|
||||
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
|
||||
@@ -226,14 +224,14 @@ const DocumentsReport = (props) => {
|
||||
} else {
|
||||
alert(t("user-not-exist"));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// `handleURL` is used to open microapp
|
||||
const handleURL = async (item, act) => {
|
||||
navigate(`/${act.redirectUrl}?docId=${item?.objectId}`);
|
||||
};
|
||||
|
||||
const fetchTemplate = async (templateId) => {
|
||||
const fetchTemplate = utils.withSessionValidation(async (templateId) => {
|
||||
try {
|
||||
const params = {
|
||||
templateId: templateId,
|
||||
@@ -254,29 +252,30 @@ const DocumentsReport = (props) => {
|
||||
return axiosRes;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error to fetch template in report", e);
|
||||
console.error("fetch template in report error", e);
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
console.log("Error to fetch template in report", e);
|
||||
}
|
||||
};
|
||||
});
|
||||
//function is called when there ther no any prefill role widget exist then create direct document and navigate
|
||||
const navigatePageToDoc = async (templateRes, placeholder, signer) => {
|
||||
setIsPrefillModal({});
|
||||
const res = await createDocument(
|
||||
[templateRes || templateDeatils],
|
||||
placeholder || xyPosition,
|
||||
signer || signerList,
|
||||
templateRes?.URL || templateDeatils?.URL
|
||||
);
|
||||
if (res.status === "success") {
|
||||
navigate(`/placeHolderSign/${res.id}`, {
|
||||
state: { title: "Use Template" }
|
||||
});
|
||||
} else {
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
const navigatePageToDoc = utils.withSessionValidation(
|
||||
async (templateRes, placeholder, signer) => {
|
||||
setIsPrefillModal({});
|
||||
const res = await createDocument(
|
||||
[templateRes || templateDeatils],
|
||||
placeholder || xyPosition,
|
||||
signer || signerList,
|
||||
templateRes?.URL || templateDeatils?.URL,
|
||||
);
|
||||
if (res.status === "success") {
|
||||
navigate(`/placeHolderSign/${res.id}`, {
|
||||
state: { title: "Use Template" }
|
||||
});
|
||||
} else {
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
const handleUseTemplate = async (templateId, item) => {
|
||||
try {
|
||||
const templateDeatils = await fetchTemplate(templateId);
|
||||
@@ -300,12 +299,12 @@ const DocumentsReport = (props) => {
|
||||
setActLoader({});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
console.error("use template error", err);
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
const handleActionBtn = async (act, item) => {
|
||||
const handleActionBtn = utils.withSessionValidation(async (act, item) => {
|
||||
if (act.action === "redirect") {
|
||||
handleURL(item, act);
|
||||
} else if (act.action === "delete") {
|
||||
@@ -331,7 +330,7 @@ const DocumentsReport = (props) => {
|
||||
} else if (act.action === "extendexpiry") {
|
||||
setIsModal({ [`extendexpiry_${item.objectId}`]: true });
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Get current list
|
||||
@@ -354,7 +353,7 @@ const DocumentsReport = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
const handleDelete = utils.withSessionValidation(async (item) => {
|
||||
setIsDeleteModal({});
|
||||
setActLoader({ [`${item.objectId}`]: true });
|
||||
try {
|
||||
@@ -378,11 +377,11 @@ const DocumentsReport = (props) => {
|
||||
props.setList(upldatedList);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
console.error("delete document error", err);
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleClose = () => {
|
||||
setIsRevoke({});
|
||||
setIsDeleteModal({});
|
||||
@@ -431,7 +430,7 @@ const DocumentsReport = (props) => {
|
||||
setCopied({ [email]: true });
|
||||
};
|
||||
//function to handle revoke/decline docment
|
||||
const handleRevoke = async (item) => {
|
||||
const handleRevoke = utils.withSessionValidation(async (item) => {
|
||||
const senderUser = localStorage.getItem(
|
||||
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
|
||||
);
|
||||
@@ -474,12 +473,12 @@ const DocumentsReport = (props) => {
|
||||
setReason("");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("err", err);
|
||||
console.error("decline document error", err);
|
||||
setReason("");
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// `handleDownload` is used to get valid doc url available in completed report
|
||||
const handleDownload = async (item) => {
|
||||
@@ -514,7 +513,7 @@ const DocumentsReport = (props) => {
|
||||
}
|
||||
setActLoader({});
|
||||
} catch (err) {
|
||||
console.log("err in getsignedurl", err);
|
||||
console.error("getsignedurl error", err);
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
}
|
||||
@@ -637,7 +636,7 @@ const DocumentsReport = (props) => {
|
||||
setMail((prev) => ({ ...prev, subject: res.subject, body: res.body }));
|
||||
setIsNextStep({ [user.Id]: true });
|
||||
};
|
||||
const handleResendMail = async (e, doc, user) => {
|
||||
const handleResendMail = utils.withSessionValidation(async (e, doc, user) => {
|
||||
e.preventDefault();
|
||||
setActLoader({ [user?.Id]: true });
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/sendmailv3`;
|
||||
@@ -667,14 +666,14 @@ const DocumentsReport = (props) => {
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err in sendmail", err);
|
||||
console.error("sendmail error", err);
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
} finally {
|
||||
setIsNextStep({});
|
||||
setUserDetails({});
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
const fetchUserStatus = (user, doc) => {
|
||||
const email = user.email ? user.email : user.signerPtr.Email;
|
||||
const audit = doc?.AuditTrail?.find((x) => x.UserPtr.Email === email);
|
||||
@@ -709,7 +708,7 @@ const DocumentsReport = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateExpiry = async (e, item) => {
|
||||
const handleUpdateExpiry = utils.withSessionValidation(async (e, item) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (expiryDate) {
|
||||
@@ -747,7 +746,7 @@ const DocumentsReport = (props) => {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
console.error("update expiry doc error", err);
|
||||
showAlert("danger", t("something-went-wrong-mssg"), 2000);
|
||||
} finally {
|
||||
setActLoader({});
|
||||
@@ -760,10 +759,10 @@ const DocumentsReport = (props) => {
|
||||
} else {
|
||||
showAlert("danger", t("expiry-date-error"), 2000);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// `handleRenameDoc` is used to update document name
|
||||
const handleRenameDoc = async (item) => {
|
||||
const handleRenameDoc = utils.withSessionValidation(async (item) => {
|
||||
setActLoader({ [item.objectId]: true });
|
||||
setIsModal({});
|
||||
const className = "contracts_Document";
|
||||
@@ -783,7 +782,7 @@ const DocumentsReport = (props) => {
|
||||
showAlert("danger", t("something-went-wrong-mssg"), 2000);
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleBtnVisibility = (act, item) => {
|
||||
if (!act.restrictBtn) {
|
||||
return true;
|
||||
@@ -799,18 +798,18 @@ const DocumentsReport = (props) => {
|
||||
setError("");
|
||||
setIsModal({});
|
||||
};
|
||||
const handleSaveAsTemplate = async (doc) => {
|
||||
const handleSaveAsTemplate = utils.withSessionValidation(async (doc) => {
|
||||
try {
|
||||
const params = { docId: doc?.objectId };
|
||||
const templateRes = await Parse.Cloud.run("saveastemplate", params);
|
||||
setTemplateId(templateRes?.id);
|
||||
setIsSuccess({ [doc.objectId]: true });
|
||||
} catch (err) {
|
||||
console.log("Err in saveastemplate", err);
|
||||
console.error("saveastemplate error", err);
|
||||
} finally {
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleCloseTemplate = () => {
|
||||
setTemplateId("");
|
||||
setIsSuccess({});
|
||||
@@ -821,20 +820,13 @@ const DocumentsReport = (props) => {
|
||||
|
||||
// `handleBulkSendTemplate` is used to open modal as well as fetch template
|
||||
// and show Ui on the basis template response
|
||||
const handleBulkSendTemplate = async (templateId, docId) => {
|
||||
setIsBulkSend({ [docId]: true });
|
||||
setIsLoader({ [docId]: true });
|
||||
try {
|
||||
const axiosRes = await fetchTemplate(templateId);
|
||||
const templateRes = axiosRes.data && axiosRes.data.result;
|
||||
const isPrefillExist = templateRes?.Placeholders.some(
|
||||
(x) => x.Role === "prefill"
|
||||
);
|
||||
if (isPrefillExist) {
|
||||
setIsBulkSend({});
|
||||
setIsLoader({});
|
||||
showAlert("danger", t("prefill-bulk-error"));
|
||||
} else {
|
||||
const handleBulkSendTemplate = utils.withSessionValidation(
|
||||
async (templateId, docId) => {
|
||||
setIsBulkSend({ [docId]: true });
|
||||
setIsLoader({ [docId]: true });
|
||||
try {
|
||||
const axiosRes = await fetchTemplate(templateId);
|
||||
const templateRes = axiosRes.data && axiosRes.data.result;
|
||||
const tenantSignTypes = await fetchTenantDetails();
|
||||
const docSignTypes = templateRes?.SignatureType || signatureTypes;
|
||||
const updatedSignatureType = await handleSignatureType(
|
||||
@@ -845,13 +837,13 @@ const DocumentsReport = (props) => {
|
||||
setPlaceholders(templateRes?.Placeholders);
|
||||
setTemplateDetails(templateRes);
|
||||
setIsLoader({});
|
||||
} catch (err) {
|
||||
console.error("fetch template in bulk modal error", err);
|
||||
setIsBulkSend({});
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err in fetch template in bulk modal", err);
|
||||
setIsBulkSend({});
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
const handleResendClose = () => {
|
||||
setIsResendMail({});
|
||||
@@ -859,7 +851,7 @@ const DocumentsReport = (props) => {
|
||||
setUserDetails({});
|
||||
};
|
||||
|
||||
const handleRecreateDoc = async (item) => {
|
||||
const handleRecreateDoc = utils.withSessionValidation(async (item) => {
|
||||
setActLoader({ [item.objectId]: true });
|
||||
try {
|
||||
const res = await Parse.Cloud.run("recreatedoc", {
|
||||
@@ -872,11 +864,11 @@ const DocumentsReport = (props) => {
|
||||
handleCloseModal();
|
||||
showAlert("danger", err.message);
|
||||
// showAlert("danger", t("something-went-wrong-mssg"));
|
||||
console.log("Err while create duplicate template", err);
|
||||
console.error("create duplicate template error", err);
|
||||
} finally {
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const restrictBtn = (item, act) => {
|
||||
return item.IsSignyourself && act.action === "recreatedocument"
|
||||
@@ -944,12 +936,14 @@ const DocumentsReport = (props) => {
|
||||
);
|
||||
if (res?.status === "unfilled") {
|
||||
const emptyWidget = res?.emptyResponseObjects
|
||||
.map((item) => `[ ${item.options.name}]`)
|
||||
.join(", ");
|
||||
?.map((item) => item.options.name)
|
||||
?.join(", ");
|
||||
const timeInMiliSec = 6000;
|
||||
showAlert(
|
||||
"danger",
|
||||
t("prefill-unfilled-widget", { emptyWidget: emptyWidget }),
|
||||
t("prefill-unfilled-widget", {
|
||||
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
|
||||
}),
|
||||
timeInMiliSec
|
||||
);
|
||||
} else if (res?.status === "unattach signer") {
|
||||
@@ -962,7 +956,7 @@ const DocumentsReport = (props) => {
|
||||
try {
|
||||
await fetchTenantDetails();
|
||||
} catch (e) {
|
||||
console.log("error in fetchTenantDetails", e);
|
||||
console.error("fetchTenantDetails error", e);
|
||||
alert(t("user-not-exist"));
|
||||
}
|
||||
}
|
||||
@@ -1151,6 +1145,7 @@ const DocumentsReport = (props) => {
|
||||
</thead>
|
||||
<tbody className="text-[12px]">
|
||||
{props.List?.length > 0 &&
|
||||
!props.searchLoader &&
|
||||
currentList.map((item, index) => (
|
||||
<tr
|
||||
className={`${
|
||||
@@ -1452,9 +1447,12 @@ const DocumentsReport = (props) => {
|
||||
{isBulkSend[item.objectId] && (
|
||||
<ModalUi
|
||||
isOpen
|
||||
showScrollBar
|
||||
title={
|
||||
t("quick-send")
|
||||
}
|
||||
reduceWidth={"md:min-w-[80%]"}
|
||||
isLoader={isBulkLoader}
|
||||
handleClose={() => setIsBulkSend({})}
|
||||
>
|
||||
{isLoader[item.objectId] ? (
|
||||
@@ -1706,22 +1704,31 @@ const DocumentsReport = (props) => {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{props.List?.length <= 0 && (
|
||||
{(props.searchLoader || props.List?.length <= 0) && (
|
||||
<div
|
||||
className={`${
|
||||
isDashboard ? "h-[317px]" : ""
|
||||
} flex flex-col items-center justify-center w-ful bg-base-100 text-base-content rounded-xl py-4`}
|
||||
>
|
||||
<div className="w-[60px] h-[60px] overflow-hidden">
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={pad}
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
{t("no-data-avaliable")}
|
||||
</div>
|
||||
{props.searchLoader ? (
|
||||
<>
|
||||
<Loader />
|
||||
<div className="text-sm ">{t("loading-mssg")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-[60px] h-[60px] overflow-hidden">
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={pad}
|
||||
alt={t("no-data-avaliable")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
{t("no-data-avaliable")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
handleSignatureType,
|
||||
replaceMailVaribles,
|
||||
signatureTypes,
|
||||
openInNewTab,
|
||||
createDocument,
|
||||
defaultMailBody,
|
||||
defaultMailSubject
|
||||
@@ -27,24 +26,27 @@ import EditorToolbar, {
|
||||
} from "../../components/pdf/EditorToolbar";
|
||||
import ReactQuill from "react-quill-new";
|
||||
import "../../styles/quill.css";
|
||||
import BulkSendUi from "../../components/BulkSendUi";
|
||||
import BulkSendUi from "../../components/bulksend/BulkSendUi";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { serverUrl_fn } from "../../constant/appinfo";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
useTranslation
|
||||
} from "react-i18next";
|
||||
import { useElSize } from "../../hook/useElSize";
|
||||
import LottieWithLoader from "../../primitives/DotLottieReact";
|
||||
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
|
||||
import * as utils from "../../utils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RenderReportCell } from "../../primitives/RenderReportCell";
|
||||
import CustomizeMail from "../../components/pdf/CustomizeMail";
|
||||
import { resetWidgetState } from "../../redux/reducers/widgetSlice";
|
||||
|
||||
const TemplatesReport = (props) => {
|
||||
const copyUrlRef = useRef(null);
|
||||
const titleRef = useRef(null);
|
||||
const journey = "Use Template";
|
||||
const dispatch = useDispatch();
|
||||
const titleElement = useElSize(titleRef);
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const { prefillImg, isBulkLoader } = useSelector((state) => state.widget);
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const { t } = useTranslation();
|
||||
@@ -92,8 +94,9 @@ const TemplatesReport = (props) => {
|
||||
const [isMailModal, setIsMailModal] = useState(false);
|
||||
const [customizeMail, setCustomizeMail] = useState({ body: "", subject: "" });
|
||||
const [defaultMail, setDefaultMail] = useState({ body: "", subject: "" });
|
||||
const [currUserId, setCurrUserId] = useState(false);
|
||||
const [currUserId, setCurrUserId] = useState("");
|
||||
const [documentDetails, setDocumentDetails] = useState();
|
||||
const [docId, setDocId] = useState();
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -160,6 +163,7 @@ const TemplatesReport = (props) => {
|
||||
const pageNumbers = getPaginationRange();
|
||||
// below useEffect reset currenpage to 1 if user change route
|
||||
useEffect(() => {
|
||||
dispatch(resetWidgetState([]));
|
||||
checkTourStatus();
|
||||
fetchTeamList();
|
||||
return () => setCurrentPage(1);
|
||||
@@ -194,7 +198,7 @@ const TemplatesReport = (props) => {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err in fetch top level teamlist", err);
|
||||
console.error("fetch top level teamlist error", err);
|
||||
}
|
||||
};
|
||||
// below useEffect is used to render next record if IsMoreDoc is true
|
||||
@@ -206,7 +210,7 @@ const TemplatesReport = (props) => {
|
||||
}, [isMoreDocs, pageNumbers, currentPage, setIsNextRecord]);
|
||||
|
||||
//function to fetch tenant Details
|
||||
const fetchTenantDetails = async () => {
|
||||
const fetchTenantDetails = utils.withSessionValidation(async () => {
|
||||
const user = JSON.parse(
|
||||
localStorage.getItem(
|
||||
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
|
||||
@@ -230,7 +234,7 @@ const TemplatesReport = (props) => {
|
||||
} else {
|
||||
alert(t("user-not-exist"));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// `handleURL` is used to open microapp
|
||||
const handleURL = async (item, act) => {
|
||||
@@ -276,7 +280,7 @@ const TemplatesReport = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTemplate = async (templateId) => {
|
||||
const fetchTemplate = utils.withSessionValidation(async (templateId) => {
|
||||
try {
|
||||
const params = {
|
||||
templateId: templateId,
|
||||
@@ -297,30 +301,32 @@ const TemplatesReport = (props) => {
|
||||
return axiosRes;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error to fetch template in report", e);
|
||||
console.error("fetch template in report error", e);
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
//function is called when there ther no any prefill role widget exist then create direct document and navigate
|
||||
const navigatePageToDoc = async (templateRes, placeholder, signer) => {
|
||||
setIsPrefillModal({});
|
||||
const res = await createDocument(
|
||||
[templateRes || templateDetails],
|
||||
placeholder || xyPosition,
|
||||
signer || signerList,
|
||||
templateRes?.URL || templateDetails?.URL
|
||||
);
|
||||
if (res.status === "success") {
|
||||
navigate(`/placeHolderSign/${res.id}`, {
|
||||
state: { title: "Use Template" }
|
||||
});
|
||||
} else {
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
const navigatePageToDoc = utils.withSessionValidation(
|
||||
async (templateRes, placeholder, signer) => {
|
||||
setIsPrefillModal({});
|
||||
const res = await createDocument(
|
||||
[templateRes || templateDetails],
|
||||
placeholder || xyPosition,
|
||||
signer || signerList,
|
||||
templateRes?.URL || templateDetails?.URL,
|
||||
);
|
||||
if (res.status === "success") {
|
||||
navigate(`/placeHolderSign/${res.id}`, {
|
||||
state: { title: "Use Template" }
|
||||
});
|
||||
} else {
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
const handleActionBtn = async (act, item) => {
|
||||
const handleActionBtn = utils.withSessionValidation(async (act, item) => {
|
||||
setIsTour(false);
|
||||
if (act.action === "redirect") {
|
||||
handleURL(item, act);
|
||||
@@ -362,7 +368,7 @@ const TemplatesReport = (props) => {
|
||||
} else if (act.action === "extendexpiry") {
|
||||
setIsModal({ [`extendexpiry_${item.objectId}`]: true });
|
||||
}
|
||||
};
|
||||
});
|
||||
// Get current list
|
||||
const indexOfLastDoc = currentPage * props.docPerPage;
|
||||
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
|
||||
@@ -383,7 +389,7 @@ const TemplatesReport = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
const handleDelete = utils.withSessionValidation(async (item) => {
|
||||
setIsDeleteModal({});
|
||||
setActLoader({ [`${item.objectId}`]: true });
|
||||
try {
|
||||
@@ -407,11 +413,11 @@ const TemplatesReport = (props) => {
|
||||
props.setList(upldatedList);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
console.error("delete template error", err);
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
const handleClose = (
|
||||
) => {
|
||||
setIsDeleteModal({});
|
||||
@@ -534,7 +540,7 @@ const TemplatesReport = (props) => {
|
||||
|
||||
setActLoader({});
|
||||
} catch (err) {
|
||||
console.log("err in getsignedurl", err);
|
||||
console.error("getsignedurl error", err);
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
setActLoader({});
|
||||
}
|
||||
@@ -608,7 +614,7 @@ const TemplatesReport = (props) => {
|
||||
};
|
||||
// `handleNextBtn` is used to open edit mail template screen in resend mail modal
|
||||
// as well as replace variable with original one
|
||||
const handleNextBtn = (user, doc) => {
|
||||
const handleNextBtn = utils.withSessionValidation((user, doc) => {
|
||||
const userdata = {
|
||||
Name: user?.signerPtr?.Name,
|
||||
Email: user.email ? user?.email : user.signerPtr?.Email,
|
||||
@@ -655,8 +661,8 @@ const TemplatesReport = (props) => {
|
||||
const res = replaceMailVaribles(subject, body, variables);
|
||||
setMail((prev) => ({ ...prev, subject: res.subject, body: res.body }));
|
||||
setIsNextStep({ [user.Id]: true });
|
||||
};
|
||||
const handleResendMail = async (e, doc, user) => {
|
||||
});
|
||||
const handleResendMail = utils.withSessionValidation(async (e, doc, user) => {
|
||||
e.preventDefault();
|
||||
setActLoader({ [user?.Id]: true });
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/sendmailv3`;
|
||||
@@ -686,14 +692,14 @@ const TemplatesReport = (props) => {
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err in sendmail", err);
|
||||
console.error("sendmail error", err);
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
} finally {
|
||||
setIsNextStep({});
|
||||
setUserDetails({});
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
const fetchUserStatus = (user, doc) => {
|
||||
const email = user.email ? user.email : user.signerPtr.Email;
|
||||
const audit = doc?.AuditTrail?.find((x) => x.UserPtr.Email === email);
|
||||
@@ -731,38 +737,31 @@ const TemplatesReport = (props) => {
|
||||
// `handleBulkSend` is used to open modal as well as fetch template
|
||||
// and show Ui on the basis template response handleBulkSendTemplate
|
||||
const handleBulkSend = async (template) => {
|
||||
const isPrefillExist = template?.Placeholders?.some(
|
||||
(x) => x.Role === "prefill"
|
||||
);
|
||||
if (isPrefillExist) {
|
||||
showAlert("danger", t("prefill-bulk-error"));
|
||||
} else {
|
||||
setIsBulkSend({ [template.objectId]: true });
|
||||
setIsLoader({ [template.objectId]: true });
|
||||
try {
|
||||
const axiosRes = await fetchTemplate(template.objectId);
|
||||
const templateRes = axiosRes.data && axiosRes.data.result;
|
||||
const tenantSignTypes = await fetchTenantDetails();
|
||||
const docSignTypes = templateRes?.SignatureType || signatureTypes;
|
||||
const updatedSignatureType = await handleSignatureType(
|
||||
tenantSignTypes,
|
||||
docSignTypes
|
||||
);
|
||||
setSignatureType(updatedSignatureType);
|
||||
setPlaceholders(templateRes?.Placeholders);
|
||||
setTemplateDetails(templateRes);
|
||||
setIsLoader({});
|
||||
} catch (err) {
|
||||
console.log("err in fetch template in bulk modal", err);
|
||||
setIsBulkSend({});
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
setIsBulkSend({ [template.objectId]: true });
|
||||
setIsLoader({ [template.objectId]: true });
|
||||
try {
|
||||
const axiosRes = await fetchTemplate(template.objectId);
|
||||
const templateRes = axiosRes.data && axiosRes.data.result;
|
||||
const tenantSignTypes = await fetchTenantDetails();
|
||||
const docSignTypes = templateRes?.SignatureType || signatureTypes;
|
||||
const updatedSignatureType = await handleSignatureType(
|
||||
tenantSignTypes,
|
||||
docSignTypes
|
||||
);
|
||||
setSignatureType(updatedSignatureType);
|
||||
setPlaceholders(templateRes?.Placeholders);
|
||||
setTemplateDetails(templateRes);
|
||||
setIsLoader({});
|
||||
} catch (err) {
|
||||
console.error("fetch template in bulk modal err", err);
|
||||
setIsBulkSend({});
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// `handleShareWith` is used to save teams in sharedWith field
|
||||
const handleShareWith = async (e, template) => {
|
||||
const handleShareWith = utils.withSessionValidation(async (e, template) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsShareWith({});
|
||||
@@ -785,10 +784,10 @@ const TemplatesReport = (props) => {
|
||||
} finally {
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// `handleCreateDuplicate` is used to create duplicate from current entry using objectId
|
||||
const handleCreateDuplicate = async (item) => {
|
||||
const handleCreateDuplicate = utils.withSessionValidation(async (item) => {
|
||||
setActLoader({ [item.objectId]: true });
|
||||
setIsModal({});
|
||||
try {
|
||||
@@ -802,13 +801,13 @@ const TemplatesReport = (props) => {
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert("danger", t("something-went-wrong-mssg"));
|
||||
console.log("Err while create duplicate template", err);
|
||||
console.error("create duplicate template error", err);
|
||||
} finally {
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
// `handleRenameDoc` is used to update document name
|
||||
const handleRenameDoc = async (item) => {
|
||||
const handleRenameDoc = utils.withSessionValidation(async (item) => {
|
||||
setActLoader({ [item.objectId]: true });
|
||||
setIsModal({});
|
||||
const className = "contracts_Template";
|
||||
@@ -829,7 +828,7 @@ const TemplatesReport = (props) => {
|
||||
showAlert("danger", t("something-went-wrong-mssg"), 2000);
|
||||
setActLoader({});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModal({});
|
||||
@@ -885,7 +884,7 @@ const TemplatesReport = (props) => {
|
||||
setXyPosition([]);
|
||||
};
|
||||
//`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document
|
||||
const handlePrefillWidgetCreateDoc = async () => {
|
||||
const handlePrefillWidgetCreateDoc = utils.withSessionValidation(async () => {
|
||||
setIsSubmit(true);
|
||||
const scale = 1;
|
||||
const res = await utils?.handleCheckPrefillCreateDoc(
|
||||
@@ -900,12 +899,14 @@ const TemplatesReport = (props) => {
|
||||
);
|
||||
if (res?.status === "unfilled") {
|
||||
const emptyWidget = res?.emptyResponseObjects
|
||||
.map((item) => `[ ${item.options.name}]`)
|
||||
.join(", ");
|
||||
?.map((item) => item.options.name)
|
||||
?.join(", ");
|
||||
const timeInMiliSec = 6000;
|
||||
showAlert(
|
||||
"danger",
|
||||
t("prefill-unfilled-widget", { emptyWidget: emptyWidget }),
|
||||
t("prefill-unfilled-widget", {
|
||||
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
|
||||
}),
|
||||
timeInMiliSec
|
||||
);
|
||||
} else if (res?.status === "unattach signer") {
|
||||
@@ -913,12 +914,27 @@ const TemplatesReport = (props) => {
|
||||
} else if (res?.status === "success") {
|
||||
setDocumentId(res.id);
|
||||
setActLoader({});
|
||||
setIsMailModal(true);
|
||||
const user = JSON.parse(
|
||||
localStorage.getItem(
|
||||
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
|
||||
)
|
||||
);
|
||||
const ownerId = templateDetails.ExtUserPtr?.UserId?.objectId;
|
||||
const firstSigner = signerList[0];
|
||||
const isOwner = firstSigner?.UserId?.objectId === ownerId;
|
||||
setDocId(res.id);
|
||||
if (templateDetails?.SendinOrder && isOwner) {
|
||||
setCurrUserId(firstSigner?.objectId);
|
||||
setIsSend(true);
|
||||
} else {
|
||||
setIsMailModal(true);
|
||||
const currentSigner = signerList?.find(
|
||||
(x) => x?.UserId?.objectId === ownerId
|
||||
);
|
||||
if (currentSigner) {
|
||||
setCurrUserId(currentSigner?.objectId);
|
||||
}
|
||||
}
|
||||
if (user) {
|
||||
try {
|
||||
const tenantDetails = await getTenantDetails(user?.objectId);
|
||||
@@ -949,7 +965,7 @@ const TemplatesReport = (props) => {
|
||||
}
|
||||
}
|
||||
setIsSubmit(false);
|
||||
};
|
||||
});
|
||||
//function show signer list and share link to share signUrl
|
||||
const handleShareList = () => {
|
||||
const shareLinkList = [];
|
||||
@@ -1130,6 +1146,7 @@ const TemplatesReport = (props) => {
|
||||
</thead>
|
||||
<tbody className="text-[12px]">
|
||||
{props.List?.length > 0 &&
|
||||
!props.searchLoader &&
|
||||
currentList.map((item, index) => (
|
||||
<tr
|
||||
className={`${
|
||||
@@ -1332,9 +1349,12 @@ const TemplatesReport = (props) => {
|
||||
{isBulkSend[item.objectId] && (
|
||||
<ModalUi
|
||||
isOpen
|
||||
showScrollBar
|
||||
title={
|
||||
t("quick-send")
|
||||
}
|
||||
reduceWidth={"md:min-w-[80%]"}
|
||||
isLoader={isBulkLoader}
|
||||
handleClose={() => setIsBulkSend({})}
|
||||
>
|
||||
{isLoader[item.objectId] ? (
|
||||
@@ -1542,22 +1562,31 @@ const TemplatesReport = (props) => {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{props.List?.length <= 0 && (
|
||||
{(props.searchLoader || props.List?.length <= 0) && (
|
||||
<div
|
||||
className={`${
|
||||
isDashboard ? "h-[317px]" : ""
|
||||
} flex flex-col items-center justify-center w-ful bg-base-100 text-base-content rounded-xl py-4`}
|
||||
>
|
||||
<div className="w-[60px] h-[60px] overflow-hidden">
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={pad}
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
{t("no-data-avaliable")}
|
||||
</div>
|
||||
{props.searchLoader ? (
|
||||
<>
|
||||
<Loader />
|
||||
<div className="text-sm ">{t("loading-mssg")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-[60px] h-[60px] overflow-hidden">
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={pad}
|
||||
alt={t("no-data-avaliable")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-semibold">
|
||||
{t("no-data-avaliable")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1608,11 +1637,13 @@ const TemplatesReport = (props) => {
|
||||
<ModalUi
|
||||
isOpen={isSend}
|
||||
title={
|
||||
mailStatus === "success"
|
||||
? t("mails-sent")
|
||||
: mailStatus === "quotareached"
|
||||
? t("quota-mail-head")
|
||||
: t("mail-not-delivered")
|
||||
!templateDetails?.SendinOrder
|
||||
? mailStatus === "success"
|
||||
? t("mails-sent")
|
||||
: mailStatus === "quotareached"
|
||||
? t("quota-mail-head")
|
||||
: t("mail-not-delivered")
|
||||
: t("mail-status-head")
|
||||
}
|
||||
handleClose={() => {
|
||||
setIsSend(false);
|
||||
@@ -1620,68 +1651,102 @@ const TemplatesReport = (props) => {
|
||||
}}
|
||||
>
|
||||
<div className="h-[100%] p-[20px] text-base-content">
|
||||
{mailStatus === "success" ? (
|
||||
<div className="text-center mb-[10px]">
|
||||
<LottieWithLoader />
|
||||
{documentDetails.SendinOrder ? (
|
||||
<p>
|
||||
{currUserId
|
||||
? t("placeholder-mail-alert-you")
|
||||
: t("placeholder-mail-alert", {
|
||||
name: signerList[0]?.Name
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-5">
|
||||
<div>
|
||||
{mailStatus === "success" ? (
|
||||
<div className="text-center mb-[10px]">
|
||||
<LottieWithLoader />
|
||||
{documentDetails.SendinOrder ? (
|
||||
<p>
|
||||
{currUserId
|
||||
? t("placeholder-mail-alert-you")
|
||||
: t("placeholder-mail-alert", {
|
||||
name: signerList[0]?.Name
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-4")}</p>
|
||||
)}
|
||||
</div>
|
||||
) : mailStatus === "quotareached" ? (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="my-3">{handleShareList()}</div>
|
||||
</div>
|
||||
) : mailStatus === "failed" ? (
|
||||
<p>{t("mail-failed")} </p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-4")}</p>
|
||||
<div className="mb-[10px]">
|
||||
{!templateDetails?.SendinOrder &&
|
||||
(mailStatus === "dailyquotareached" ? (
|
||||
<p>{t("daily-quota-reached")}</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-6")}</p>
|
||||
))}
|
||||
{currUserId && (
|
||||
<span className="mt-1">{t("placeholder-alert-5")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currUserId && <p>{t("placeholder-alert-5")}</p>}
|
||||
</div>
|
||||
) : mailStatus === "quotareached" ? (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="my-3">{handleShareList()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-[10px]">
|
||||
{mailStatus === "dailyquotareached" ? (
|
||||
<p>{t("daily-quota-reached")}</p>
|
||||
) : (
|
||||
<p>{t("placeholder-alert-6")}</p>
|
||||
)}
|
||||
{currUserId && (
|
||||
<p className="mt-1">{t("placeholder-alert-5")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!mailStatus && (
|
||||
<div className="w-full h-[1px] bg-[#9f9f9f] my-[15px]"></div>
|
||||
)}
|
||||
{mailStatus !== "quotareached" && (
|
||||
<div
|
||||
className={
|
||||
mailStatus === "success" ? "flex justify-center mt-1" : ""
|
||||
}
|
||||
>
|
||||
{currUserId && (
|
||||
<button
|
||||
onClick={() =>
|
||||
handleRecipientSign(documentDetails?.objectId, currUserId)
|
||||
|
||||
{mailStatus !== "quotareached" && mailStatus !== "failed" && (
|
||||
<div
|
||||
className={
|
||||
mailStatus === "success"
|
||||
? "flex justify-center mt-1"
|
||||
: "flex items-center justify-center mt-7"
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary mr-1"
|
||||
>
|
||||
{t("yes")}
|
||||
</button>
|
||||
{currUserId && (
|
||||
<button
|
||||
onClick={() => handleRecipientSign(docId, currUserId)}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary mr-1"
|
||||
>
|
||||
{t("sign-now")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
}}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
>
|
||||
{currUserId ? t("no") : t("close")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
}}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
>
|
||||
{currUserId ? t("no") : t("close")}
|
||||
</button>
|
||||
</div>
|
||||
{mailStatus !== "success" &&
|
||||
currUserId &&
|
||||
templateDetails?.SendinOrder && (
|
||||
<div className="op-divider text-base-content mx-[0%] my-1 font-medium">
|
||||
{t("or")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mailStatus !== "success" &&
|
||||
currUserId &&
|
||||
templateDetails?.SendinOrder && (
|
||||
<>
|
||||
<div
|
||||
className="op-btn op-btn-outline w-[50%] md:w-[35%] mt-1"
|
||||
onClick={() => {
|
||||
setIsSend(false);
|
||||
setIsMailModal(true);
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="fa-regular fa-envelope"
|
||||
style={{ color: "#002864", fontSize: "19px" }}
|
||||
></i>{" "}
|
||||
<span>{t("send-to-email")}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!mailStatus && (
|
||||
<div className="op-divider text-base-content mx-[0%] mt-3"></div>
|
||||
)}
|
||||
</div>
|
||||
</ModalUi>
|
||||
|
||||
@@ -31,7 +31,10 @@
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.prefillCanvas {
|
||||
width: 205px;
|
||||
height: 81px;
|
||||
}
|
||||
.signatureCanvas {
|
||||
width: 440px;
|
||||
height: 167px;
|
||||
@@ -44,8 +47,20 @@
|
||||
}
|
||||
|
||||
.mysignatureCanvas {
|
||||
width: 456px;
|
||||
height: 180px;
|
||||
width: 370px;
|
||||
height: 146px;
|
||||
}
|
||||
|
||||
.mystampCanvas {
|
||||
width: 370px;
|
||||
height: 146px;
|
||||
}
|
||||
|
||||
.intialSignature {
|
||||
border: 2px solid #888;
|
||||
background-color: rgb(255, 255, 255);
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
[data-theme="opensigndark"] .tabWidth {
|
||||
@@ -70,13 +85,6 @@
|
||||
/* blue-500 */
|
||||
}
|
||||
|
||||
.intialSignature {
|
||||
border: 2px solid #888;
|
||||
background-color: rgb(255, 255, 255);
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
/* Dark mode support for initials box in /managesign */
|
||||
[data-theme="opensigndark"] .intialSignature {
|
||||
background-color: #1f2937 !important;
|
||||
@@ -117,10 +125,6 @@
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
.penContainerDefault {
|
||||
width: 460px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
@@ -450,7 +454,7 @@ option {
|
||||
to prevent sudden quick movement (as the
|
||||
navigation bar gets a new position at the top of the
|
||||
page (position:fixed and top:0) */
|
||||
.stickyHead+.content {
|
||||
.stickyHead + .content {
|
||||
padding-top: 60px;
|
||||
}
|
||||
}
|
||||
@@ -466,11 +470,12 @@ option {
|
||||
height: 118px;
|
||||
}
|
||||
|
||||
.tabWidth {
|
||||
.mystampCanvas {
|
||||
width: 300px;
|
||||
height: 118px;
|
||||
}
|
||||
|
||||
.penContainerDefault {
|
||||
.tabWidth {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@@ -498,18 +503,17 @@ option {
|
||||
width: 280px;
|
||||
height: 111px;
|
||||
}
|
||||
.mystampCanvas {
|
||||
width: 280px;
|
||||
height: 111px;
|
||||
}
|
||||
|
||||
.tabWidth {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.penContainerDefault {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.signTab {
|
||||
font-weight: 500 !important;
|
||||
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
@@ -528,12 +532,12 @@ option {
|
||||
width: 230px;
|
||||
height: 91px;
|
||||
}
|
||||
|
||||
.tabWidth {
|
||||
.mystampCanvas {
|
||||
width: 230px;
|
||||
height: 91px;
|
||||
}
|
||||
|
||||
.penContainerDefault {
|
||||
.tabWidth {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
@@ -545,4 +549,4 @@ option {
|
||||
.uploadImgLogo {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,3 +123,56 @@ export function buildDownloadFilename(formatId, ctx) {
|
||||
const safeExt = ext.replace(/\.+/g, "").toLowerCase() || "pdf";
|
||||
return `${stem}.${safeExt}`;
|
||||
}
|
||||
|
||||
export function parseCSV(text) {
|
||||
const rows = [];
|
||||
let row = [];
|
||||
let field = "";
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
const next = text[i + 1];
|
||||
|
||||
if (ch === '"') {
|
||||
// If inside quotes and next is also quote => escaped quote
|
||||
if (inQuotes && next === '"') {
|
||||
field += '"';
|
||||
i++; // skip next quote
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "," && !inQuotes) {
|
||||
row.push(field.trim());
|
||||
field = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((ch === "\n" || ch === "\r") && !inQuotes) {
|
||||
// handle CRLF (\r\n)
|
||||
if (ch === "\r" && next === "\n") i++;
|
||||
|
||||
row.push(field.trim());
|
||||
field = "";
|
||||
|
||||
// ignore completely empty lines
|
||||
const hasAny = row.some((c) => c !== "");
|
||||
if (hasAny) rows.push(row);
|
||||
|
||||
row = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
field += ch;
|
||||
}
|
||||
|
||||
// last field
|
||||
row.push(field.trim());
|
||||
const hasAny = row.some((c) => c !== "");
|
||||
if (hasAny) rows.push(row);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./sanitizeFileName";
|
||||
export * from "./lazyWithRetry";
|
||||
export * from "./upgradeProgress";
|
||||
export * from "./widgetUtils";
|
||||
export * from "./withSessionValidation";
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
generatePdfName,
|
||||
embedWidgetsToDoc,
|
||||
randomId,
|
||||
getBase64FromUrl
|
||||
getBase64FromUrl,
|
||||
drawWidget
|
||||
} from "../constant/Utils";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
|
||||
@@ -44,6 +45,9 @@ export const handleEmbedPrefillToDoc = async (
|
||||
scale,
|
||||
prefillImg
|
||||
);
|
||||
if (pdfBase64?.error) {
|
||||
return { error: pdfBase64.error };
|
||||
}
|
||||
const pdfName = generatePdfName(16);
|
||||
const pdfUrl = await convertBase64ToFile(pdfName, pdfBase64);
|
||||
const tenantId = localStorage.getItem("TenantId");
|
||||
@@ -98,9 +102,9 @@ export const isValidPrefill = (prefillData) => {
|
||||
const emptyResponseObjects = getPlaceholder.flatMap((page) =>
|
||||
page.pos.filter(
|
||||
(item) =>
|
||||
item?.type !== "checkbox" &&
|
||||
!item?.options?.defaultValue &&
|
||||
!item?.options?.response
|
||||
!item?.options?.response &&
|
||||
item?.options?.status === "required"
|
||||
)
|
||||
);
|
||||
if (emptyResponseObjects.length > 0) {
|
||||
@@ -161,6 +165,8 @@ export const handleCheckPrefillCreateDoc = async (
|
||||
);
|
||||
if (res.status === "success") {
|
||||
return res;
|
||||
} else if (res.status === "error") {
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
const res = { status: "unattach signer" };
|
||||
@@ -173,21 +179,29 @@ export const savePrefillImg = async (Placeholders) => {
|
||||
const prefillData = Placeholders?.find((x) => x.Role === "prefill");
|
||||
if (prefillData) {
|
||||
const allImageFields = prefillData?.placeHolder.flatMap((p) =>
|
||||
p.pos.filter((item) => item.type === "image")
|
||||
p.pos.filter((item) => item.type === "image" || item.type === drawWidget)
|
||||
);
|
||||
const hasImageType = allImageFields.some(
|
||||
(p) => p.type === "image" || p.type === drawWidget
|
||||
);
|
||||
const hasImageType = allImageFields.some((p) => p.type === "image");
|
||||
if (hasImageType) {
|
||||
const imgArr = [];
|
||||
for (const ph of prefillData?.placeHolder || []) {
|
||||
for (const pos of ph?.pos || []) {
|
||||
if (pos?.type === "image" && pos?.SignUrl) {
|
||||
if (
|
||||
(pos?.type === "image" || pos?.type === drawWidget) &&
|
||||
pos?.options?.response
|
||||
) {
|
||||
const addSuffix = true;
|
||||
const base64 = await getBase64FromUrl(pos?.SignUrl, addSuffix);
|
||||
const base64 = await getBase64FromUrl(
|
||||
pos?.options?.response,
|
||||
addSuffix
|
||||
);
|
||||
imgArr.push({
|
||||
id: pos?.key,
|
||||
base64: base64
|
||||
});
|
||||
} else if (pos?.type === "image") {
|
||||
} else if (pos?.type === "image" || pos?.type === drawWidget) {
|
||||
imgArr.push({
|
||||
id: pos?.key,
|
||||
base64: ""
|
||||
@@ -221,3 +235,5 @@ export const handleSignersList = (item) => {
|
||||
});
|
||||
return updatedSigners;
|
||||
};
|
||||
|
||||
export const normalizeKey = (value) => value?.trim()?.toLowerCase() || "";
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import { generateTitleFromFilename } from "../constant/Utils";
|
||||
import moment from "moment";
|
||||
import {
|
||||
generateTitleFromFilename,
|
||||
selectFormat,
|
||||
changeDateToMomentFormat
|
||||
} from "../constant/Utils";
|
||||
import { base64StringtoFile, uploadFile } from "./fileUtils";
|
||||
import { sanitizeFileName } from "./sanitizeFileName";
|
||||
import Parse from "parse";
|
||||
export const dateFormat = [
|
||||
"L",
|
||||
"MM-DD-YYYY",
|
||||
"MM.DD.YYYY",
|
||||
"DD/MM/YYYY",
|
||||
"DD-MM-YYYY",
|
||||
"DD.MM.YYYY",
|
||||
"YYYY-MM-DD",
|
||||
"MMM DD, YYYY",
|
||||
"LL",
|
||||
"DD MMM, YYYY",
|
||||
"DD MMMM, YYYY"
|
||||
];
|
||||
// `handlesavesign` is used to save signature, initials, stamp as a default
|
||||
export const saveToMySign = async (widget) => {
|
||||
const base64 = widget?.base64;
|
||||
@@ -13,7 +31,9 @@ export const saveToMySign = async (widget) => {
|
||||
const fileName =
|
||||
widget?.type === "signature"
|
||||
? `${replaceSpace}__sign`
|
||||
: `${replaceSpace}__initials`;
|
||||
: widget?.type === "initials"
|
||||
? `${replaceSpace}__initials`
|
||||
: `${replaceSpace}_stamp`;
|
||||
const file = base64StringtoFile(base64, fileName);
|
||||
const fileUrl = await uploadFile(file, User?.id);
|
||||
// below code is used to save or update default signature, initials, stamp
|
||||
@@ -25,6 +45,8 @@ export const saveToMySign = async (widget) => {
|
||||
params.initials = fileUrl; // save initials image url
|
||||
} else if (widget?.type === "signature") {
|
||||
params.signature = fileUrl; // save signature image url
|
||||
} else if (widget?.type === "stamp") {
|
||||
params.stamp = fileUrl; // save stamp image url
|
||||
}
|
||||
const signRes = await Parse.Cloud.run("savesignature", params);
|
||||
return { base64File: base64, id: signRes?.id };
|
||||
@@ -113,3 +135,244 @@ export const applyNumberFormulasToPages = (pages = []) => {
|
||||
// Return the cloned, updated pages
|
||||
return clonedPages;
|
||||
};
|
||||
|
||||
export const getInitials = (name) => {
|
||||
if (!name) return "";
|
||||
|
||||
// Trim and split by any amount of whitespace
|
||||
const parts = name.trim().split(/\s+/);
|
||||
|
||||
// Only one word → return first letter of that word
|
||||
if (parts.length === 1) {
|
||||
return parts[0][0].toUpperCase();
|
||||
}
|
||||
|
||||
// More than one word → take first and second words
|
||||
const first = parts[0];
|
||||
const second = parts[1];
|
||||
|
||||
return `${first[0].toUpperCase()} ${second[0].toUpperCase()}`;
|
||||
};
|
||||
|
||||
// Function to check if a string is valid Base64 (supports data URL)
|
||||
export const isValidBase64 = (str) => {
|
||||
if (!str || typeof str !== "string") return false;
|
||||
|
||||
// Remove data URL prefix if present
|
||||
const cleanedStr = str.split(",").pop()?.trim();
|
||||
if (!cleanedStr) return false;
|
||||
|
||||
// Length must be divisible by 4
|
||||
if (cleanedStr.length % 4 !== 0) return false;
|
||||
|
||||
// Base64 or Base64URL regex
|
||||
const base64Regex =
|
||||
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||
|
||||
if (!base64Regex.test(cleanedStr)) return false;
|
||||
|
||||
try {
|
||||
// atob throws if invalid
|
||||
atob(cleanedStr);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const addPreferenceOpt = (owner, type, role, isSignyourself) => {
|
||||
const widget = owner?.WidgetPreferences?.find((w) => w.type === type) || {};
|
||||
switch (type) {
|
||||
case "date": {
|
||||
const signingDate = widget?.isSigningDate ? "today" : widget?.date || "";
|
||||
const isReadOnly = widget?.isReadOnly || false;
|
||||
const isPrefill = role && role === "prefill";
|
||||
const format =
|
||||
widget?.format || selectFormat(owner?.DateFormat) || "MM/DD/YYYY";
|
||||
const prefillDate = new Date();
|
||||
const response = isPrefill
|
||||
? formatDate({ date: prefillDate, format: format })
|
||||
: signingDate || "";
|
||||
const option =
|
||||
isSignyourself || isPrefill ? {} : { isReadOnly: isReadOnly };
|
||||
return {
|
||||
response: response,
|
||||
...option,
|
||||
format: widget?.format || "MM/dd/yyyy"
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export function formatDate(dateObj) {
|
||||
const format = dateObj?.format?.toLowerCase();
|
||||
const date = new Date(dateObj.date);
|
||||
|
||||
// zero-padded day and month
|
||||
const dd = String(date.getDate()).padStart(2, "0");
|
||||
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = date.getFullYear();
|
||||
|
||||
// month names
|
||||
const shortNames = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec"
|
||||
];
|
||||
const longNames = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
];
|
||||
const mmm = shortNames[date.getMonth()];
|
||||
const mmmm = longNames[date.getMonth()];
|
||||
|
||||
// a map of token → replacement
|
||||
const tokens = { yyyy: yyyy, mm: mm, dd: dd, mmm: mmm, mmmm: mmmm };
|
||||
|
||||
// Replace longer tokens first
|
||||
return format
|
||||
.replace(/yyyy/, tokens.yyyy)
|
||||
.replace(/mmmm/, tokens.mmmm)
|
||||
.replace(/mmm/, tokens.mmm)
|
||||
.replace(/mm/, tokens.mm)
|
||||
.replace(/dd/, tokens.dd);
|
||||
}
|
||||
|
||||
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
|
||||
export const getDatePickerDate = (selectedDate, format = "dd-MM-yyyy") => {
|
||||
let date;
|
||||
if (!selectedDate) {
|
||||
return;
|
||||
}
|
||||
if (format && format === "dd-MM-yyyy") {
|
||||
const [day, month, year] = selectedDate?.split("-");
|
||||
date = new Date(`${year}-${month}-${day}`);
|
||||
} else if (format && format === "dd.MM.yyyy") {
|
||||
const [day, month, year] = selectedDate?.split(".");
|
||||
date = new Date(`${year}.${month}.${day}`);
|
||||
} else if (format && format === "dd/MM/yyyy") {
|
||||
const [day, month, year] = selectedDate?.split("/");
|
||||
date = new Date(`${year}/${month}/${day}`);
|
||||
} else {
|
||||
date = new Date(selectedDate);
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
// Parses strictly "mm/dd/yyyy" (e.g., "01/30/2026")
|
||||
// Returns "YYYY-MM-DD" (e.g., "2026-01-30") or null if invalid.
|
||||
export function parseMMDDYYYY(input) {
|
||||
if (typeof input !== "string") return null;
|
||||
|
||||
const match = input.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
||||
if (!match) return null;
|
||||
|
||||
const month = Number(match[1]); // 1..12
|
||||
const day = Number(match[2]); // 1..31
|
||||
const year = Number(match[3]);
|
||||
|
||||
if (month < 1 || month > 12) return null;
|
||||
if (day < 1 || day > 31) return null;
|
||||
|
||||
// Strict calendar validation (rejects 02/30/2026 etc.)
|
||||
const dt = new Date(year, month - 1, day);
|
||||
if (
|
||||
dt.getFullYear() !== year ||
|
||||
dt.getMonth() !== month - 1 ||
|
||||
dt.getDate() !== day
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MM = String(month).padStart(2, "0");
|
||||
const DD = String(day).padStart(2, "0");
|
||||
return `${year}-${MM}-${DD}`; // ISO-like (date-only)
|
||||
}
|
||||
|
||||
// Parses strictly "dd-mm-yyyy" (e.g., "30-01-2026")
|
||||
// Returns "YYYY-MM-DD" (e.g., "2026-01-30") or null if invalid.
|
||||
export function parseDDMMYYYY(input) {
|
||||
if (typeof input !== "string") return null;
|
||||
|
||||
const match = input.trim().match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
|
||||
if (!match) return null;
|
||||
|
||||
const day = Number(match[1]); // 1..31
|
||||
const month = Number(match[2]); // 1..12
|
||||
const year = Number(match[3]);
|
||||
|
||||
if (month < 1 || month > 12) return null;
|
||||
if (day < 1 || day > 31) return null;
|
||||
|
||||
// Strict calendar validation (rejects 31-02-2026 etc.)
|
||||
const dt = new Date(year, month - 1, day);
|
||||
if (
|
||||
dt.getFullYear() !== year ||
|
||||
dt.getMonth() !== month - 1 ||
|
||||
dt.getDate() !== day
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MM = String(month).padStart(2, "0");
|
||||
const DD = String(day).padStart(2, "0");
|
||||
return `${year}-${MM}-${DD}`; // ISO-like (date-only)
|
||||
}
|
||||
|
||||
// Turn "/^[A-Z]+$/i" OR "^[A-Z]+$" into a real RegExp
|
||||
export function toRegExp(maybeRegex, defaultFlags = "") {
|
||||
if (maybeRegex instanceof RegExp) return maybeRegex;
|
||||
|
||||
const s = String(maybeRegex ?? "").trim();
|
||||
const m = s.match(/^\/([\s\S]*)\/([gimsuy]*)$/); // /pattern/flags
|
||||
|
||||
if (m) {
|
||||
const [, pattern, flags] = m;
|
||||
return new RegExp(pattern, flags);
|
||||
}
|
||||
return new RegExp(s, defaultFlags);
|
||||
}
|
||||
|
||||
// For <input pattern="..."> (must NOT include /.../ delimiters)
|
||||
export function toHtmlPattern(maybeRegex) {
|
||||
if (!maybeRegex) return;
|
||||
|
||||
const re = toRegExp(maybeRegex);
|
||||
// pattern attribute is implicitly full-match; stripping anchors is optional
|
||||
return re.source.replace(/^\^/, "").replace(/\$$/, "");
|
||||
}
|
||||
|
||||
export const formatCSVDate = (widget, response, formatInISO = false) => {
|
||||
const format = widget?.options?.validation?.format ?? "dd-MM-yyyy";
|
||||
const input = response === "today" ? new Date() : response;
|
||||
// moment strict parse; fallback to "today" if invalid
|
||||
const m = moment(input, changeDateToMomentFormat(format), true);
|
||||
const date = m.isValid()
|
||||
? m
|
||||
: moment(new Date(), changeDateToMomentFormat(format), true);
|
||||
|
||||
const finalResponse = formatInISO
|
||||
? date.format("DD-MM-YYYY")
|
||||
: date.format(format?.toUpperCase());
|
||||
return finalResponse;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||
import { store } from "../redux/store";
|
||||
import Parse from "parse";
|
||||
|
||||
export function withSessionValidation(fn) {
|
||||
return async (...args) => {
|
||||
try {
|
||||
const tenantId = localStorage.getItem("TenantId");
|
||||
const user =
|
||||
typeof Parse !== "undefined" ? Parse.User?.current?.() : null;
|
||||
const sessionToken = user?.getSessionToken?.();
|
||||
|
||||
if (!tenantId || !sessionToken) {
|
||||
store.dispatch(sessionStatus(false));
|
||||
throw new Error("invalid session token");
|
||||
}
|
||||
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
if (error?.message === "invalid session token") {
|
||||
console.error("invalid session or missing tenantId", error);
|
||||
store.dispatch(sessionStatus(false));
|
||||
return;
|
||||
} else {
|
||||
throw error; // important: don't silently swallow errors
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,11 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
require("daisyui"),
|
||||
function ({ addUtilities, theme }) {
|
||||
function ({ addUtilities, addVariant }) {
|
||||
// ✅ Variants that match html[data-theme="..."] (or any ancestor with data-theme)
|
||||
addVariant("opensigncss", '[data-theme="opensigncss"] &');
|
||||
addVariant("opensigndark", '[data-theme="opensigndark"] &');
|
||||
|
||||
addUtilities({
|
||||
// Prevent iOS long-press popup
|
||||
".touch-callout-none": {
|
||||
|
||||
@@ -160,6 +160,17 @@ export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '46
|
||||
export const smtpenable =
|
||||
process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false;
|
||||
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
export function signPayload(payload, secret) {
|
||||
if (payload && secret) {
|
||||
const signature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(JSON.stringify(payload))
|
||||
.digest('hex');
|
||||
return { 'x-webhook-signature': signature };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// `generateId` is used to unique Id for fileAdapter
|
||||
export function generateId(length) {
|
||||
@@ -172,35 +183,6 @@ export function generateId(length) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Format date and time for the selected timezone
|
||||
export const formatTimeInTimezone = (date, timezone) => {
|
||||
const nyDate = timezone && toZonedTime(date, timezone);
|
||||
const generatedDate = timezone
|
||||
? format(nyDate, 'EEE, dd MMM yyyy HH:mm:ss zzz', { timeZone: timezone })
|
||||
: new Date(date).toUTCString();
|
||||
return generatedDate;
|
||||
};
|
||||
|
||||
// `getSecureUrl` is used to return local secure url if local files
|
||||
export const getSecureUrl = url => {
|
||||
const fileUrl = new URL(url)?.pathname?.includes('files');
|
||||
if (fileUrl) {
|
||||
try {
|
||||
const file = getSignedLocalUrl(url);
|
||||
if (file) {
|
||||
return { url: file };
|
||||
} else {
|
||||
return { url: '' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err while fileupload ', err);
|
||||
return { url: '' };
|
||||
}
|
||||
} else {
|
||||
return { url: url };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FlattenPdf is used to remove existing widgets if present any and flatten pdf.
|
||||
* @param {string | Uint8Array | ArrayBuffer} pdfFile - pdf file.
|
||||
@@ -238,13 +220,42 @@ export const flattenPdf = async pdfFile => {
|
||||
}
|
||||
};
|
||||
|
||||
// Format date and time for the selected timezone
|
||||
export const formatTimeInTimezone = (date, timezone) => {
|
||||
const nyDate = timezone && toZonedTime(date, timezone);
|
||||
const generatedDate = timezone
|
||||
? format(nyDate, 'EEE, dd MMM yyyy HH:mm:ss zzz', { timeZone: timezone })
|
||||
: new Date(date).toUTCString();
|
||||
return generatedDate;
|
||||
};
|
||||
|
||||
// `getSecureUrl` is used to return local secure url if local files
|
||||
export const getSecureUrl = url => {
|
||||
const fileUrl = new URL(url)?.pathname?.includes('files');
|
||||
if (fileUrl) {
|
||||
try {
|
||||
const file = getSignedLocalUrl(url);
|
||||
if (file) {
|
||||
return { url: file };
|
||||
} else {
|
||||
return { url: '' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err while fileupload ', err);
|
||||
return { url: '' };
|
||||
}
|
||||
} else {
|
||||
return { url: url };
|
||||
}
|
||||
};
|
||||
|
||||
export const mailTemplate = param => {
|
||||
const themeColor = '#47a3ad';
|
||||
const subject = `${param.senderName} has requested you to sign "${param.title}"`;
|
||||
const AppName = appName;
|
||||
const logo = `<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' />`;
|
||||
|
||||
const opurl = ` <a href='www.opensignlabs.com' target=_blank>here</a>`;
|
||||
const opurl = ` <a href='mailto:complaint@opensiglabs.com' target=_blank>here</a>`;
|
||||
|
||||
const body =
|
||||
"<html><head><meta http-equiv='Content-Type' content='text/html;charset=UTF-8' /></head><body><div style='background-color:#f5f5f5;padding:20px'><div style='background:white;padding-bottom:20px'><div style='padding:10px'>" +
|
||||
@@ -267,7 +278,7 @@ export const mailTemplate = param => {
|
||||
AppName +
|
||||
'. For any queries regarding this email, please contact the sender ' +
|
||||
param.senderMail +
|
||||
` directly. If you think this email is inappropriate or spam, you may file a complaint with ${AppName}${opurl}.</p></div></div></body></html>`;
|
||||
` directly. If you think this email is inappropriate or spam, you may file a complaints with ${AppName}${opurl}.</p></div></div></body></html>`;
|
||||
|
||||
return { subject, body };
|
||||
};
|
||||
@@ -358,34 +369,27 @@ export const handleValidImage = async Placeholder => {
|
||||
for (const item of placeholder.placeHolder || []) {
|
||||
const updatedPos = [];
|
||||
for (const posItem of item.pos || []) {
|
||||
if (posItem?.type === 'image' && posItem?.SignUrl) {
|
||||
const validUrl = await getPresignedUrl(posItem?.SignUrl);
|
||||
if (
|
||||
(posItem?.type === 'image' || posItem?.type === 'draw') &&
|
||||
posItem?.options?.response
|
||||
) {
|
||||
const validUrl = await getPresignedUrl(posItem?.options?.response);
|
||||
updatedPos.push({
|
||||
...posItem,
|
||||
SignUrl: validUrl,
|
||||
...(item.SignUrl !== undefined && { SignUrl: validUrl }),
|
||||
options: { ...posItem.options, response: validUrl },
|
||||
});
|
||||
} else {
|
||||
updatedPos.push(posItem);
|
||||
}
|
||||
}
|
||||
updatedRole.push({
|
||||
...item,
|
||||
pos: updatedPos,
|
||||
});
|
||||
updatedRole.push({ ...item, pos: updatedPos });
|
||||
}
|
||||
|
||||
updatedPlaceholders.push({
|
||||
...placeholder,
|
||||
signerPtr,
|
||||
placeHolder: updatedRole,
|
||||
});
|
||||
updatedPlaceholders.push({ ...placeholder, signerPtr, placeHolder: updatedRole });
|
||||
} else {
|
||||
// Not prefill role, just push as-is
|
||||
updatedPlaceholders.push({
|
||||
...placeholder,
|
||||
signerPtr,
|
||||
});
|
||||
updatedPlaceholders.push({ ...placeholder, signerPtr });
|
||||
}
|
||||
}
|
||||
return updatedPlaceholders;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import uploadFile from './uploadFile.js';
|
||||
|
||||
import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
|
||||
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
|
||||
@@ -16,7 +15,6 @@ app.use(cors());
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
app.use(express.urlencoded({ limit: '100mb', extended: true }));
|
||||
|
||||
app.post('/file_upload', uploadFile);
|
||||
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
|
||||
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
|
||||
app.get('/delete-account/:userId', deleteUserGet);
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
import axios from 'axios';
|
||||
import multer from 'multer';
|
||||
import libre from 'libreoffice-convert';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { cloudServerUrl, getSecureUrl, serverAppId } from '../../Utils.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// -------------------- Process Management --------------------
|
||||
/**
|
||||
* Kill stuck LibreOffice processes
|
||||
*/
|
||||
async function killStuckProcesses() {
|
||||
try {
|
||||
// Kill soffice processes older than 2 minutes
|
||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
||||
await execAsync("pkill -9 -f 'soffice.*--headless' || true");
|
||||
console.log('[DOCX2PDF] Cleaned up stuck processes');
|
||||
} else if (process.platform === 'win32') {
|
||||
await execAsync('taskkill /F /IM soffice.bin /T || exit 0');
|
||||
console.log('[DOCX2PDF] Cleaned up stuck processes (Windows)');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[DOCX2PDF] Error killing processes:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<Buffer>} */
|
||||
async function convertLibre(input, ext, opts) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
@@ -14,7 +37,7 @@ async function convertLibre(input, ext, opts) {
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Concurrency limiter --------------------
|
||||
// -------------------- Concurrency limiter with queue limits --------------------
|
||||
// CRITICAL FIX: Reduced to 1 for CPU-intensive LibreOffice conversions
|
||||
const MAX_CONCURRENCY = Number(process.env.DOCX2PDF_CONCURRENCY || 1);
|
||||
let active = 0;
|
||||
@@ -22,22 +45,27 @@ const queue = [];
|
||||
|
||||
function runWithLimit(task) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const run = () => {
|
||||
const run = async () => {
|
||||
active++;
|
||||
Promise.resolve()
|
||||
.then(task)
|
||||
.then(resolve, reject)
|
||||
.finally(() => {
|
||||
active--;
|
||||
if (queue.length) queue.shift()();
|
||||
});
|
||||
try {
|
||||
const result = await task();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
active--;
|
||||
if (queue.length) {
|
||||
const next = queue.shift();
|
||||
next();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (active < MAX_CONCURRENCY) run();
|
||||
else queue.push(run);
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- Timeout helper --------------------
|
||||
// -------------------- Timeout helper with cleanup --------------------
|
||||
/**
|
||||
* @template T
|
||||
* @param {Promise<T>} promise
|
||||
@@ -49,7 +77,11 @@ export async function withTimeout(promise, ms, label = 'operation') {
|
||||
let timer;
|
||||
try {
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
||||
timer = setTimeout(async () => {
|
||||
// Kill stuck processes on timeout
|
||||
await killStuckProcesses();
|
||||
reject(new Error(`${label} timed out after ${ms}ms`));
|
||||
}, ms);
|
||||
});
|
||||
return await Promise.race([promise, timeout]);
|
||||
} finally {
|
||||
@@ -116,22 +148,29 @@ export default async function docxtopdf(req, res) {
|
||||
// ---- DOCX -> PDF conversion with concurrency control and timeout ----
|
||||
const fileName = `${generatePdfName(16)}.pdf`;
|
||||
|
||||
// FIX: Increased timeout to 90s for large files, added nice priority
|
||||
// Adjust timeout based on file size
|
||||
const timeoutMs = uploadedSizeBytes > 10 * 1024 * 1024 ? 120_000 : 90_000;
|
||||
|
||||
// FIX: Increased timeout for large files, added nice priority
|
||||
const pdfBuffer = await runWithLimit(async () => {
|
||||
// Log for monitoring
|
||||
console.log(`[DOCX2PDF] Starting conversion, active: ${active}, queued: ${queue.length}`);
|
||||
console.log(
|
||||
`[DOCX2PDF] Starting conversion, size: ${(uploadedSizeBytes / 1024 / 1024).toFixed(2)}MB, active: ${active}, queued: ${queue.length}`
|
||||
);
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const result = await withTimeout(
|
||||
convertLibre(req.file.buffer, '.pdf', undefined),
|
||||
90_000, // Increased from 60s to 90s
|
||||
timeoutMs,
|
||||
'DOCX->PDF'
|
||||
);
|
||||
console.log(`[DOCX2PDF] Completed in ${Date.now() - startTime}ms`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`[DOCX2PDF] Failed after ${Date.now() - startTime}ms:`, error.message);
|
||||
// Clean up on error
|
||||
await killStuckProcesses();
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
// npm packages
|
||||
import multer from 'multer';
|
||||
import multerS3 from 'multer-s3';
|
||||
import aws from 'aws-sdk';
|
||||
import dotenv from 'dotenv';
|
||||
import { cloudServerUrl, serverAppId, useLocal } from '../../Utils.js';
|
||||
dotenv.config({ quiet: true });
|
||||
|
||||
function sanitizeFileName(fileName) {
|
||||
// Remove spaces and invalid characters
|
||||
return fileName.replace(/[^a-zA-Z0-9._-]/g, '');
|
||||
}
|
||||
|
||||
async function uploadFile(req, res) {
|
||||
try {
|
||||
//--size extended to 100 mb
|
||||
const size = 100 * 1024 * 1024;
|
||||
//console.log(size);
|
||||
|
||||
const accepted_extensions = [
|
||||
'jpg',
|
||||
'png',
|
||||
'gif',
|
||||
'mp4',
|
||||
'mp3',
|
||||
'pdf',
|
||||
'jpeg',
|
||||
'dwg',
|
||||
'dxf',
|
||||
'zip',
|
||||
'rar',
|
||||
'txt',
|
||||
'doc',
|
||||
'docx',
|
||||
'pptx',
|
||||
'ppt',
|
||||
'xlsx',
|
||||
'xlsm',
|
||||
'xlsb',
|
||||
'xltx',
|
||||
'xml',
|
||||
'xls',
|
||||
'xla',
|
||||
'xlx',
|
||||
];
|
||||
|
||||
const DO_ENDPOINT = process.env.DO_ENDPOINT;
|
||||
const DO_ACCESS_KEY_ID = process.env.DO_ACCESS_KEY_ID;
|
||||
const DO_SECRET_ACCESS_KEY = process.env.DO_SECRET_ACCESS_KEY;
|
||||
const DO_SPACE = process.env.DO_SPACE;
|
||||
|
||||
const parseBaseUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const parseAppId = serverAppId;
|
||||
let fileStorage;
|
||||
if (useLocal === 'true') {
|
||||
fileStorage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, 'files/files');
|
||||
},
|
||||
metadata: function (req, file, cb) {
|
||||
cb(null, { fieldName: 'OPENSIGN_METADATA' });
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
let filename = file.originalname;
|
||||
let newFileName = filename.split('.')[0];
|
||||
let extension = filename.split('.')[1];
|
||||
newFileName = sanitizeFileName(
|
||||
newFileName + '_' + new Date().toISOString() + '.' + extension
|
||||
);
|
||||
// console.log(newFileName);
|
||||
cb(null, newFileName);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const spacesEndpoint = new aws.Endpoint(DO_ENDPOINT);
|
||||
const s3 = new aws.S3({
|
||||
endpoint: spacesEndpoint,
|
||||
accessKeyId: DO_ACCESS_KEY_ID,
|
||||
secretAccessKey: DO_SECRET_ACCESS_KEY,
|
||||
signatureVersion: 'v4',
|
||||
region: process.env.DO_REGION,
|
||||
});
|
||||
fileStorage = multerS3({
|
||||
acl: 'public-read',
|
||||
s3,
|
||||
bucket: DO_SPACE,
|
||||
metadata: function (req, file, cb) {
|
||||
cb(null, { fieldName: 'OPENSIGN_METADATA' });
|
||||
},
|
||||
key: function (req, file, cb) {
|
||||
//console.log(file);
|
||||
let filename = file.originalname;
|
||||
let newFileName = filename.split('.')[0];
|
||||
let extension = filename.split('.')[1];
|
||||
newFileName = sanitizeFileName(
|
||||
newFileName + '_' + new Date().toISOString() + '.' + extension
|
||||
);
|
||||
// console.log(newFileName);
|
||||
cb(null, newFileName);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
fileStorage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, 'files/files');
|
||||
},
|
||||
metadata: function (req, file, cb) {
|
||||
cb(null, { fieldName: 'OPENSIGN_METADATA' });
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
let filename = file.originalname;
|
||||
let newFileName = filename.split('.')[0];
|
||||
let extension = filename.split('.')[1];
|
||||
newFileName = sanitizeFileName(
|
||||
newFileName + '_' + new Date().toISOString() + '.' + extension
|
||||
);
|
||||
// console.log(newFileName);
|
||||
cb(null, newFileName);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// const s3 = new aws.S3();
|
||||
const upload = multer({
|
||||
fileFilter: function (req, file, cb) {
|
||||
if (accepted_extensions.some(ext => file.originalname.toLowerCase().endsWith('.' + ext))) {
|
||||
return cb(null, true);
|
||||
}
|
||||
// otherwise, return error
|
||||
return cb('Only ' + accepted_extensions.join(', ') + ' files are allowed!');
|
||||
},
|
||||
storage: fileStorage,
|
||||
limits: { fileSize: size },
|
||||
}).single('file');
|
||||
|
||||
//--call upload function--
|
||||
upload(req, res, function (err, some) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
const status = 'Error';
|
||||
const message = err;
|
||||
const returnCode = 1029;
|
||||
return res.send({ status, returnCode, message });
|
||||
}
|
||||
|
||||
const status = 'Success';
|
||||
//res.header("Access-Control-Allow-Headers", "Content-Type");
|
||||
//res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
if (useLocal === 'true') {
|
||||
// console.log(req.file);
|
||||
var fileUrl = `${parseBaseUrl}/files/${parseAppId}/${req.file.filename}`;
|
||||
} else {
|
||||
var fileUrl = req.file.location;
|
||||
}
|
||||
|
||||
return res.json({ status, imageUrl: fileUrl });
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Exeption in query ' + err.stack);
|
||||
const status = 'Error';
|
||||
const returnCode = 1021;
|
||||
const message = 'Some error occurred';
|
||||
return res.send({ status, returnCode, message });
|
||||
}
|
||||
}
|
||||
export default uploadFile;
|
||||
@@ -62,6 +62,7 @@ import manageSign from './parsefunction/manageSign.js';
|
||||
import getSignature from './parsefunction/getSignature.js';
|
||||
import updateEmailTemplates from './parsefunction/updateEmailTemplates.js';
|
||||
import triggerEvent from './parsefunction/triggerEvent.js';
|
||||
import setWidgetPreferences from './parsefunction/setWidgetPreferences.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);
|
||||
@@ -134,3 +135,4 @@ Parse.Cloud.define('managesign', manageSign);
|
||||
Parse.Cloud.define('getdefaultsignature', getSignature);
|
||||
Parse.Cloud.define('updateemailtemplates', updateEmailTemplates);
|
||||
Parse.Cloud.define('triggerevent', triggerEvent);
|
||||
Parse.Cloud.define('setwidgetpreferences', setWidgetPreferences);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user