mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-09-04 00:38:57 +02:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9fcc69ca4 | ||
|
|
d1c405912b | ||
|
|
745aca1754 | ||
|
|
672eeea851 | ||
|
|
98efff65c9 | ||
|
|
fe21978538 | ||
|
|
ca4ccfd889 | ||
|
|
6cb2d69566 | ||
|
|
98b9ebb228 |
@@ -3,13 +3,13 @@
|
||||
"version": "2.26.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.4",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.15.2",
|
||||
"@lottiefiles/dotlottie-react": "^0.16.2",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.2",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.4.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"react-quill-new": "^3.6.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.8.2",
|
||||
"react-router": "^7.9.1",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-select": "^5.10.2",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
@@ -51,7 +51,7 @@
|
||||
"reactour": "^1.19.4",
|
||||
"redux": "^5.0.1",
|
||||
"regex-parser": "^2.3.1",
|
||||
"serve": "^14.2.4",
|
||||
"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"
|
||||
@@ -101,7 +101,7 @@
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react": "^19.1.13",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
@@ -113,7 +113,7 @@
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^26.1.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"lint-staged": "^16.1.6",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"dark-mode": "Dunkelmodus",
|
||||
"name": "Name",
|
||||
"phone": "Telefon",
|
||||
"phone-optional": "optional",
|
||||
"phone-optional": "Optional",
|
||||
"email": "E-Mail",
|
||||
"company": "Unternehmen",
|
||||
"job-title": "Berufsbezeichnung",
|
||||
@@ -901,6 +901,10 @@
|
||||
"thanks-for-feedback": "Danke für Ihr Feedback 🙏",
|
||||
"share-your-feedback": "Teilen Sie Ihr Feedback",
|
||||
"share-your-review": "Teilen Sie Ihre Bewertung",
|
||||
"please-select-rating": "Bitte wählen Sie eine Bewertung aus",
|
||||
"feedback-optional": "Feedback (optional)",
|
||||
"feedback-saved": "Feedback wurde gespeichert.",
|
||||
"feedback-save-error": "Feedback konnte nicht gespeichert werden, bitte versuchen Sie es erneut.",
|
||||
"date-format": "Datumsformat",
|
||||
"document-deleted": "Das Dokument wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
|
||||
"save-as-template-?": "Sind Sie sicher, dass Sie dieses Dokument als Vorlage speichern möchten?",
|
||||
@@ -1210,5 +1214,21 @@
|
||||
"otp-email": "Wir haben einen Bestätigungscode gesendet",
|
||||
"save-as-temp-warn": "Hinweis: Dieses Dokument enthält vorausgefüllte Widgets, die automatisch entfernt werden, da sie bereits im Basisdokument integriert sind.",
|
||||
"edit-document": "Dokument bearbeiten",
|
||||
"otp-email": "Wir haben einen Bestätigungscode gesendet"
|
||||
}
|
||||
"modify": "Ändern",
|
||||
"merge-certificate-to-pdf": "Zertifikat mit PDF zusammenführen",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "Dies stellt sicher, dass das Abschlusszertifikat im endgültigen PDF-Dokument enthalten ist. Bitte beachten Sie jedoch, dass das Zertifikat nach dem Zusammenführen nicht mehr vom Hauptdokument getrennt werden kann.",
|
||||
"p2": "Wenn Sie sich entscheiden, nicht zusammenzuführen, wird das Abschlusszertifikat als separate PDF-Datei zusammen mit dem unterzeichneten Dokument bereitgestellt."
|
||||
},
|
||||
"read-only-date-error": "Das schreibgeschützte Datums-Widget muss ein Standarddatum haben.",
|
||||
"set-date": "Datum festlegen",
|
||||
"set-today": "Unterzeichnungsdatum",
|
||||
"enter-name": "Name eingeben",
|
||||
"enter-email": "E-Mail eingeben",
|
||||
"subscribe-to-opensign-msg": "Abonnieren Sie {{appName}} und genießen Sie unbegrenzt kostenlose digitale Signaturen.",
|
||||
"duplicate-subscribe-msg": "Mit Duplizieren können Sie eine exakte Kopie der ausgewählten Vorlage erstellen, die Sie wiederverwenden oder ändern können, ohne das Original zu beeinflussen.",
|
||||
"save-as-template-msg": "Speichern Sie dieses Dokument als wiederverwendbare Vorlage, die Sie für zukünftige Dokumente erneut nutzen können.",
|
||||
"public-credit-alert": "Das Signieren über diesen Link verbraucht Ihre kostenlosen E-Mail-Credits. Als kostenloser Nutzer erhalten Sie 15 E-Mail-Credits pro Monat, um zu verhindern, dass Spammer unsere Systeme missbrauchen. Um höhere Limits und ununterbrochenen Zugriff zu genießen, abonnieren Sie die <1>OpenSign™-Bezahlpläne.</1>",
|
||||
"know-more-about": "Mehr erfahren über",
|
||||
"free-unlimited-signatures": "Kostenlose unbegrenzte Signaturen"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"dark-mode": "Dark mode",
|
||||
"name": "Name",
|
||||
"phone": "Phone",
|
||||
"phone-optional": "optional",
|
||||
"phone-optional": "Optional",
|
||||
"email": "Email",
|
||||
"company": "Company",
|
||||
"job-title": "Job title",
|
||||
@@ -902,6 +902,10 @@
|
||||
"thanks-for-feedback": "Thanks for your feedback 🙏",
|
||||
"share-your-feedback": "Share your feedback",
|
||||
"share-your-review": "Share your review",
|
||||
"please-select-rating": "Please select a rating",
|
||||
"feedback-optional": "Feedback (Optional)",
|
||||
"feedback-saved": "Feedback has been saved.",
|
||||
"feedback-save-error": "Unable to save feedback, please try again.",
|
||||
"date-format": "Date format",
|
||||
"document-deleted": "The document has been deleted or you don't have access. Please contact the sender.",
|
||||
"save-as-template-?": "Are you sure you want to save this document as template?",
|
||||
@@ -1208,8 +1212,24 @@
|
||||
"enter-region-of-space": "Enter region of space",
|
||||
"enter-access-key": "Enter access key",
|
||||
"enter-secret-access-key": "Enter secret access key",
|
||||
"otp-email":"We’ve sent a verification code to",
|
||||
"save-as-temp-warn":"Note: This document includes prefilled widgets, which will be automatically removed as they are already incorporated into the base document.",
|
||||
"otp-email": "We've sent a verification code to",
|
||||
"save-as-temp-warn": "Note: This document includes prefilled widgets, which will be automatically removed as they are already incorporated into the base document.",
|
||||
"edit-document": "Edit document",
|
||||
"otp-email":"We've sent a verification code to"
|
||||
}
|
||||
"modify": "Modify",
|
||||
"merge-certificate-to-pdf": "Merge Certificate to PDF",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "This will ensure that the completion certificate is included in the final PDF document. However, please note that once merged, the certificate cannot be separated from the main document.",
|
||||
"p2": "If you choose not to merge, the completion certificate will be provided as a separate PDF file along with the signed document."
|
||||
},
|
||||
"read-only-date-error": "Read-only date widget must have a default date.",
|
||||
"set-date": "Set Date",
|
||||
"set-today": "Signing Date",
|
||||
"enter-name": "Enter name",
|
||||
"enter-email": "Enter email",
|
||||
"subscribe-to-opensign-msg": "Subscribe to {{appName}} and enjoy unlimited free digital signatures.",
|
||||
"duplicate-subscribe-msg": "Duplicate lets you create an exact copy of the selected template, allowing you to reuse or modify it without affecting the original.",
|
||||
"save-as-template-msg": "Save this document as a reusable template that you can use again for future documents.",
|
||||
"public-credit-alert": "Signing through this link consumes your free email credits. As a Free user, you are allotted 15 email credits per month to prevent spammers from abusing our systems. To enjoy higher limits and uninterrupted access, Subscribe to <1>OpenSign™ Paid plans.</1>",
|
||||
"know-more-about": "Know more about",
|
||||
"free-unlimited-signatures": "Free Unlimited Signatures"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"dark-mode": "Modo oscuro",
|
||||
"name": "Nombre",
|
||||
"phone": "Teléfono",
|
||||
"phone-optional": "opcional",
|
||||
"phone-optional": "Opcional",
|
||||
"email": "Correo",
|
||||
"company": "Compañía",
|
||||
"job-title": "Título del puesto",
|
||||
@@ -902,6 +902,10 @@
|
||||
"thanks-for-feedback": "Gracias por su comentario 🙏",
|
||||
"share-your-feedback": "Comparta sus comentarios",
|
||||
"share-your-review": "Comparta su reseña",
|
||||
"please-select-rating": "Por favor seleccione una calificación",
|
||||
"feedback-optional": "Comentarios (opcional)",
|
||||
"feedback-saved": "Los comentarios han sido guardados.",
|
||||
"feedback-save-error": "No se pudieron guardar los comentarios, inténtelo de nuevo.",
|
||||
"date-format": "Formato de fecha",
|
||||
"document-deleted": "El documento ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
|
||||
"save-as-template-?": "¿Está seguro de que desea guardar este documento como plantilla?",
|
||||
@@ -1211,5 +1215,21 @@
|
||||
"otp-email": "Hemos enviado un código de verificación",
|
||||
"save-as-temp-warn": "Nota: Este documento incluye widgets prellenados, que se eliminarán automáticamente ya que ya están incorporados en el documento base.",
|
||||
"edit-document": "Editar documento",
|
||||
"otp-email": "Hemos enviado un código de verificación"
|
||||
}
|
||||
"modify": "Modificar",
|
||||
"merge-certificate-to-pdf": "Combinar certificado con PDF",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "Esto garantizará que el certificado de finalización esté incluido en el documento PDF final. Sin embargo, tenga en cuenta que una vez combinado, el certificado no se podrá separar del documento principal.",
|
||||
"p2": "Si elige no combinar, el certificado de finalización se proporcionará como un archivo PDF separado junto con el documento firmado."
|
||||
},
|
||||
"read-only-date-error": "El widget de fecha de solo lectura debe tener una fecha predeterminada.",
|
||||
"set-date": "Establecer fecha",
|
||||
"set-today": "Fecha de firma",
|
||||
"enter-name": "Ingrese nombre",
|
||||
"enter-email": "Ingrese correo electrónico",
|
||||
"subscribe-to-opensign-msg": "Suscríbase a {{appName}} y disfrute de firmas digitales gratuitas ilimitadas.",
|
||||
"duplicate-subscribe-msg": "Duplicar le permite crear una copia exacta de la plantilla seleccionada, lo que le permite reutilizarla o modificarla sin afectar al original.",
|
||||
"save-as-template-msg": "Guarde este documento como una plantilla reutilizable que podrá usar nuevamente para futuros documentos.",
|
||||
"public-credit-alert": "Firmar a través de este enlace consumirá sus créditos de correo electrónico gratuitos. Como usuario gratuito, se le asignan 15 créditos de correo electrónico por mes para evitar que los spammers abusen de nuestros sistemas. Para disfrutar de límites más altos y acceso ininterrumpido, suscríbase a los planes de pago de <1>OpenSign™.</1>",
|
||||
"know-more-about": "Saber más sobre",
|
||||
"free-unlimited-signatures": "¿Firmas ilimitadas gratis"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"dark-mode": "Mode sombre",
|
||||
"name": "Nom et Prénom",
|
||||
"phone": "Téléphone",
|
||||
"phone-optional": "(facultatif)",
|
||||
"phone-optional": "Optionnel",
|
||||
"email": "E-mail",
|
||||
"company": "Organisation",
|
||||
"job-title": "Votre Fonction",
|
||||
@@ -901,6 +901,10 @@
|
||||
"thanks-for-feedback": "Merci pour votre retour 🙏",
|
||||
"share-your-feedback": "Partagez votre avis",
|
||||
"share-your-review": "Partagez votre avis",
|
||||
"please-select-rating": "Veuillez sélectionner une note",
|
||||
"feedback-optional": "Retour (optionnel)",
|
||||
"feedback-saved": "Le retour a été enregistré.",
|
||||
"feedback-save-error": "Impossible d'enregistrer le retour, veuillez réessayer.",
|
||||
"date-format": "Format de date",
|
||||
"document-deleted": "Le document a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.",
|
||||
"save-as-template-?": "Êtes-vous sûr de vouloir enregistrer ce document comme modèle ?",
|
||||
@@ -1210,5 +1214,21 @@
|
||||
"otp-email": "Nous avons envoyé un code de vérification",
|
||||
"save-as-temp-warn": "Remarque : Ce document contient des widgets préremplis, qui seront automatiquement supprimés car ils sont déjà intégrés dans le document de base.",
|
||||
"edit-document": "Modifier le document",
|
||||
"otp-email": "Nous avons envoyé un code de vérification"
|
||||
}
|
||||
"modify": "Modifier",
|
||||
"merge-certificate-to-pdf": "Fusionner le certificat avec le PDF",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "Cela garantira que le certificat de finalisation est inclus dans le document PDF final. Cependant, veuillez noter qu'une fois fusionné, le certificat ne peut plus être séparé du document principal.",
|
||||
"p2": "Si vous choisissez de ne pas fusionner, le certificat de finalisation sera fourni sous forme de fichier PDF distinct accompagné du document signé."
|
||||
},
|
||||
"read-only-date-error": "Le widget de date en lecture seule doit avoir une date par défaut.",
|
||||
"set-date": "Définir la date",
|
||||
"set-today": "Date de signature",
|
||||
"enter-name": "Saisir le nom",
|
||||
"enter-email": "Saisir l'e-mail",
|
||||
"subscribe-to-opensign-msg": "Abonnez-vous à {{appName}} et profitez de signatures numériques gratuites illimitées.",
|
||||
"duplicate-subscribe-msg": "Dupliquer vous permet de créer une copie exacte du modèle sélectionné, que vous pouvez réutiliser ou modifier sans affecter l'original.",
|
||||
"save-as-template-msg": "Enregistrez ce document comme modèle réutilisable que vous pourrez utiliser à nouveau pour de futurs documents.",
|
||||
"public-credit-alert": "Signer via ce lien consomme vos crédits e-mail gratuits. En tant qu'utilisateur gratuit, vous disposez de 15 crédits e-mail par mois afin d’empêcher les spammeurs d’abuser de nos systèmes. Pour profiter de limites plus élevées et d'un accès ininterrompu, abonnez-vous aux forfaits payants de <1>OpenSign™.</1>",
|
||||
"know-more-about": "En savoir plus sur",
|
||||
"free-unlimited-signatures": "Signatures illimitées gratuites"
|
||||
}
|
||||
@@ -901,6 +901,10 @@
|
||||
"thanks-for-feedback": "आपकी प्रतिक्रिया के लिए धन्यवाद 🙏",
|
||||
"share-your-feedback": "अपनी प्रतिक्रिया साझा करें",
|
||||
"share-your-review": "अपनी समीक्षा साझा करें",
|
||||
"please-select-rating": "कृपया एक रेटिंग चुनें",
|
||||
"feedback-optional": "प्रतिक्रिया (वैकल्पिक)",
|
||||
"feedback-saved": "प्रतिक्रिया सहेज ली गई है।",
|
||||
"feedback-save-error": "प्रतिक्रिया सहेजी नहीं जा सकी, कृपया पुनः प्रयास करें।",
|
||||
"date-format": "दिनांक प्रारूप",
|
||||
"document-deleted": "दस्तावेज़ हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
|
||||
"save-as-template-?": "क्या आप वाकई इस दस्तावेज़ को टेम्पलेट के रूप में सहेजना चाहते हैं?",
|
||||
@@ -1210,5 +1214,21 @@
|
||||
"otp-email": "हमने एक सत्यापन कोड भेजा है",
|
||||
"save-as-temp-warn": "नोट: इस दस्तावेज़ में पहले से भरे हुए विजेट शामिल हैं, जिन्हें स्वचालित रूप से हटा दिया जाएगा क्योंकि वे पहले से ही मूल दस्तावेज़ में सम्मिलित हैं।",
|
||||
"edit-document": "दस्तावेज़ संपादित करें",
|
||||
"otp-email": "हमने एक सत्यापन कोड भेजा है"
|
||||
}
|
||||
"modify": "संशोधित करें",
|
||||
"merge-certificate-to-pdf": "प्रमाणपत्र को PDF में मिलाएँ",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "यह सुनिश्चित करेगा कि पूर्णता प्रमाणपत्र अंतिम PDF दस्तावेज़ में शामिल हो। हालाँकि, कृपया ध्यान दें कि एक बार मिलाने के बाद, प्रमाणपत्र को मुख्य दस्तावेज़ से अलग नहीं किया जा सकता।",
|
||||
"p2": "यदि आप मिलाना नहीं चुनते हैं, तो पूर्णता प्रमाणपत्र हस्ताक्षरित दस्तावेज़ के साथ एक अलग PDF फ़ाइल के रूप में प्रदान किया जाएगा।"
|
||||
},
|
||||
"read-only-date-error": "रीड-ओनली दिनांक विजेट में एक डिफ़ॉल्ट दिनांक होना आवश्यक है।",
|
||||
"set-date": "दिनांक सेट करें",
|
||||
"set-today": "हस्ताक्षर की तिथि",
|
||||
"enter-name": "नाम दर्ज करें",
|
||||
"enter-email": "ईमेल दर्ज करें",
|
||||
"subscribe-to-opensign-msg": "{{appName}} की सदस्यता लें और असीमित निःशुल्क डिजिटल हस्ताक्षरों का आनंद लें।",
|
||||
"duplicate-subscribe-msg": "डुप्लिकेट करने से आप चयनित टेम्पलेट की एक सटीक प्रति बना सकते हैं, जिसे आप पुनः उपयोग या संशोधित कर सकते हैं बिना मूल को प्रभावित किए।",
|
||||
"save-as-template-msg": "इस दस्तावेज़ को एक पुन: प्रयोज्य टेम्पलेट के रूप में सहेजें जिसे आप भविष्य के दस्तावेज़ों के लिए फिर से उपयोग कर सकते हैं।",
|
||||
"public-credit-alert": "इस लिंक के माध्यम से हस्ताक्षर करने पर आपके निःशुल्क ईमेल क्रेडिट का उपभोग होगा। एक निःशुल्क उपयोगकर्ता के रूप में, आपको स्पैमर्स को हमारी प्रणाली का दुरुपयोग करने से रोकने के लिए प्रति माह 15 ईमेल क्रेडिट आवंटित किए जाते हैं। उच्च सीमा और निर्बाध पहुँच का आनंद लेने के लिए, <1>OpenSign™ पेड प्लान</1> की सदस्यता लें।",
|
||||
"know-more-about": "इसके बारे में और जानें",
|
||||
"free-unlimited-signatures": "मुफ़्त असीमित हस्ताक्षर"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"dark-mode": "Modalità scura",
|
||||
"name": "Nome",
|
||||
"phone": "Telefono",
|
||||
"phone-optional": "facoltativo",
|
||||
"phone-optional": "Opzionale",
|
||||
"email": "Email",
|
||||
"company": "Azienda",
|
||||
"job-title": "Titolo professionale",
|
||||
@@ -901,6 +901,10 @@
|
||||
"thanks-for-feedback": "Grazie per il tuo feedback 🙏",
|
||||
"share-your-feedback": "Condividi il tuo feedback",
|
||||
"share-your-review": "Condividi la tua recensione",
|
||||
"please-select-rating": "Si prega di selezionare una valutazione",
|
||||
"feedback-optional": "Feedback (opzionale)",
|
||||
"feedback-saved": "Il feedback è stato salvato.",
|
||||
"feedback-save-error": "Impossibile salvare il feedback, riprova.",
|
||||
"date-format": "Formato data",
|
||||
"document-deleted": "Il documento è stato eliminato o non hai accesso. Si prega di contattare il mittente.",
|
||||
"save-as-template-?": "Sei sicuro di voler salvare questo documento come modello?",
|
||||
@@ -1210,5 +1214,21 @@
|
||||
"otp-email": "Abbiamo inviato un codice di verifica",
|
||||
"save-as-temp-warn": "Nota: Questo documento include widget precompilati, che verranno automaticamente rimossi poiché sono già incorporati nel documento di base.",
|
||||
"edit-document": "Modifica documento",
|
||||
"otp-email": "Abbiamo inviato un codice di verifica"
|
||||
}
|
||||
"modify": "Modifica",
|
||||
"merge-certificate-to-pdf": "Unisci certificato al PDF",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "Questo garantirà che il certificato di completamento sia incluso nel documento PDF finale. Tuttavia, si noti che una volta unito, il certificato non può essere separato dal documento principale.",
|
||||
"p2": "Se scegli di non unire, il certificato di completamento sarà fornito come file PDF separato insieme al documento firmato."
|
||||
},
|
||||
"read-only-date-error": "Il widget data di sola lettura deve avere una data predefinita.",
|
||||
"set-date": "Imposta data",
|
||||
"set-today": "Data di firma",
|
||||
"enter-name": "Inserisci nome",
|
||||
"enter-email": "Inserisci e-mail",
|
||||
"subscribe-to-opensign-msg": "Abbonati a {{appName}} e goditi firme digitali gratuite illimitate.",
|
||||
"duplicate-subscribe-msg": "Duplicare consente di creare una copia esatta del modello selezionato, permettendoti di riutilizzarlo o modificarlo senza influire sull'originale.",
|
||||
"save-as-template-msg": "Salva questo documento come modello riutilizzabile che potrai usare di nuovo per documenti futuri.",
|
||||
"public-credit-alert": "Firmare tramite questo link consumerà i tuoi crediti e-mail gratuiti. Come utente gratuito, ti vengono assegnati 15 crediti e-mail al mese per impedire agli spammer di abusare dei nostri sistemi. Per usufruire di limiti più elevati e accesso ininterrotto, abbonati ai piani a pagamento di <1>OpenSign™.</1>",
|
||||
"know-more-about": "Scopri di più su",
|
||||
"free-unlimited-signatures": "Firme illimitate gratuite"
|
||||
}
|
||||
@@ -244,6 +244,7 @@ const AddSigner = (props) => {
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
@@ -263,6 +264,7 @@ const AddSigner = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
|
||||
@@ -179,6 +179,7 @@ const AddUser = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
@@ -200,6 +201,7 @@ const AddUser = (props) => {
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Parse from "parse";
|
||||
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
||||
@@ -168,25 +168,21 @@ const DashboardCard = (props) => {
|
||||
}
|
||||
}
|
||||
setresponse(arr.length);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
await axios.get(url, { headers: headers }).then((res) => {
|
||||
if (res.data.results.length > 0) {
|
||||
setLoading(false);
|
||||
if (props.Data.key !== "count") {
|
||||
setresponse(res.data.results[0][props.Data.key]);
|
||||
} else {
|
||||
setresponse(res.data[props.Data.key]);
|
||||
}
|
||||
if (res?.data?.[props.Data.key]) {
|
||||
setresponse(parseInt(res.data[props.Data.key]));
|
||||
} else if (res?.data?.results?.length > 0) {
|
||||
setresponse(res.data.results.length);
|
||||
} else {
|
||||
setresponse(0);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Problem", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ const EditTemplate = ({
|
||||
Bcc: template?.Bcc,
|
||||
RedirectUrl: template?.RedirectUrl || "",
|
||||
AllowModifications: template?.AllowModifications || false,
|
||||
TimeToCompleteDays: template?.TimeToCompleteDays || 15
|
||||
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
|
||||
});
|
||||
const [isUpdate, setIsUpdate] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
@@ -244,6 +244,7 @@ const EditTemplate = ({
|
||||
setShowConfirm(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi
|
||||
isOpen
|
||||
|
||||
@@ -75,9 +75,12 @@ function Header(props) {
|
||||
alert(t("only-pdf-allowed"));
|
||||
return;
|
||||
}
|
||||
const mb = Math.round(file?.size / Math.pow(1024, 2));
|
||||
if (mb > maxFileSize) {
|
||||
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
|
||||
const fileSize =
|
||||
maxFileSize;
|
||||
const pdfsize = file?.size;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
@@ -134,6 +137,13 @@ function Header(props) {
|
||||
useObjectStreams: false
|
||||
});
|
||||
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
|
||||
const pdfsize = pdfBuffer?.byteLength;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
props.setPdfArrayBuffer(pdfBuffer);
|
||||
props.setPdfBase64Url(pdfBase64);
|
||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||
@@ -167,7 +177,9 @@ function Header(props) {
|
||||
);
|
||||
};
|
||||
const handleDownloadBtn = async () => {
|
||||
if (props?.isCompleted) {
|
||||
if (
|
||||
props?.isCompleted
|
||||
) {
|
||||
props?.setIsDownloadModal(true);
|
||||
} else {
|
||||
await handleDownloadDoc();
|
||||
@@ -222,25 +234,27 @@ function Header(props) {
|
||||
{t("download")}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
{props?.isCompleted && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="border-none bg-[#fff]">
|
||||
<i
|
||||
className="fa-light fa-award mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{t("certificate")}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
{
|
||||
props?.isCompleted && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="border-none bg-[#fff]">
|
||||
<i
|
||||
className="fa-light fa-award mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{t("certificate")}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)
|
||||
}
|
||||
{props?.isSignYourself && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
@@ -518,24 +532,28 @@ function Header(props) {
|
||||
></i>
|
||||
<span className="hidden lg:block">{t("print")}</span>
|
||||
</button>
|
||||
{props?.isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">{t("certificate")}</span>
|
||||
</button>
|
||||
)}
|
||||
{
|
||||
props?.isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">
|
||||
{t("certificate")}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
|
||||
@@ -592,23 +610,25 @@ function Header(props) {
|
||||
)
|
||||
) : props?.isCompleted ? (
|
||||
<div className="flex flex-row">
|
||||
{props?.isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
>
|
||||
<i className="fa-light fa-award" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">
|
||||
{t("certificate")}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{
|
||||
props?.isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
>
|
||||
<i className="fa-light fa-award" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">
|
||||
{t("certificate")}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<button
|
||||
onClick={(e) =>
|
||||
handleToPrint(e, setIsDownloading, props?.pdfDetails)
|
||||
@@ -622,7 +642,8 @@ function Header(props) {
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
onClick={() => props?.setIsDownloadModal(true)}
|
||||
// onClick={() => props?.setIsDownloadModal(true)}
|
||||
onClick={() => handleDownloadBtn()}
|
||||
>
|
||||
<i className="fa-light fa-download" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">{t("download")}</span>
|
||||
|
||||
@@ -67,9 +67,12 @@ function PdfTools(props) {
|
||||
alert(t("only-pdf-allowed"));
|
||||
return;
|
||||
}
|
||||
const mb = Math.round(file?.size / Math.pow(1024, 2));
|
||||
if (mb > maxFileSize) {
|
||||
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
|
||||
const fileSize =
|
||||
maxFileSize;
|
||||
const pdfsize = file?.size;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
@@ -126,6 +129,13 @@ function PdfTools(props) {
|
||||
useObjectStreams: false
|
||||
});
|
||||
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
|
||||
const pdfsize = pdfBuffer?.byteLength;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
props.setPdfArrayBuffer(pdfBuffer);
|
||||
props.setPdfBase64Url(pdfBase64);
|
||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, forwardRef } from "react";
|
||||
import BorderResize from "./BorderResize";
|
||||
import { Rnd } from "react-rnd";
|
||||
import {
|
||||
@@ -15,7 +15,13 @@ import {
|
||||
cellsWidget,
|
||||
textWidget,
|
||||
selectFormat,
|
||||
randomId
|
||||
randomId,
|
||||
getYear,
|
||||
getMonth,
|
||||
months,
|
||||
years,
|
||||
getDefaultDate,
|
||||
getDefaultFormat
|
||||
} from "../../constant/Utils";
|
||||
import PlaceholderType from "./PlaceholderType";
|
||||
import moment from "moment";
|
||||
@@ -29,47 +35,34 @@ import {
|
||||
} from "../../redux/reducers/widgetSlice";
|
||||
import { themeColor } from "../../constant/const";
|
||||
import { useGuidelinesContext } from "../../context/GuidelinesContext";
|
||||
|
||||
//function to get default format
|
||||
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
|
||||
|
||||
//function to convert formated date to new Date() format
|
||||
const getDefaultDate = (dateStr, format) => {
|
||||
//get valid date format for moment to convert formated date to new Date() format
|
||||
const formats = changeDateToMomentFormat(format);
|
||||
const parsedDate = moment(dateStr, formats);
|
||||
let date;
|
||||
if (parsedDate.isValid()) {
|
||||
date = new Date(parsedDate.toISOString());
|
||||
return date;
|
||||
} else {
|
||||
date = new Date();
|
||||
return date;
|
||||
}
|
||||
};
|
||||
import DatePicker from "react-datepicker";
|
||||
|
||||
function Placeholder(props) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const { showGuidelines } = useGuidelinesContext();
|
||||
const statusArr = ["Required", "Optional"];
|
||||
const widgetData =
|
||||
props.pos?.options?.defaultValue || props.pos?.options?.response;
|
||||
const [isDateModal, setIsDateModal] = useState(false);
|
||||
const [containerScale, setContainerScale] = useState();
|
||||
const [selectDate, setSelectDate] = useState({});
|
||||
const [dateFormat, setDateFormat] = useState([]);
|
||||
const [dateFormatList, setDateFormatList] = useState([]);
|
||||
const [clickonWidget, setClickonWidget] = useState({});
|
||||
const [isDateReadOnly, setIsDateReadOnly] = useState(
|
||||
props?.pos?.options?.isReadOnly || false
|
||||
const [formdata, setFormdata] = useState({
|
||||
status: props?.pos?.options?.status || "required",
|
||||
isReadOnly: props?.pos?.options?.isReadOnly || false
|
||||
});
|
||||
const [isToday, setIsToday] = useState(
|
||||
props.pos?.options?.response === "today" ? true : false
|
||||
);
|
||||
const startDate = props?.pos?.options?.response
|
||||
? getDefaultDate(
|
||||
props?.pos?.options?.response,
|
||||
props.pos?.options?.validation?.format
|
||||
)
|
||||
: new Date();
|
||||
|
||||
: "";
|
||||
useEffect(() => {
|
||||
const getPdfPageWidth = props.pdfOriginalWH.find(
|
||||
(data) => data.pageNumber === props.pageNumber
|
||||
@@ -102,29 +95,87 @@ function Placeholder(props) {
|
||||
}
|
||||
}, [widgetData]);
|
||||
|
||||
//function is used to convert date according to selected format
|
||||
const handleGetFormatDate = (data, isDateChange) => {
|
||||
const isSpecialDateFormat =
|
||||
data?.format &&
|
||||
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
|
||||
let updateDate = data.date;
|
||||
let date = "";
|
||||
if (isSpecialDateFormat && updateDate) {
|
||||
date = isDateChange
|
||||
? moment(updateDate).format(changeDateToMomentFormat(data.format))
|
||||
: updateDate;
|
||||
} else if (updateDate) {
|
||||
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
|
||||
const newDate = new Date(updateDate);
|
||||
date = moment(newDate.getTime()).format(
|
||||
changeDateToMomentFormat(data?.format)
|
||||
);
|
||||
}
|
||||
setSelectDate({ date: date, format: data?.format });
|
||||
return date;
|
||||
};
|
||||
const saveDateSetting = (data) => {
|
||||
if (formdata?.isReadOnly && !selectDate.date && !isToday) {
|
||||
alert(t("read-only-date-error"));
|
||||
return;
|
||||
}
|
||||
handleSaveDate(data);
|
||||
setIsDateModal(false);
|
||||
props.setFontColor("");
|
||||
props.setFontSize("");
|
||||
};
|
||||
//function to save date and format on local array onchange date and onclick format
|
||||
const handleSaveDate = (data, isDateChange) => {
|
||||
const date = isToday ? "today" : handleGetFormatDate(data, isDateChange);
|
||||
//`onChangeInput` is used to save data related to date in a placeholder field
|
||||
onChangeInput(
|
||||
date,
|
||||
props.pos,
|
||||
props.xyPosition,
|
||||
props.index,
|
||||
props.setXyPosition,
|
||||
props.data && props.data.Id,
|
||||
false,
|
||||
data?.format,
|
||||
props.fontSize || props.pos?.options?.fontSize || 12,
|
||||
props.fontColor || props.pos?.options?.fontColor || "black",
|
||||
formdata
|
||||
);
|
||||
};
|
||||
|
||||
//Date-picker- accept date - Thu Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
|
||||
//if date have in this format - "30/12/2025"
|
||||
//then first need to be convert in this format - Tue Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
|
||||
//Using moment - moment("30/12/2025", "DD/MM/YYYY").toDate();
|
||||
|
||||
//moment package where we show date according to format in list
|
||||
//it support date in this type - "2025-09-11"// ISO string, "2025-09-11T14:30:00" // ISO datetime
|
||||
//function change format array list with selected date and format
|
||||
const changeDateFormat = () => {
|
||||
const updateDate = [];
|
||||
dateFormatArr.map((data) => {
|
||||
let date;
|
||||
if (selectDate && selectDate.format === "dd-MM-yyyy") {
|
||||
const [day, month, year] = selectDate.date.split("-");
|
||||
let date = selectDate?.date || new Date();
|
||||
const isString = typeof date === "string";
|
||||
if (selectDate.format === "dd-MM-yyyy" && isString) {
|
||||
const [day, month, year] = date.split("-");
|
||||
date = new Date(`${year}-${month}-${day}`);
|
||||
} else if (selectDate && selectDate.format === "dd.MM.yyyy") {
|
||||
const [day, month, year] = selectDate.date.split(".");
|
||||
} else if (selectDate.format === "dd.MM.yyyy" && isString) {
|
||||
const [day, month, year] = date.split(".");
|
||||
date = new Date(`${year}.${month}.${day}`);
|
||||
} else if (selectDate && selectDate.format === "dd/MM/yyyy") {
|
||||
const [day, month, year] = selectDate.date.split("/");
|
||||
} else if (selectDate.format === "dd/MM/yyyy" && isString) {
|
||||
const [day, month, year] = date.split("/");
|
||||
date = new Date(`${year}/${month}/${day}`);
|
||||
} else {
|
||||
date = new Date(selectDate?.date);
|
||||
date = selectDate?.date ? new Date(selectDate?.date) : new Date();
|
||||
}
|
||||
const milliseconds = date.getTime();
|
||||
const newDate = moment(milliseconds).format(data);
|
||||
const dateObj = { date: newDate, format: selectFormat(data) };
|
||||
updateDate.push(dateObj);
|
||||
});
|
||||
setDateFormat(updateDate);
|
||||
setDateFormatList(updateDate);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -287,41 +338,6 @@ function Placeholder(props) {
|
||||
}
|
||||
};
|
||||
|
||||
//function to save date and format on local array onchange date and onclick format
|
||||
const handleSaveDate = (data, isDateChange) => {
|
||||
const isSpecialDateFormat =
|
||||
data?.format &&
|
||||
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
|
||||
let updateDate = data.date;
|
||||
let date;
|
||||
if (isSpecialDateFormat) {
|
||||
date = isDateChange
|
||||
? moment(updateDate).format(changeDateToMomentFormat(data.format))
|
||||
: updateDate;
|
||||
} else {
|
||||
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
|
||||
const newDate = new Date(updateDate);
|
||||
date = moment(newDate.getTime()).format(
|
||||
changeDateToMomentFormat(data?.format)
|
||||
);
|
||||
}
|
||||
//`onChangeInput` is used to save data related to date in a placeholder field
|
||||
onChangeInput(
|
||||
date,
|
||||
props.pos,
|
||||
props.xyPosition,
|
||||
props.index,
|
||||
props.setXyPosition,
|
||||
props.data && props.data.Id,
|
||||
false,
|
||||
data?.format,
|
||||
props.fontSize || props.pos?.options?.fontSize || 12,
|
||||
props.fontColor || props.pos?.options?.fontColor || "black",
|
||||
isDateReadOnly || false
|
||||
);
|
||||
setSelectDate({ date: date, format: data?.format });
|
||||
};
|
||||
|
||||
const setCellCount = (key, newCount) => {
|
||||
props.setXyPosition((prev) => {
|
||||
const isSignerList = prev.some((d) => d.signerPtr);
|
||||
@@ -650,6 +666,40 @@ 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 "
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
{value}
|
||||
<i className="fa-light fa-calendar ml-[5px]"></i>
|
||||
</div>
|
||||
));
|
||||
ExampleCustomInput.displayName = "ExampleCustomInput";
|
||||
const handleChangeFormat = (e) => {
|
||||
const selectedIndex = e.target.value;
|
||||
e.stopPropagation();
|
||||
if (
|
||||
props?.isPlaceholder &&
|
||||
props?.data?.Role !== "prefill" &&
|
||||
!selectDate?.date
|
||||
) {
|
||||
setSelectDate((prev) => ({
|
||||
...prev,
|
||||
format: dateFormatList[selectedIndex]?.format
|
||||
}));
|
||||
} else {
|
||||
setSelectDate(dateFormatList[selectedIndex]);
|
||||
}
|
||||
};
|
||||
const handleClearDate = () => {
|
||||
setIsToday(false);
|
||||
setSelectDate((prev) => ({
|
||||
...prev,
|
||||
date: ""
|
||||
}));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{/* Check if a text widget (prefill type) exists. Once the user enters a value and clicks outside or the widget becomes non-selectable, it should appear as plain text (just like embedded text in a document). When the user clicks on the text again, it should become editable. */}
|
||||
@@ -889,10 +939,8 @@ function Placeholder(props) {
|
||||
(props?.isAlllowModify &&
|
||||
!props?.assignedWidgetId.includes(props.pos.key))
|
||||
}
|
||||
setSelectDate={setSelectDate}
|
||||
selectDate={selectDate}
|
||||
startDate={startDate}
|
||||
handleSaveDate={handleSaveDate}
|
||||
xPos={props.xPos}
|
||||
calculateFont={calculateFont}
|
||||
setCellCount={setCellCount}
|
||||
@@ -902,22 +950,18 @@ function Placeholder(props) {
|
||||
)}
|
||||
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
|
||||
<div className="text-base-content h-[100%] p-[20px]">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-y-3">
|
||||
<div className="flex flex-row items-center gap-x-1">
|
||||
<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"
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-4"
|
||||
defaultValue={""}
|
||||
onChange={(e) => {
|
||||
const selectedIndex = e.target.value;
|
||||
e.stopPropagation();
|
||||
setSelectDate(dateFormat[selectedIndex]);
|
||||
}}
|
||||
onChange={(e) => handleChangeFormat(e)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("select-date-format")}
|
||||
</option>
|
||||
{dateFormat.map((data, ind) => {
|
||||
{dateFormatList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" value={ind} key={ind}>
|
||||
{data?.date ? data?.date : "nodata"}
|
||||
@@ -925,79 +969,190 @@ function Placeholder(props) {
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
{selectDate.format}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
{selectDate.format}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2 mt-3">
|
||||
<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>
|
||||
{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>
|
||||
</label>
|
||||
<span
|
||||
onClick={() => handleClearDate()}
|
||||
className="underline text-blue-500 cursor-pointer md:ml-2"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-[10px]">
|
||||
{statusArr.map((data, ind) => {
|
||||
return (
|
||||
<div
|
||||
key={ind}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
name="status"
|
||||
onChange={() =>
|
||||
setFormdata({
|
||||
...formdata,
|
||||
status: data.toLowerCase()
|
||||
})
|
||||
}
|
||||
checked={
|
||||
formdata.status.toLowerCase() === data.toLowerCase()
|
||||
}
|
||||
/>
|
||||
<div className="text-[13px]">
|
||||
{t(`widget-status.${data}`)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2">
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="capitalize">{t("font-size")} :</span>
|
||||
<select
|
||||
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
value={
|
||||
props.fontSize || clickonWidget.options?.fontSize || 12
|
||||
}
|
||||
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
|
||||
>
|
||||
{fontsizeArr.map((size, ind) => (
|
||||
<option className="text-[13px]" value={size} key={ind}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<span className="capitalize">{t("color")} :</span>
|
||||
<select
|
||||
value={
|
||||
props.fontColor ||
|
||||
clickonWidget.options?.fontColor ||
|
||||
"black"
|
||||
}
|
||||
onChange={(e) => props.setFontColor(e.target.value)}
|
||||
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
>
|
||||
{fontColorArr.map((color, ind) => (
|
||||
<option value={color} key={ind}>
|
||||
{t(`color-type.${color}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
props.fontColor || props.pos.options?.fontColor || "black"
|
||||
}}
|
||||
className="w-5 h-[19px] ml-1"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div 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 mt-3">
|
||||
<input
|
||||
id="isReadOnly"
|
||||
name="isReadOnly"
|
||||
type="checkbox"
|
||||
checked={
|
||||
isDateReadOnly || props.pos.options?.isReadOnly || false
|
||||
}
|
||||
className="op-checkbox op-checkbox-xs"
|
||||
onChange={() => setIsDateReadOnly(!isDateReadOnly)}
|
||||
/>
|
||||
<label
|
||||
className="ml-1.5 mb-0 capitalize text-[13px]"
|
||||
htmlFor="isreadonly"
|
||||
>
|
||||
{t("read-only")}
|
||||
{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>
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary"
|
||||
onClick={() => {
|
||||
setIsDateModal(false);
|
||||
handleSaveDate(selectDate);
|
||||
props.setFontColor("");
|
||||
props.setFontSize("");
|
||||
saveDateSetting(selectDate);
|
||||
}}
|
||||
>
|
||||
{t("save")}
|
||||
|
||||
@@ -34,8 +34,9 @@ function PlaceholderType(props) {
|
||||
((props.isSelfSign || props.isNeedSign) &&
|
||||
props.data?.signerObjId === props.signerObjId);
|
||||
const isReadOnly =
|
||||
props.pos.options?.isReadOnly ||
|
||||
props.data?.signerObjId !== props.signerObjId;
|
||||
props?.data?.Role !== "prefill" &&
|
||||
(props.pos.options?.isReadOnly ||
|
||||
props.data?.signerObjId !== props.signerObjId);
|
||||
// prefer the latest response value over any default value
|
||||
const widgetData =
|
||||
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
|
||||
@@ -83,8 +84,7 @@ function PlaceholderType(props) {
|
||||
color: fontColor,
|
||||
fontFamily: "Arial, sans-serif"
|
||||
}}
|
||||
className={`${isReadOnly ? `select-none` : ``} ${selectWidgetCls} overflow-hidden`}
|
||||
disabled={isReadOnly}
|
||||
className={`${isReadOnly ? `select-none opacity-25` : ``} ${selectWidgetCls} overflow-hidden`}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
@@ -399,11 +399,9 @@ function PlaceholderType(props) {
|
||||
popperPlacement="top-end"
|
||||
customInput={<ExampleCustomInput />}
|
||||
dateFormat={
|
||||
props.selectDate
|
||||
? props.selectDate?.format
|
||||
: props.pos?.options?.validation?.format
|
||||
? props.pos?.options?.validation?.format
|
||||
: "MM/dd/yyyy"
|
||||
props?.selectDate?.format ||
|
||||
props.pos?.options?.validation?.format ||
|
||||
"MM/dd/yyyy"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@@ -412,11 +410,9 @@ function PlaceholderType(props) {
|
||||
className="select-none-cls overflow-hidden"
|
||||
>
|
||||
<span>
|
||||
{props.selectDate
|
||||
? props.selectDate?.format
|
||||
: props.pos?.options?.validation?.format
|
||||
? props.pos?.options?.validation?.format
|
||||
: "MM/dd/yyyy"}
|
||||
{props?.selectDate?.format ||
|
||||
props.pos?.options?.validation?.format ||
|
||||
"MM/dd/yyyy"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,6 @@ function PrefillWidgetModal(props) {
|
||||
const loadedSet = useRef(new Set());
|
||||
const initializedRef = useRef(false); // prevent rerun on state updates
|
||||
const prefillImg = useSelector((state) => state.widget.prefillImg);
|
||||
const [uniqueWidget, setUniqueWidget] = useState([]);
|
||||
const [image, setImage] = useState(null);
|
||||
const [imageLoaders, setImageLoaders] = useState({});
|
||||
const [currentWidget, setCurrentWidget] = useState("");
|
||||
@@ -68,7 +67,7 @@ function PrefillWidgetModal(props) {
|
||||
);
|
||||
|
||||
// useMemo to memoize the calculation of unique widgets
|
||||
const memoizedUniqueWidget = useMemo(() => {
|
||||
const uniqueWidget = useMemo(() => {
|
||||
//functions to used remove duplicate name values across all pages
|
||||
if (!props.prefillData) return [];
|
||||
//This will help us track which name values have already been encountered across all pages.
|
||||
@@ -119,10 +118,6 @@ function PrefillWidgetModal(props) {
|
||||
savePrefillImg();
|
||||
}, [props.xyPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
setUniqueWidget(memoizedUniqueWidget);
|
||||
}, [memoizedUniqueWidget]);
|
||||
|
||||
useEffect(() => {
|
||||
if (totalImages > 0 && loadedImages === totalImages) {
|
||||
setLoading(false);
|
||||
@@ -441,14 +436,12 @@ function PrefillWidgetModal(props) {
|
||||
handleCheckboxValue(e.target.checked, ind, position);
|
||||
}}
|
||||
/>
|
||||
{!position.options?.isHideLabel && (
|
||||
<label
|
||||
htmlFor={`checkbox-${position.key + ind}`}
|
||||
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
|
||||
>
|
||||
{data}
|
||||
</label>
|
||||
)}
|
||||
<label
|
||||
htmlFor={`checkbox-${position.key + ind}`}
|
||||
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
|
||||
>
|
||||
{data}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -560,14 +553,12 @@ function PrefillWidgetModal(props) {
|
||||
handleWidgetDetails(position, data);
|
||||
}}
|
||||
/>
|
||||
{!position.options?.isHideLabel && (
|
||||
<label
|
||||
htmlFor={`radio-${position.key + ind}`}
|
||||
className="text-xs mb-0 ml-[2px] cursor-pointer"
|
||||
>
|
||||
{data}
|
||||
</label>
|
||||
)}
|
||||
<label
|
||||
htmlFor={`radio-${position.key + ind}`}
|
||||
className="text-xs mb-0 ml-[2px] cursor-pointer"
|
||||
>
|
||||
{data}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -591,8 +582,8 @@ function PrefillWidgetModal(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmbedPrefill = (item) => {
|
||||
props.handleCreateDocument();
|
||||
const handleEmbedPrefill = async (item) => {
|
||||
await props.handleCreateDocument();
|
||||
};
|
||||
//`loadOptions` function to use show all list of signer in dropdown
|
||||
const loadOptions = async (inputValue) => {
|
||||
@@ -654,7 +645,7 @@ function PrefillWidgetModal(props) {
|
||||
handleClose={props.handleClosePrefillModal}
|
||||
>
|
||||
<div className="relative">
|
||||
{loading && (
|
||||
{(props?.isSubmit || loading) && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/70 z-[9999]">
|
||||
<Loader />
|
||||
</div>
|
||||
@@ -733,7 +724,7 @@ function PrefillWidgetModal(props) {
|
||||
</div>
|
||||
<div className="flex gap-2 mx-4 mb-3">
|
||||
<button
|
||||
disabled={isAnyLoaderActive}
|
||||
disabled={isAnyLoaderActive || props?.isSubmit}
|
||||
className="op-btn op-btn-primary op-btn-sm w-[80px]"
|
||||
onClick={() => handleEmbedPrefill(props?.item)}
|
||||
>
|
||||
|
||||
@@ -85,9 +85,12 @@ function RenderAllPdfPage(props) {
|
||||
alert(t("only-pdf-allowed"));
|
||||
return;
|
||||
}
|
||||
const mb = Math.round(file?.size / Math.pow(1024, 2));
|
||||
if (mb > maxFileSize) {
|
||||
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
|
||||
const fileSize =
|
||||
maxFileSize;
|
||||
const pdfsize = file?.size;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
@@ -144,6 +147,13 @@ function RenderAllPdfPage(props) {
|
||||
useObjectStreams: false
|
||||
});
|
||||
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
|
||||
const pdfsize = pdfBuffer?.byteLength;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
props.setPdfArrayBuffer(pdfBuffer);
|
||||
props.setPdfBase64Url(pdfBase64);
|
||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
convertTextToImg,
|
||||
convertJpegToPng,
|
||||
convertBase64ToFile,
|
||||
generatePdfName
|
||||
generatePdfName,
|
||||
getDefaultDate,
|
||||
getDefaultFormat
|
||||
} from "../../constant/Utils";
|
||||
import CellsWidget from "./CellsWidget";
|
||||
import DatePicker from "react-datepicker";
|
||||
@@ -36,7 +38,8 @@ import {
|
||||
setLastIndex,
|
||||
setScrollTriggerId,
|
||||
setPrefillImg,
|
||||
setPrefillImgLoad
|
||||
setPrefillImgLoad,
|
||||
setTypedSignFont
|
||||
} from "../../redux/reducers/widgetSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import Loader from "../../primitives/Loader";
|
||||
@@ -44,22 +47,6 @@ import { emailRegex } from "../../constant/const";
|
||||
import RegexParser from "regex-parser";
|
||||
import { saveToMySign } from "../../utils/widgetUtils";
|
||||
|
||||
//function to get default format
|
||||
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
|
||||
//function to convert formatted date to new Date() format
|
||||
const getDefaultDate = (dateStr, format) => {
|
||||
//get valid date format for moment to convert formatted date to new Date() format
|
||||
const formats = changeDateToMomentFormat(format);
|
||||
const parsedDate = moment(dateStr, formats);
|
||||
let date;
|
||||
if (parsedDate.isValid()) {
|
||||
date = new Date(parsedDate.toISOString());
|
||||
return date;
|
||||
} else {
|
||||
date = new Date();
|
||||
return date;
|
||||
}
|
||||
};
|
||||
const fontOptions = [
|
||||
{ value: "Fasthand" },
|
||||
{ value: "Dancing Script" },
|
||||
@@ -79,6 +66,7 @@ function WidgetsValueModal(props) {
|
||||
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
|
||||
const myInitial = useSelector((state) => state.widget.myInitial);
|
||||
const lastWidget = useSelector((state) => state.widget.lastIndex);
|
||||
const typedSignFont = useSelector((state) => state.widget.typedSignFont);
|
||||
const { t } = useTranslation();
|
||||
const canvasRef = useRef(null);
|
||||
const imageRef = useRef(null);
|
||||
@@ -173,7 +161,7 @@ function WidgetsValueModal(props) {
|
||||
currWidgetsDetails?.options?.response,
|
||||
currWidgetsDetails?.options?.validation?.format
|
||||
)
|
||||
: new Date()
|
||||
: ""
|
||||
);
|
||||
useEffect(() => {
|
||||
dispatch(setScrollTriggerId(currWidgetsDetails?.key));
|
||||
@@ -254,9 +242,7 @@ function WidgetsValueModal(props) {
|
||||
setXyPosition,
|
||||
uniqueId,
|
||||
false,
|
||||
data?.format,
|
||||
currWidgetsDetails?.options?.fontSize || 12,
|
||||
currWidgetsDetails?.options?.fontColor || "black"
|
||||
data?.format
|
||||
);
|
||||
setSelectDate({ date: date, format: data?.format });
|
||||
};
|
||||
@@ -278,10 +264,7 @@ function WidgetsValueModal(props) {
|
||||
setIsLoader(true);
|
||||
try {
|
||||
dispatch(
|
||||
setPrefillImg({
|
||||
id: currWidgetsDetails?.key,
|
||||
base64: image?.src
|
||||
})
|
||||
setPrefillImg({ id: currWidgetsDetails?.key, base64: image?.src })
|
||||
);
|
||||
const imageName = generatePdfName(16);
|
||||
const imageUrl = await convertBase64ToFile(
|
||||
@@ -630,11 +613,12 @@ function WidgetsValueModal(props) {
|
||||
useEffect(() => {
|
||||
const loadFont = async () => {
|
||||
try {
|
||||
await document.fonts.load(`20px ${fontSelect}`);
|
||||
const selectFontSTyle = fontOptions.find(
|
||||
(font) => font.value === fontSelect
|
||||
const selectedFont = typedSignFont || fontSelect;
|
||||
await document.fonts.load(`20px ${selectedFont}`);
|
||||
const selectFontStyle = fontOptions.find(
|
||||
(font) => font.value === selectedFont
|
||||
);
|
||||
setFontSelect(selectFontSTyle?.value || fontOptions[0].value);
|
||||
setFontSelect(selectFontStyle?.value || fontOptions[0].value);
|
||||
} catch (error) {
|
||||
console.error("Error loading font:", error);
|
||||
}
|
||||
@@ -671,18 +655,21 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
}
|
||||
if (isTab === "type") {
|
||||
const signerName = localStorage.getItem("signer")
|
||||
? JSON.parse(localStorage.getItem("signer"))?.Name
|
||||
: currentUserName;
|
||||
//trim user name or typed name value to show in initial signature
|
||||
const trimmedName = typedSignature
|
||||
? typedSignature?.trim()
|
||||
: props?.journey === "kiosk-signing" && kiosk_signer
|
||||
? kiosk_signer[0]?.Name?.trim()
|
||||
: currentUserName?.trim();
|
||||
: signerName?.trim();
|
||||
//get full name of user
|
||||
const fullUserName =
|
||||
typedSignature ||
|
||||
(props?.journey === "kiosk-signing" && kiosk_signer
|
||||
? kiosk_signer[0]?.Name
|
||||
: currentUserName);
|
||||
: signerName);
|
||||
const firstCharacter = trimmedName?.charAt(0);
|
||||
const userName =
|
||||
currWidgetsDetails?.type === "initials" ? firstCharacter : fullUserName;
|
||||
@@ -697,12 +684,13 @@ function WidgetsValueModal(props) {
|
||||
const maxWidth = currWidgetsDetails?.Width;
|
||||
const maxHeight = currWidgetsDetails?.Height;
|
||||
const widgetDims = { Width: maxWidth, Height: maxHeight };
|
||||
const fontFamily = fontStyle || fontSelect || "Fasthand";
|
||||
const fontFamily = fontStyle || typedSignFont || fontSelect || "Fasthand";
|
||||
const fillColor = color || penColor;
|
||||
const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims);
|
||||
setSignature(dataUrl);
|
||||
setTextWidth(maxWidth);
|
||||
setTextHeight(maxHeight);
|
||||
dispatch(setTypedSignFont(fontFamily));
|
||||
};
|
||||
const PenColorComponent = () => {
|
||||
return (
|
||||
@@ -876,13 +864,14 @@ function WidgetsValueModal(props) {
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
{value}
|
||||
{value ? value : "Select date"}
|
||||
<i className="fa-light fa-calendar ml-[5px]"></i>
|
||||
</div>
|
||||
));
|
||||
ExampleCustomInput.displayName = "ExampleCustomInput";
|
||||
//function to set onchange date
|
||||
const handleOnDateChange = (date) => {
|
||||
setWidgetValue(date);
|
||||
setStartDate(date);
|
||||
};
|
||||
const handleOnchangeTextBox = (e) => {
|
||||
@@ -1200,8 +1189,8 @@ function WidgetsValueModal(props) {
|
||||
)
|
||||
) : isTab === "type" ? (
|
||||
<>
|
||||
<div className="flex justify-between items-center tabWidth">
|
||||
<span className="mr-[5px] text-[12px]">
|
||||
<div className="flex justify-between items-center tabWidth rounded-[4px]">
|
||||
<span className="ml-[5px] text-[12px] text-base-content">
|
||||
{currWidgetsDetails?.type === "initials"
|
||||
? t("initial-teb")
|
||||
: t("signature-tab")}
|
||||
@@ -1228,7 +1217,7 @@ function WidgetsValueModal(props) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-[1px] border-[#d6d3d3] mt-[10px] ml-[5px]">
|
||||
<div className="border-[1px] border-[#d6d3d3] mt-[10px] rounded-[4px]">
|
||||
{fontOptions.map((font, ind) => {
|
||||
return (
|
||||
<div
|
||||
@@ -1251,7 +1240,7 @@ function WidgetsValueModal(props) {
|
||||
>
|
||||
{typedSignature
|
||||
? typedSignature
|
||||
: "Your signature"}
|
||||
: t("Your-Signature")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1466,52 +1455,59 @@ function WidgetsValueModal(props) {
|
||||
);
|
||||
case "date":
|
||||
return (
|
||||
<div
|
||||
className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`}
|
||||
>
|
||||
<DatePicker
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<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={startDate}
|
||||
onChange={(date) => handleOnDateChange(date)}
|
||||
popperPlacement="top-end"
|
||||
customInput={<ExampleCustomInput />}
|
||||
dateFormat={
|
||||
selectDate
|
||||
? selectDate?.format
|
||||
: currWidgetsDetails?.options?.validation?.format
|
||||
? currWidgetsDetails?.options?.validation?.format
|
||||
: "MM/dd/yyyy"
|
||||
}
|
||||
portalId="root-portal"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div
|
||||
className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`}
|
||||
>
|
||||
<DatePicker
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<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={startDate}
|
||||
onChange={(date) => handleOnDateChange(date)}
|
||||
popperPlacement="top-end"
|
||||
customInput={<ExampleCustomInput />}
|
||||
dateFormat={
|
||||
selectDate
|
||||
? selectDate?.format
|
||||
: currWidgetsDetails?.options?.validation?.format
|
||||
? currWidgetsDetails?.options?.validation?.format
|
||||
: "MM/dd/yyyy"
|
||||
}
|
||||
portalId="root-portal"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<span className="text-gray-300">
|
||||
{currWidgetsDetails?.options?.validation?.format}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "email":
|
||||
@@ -1591,9 +1587,6 @@ function WidgetsValueModal(props) {
|
||||
const isCheckBox =
|
||||
!currWidgetsDetails.options?.isReadOnly &&
|
||||
currWidgetsDetails.type === "checkbox";
|
||||
const isRadio =
|
||||
!currWidgetsDetails?.options?.isReadOnly &&
|
||||
currWidgetsDetails?.type === radioButtonWidget;
|
||||
if (isCheckBox) {
|
||||
//get minimum required count if exist
|
||||
const minCount =
|
||||
@@ -1655,7 +1648,7 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<span className="text-center text-[12px]">
|
||||
<span className="text-center text-[12px] text-base-content">
|
||||
{t("required-mssg", { leftRequiredWidget, totalWidget })}
|
||||
</span>
|
||||
);
|
||||
@@ -1887,15 +1880,15 @@ function WidgetsValueModal(props) {
|
||||
{isFinish ? (
|
||||
<>
|
||||
<div className="p-1 mt-3">
|
||||
<span className="text-base">{t("finish-mssg")}</span>
|
||||
<span className="text-base text-base-content">
|
||||
{t("finish-mssg")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm px-4"
|
||||
onClick={() => {
|
||||
handleFinish();
|
||||
}}
|
||||
onClick={() => handleFinish()}
|
||||
>
|
||||
{t("finish")}
|
||||
</button>
|
||||
@@ -1955,9 +1948,7 @@ function WidgetsValueModal(props) {
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm"
|
||||
onClick={() => {
|
||||
handleClickOnNext();
|
||||
}}
|
||||
onClick={() => handleClickOnNext()}
|
||||
disabled={handleDisable()}
|
||||
>
|
||||
{t("save")}
|
||||
@@ -1967,10 +1958,7 @@ function WidgetsValueModal(props) {
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm"
|
||||
disabled={handleDisable()}
|
||||
onClick={() => {
|
||||
const isFinishDoc = true;
|
||||
handleClickOnNext(isFinishDoc);
|
||||
}}
|
||||
onClick={() => handleClickOnNext(true)} // isFinishDoc
|
||||
>
|
||||
{t("done")}
|
||||
</button>
|
||||
@@ -1978,9 +1966,7 @@ function WidgetsValueModal(props) {
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm text-xs md:text-sm"
|
||||
onClick={() => {
|
||||
handleClickOnNext();
|
||||
}}
|
||||
onClick={() => handleClickOnNext()}
|
||||
disabled={handleDisable()}
|
||||
>
|
||||
{t("next-field")}
|
||||
|
||||
@@ -410,9 +410,14 @@ export const changeDateToMomentFormat = (format) => {
|
||||
return "L";
|
||||
}
|
||||
};
|
||||
export const addWidgetOptions = (type, signer, placeholder, widgetValue) => {
|
||||
export const addWidgetOptions = (
|
||||
type,
|
||||
signer,
|
||||
placeholder,
|
||||
role,
|
||||
widgetValue
|
||||
) => {
|
||||
let defaultOpt;
|
||||
//condition to handle widgets name field
|
||||
if (placeholder) {
|
||||
const countSameWidget = placeholder?.reduce((count, page) => {
|
||||
return count + page.pos.filter((item) => item.type === type).length;
|
||||
@@ -472,7 +477,7 @@ export const addWidgetOptions = (type, signer, placeholder, widgetValue) => {
|
||||
: "MM/dd/yyyy";
|
||||
return {
|
||||
...defaultOpt,
|
||||
response: getDate(signer?.DateFormat),
|
||||
response: role === "prefill" ? getDate(signer?.DateFormat) : "",
|
||||
validation: { format: dateFormat, type: "date-format" }
|
||||
};
|
||||
}
|
||||
@@ -823,6 +828,8 @@ export const createDocument = async (
|
||||
) => {
|
||||
if (template && template.length > 0) {
|
||||
const Doc = template[0];
|
||||
const date = new Date();
|
||||
const isoDate = date.toISOString();
|
||||
let extUserId = Doc.ExtUserPtr.objectId;
|
||||
let creatorId = Doc.CreatedBy.objectId;
|
||||
const Extand_Class = localStorage.getItem("Extand_Class");
|
||||
@@ -898,6 +905,7 @@ export const createDocument = async (
|
||||
IsTourEnabled: Doc?.IsTourEnabled || false,
|
||||
AllowModifications: Doc?.AllowModifications || false,
|
||||
TimeToCompleteDays: parseInt(Doc?.TimeToCompleteDays) || 15,
|
||||
DocSentAt: { __type: "Date", iso: isoDate },
|
||||
...SignatureType,
|
||||
...NotifyOnSignatures,
|
||||
...Bcc,
|
||||
@@ -999,7 +1007,7 @@ export const onChangeInput = (
|
||||
dateFormat,
|
||||
fontSize,
|
||||
fontColor,
|
||||
isDateReadOnly
|
||||
dateDetails
|
||||
) => {
|
||||
const isSigners = xyPosition.some(
|
||||
(data) => data.signerPtr || data.Role === "prefill"
|
||||
@@ -1031,9 +1039,18 @@ export const onChangeInput = (
|
||||
options: {
|
||||
...position.options,
|
||||
response: value,
|
||||
fontSize: fontSize,
|
||||
fontColor: fontColor,
|
||||
isReadOnly: isDateReadOnly || false,
|
||||
fontSize: fontSize ? fontSize : position.options?.fontSize,
|
||||
fontColor: fontColor
|
||||
? fontColor
|
||||
: position.options?.fontColor,
|
||||
isReadOnly:
|
||||
dateDetails?.isReadOnly !== "undefined"
|
||||
? dateDetails?.isReadOnly
|
||||
: position.options?.isReadOnly,
|
||||
status:
|
||||
dateDetails?.status !== "undefined"
|
||||
? dateDetails?.status
|
||||
: position.options?.status,
|
||||
validation: {
|
||||
type: "date-format",
|
||||
format: dateFormat // This indicates the required date format explicitly.
|
||||
@@ -1993,7 +2010,17 @@ export const embedWidgetsToDoc = async (
|
||||
} else if (isTextTypeWidget) {
|
||||
let textContent = "";
|
||||
if (position?.options?.response) {
|
||||
textContent = position.options?.response;
|
||||
if (
|
||||
position.type === "date" &&
|
||||
position.options?.response === "today"
|
||||
) {
|
||||
const getFormat = changeDateToMomentFormat(
|
||||
position?.options?.validation?.format
|
||||
);
|
||||
textContent = moment().format(getFormat);
|
||||
} else {
|
||||
textContent = position.options?.response;
|
||||
}
|
||||
} else if (position?.options?.defaultValue) {
|
||||
textContent = position?.options?.defaultValue;
|
||||
}
|
||||
@@ -3429,10 +3456,10 @@ export const updateDateWidgetsRes = (
|
||||
journey,
|
||||
isRemovePrefill
|
||||
) => {
|
||||
const extUser = localStorage.getItem("Extand_Class");
|
||||
const contactUser = documentData?.Signers.find(
|
||||
const contactUser = documentData?.Signers?.find(
|
||||
(data) => data.objectId === signerId
|
||||
);
|
||||
const extUser = JSON.stringify([contactUser]);
|
||||
let placeHolders = documentData?.Placeholders;
|
||||
if (isRemovePrefill) {
|
||||
placeHolders = placeHolders?.filter((x) => x.Role !== "prefill");
|
||||
@@ -3445,18 +3472,7 @@ export const updateDateWidgetsRes = (
|
||||
placeHolder: item?.placeHolder?.map((ph) => ({
|
||||
...ph,
|
||||
pos: ph?.pos?.map((widget) => {
|
||||
// only for date widgets *and* missing response
|
||||
if (widget.type === "date" && !widget.options.response) {
|
||||
return {
|
||||
...widget,
|
||||
options: {
|
||||
...widget.options,
|
||||
response: getDate(
|
||||
changeDateToMomentFormat(widget.options.validation.format)
|
||||
)
|
||||
}
|
||||
};
|
||||
} else if (
|
||||
if (
|
||||
["name", "email", "job title", "company"].includes(widget.type) &&
|
||||
!widget.options.defaultValue &&
|
||||
!widget.options.response
|
||||
@@ -3590,6 +3606,7 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
|
||||
//in `defaultValue` variable is used to get how many checkbox checked by default
|
||||
const defaultValue =
|
||||
requiredCheckbox[i].options?.defaultValue?.length;
|
||||
const checkboxValue = response ? response : defaultValue;
|
||||
//condition to check parseMin and parseMax greater than 0 then consider it as a required check box
|
||||
if (
|
||||
parseMin > 0 &&
|
||||
@@ -3604,7 +3621,10 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
|
||||
setminRequiredCount(parseMin);
|
||||
}
|
||||
//else condition to validate minimum required checkbox
|
||||
else if (parseMin > 0 && (parseMin > response || !response)) {
|
||||
else if (
|
||||
parseMin > 0 &&
|
||||
(parseMin > checkboxValue || !checkboxValue)
|
||||
) {
|
||||
if (!showAlert) {
|
||||
showAlert = true;
|
||||
widgetKey = requiredCheckbox[i].key;
|
||||
@@ -3803,7 +3823,7 @@ export const sendEmailToSigners = async (
|
||||
htmlReqBody,
|
||||
variables
|
||||
);
|
||||
} else if (defaultMail) {
|
||||
} else if (defaultMail?.body && defaultMail?.subject) {
|
||||
const mailBody = defaultMail?.body;
|
||||
const mailSubject = defaultMail.subject;
|
||||
const replacedRequestBody = mailBody.replace(/"/g, "'");
|
||||
@@ -4031,3 +4051,19 @@ export const handleDeleteWidget = (key, Id, pageNumber, signerPos) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
//function to convert formatted date to new Date() format
|
||||
export const getDefaultDate = (dateStr, format) => {
|
||||
//get valid date format for moment to convert formatted date to new Date() format
|
||||
const formats = changeDateToMomentFormat(format);
|
||||
const parsedDate = moment(dateStr, formats);
|
||||
let date;
|
||||
if (parsedDate.isValid()) {
|
||||
date = new Date(parsedDate.toISOString());
|
||||
return date;
|
||||
} else if (dateStr === "today") {
|
||||
date = new Date();
|
||||
return date;
|
||||
}
|
||||
};
|
||||
//function to get default format
|
||||
export const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
|
||||
|
||||
@@ -9,7 +9,7 @@ export const getThemeIconColor = () => {
|
||||
return theme === "opensigndark" ? "#CCCCCC" : "#686968";
|
||||
};
|
||||
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
export const maxFileSize = 10; // 10MB
|
||||
export const maxTitleLength = 250; // 250 characters
|
||||
export const maxNoteLength = 200; // 200 characters
|
||||
export const maxDescriptionLength = 500; // 500 characters
|
||||
export const maxFileSize = 80; // for cloud 10MB / 80MB for self-hosted
|
||||
|
||||
@@ -2,10 +2,11 @@ import { useEffect, useMemo } from "react";
|
||||
|
||||
export function useManifestUrl(appName, logo) {
|
||||
const url = useMemo(() => {
|
||||
const start_url = window.location.origin || ".";
|
||||
const manifest = {
|
||||
short_name: appName,
|
||||
name: appName,
|
||||
start_url: ".",
|
||||
start_url: start_url,
|
||||
display: "standalone",
|
||||
theme_color: "#000000",
|
||||
background_color: "#ffffff",
|
||||
|
||||
@@ -11,7 +11,7 @@ export const extraCols = [
|
||||
export default function reportJson(id) {
|
||||
const head = ["Title", "Note", "Folder", "File", "Owner", "Signers"];
|
||||
const declineHead = ["Title", "Reason", "Folder", "File", "Owner", "Signers"];
|
||||
const iphead = ["Title", "Note", "Folder", "File", "Signers"];
|
||||
const iphead = ["Title", "Note", "Folder", "File", "Signers", "Sent Date"];
|
||||
const contactbook = ["Name", "Email", "Phone", "Company", "JobTitle"];
|
||||
const dashboardReportHead = ["Title", "File", "Owner", "Signers"];
|
||||
const templateReport = ["Title", "File", "Owner", "Signers"];
|
||||
@@ -38,25 +38,6 @@ export default function reportJson(id) {
|
||||
redirectUrl: "",
|
||||
action: "delete"
|
||||
}
|
||||
// {
|
||||
// btnId: "22534",
|
||||
// hoverLabel: "option",
|
||||
// btnColor: "",
|
||||
// restrictBtn: true,
|
||||
// textColor: "black",
|
||||
// btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
|
||||
// action: "option",
|
||||
// subaction: [
|
||||
// {
|
||||
// btnId: "1630",
|
||||
// btnLabel: "Save as template",
|
||||
// hoverLabel: "Save as template",
|
||||
// btnIcon: "fa-light fa-envelope",
|
||||
// redirectUrl: "",
|
||||
// action: "saveastemplate"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
helpMsg:
|
||||
"These are documents you have started but have not finalized for sending."
|
||||
@@ -429,25 +410,6 @@ export default function reportJson(id) {
|
||||
redirectUrl: "",
|
||||
action: "delete"
|
||||
}
|
||||
// {
|
||||
// btnId: "55534",
|
||||
// hoverLabel: "option",
|
||||
// btnColor: "",
|
||||
// restrictBtn: true,
|
||||
// textColor: "black",
|
||||
// btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
|
||||
// action: "option",
|
||||
// subaction: [
|
||||
// {
|
||||
// btnId: "6630",
|
||||
// btnLabel: "Save as template",
|
||||
// hoverLabel: "Save as template",
|
||||
// btnIcon: "fa-light fa-envelope",
|
||||
// redirectUrl: "",
|
||||
// action: "saveastemplate"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
]
|
||||
};
|
||||
// contactbook report
|
||||
|
||||
@@ -36,7 +36,7 @@ const dashboardJson = [
|
||||
queryType: "",
|
||||
class: "contracts_Document",
|
||||
query:
|
||||
'where={"Type":null,"Signers":{"#*exists":true},"Placeholders":{"#*exists":true},"SignedUrl":{"#*exists":true},"IsCompleted":false,"IsDeclined":false,"IsArchive":null,"CreatedBy":{"__type":"Pointer","className":"_User","objectId":"#UserId.objectId#"},"ExpiryDate":{"#*gt":{"__type":"#Date#","iso":"#today#"}}}&keys=Name,ExpiryDate,SignedUrl,Signers&count=1',
|
||||
'where={"Type":{"#*ne":"Folder"},"Signers":{"#*exists":true},"Placeholders":{"#*exists":true},"SignedUrl":{"#*exists":true},"IsCompleted":{"#*ne":true},"IsDeclined":{"#*ne":true},"IsArchive":{"#*ne":true},"CreatedBy":{"__type":"Pointer","className":"_User","objectId":"#UserId.objectId#"},"ExpiryDate":{"#*gt":{"__type":"#Date#","iso":"#today#"}}}&count=1&limit=0',
|
||||
key: "count",
|
||||
Redirect_type: "Report",
|
||||
Redirect_id: "1MwEuxLEkF",
|
||||
|
||||
@@ -27,7 +27,7 @@ function processDimensions(x, y, width, height) {
|
||||
};
|
||||
}
|
||||
const DebugPdf = () => {
|
||||
const {t} = useTranslation()
|
||||
const { t } = useTranslation();
|
||||
const { width } = useWindowSize();
|
||||
const [pdf, setPdf] = useState("");
|
||||
const [isModal, setIsModal] = useState(true);
|
||||
|
||||
@@ -41,7 +41,7 @@ const DocSuccessPage = () => {
|
||||
objectId: docId,
|
||||
SignedUrl: docUrl,
|
||||
CertificateUrl: certificate,
|
||||
IsCompleted: completed
|
||||
IsCompleted: completed,
|
||||
};
|
||||
setPdfDetails([details]);
|
||||
const base64Pdf = await getBase64FromUrl(docUrl);
|
||||
@@ -52,7 +52,9 @@ const DocSuccessPage = () => {
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (pdfDetails?.[0]?.IsCompleted) {
|
||||
if (
|
||||
pdfDetails?.[0]?.IsCompleted
|
||||
) {
|
||||
setIsDownloadModal(true);
|
||||
} else {
|
||||
handleDownloadPdf(pdfDetails, setIsDownloading, pdfBase64Url);
|
||||
@@ -99,21 +101,26 @@ const DocSuccessPage = () => {
|
||||
<span>{t("download")}</span>
|
||||
</button>
|
||||
|
||||
{pdfDetails?.[0]?.IsCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(pdfDetails, setIsDownloading)
|
||||
}
|
||||
className="font-medium text-sm md:text-[13px] md:px-4 py-2 op-btn op-btn-secondary"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award mx-[3px] md:mx-0"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{t("certificate")}</span>
|
||||
</button>
|
||||
)}
|
||||
{
|
||||
pdfDetails?.[0]?.IsCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="font-medium text-sm md:text-[13px] md:px-4 py-2 op-btn op-btn-secondary"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award mx-[3px] md:mx-0"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{t("certificate")}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<button
|
||||
onClick={(e) =>
|
||||
handleToPrint(e, setIsDownloading, pdfDetails)
|
||||
|
||||
@@ -67,7 +67,7 @@ const Forms = (props) => {
|
||||
NotifyOnSignatures: "",
|
||||
Bcc: [],
|
||||
RedirectUrl: "",
|
||||
AllowModifications: false
|
||||
AllowModifications: false,
|
||||
});
|
||||
const [fileupload, setFileUpload] = useState("");
|
||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||
@@ -98,6 +98,8 @@ const Forms = (props) => {
|
||||
extUserData?.IsTourEnabled === false
|
||||
? "false"
|
||||
: "true";
|
||||
const fileSize =
|
||||
maxFileSize;
|
||||
useEffect(() => {
|
||||
handleReset();
|
||||
return () => abortController.abort();
|
||||
@@ -136,11 +138,10 @@ const Forms = (props) => {
|
||||
return;
|
||||
}
|
||||
// setFormData((prev) => ({ ...prev, file: files[0] }));
|
||||
const totalMb = Math.round(
|
||||
files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2)
|
||||
);
|
||||
if (totalMb > maxFileSize) {
|
||||
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
|
||||
const totalBytes = Math.round(files.reduce((sum, f) => sum + f.size, 0)); // in bytes
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (totalBytes > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
setFileUpload("");
|
||||
setSelectedFiles([]);
|
||||
removeFile(e);
|
||||
@@ -336,6 +337,7 @@ const Forms = (props) => {
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
setSelectedFiles([]);
|
||||
removeFile(e);
|
||||
}
|
||||
};
|
||||
// `isValidURL` is used to check valid webhook url
|
||||
@@ -461,7 +463,7 @@ const Forms = (props) => {
|
||||
IsEnableOTP: "false",
|
||||
IsTourEnabled: istourenabled,
|
||||
RedirectUrl: "",
|
||||
AllowModifications: false
|
||||
AllowModifications: false,
|
||||
});
|
||||
setFileUpload("");
|
||||
setSelectedFiles([]);
|
||||
@@ -541,7 +543,7 @@ const Forms = (props) => {
|
||||
IsTourEnabled: istourenabled,
|
||||
NotifyOnSignatures: notifySign,
|
||||
RedirectUrl: "",
|
||||
AllowModifications: false
|
||||
AllowModifications: false,
|
||||
};
|
||||
setFormData(obj);
|
||||
removeFile();
|
||||
@@ -631,10 +633,12 @@ const Forms = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content">
|
||||
<div
|
||||
className={`${isSubmit ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
|
||||
>
|
||||
{isAlert?.message && <Alert type={isAlert.type}>{isAlert.message}</Alert>}
|
||||
{isSubmit ? (
|
||||
<div className="h-[300px] flex justify-center items-center">
|
||||
<div className="flex flex-col justify-center items-center h-[100vh]">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
@@ -1047,7 +1051,7 @@ const Forms = (props) => {
|
||||
></div>
|
||||
)}
|
||||
{isAdvanceOpt && (
|
||||
<div className="card bg-base-100 rounded-box p-3 flex-grow ">
|
||||
<div className="card bg-base-100 rounded-box p-3 flex-grow ">
|
||||
{formData?.autoreminder === true &&
|
||||
props.title !== "New Template" && (
|
||||
<div className="text-xs mt-2">
|
||||
|
||||
@@ -378,6 +378,7 @@ function GuestLogin() {
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
placeholder={t("enter-name")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -395,6 +396,7 @@ function GuestLogin() {
|
||||
value={contact.email}
|
||||
onChange={handleInputChange}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
required
|
||||
disabled
|
||||
/>
|
||||
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
setMyInitial,
|
||||
setDefaultSignImg,
|
||||
resetWidgetState,
|
||||
setPrefillImg
|
||||
} from "../redux/reducers/widgetSlice.js";
|
||||
setPrefillImg,
|
||||
setTypedSignFont
|
||||
} from "../redux/reducers/widgetSlice";
|
||||
import {
|
||||
contractDocument,
|
||||
embedWidgetsToDoc,
|
||||
@@ -736,6 +737,7 @@ function PdfRequestFiles(
|
||||
widgets
|
||||
);
|
||||
if (resSign && resSign.status === "success") {
|
||||
dispatch(setTypedSignFont("Fasthand"));
|
||||
setPdfUrl(resSign.data);
|
||||
setIsSigned(true);
|
||||
setSignedSigners([]);
|
||||
@@ -884,7 +886,8 @@ function PdfRequestFiles(
|
||||
?.IsCompleted
|
||||
? `&completed=true`
|
||||
: "";
|
||||
const params = `docid=${updatedDoc.updatedPdfDetails[0].objectId}&docurl=${encodeURIComponent(url)}${isCompleted}${fileAdapter}`;
|
||||
const params =
|
||||
`docid=${updatedDoc.updatedPdfDetails[0].objectId}&docurl=${encodeURIComponent(url)}${isCompleted}${fileAdapter}`;
|
||||
window.location.href = `/success?${params}`;
|
||||
}
|
||||
} else {
|
||||
@@ -1509,6 +1512,7 @@ function PdfRequestFiles(
|
||||
dragTypeValue,
|
||||
owner,
|
||||
filterSignerPos?.placeHolder,
|
||||
null,
|
||||
widgetValue
|
||||
),
|
||||
Width: widgetWidth / (containerScale * scale),
|
||||
@@ -1545,6 +1549,7 @@ function PdfRequestFiles(
|
||||
dragTypeValue,
|
||||
owner,
|
||||
filterSignerPos?.placeHolder,
|
||||
null,
|
||||
widgetValue
|
||||
),
|
||||
Width: widgetWidth / (containerScale * scale),
|
||||
@@ -1714,6 +1719,10 @@ function PdfRequestFiles(
|
||||
}
|
||||
return email;
|
||||
};
|
||||
const handleDownload = async () => {
|
||||
setIsCompleted((prev) => ({ ...prev, isModal: false }));
|
||||
setIsDownloadModal(true);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoading.isLoad ? (
|
||||
@@ -1925,34 +1934,28 @@ function PdfRequestFiles(
|
||||
{t("print")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-secondary"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award mx-[3px] lg:mx-0"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">
|
||||
{t("certificate")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-secondary"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award mx-[3px] lg:mx-0"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">
|
||||
{t("certificate")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-primary"
|
||||
onClick={() => {
|
||||
setIsCompleted((prev) => ({
|
||||
...prev,
|
||||
isModal: false
|
||||
}));
|
||||
setIsDownloadModal(true);
|
||||
}}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-download"
|
||||
|
||||
@@ -553,7 +553,8 @@ function PlaceHolderSign() {
|
||||
options: addWidgetOptions(
|
||||
dragTypeValue,
|
||||
owner,
|
||||
filterSignerPos?.placeHolder
|
||||
filterSignerPos?.placeHolder,
|
||||
roleName
|
||||
),
|
||||
Width: widgetWidth / (containerScale * scale),
|
||||
Height: widgetHeight / (containerScale * scale)
|
||||
@@ -588,7 +589,8 @@ function PlaceHolderSign() {
|
||||
options: addWidgetOptions(
|
||||
dragTypeValue,
|
||||
owner,
|
||||
filterSignerPos?.placeHolder
|
||||
filterSignerPos?.placeHolder,
|
||||
roleName
|
||||
),
|
||||
Width: widgetWidth / (containerScale * scale),
|
||||
Height: widgetHeight / (containerScale * scale)
|
||||
@@ -2045,12 +2047,12 @@ function PlaceHolderSign() {
|
||||
setSignBtnPosition={setSignBtnPosition}
|
||||
pageNumber={pageNumber}
|
||||
pdfBase64Url={pdfBase64Url}
|
||||
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
|
||||
setPdfArrayBuffer={setPdfArrayBuffer}
|
||||
setPdfBase64Url={setPdfBase64Url}
|
||||
setIsUploadPdf={setIsUploadPdf}
|
||||
pdfArrayBuffer={pdfArrayBuffer}
|
||||
isMergePdfBtn={true}
|
||||
pdfDetails={pdfDetails}
|
||||
/>
|
||||
{/* pdf render view */}
|
||||
<div className="w-full md:w-[57%] flex mr-4">
|
||||
@@ -2070,6 +2072,7 @@ function PlaceHolderSign() {
|
||||
setAllPages={setAllPages}
|
||||
setPageNumber={setPageNumber}
|
||||
setIsTour={setPlaceholderTour}
|
||||
pdfDetails={pdfDetails}
|
||||
/>
|
||||
<div className=" w-full md:w-[95%] ">
|
||||
{/* this modal is used show alert set placeholder for all signers before send mail */}
|
||||
|
||||
@@ -61,6 +61,7 @@ const Preferences = () => {
|
||||
completionMail: false
|
||||
});
|
||||
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
|
||||
|
||||
useEffect(() => {
|
||||
fetchSignType();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -194,7 +195,7 @@ const Preferences = () => {
|
||||
DateFormat: dateFormat,
|
||||
Is12HourTime: is12HourTime,
|
||||
IsLTVEnabled: isLTVEnabled,
|
||||
DownloadFilenameFormat: fileNameFormat
|
||||
DownloadFilenameFormat: fileNameFormat,
|
||||
};
|
||||
const updateRes = await Parse.Cloud.run("updatepreferences", params);
|
||||
if (updateRes) {
|
||||
@@ -804,7 +805,7 @@ const Preferences = () => {
|
||||
onClick={() => handleModifyMail("request")}
|
||||
className="op-btn op-btn-primary shadow-lg"
|
||||
>
|
||||
Modify
|
||||
{t("modify")}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -880,7 +881,7 @@ const Preferences = () => {
|
||||
onClick={() => handleModifyMail("completion")}
|
||||
className="op-btn op-btn-primary shadow-lg"
|
||||
>
|
||||
Modify
|
||||
{t("modify")}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -32,7 +32,11 @@ const Report = () => {
|
||||
const [defaultColumns, setDefaultColumns] = useState([]);
|
||||
const [isColumnModal, setIsColumnModal] = useState(false);
|
||||
const debounceTimer = useRef(null);
|
||||
const docPerPage = 20;
|
||||
// Number of documents to display per page (should always be half of docLimit for proper pagination)
|
||||
const docPerPage = 10;
|
||||
// Number of documents to fetch per API call
|
||||
const docLimit = 20;
|
||||
|
||||
|
||||
// below useEffect is call when id param change
|
||||
useEffect(() => {
|
||||
@@ -48,7 +52,7 @@ const Report = () => {
|
||||
setVisibleColumns([]);
|
||||
setColumnLabels({});
|
||||
}
|
||||
getReportData(0, docPerPage, "");
|
||||
getReportData(0, docLimit, "");
|
||||
|
||||
// Function returned from useEffect is called on unmount
|
||||
return () => {
|
||||
@@ -64,7 +68,7 @@ const Report = () => {
|
||||
// below useEffect call when isNextRecord state is true and fetch next record
|
||||
useEffect(() => {
|
||||
if (isNextRecord) {
|
||||
getReportData(list.length, 20, searchTerm);
|
||||
getReportData(list?.length, docLimit, searchTerm);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
@@ -63,8 +63,9 @@ import {
|
||||
setMyInitial,
|
||||
setDefaultSignImg,
|
||||
setIsShowModal,
|
||||
resetWidgetState
|
||||
} from "../redux/reducers/widgetSlice.js";
|
||||
resetWidgetState,
|
||||
setTypedSignFont
|
||||
} from "../redux/reducers/widgetSlice";
|
||||
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
|
||||
import WidgetNameModal from "../components/pdf/WidgetNameModal";
|
||||
import CellsSettingModal from "../components/pdf/CellsSettingModal";
|
||||
@@ -745,6 +746,7 @@ function SignYourSelf() {
|
||||
if (resSignPdf) {
|
||||
const signedpdf = JSON.parse(JSON.stringify(resSignPdf));
|
||||
setPdfUrl(signedpdf);
|
||||
dispatch(setTypedSignFont("Fasthand"));
|
||||
getDocumentDetails(false);
|
||||
}
|
||||
};
|
||||
@@ -1248,12 +1250,12 @@ function SignYourSelf() {
|
||||
pageNumber={pageNumber}
|
||||
containerWH={containerWH}
|
||||
pdfBase64Url={pdfBase64Url}
|
||||
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
|
||||
setPdfArrayBuffer={setPdfArrayBuffer}
|
||||
setPdfBase64Url={setPdfBase64Url}
|
||||
setIsUploadPdf={setIsUploadPdf}
|
||||
pdfArrayBuffer={pdfArrayBuffer}
|
||||
isMergePdfBtn={!pdfDetails?.[0]?.IsCompleted}
|
||||
pdfDetails={pdfDetails}
|
||||
/>
|
||||
<div className="w-full md:w-[57%] flex mr-4">
|
||||
<PdfTools
|
||||
@@ -1271,6 +1273,7 @@ function SignYourSelf() {
|
||||
setAllPages={setAllPages}
|
||||
setPageNumber={setPageNumber}
|
||||
isDisableEditTools={isCompleted}
|
||||
pdfDetails={pdfDetails}
|
||||
/>
|
||||
<div className="w-full md:w-[95%]">
|
||||
<ModalUi
|
||||
|
||||
@@ -165,6 +165,7 @@ const TemplatePlaceholder = () => {
|
||||
const [currUserId, setCurrUserId] = useState(false);
|
||||
const [documentDetails, setDocumentDetails] = useState();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isUseTemplate, setIsUseTemplate] = useState(false);
|
||||
const currentUser = localStorage.getItem(
|
||||
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
|
||||
);
|
||||
@@ -347,8 +348,8 @@ const TemplatePlaceholder = () => {
|
||||
setPrefillSigner([utils?.prefillObj()]);
|
||||
}
|
||||
setSignersData(updatedSigners);
|
||||
setUniqueId(updatedSigners[0].Id);
|
||||
setBlockColor(updatedSigners[0].blockColor);
|
||||
setUniqueId(updatedSigners[0]?.Id);
|
||||
setBlockColor(updatedSigners[0]?.blockColor);
|
||||
setIsSelectId(0);
|
||||
} else {
|
||||
const updatedSigners = documentData[0].Signers.map((x, index) => ({
|
||||
@@ -357,8 +358,8 @@ const TemplatePlaceholder = () => {
|
||||
Role: "Role " + (index + 1)
|
||||
}));
|
||||
setSignersData(updatedSigners);
|
||||
setUniqueId(updatedSigners[0].Id);
|
||||
setBlockColor(updatedSigners[0].blockColor);
|
||||
setUniqueId(updatedSigners[0]?.Id);
|
||||
setBlockColor(updatedSigners[0]?.blockColor);
|
||||
setPrefillSigner([utils?.prefillObj()]);
|
||||
}
|
||||
} else {
|
||||
@@ -436,7 +437,6 @@ const TemplatePlaceholder = () => {
|
||||
const addPositionOfSignature = (item, monitor) => {
|
||||
getSignerPos(item, monitor);
|
||||
};
|
||||
|
||||
// `getSignerPos` is used to get placeholder position when user place it and save it in array
|
||||
const getSignerPos = (item, monitor) => {
|
||||
if (uniqueId) {
|
||||
@@ -482,7 +482,8 @@ const TemplatePlaceholder = () => {
|
||||
options: addWidgetOptions(
|
||||
dragTypeValue,
|
||||
owner,
|
||||
filterSignerPos?.placeHolder
|
||||
filterSignerPos?.placeHolder,
|
||||
roleName
|
||||
),
|
||||
Width: widgetWidth / (containerScale * scale),
|
||||
Height: widgetHeight / (containerScale * scale)
|
||||
@@ -516,7 +517,8 @@ const TemplatePlaceholder = () => {
|
||||
options: addWidgetOptions(
|
||||
dragTypeValue,
|
||||
owner,
|
||||
filterSignerPos?.placeHolder
|
||||
filterSignerPos?.placeHolder,
|
||||
roleName
|
||||
),
|
||||
Width: widgetWidth / (containerScale * scale),
|
||||
Height: widgetHeight / (containerScale * scale)
|
||||
@@ -613,7 +615,6 @@ const TemplatePlaceholder = () => {
|
||||
setIsAddRole(true);
|
||||
}
|
||||
};
|
||||
|
||||
const tourAddRole = [
|
||||
{
|
||||
selector: '[data-tut="reactourAddbtn"]',
|
||||
@@ -931,6 +932,7 @@ const TemplatePlaceholder = () => {
|
||||
}
|
||||
};
|
||||
const handleCreateDocument = async () => {
|
||||
setIsUseTemplate(true);
|
||||
try {
|
||||
setIsUiLoading(true);
|
||||
const res = await utils?.handleCheckPrefillCreateDoc(
|
||||
@@ -974,6 +976,7 @@ const TemplatePlaceholder = () => {
|
||||
} catch (e) {
|
||||
console.log("error in create document function", e);
|
||||
}
|
||||
setIsUseTemplate(false);
|
||||
};
|
||||
const navigatePageToDoc = async () => {
|
||||
try {
|
||||
@@ -1771,12 +1774,12 @@ const TemplatePlaceholder = () => {
|
||||
setSignBtnPosition={setSignBtnPosition}
|
||||
pageNumber={pageNumber}
|
||||
pdfBase64Url={pdfBase64Url}
|
||||
signedUrl={pdfDetails?.[0]?.SignedUrl || ""}
|
||||
setPdfArrayBuffer={setPdfArrayBuffer}
|
||||
setPdfBase64Url={setPdfBase64Url}
|
||||
setIsUploadPdf={setIsUploadPdf}
|
||||
pdfArrayBuffer={pdfArrayBuffer}
|
||||
isMergePdfBtn={true}
|
||||
pdfDetails={pdfDetails}
|
||||
/>
|
||||
|
||||
{/* pdf render view */}
|
||||
@@ -1796,6 +1799,7 @@ const TemplatePlaceholder = () => {
|
||||
allPages={allPages}
|
||||
setAllPages={setAllPages}
|
||||
setPageNumber={setPageNumber}
|
||||
pdfDetails={pdfDetails}
|
||||
/>
|
||||
<div className="w-full md:w-[95%]">
|
||||
<ModalUi
|
||||
@@ -2138,6 +2142,7 @@ const TemplatePlaceholder = () => {
|
||||
isNewContact={isNewContact}
|
||||
documentFlow={"template"}
|
||||
docId={pdfDetails[0]?.objectId}
|
||||
isSubmit={isUseTemplate}
|
||||
/>
|
||||
)}
|
||||
{isShowModal[currWidgetsDetails?.key] && (
|
||||
|
||||
@@ -487,7 +487,7 @@ function UserProfile() {
|
||||
) : (
|
||||
<>
|
||||
{deleteUserRes ? (
|
||||
<div className="h-[100px] p-[20px] flex justify-center items-center text-sm md:text-base">
|
||||
<div className="h-[100px] p-[20px] flex justify-center items-center text-base-content text-sm md:text-base">
|
||||
{deleteUserRes}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -174,6 +174,7 @@ const AddContact = (props) => {
|
||||
required
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.75rem]">
|
||||
@@ -193,6 +194,7 @@ const AddContact = (props) => {
|
||||
required
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase"
|
||||
placeholder={t("enter-email")}
|
||||
/>
|
||||
</div>
|
||||
{isOptionalDetails && (
|
||||
|
||||
@@ -96,8 +96,8 @@ function DownloadPdfZip(props) {
|
||||
handleClose={() => props.setIsDownloadModal(false)}
|
||||
>
|
||||
<div className="p-[20px] h-full text-base-content">
|
||||
{downloadType.map((data, ind) => {
|
||||
return (
|
||||
{downloadType
|
||||
.map((data, ind) => (
|
||||
<label
|
||||
key={ind}
|
||||
className="flex items-center gap-1 mb-2 cursor-pointer"
|
||||
@@ -111,8 +111,7 @@ function DownloadPdfZip(props) {
|
||||
/>
|
||||
{data.label}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<button
|
||||
onClick={() => handleDownload()}
|
||||
|
||||
@@ -9,10 +9,12 @@ const ModalUi = ({
|
||||
showHeader = true,
|
||||
showClose = true,
|
||||
reduceWidth,
|
||||
position
|
||||
position,
|
||||
crossColor
|
||||
}) => {
|
||||
const width = reduceWidth;
|
||||
const isBottom = position === "bottom" ? "items-end pb-2 !bg-black/10" : "";
|
||||
const crossBtnColor = crossColor ?? "text-base-content";
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
@@ -38,7 +40,7 @@ const ModalUi = ({
|
||||
)}
|
||||
{showClose && (
|
||||
<button
|
||||
className="op-btn op-btn-sm op-btn-circle op-btn-ghost text-base-content absolute right-2 top-2"
|
||||
className={`${crossBtnColor} op-btn op-btn-sm op-btn-circle op-btn-ghost absolute right-2 top-2 z-40`}
|
||||
onClick={() => handleClose && handleClose()}
|
||||
>
|
||||
✕
|
||||
|
||||
@@ -63,7 +63,7 @@ function CustomModal(props) {
|
||||
className="op-btn op-btn-secondary ml-[10px] mb-3 mt-1"
|
||||
onClick={() => props.handleDownloadBtn()}
|
||||
>
|
||||
Download
|
||||
{t("download")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -138,6 +138,12 @@ export const RenderReportCell = ({
|
||||
{rowData?.ExpiryDate ? formatDate(rowData?.ExpiryDate?.iso) : "-"}
|
||||
</td>
|
||||
);
|
||||
case "Sent Date":
|
||||
return (
|
||||
<td key={col} className="p-2 text-center">
|
||||
{rowData?.DocSentAt ? formatDate(rowData?.DocSentAt?.iso) : "-"}
|
||||
</td>
|
||||
);
|
||||
case "Signers":
|
||||
return (
|
||||
<SignerCell
|
||||
|
||||
@@ -13,7 +13,8 @@ const initialState = {
|
||||
lastIndex: "",
|
||||
scrollTriggerId: "",
|
||||
prefillImg: [],
|
||||
prefillImgLoad: {}
|
||||
prefillImgLoad: {},
|
||||
typedSignFont: "Fasthand"
|
||||
};
|
||||
|
||||
const widgetSlice = createSlice({
|
||||
@@ -41,6 +42,9 @@ const widgetSlice = createSlice({
|
||||
setScrollTriggerId: (state, action) => {
|
||||
state.scrollTriggerId = action.payload;
|
||||
},
|
||||
setTypedSignFont: (state, action) => {
|
||||
state.typedSignFont = action.payload;
|
||||
},
|
||||
setPrefillImg: (state, action) => {
|
||||
const existingIndex = state.prefillImg.findIndex(
|
||||
(img) => img.id === action.payload.id
|
||||
@@ -73,7 +77,8 @@ export const {
|
||||
setLastIndex,
|
||||
setScrollTriggerId,
|
||||
setPrefillImg,
|
||||
setPrefillImgLoad
|
||||
setPrefillImgLoad,
|
||||
setTypedSignFont
|
||||
} = widgetSlice.actions;
|
||||
|
||||
export default widgetSlice.reducer;
|
||||
|
||||
@@ -88,6 +88,7 @@ const EditContactForm = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -105,6 +106,7 @@ const EditContactForm = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,7 @@ const DocumentsReport = (props) => {
|
||||
const [documentId, setDocumentId] = useState("");
|
||||
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
|
||||
const [isPrefillModal, setIsPrefillModal] = useState({});
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [isMailModal, setIsMailModal] = useState(false);
|
||||
const [customizeMail, setCustomizeMail] = useState({ body: "", subject: "" });
|
||||
@@ -329,6 +330,8 @@ const DocumentsReport = (props) => {
|
||||
setIsModal({ [`extendexpiry_${item.objectId}`]: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Get current list
|
||||
const indexOfLastDoc = currentPage * props.docPerPage;
|
||||
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
|
||||
@@ -495,7 +498,9 @@ const DocumentsReport = (props) => {
|
||||
const docId = props?.ReportName !== "Templates" && item.objectId;
|
||||
if (url) {
|
||||
try {
|
||||
if (isCompleted) {
|
||||
if (
|
||||
isCompleted
|
||||
) {
|
||||
setIsDownloadModal({ [item.objectId]: true });
|
||||
} else {
|
||||
const signedUrl = await getSignedUrl(
|
||||
@@ -789,6 +794,7 @@ const DocumentsReport = (props) => {
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setError("");
|
||||
setIsModal({});
|
||||
};
|
||||
const handleSaveAsTemplate = async (doc) => {
|
||||
@@ -920,6 +926,7 @@ const DocumentsReport = (props) => {
|
||||
};
|
||||
//`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document
|
||||
const handlePrefillWidgetCreateDoc = async () => {
|
||||
setIsSubmit(true);
|
||||
const scale = 1;
|
||||
const key = Object.keys(isPrefillModal)[0];
|
||||
setActLoader({ [key]: true });
|
||||
@@ -957,6 +964,7 @@ const DocumentsReport = (props) => {
|
||||
alert(t("user-not-exist"));
|
||||
}
|
||||
}
|
||||
setIsSubmit(false);
|
||||
setActLoader({});
|
||||
};
|
||||
const handleRecipientSign = (documentId, currentId) => {
|
||||
@@ -1027,10 +1035,10 @@ const DocumentsReport = (props) => {
|
||||
if (isPrefill) {
|
||||
return (
|
||||
<span className="flex text-sm mt-3 text-red-500">
|
||||
{t("save-as-temp-warn")}
|
||||
{t("save-as-temp-warn")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -1367,6 +1375,7 @@ const DocumentsReport = (props) => {
|
||||
setIsNewContact={setIsNewContact}
|
||||
isNewContact={isNewContact}
|
||||
docId={item.objectId}
|
||||
isSubmit={isSubmit}
|
||||
/>
|
||||
)}
|
||||
{isModal["extendexpiry_" + item.objectId] && (
|
||||
|
||||
@@ -30,7 +30,7 @@ import "../../styles/quill.css";
|
||||
import BulkSendUi from "../../components/BulkSendUi";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { serverUrl_fn } from "../../constant/appinfo";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useElSize } from "../../hook/useElSize";
|
||||
import LottieWithLoader from "../../primitives/DotLottieReact";
|
||||
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
|
||||
@@ -67,7 +67,7 @@ const TemplatesReport = (props) => {
|
||||
const [userDetails, setUserDetails] = useState({});
|
||||
const [isNextStep, setIsNextStep] = useState({});
|
||||
const [isBulkSend, setIsBulkSend] = useState({});
|
||||
const [templateDeatils, setTemplateDetails] = useState({});
|
||||
const [templateDetails, setTemplateDetails] = useState({});
|
||||
const [placeholders, setPlaceholders] = useState([]);
|
||||
const [isLoader, setIsLoader] = useState({});
|
||||
const [isShareWith, setIsShareWith] = useState({});
|
||||
@@ -85,6 +85,7 @@ const TemplatesReport = (props) => {
|
||||
const [documentId, setDocumentId] = useState("");
|
||||
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
|
||||
const [isPrefillModal, setIsPrefillModal] = useState({});
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const startIndex = (currentPage - 1) * props.docPerPage;
|
||||
const { isMoreDocs, setIsNextRecord } = props;
|
||||
const [isMailModal, setIsMailModal] = useState(false);
|
||||
@@ -238,9 +239,8 @@ const TemplatesReport = (props) => {
|
||||
);
|
||||
if (checkIsSignatureExist) {
|
||||
setActLoader({ [`${item.objectId}_${act.btnId}`]: true });
|
||||
const templateDeatils = await fetchTemplate(item.objectId);
|
||||
const templateData =
|
||||
templateDeatils.data && templateDeatils.data.result;
|
||||
const template = await fetchTemplate(item.objectId);
|
||||
const templateData = template.data && template.data.result;
|
||||
if (!templateData.error) {
|
||||
setXyPosition(templateData?.Placeholders);
|
||||
const signer = utils.handleSignersList(templateData);
|
||||
@@ -283,7 +283,7 @@ const TemplatesReport = (props) => {
|
||||
}
|
||||
}
|
||||
);
|
||||
if (axiosRes) {
|
||||
if (axiosRes?.data) {
|
||||
return axiosRes;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -296,10 +296,10 @@ const TemplatesReport = (props) => {
|
||||
const navigatePageToDoc = async (templateRes, placeholder, signer) => {
|
||||
setIsPrefillModal({});
|
||||
const res = await createDocument(
|
||||
[templateRes || templateDeatils],
|
||||
[templateRes || templateDetails],
|
||||
placeholder || xyPosition,
|
||||
signer || signerList,
|
||||
templateRes?.URL || templateDeatils?.URL
|
||||
templateRes?.URL || templateDetails?.URL
|
||||
);
|
||||
if (res.status === "success") {
|
||||
navigate(`/placeHolderSign/${res.id}`, {
|
||||
@@ -871,14 +871,15 @@ const TemplatesReport = (props) => {
|
||||
};
|
||||
//`handlePrefillWidgetCreateDoc` is used to embed prefill all widgets on document, create document, and send document
|
||||
const handlePrefillWidgetCreateDoc = async () => {
|
||||
setIsSubmit(true);
|
||||
const scale = 1;
|
||||
const res = await utils?.handleCheckPrefillCreateDoc(
|
||||
xyPosition,
|
||||
signerList,
|
||||
setIsPrefillModal,
|
||||
scale,
|
||||
templateDeatils?.URL,
|
||||
[templateDeatils],
|
||||
templateDetails?.URL,
|
||||
[templateDetails],
|
||||
prefillImg,
|
||||
extClass?.[0]?.UserId?.objectId,
|
||||
);
|
||||
@@ -940,6 +941,7 @@ const TemplatesReport = (props) => {
|
||||
alert(t("user-not-exist"));
|
||||
}
|
||||
}
|
||||
setIsSubmit(false);
|
||||
};
|
||||
//function show signer list and share link to share signUrl
|
||||
const handleShareList = () => {
|
||||
@@ -1257,6 +1259,7 @@ const TemplatesReport = (props) => {
|
||||
setIsNewContact={setIsNewContact}
|
||||
isNewContact={isNewContact}
|
||||
docId={item.objectId}
|
||||
isSubmit={isSubmit}
|
||||
/>
|
||||
)}
|
||||
{isShareWith[item.objectId] && (
|
||||
@@ -1328,7 +1331,7 @@ const TemplatesReport = (props) => {
|
||||
) : (
|
||||
<BulkSendUi
|
||||
Placeholders={placeholders}
|
||||
item={templateDeatils}
|
||||
item={templateDetails}
|
||||
handleClose={handleQuickSendClose}
|
||||
signatureType={signatureType}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { format, toZonedTime } from 'date-fns-tz';
|
||||
import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
|
||||
import crypto from 'node:crypto';
|
||||
import { PDFDocument, rgb } from 'pdf-lib';
|
||||
import { parseUploadFile } from './utils/fileUtils.js';
|
||||
|
||||
dotenv.config({ quiet: true });
|
||||
|
||||
|
||||
@@ -2,20 +2,24 @@ 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';
|
||||
import { deleteUserByAdmin, deleteUserGet, deleteUserPost } from './deleteUser.js';
|
||||
import { deleteUserByAdmin, deleteUserPost } from './deleteAccount/deleteUser.js';
|
||||
import { deleteUserGet } from './deleteAccount/deleteUserGet.js';
|
||||
import { deleteUserOtp } from './deleteAccount/deleteUserOtp.js';
|
||||
|
||||
export const app = express();
|
||||
|
||||
dotenv.config({ quiet: true });
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
||||
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);
|
||||
app.post('/delete-account/:userId/otp', deleteUserOtp);
|
||||
app.post('/delete-account/:userId', deleteUserPost);
|
||||
app.post('/deleteuser/:userId', deleteUserByAdmin);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import fs from 'node:fs/promises';
|
||||
import pLimit from 'p-limit';
|
||||
import { serverAppId } from '../../Utils.js';
|
||||
import { serverAppId } from '../../../Utils.js';
|
||||
|
||||
// === Configuration ===
|
||||
const serverHost = new URL(process.env.SERVER_URL).hostname;
|
||||
+40
-136
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, generateId, serverAppId } from '../../Utils.js';
|
||||
import sendmailtoSupport from './sendMailToSupport.js';
|
||||
import { cloudServerUrl, generateId, serverAppId } from '../../../Utils.js';
|
||||
import { deleteContactsInBatch, deleteDataFiles, deleteInBatches } from './deleteFileUrl.js';
|
||||
import { MAX_ATTEMPTS } from './deleteUtils.js';
|
||||
const serverUrl = cloudServerUrl;
|
||||
const appId = serverAppId;
|
||||
|
||||
@@ -54,7 +54,6 @@ export async function deleteUser(userId, adminId) {
|
||||
userDetails = { ...userDetails, UserId: userId };
|
||||
if (!userResult) {
|
||||
const errorMessage = 'User not found.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
const contractsUserId = userResult.id;
|
||||
@@ -73,7 +72,6 @@ export async function deleteUser(userId, adminId) {
|
||||
};
|
||||
if (adminId && isAdmin) {
|
||||
const errorMessage = 'An error occurred while deleting your account.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -85,7 +83,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed during contracts_Template cleanup:', err);
|
||||
const errorMessage = 'Failed during contracts_Template cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -95,7 +92,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed during contactbook cleanup:', err);
|
||||
const errorMessage = 'Failed during contactbook cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -116,7 +112,6 @@ export async function deleteUser(userId, adminId) {
|
||||
console.error('Failed during contactbook current user cleanup:', err);
|
||||
const errorMessage =
|
||||
'Failed during contactbook current user cleanup: ' + (err?.message || err);
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -130,7 +125,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed to delete appToken entries:', err);
|
||||
const errorMessage = 'Failed to delete appToken entries:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -140,7 +134,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed during partners_DataFiles cleanup:', err);
|
||||
const errorMessage = 'Failed during partners_DataFiles cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -156,7 +149,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed to delete contracts_Organizations entry:', err);
|
||||
const errorMessage = 'Failed to delete contracts_Organizations entry:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
// STEP 7: Delete each entry in contracts_Teams by objectId from teamIds
|
||||
@@ -170,7 +162,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (teamErr) {
|
||||
console.error(`Failed to delete team with ID ${team.id}:`, teamErr);
|
||||
const errorMessage = `Failed to delete team with ID ${team.id}` + teamErr?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
}
|
||||
@@ -178,7 +169,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed during contracts_Teams deletion loop:', err);
|
||||
const errorMessage = 'Failed during contracts_Teams deletion loop:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -194,7 +184,6 @@ export async function deleteUser(userId, adminId) {
|
||||
const msg = `Failed during partners_Tenant ${'cleanup:'} `;
|
||||
console.error(msg, err);
|
||||
const errorMessage = msg + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -214,7 +203,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed during partners_TenantCredits cleanup:', err);
|
||||
const errorMessage = 'Failed during partners_TenantCredits cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
}
|
||||
@@ -228,7 +216,6 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed during contracts_Signature cleanup:', err);
|
||||
const errorMessage = 'Failed during contracts_Signature cleanup:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
|
||||
@@ -238,120 +225,20 @@ export async function deleteUser(userId, adminId) {
|
||||
} catch (err) {
|
||||
console.error('Failed to delete contracts_Users entry:', err);
|
||||
const errorMessage = 'Failed to delete contracts_Users entry:' + err?.message;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
return { code: 200, message: 'User and all associated data deleted successfully.' };
|
||||
} catch (error) {
|
||||
console.error('User deletion process failed:', error);
|
||||
const errorMessage = `User deletion failed: ${error.message || error}`;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return { code: 400, message: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
// 1. HTML Password Prompt Page
|
||||
export const deleteUserGet = async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
|
||||
const extUserQuery = new Parse.Query('Contracts_Users');
|
||||
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
const extUser = await extUserQuery.first({ useMasterKey: true });
|
||||
if (!extUser) {
|
||||
const errorMessage = 'User not found.';
|
||||
return res.send(errorMessage);
|
||||
}
|
||||
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
|
||||
const htmlForm = `
|
||||
<html>
|
||||
<head>
|
||||
<title>Delete Account</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #dc3545;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #d9534f;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #c9302c;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Confirm Account Deletion</h2>
|
||||
<p class="warning">This action is irreversible. Please confirm by entering your password.</p>
|
||||
<form method="POST" action="${routePath}/delete-account/${userId}">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" placeholder="Please provide your password" required />
|
||||
<button type="submit">Delete My Account</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
res.send(htmlForm);
|
||||
};
|
||||
|
||||
// 2. Handle Password Verification and Deletion
|
||||
export const deleteUserPost = async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
|
||||
const { password } = req.body;
|
||||
const { otp } = req.body;
|
||||
let userDetails = {
|
||||
UserRole: 'not found',
|
||||
Name: 'not found',
|
||||
@@ -369,33 +256,53 @@ export const deleteUserPost = async (req, res) => {
|
||||
const user = await userQuery.first({ useMasterKey: true });
|
||||
if (!user) {
|
||||
const errorMessage = 'User not found.';
|
||||
// sendmailtoSupport(userDetails, errorMessage);
|
||||
// return res.status(404).send(errorMessage);
|
||||
return res.send(errorMessage);
|
||||
}
|
||||
const extUserQuery = new Parse.Query('Contracts_Users');
|
||||
extUserQuery.equalTo('userId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
const extUserQuery = new Parse.Query('contracts_Users');
|
||||
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
|
||||
const extUser = await extUserQuery.first({ useMasterKey: true });
|
||||
if (!extUser) {
|
||||
const errorMessage = 'User not found.';
|
||||
return res.send(errorMessage);
|
||||
}
|
||||
// 2. Attempt login to verify password
|
||||
const username = user.get('username'); // assuming 'username' is used for login
|
||||
|
||||
// Get stored OTP info
|
||||
const savedOtp = extUser.get('DeleteOTP') || '';
|
||||
const expiry = extUser.get('DeleteOTPExpiry');
|
||||
const tries = Number(extUser.get('DeleteOTPTries') || 0);
|
||||
|
||||
if (tries >= MAX_ATTEMPTS) {
|
||||
return res.status(429).send('Too many invalid attempts. Please resend OTP and try again.');
|
||||
}
|
||||
if (!otp || typeof otp !== 'string') {
|
||||
// Count attempt
|
||||
extUser.set('DeleteOTPTries', tries + 1);
|
||||
await extUser.save(null, { useMasterKey: true });
|
||||
return res.status(400).send('OTP is required.');
|
||||
}
|
||||
if (!savedOtp) {
|
||||
return res.status(400).send('No OTP found. Please request a new OTP.');
|
||||
}
|
||||
if (expiry && Date.now() > expiry.getTime()) {
|
||||
return res.status(400).send('OTP has expired. Please request a new OTP.');
|
||||
}
|
||||
if (otp !== savedOtp) {
|
||||
// Increment tries on mismatch
|
||||
extUser.set('DeleteOTPTries', tries + 1);
|
||||
await extUser.save(null, { useMasterKey: true });
|
||||
return res.status(400).send('Invalid OTP.');
|
||||
}
|
||||
|
||||
// 2. Remove OTP related data
|
||||
try {
|
||||
// await Parse.User.logIn(username, password); // Will throw if password invalid
|
||||
// Use REST login to avoid mutating the global Parse current user
|
||||
// Will throw if password invalid
|
||||
const res = await axios.get(serverUrl + '/login', {
|
||||
params: { username, password },
|
||||
headers: { 'X-Parse-Application-Id': appId },
|
||||
});
|
||||
console.log('Res ', res?.data);
|
||||
extUser.unset('DeleteOTP');
|
||||
extUser.unset('DeleteOTPExpiry');
|
||||
extUser.unset('DeleteOTPSentAt');
|
||||
extUser.unset('DeleteOTPTries');
|
||||
await extUser.save(null, { useMasterKey: true });
|
||||
} catch (err) {
|
||||
console.log('err while validating password: ', err?.response?.data || err);
|
||||
const errorMessage = `Invalid password. <a href="${routePath}/delete-account/${userId}">Try again</a>`;
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(401).send(errorMessage);
|
||||
}
|
||||
|
||||
const response = await deleteUser(userId);
|
||||
@@ -405,7 +312,6 @@ export const deleteUserPost = async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error('Account deletion error:', error);
|
||||
const errorMessage = error?.message || 'An error occurred while deleting your account.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(500).send(errorMessage);
|
||||
}
|
||||
};
|
||||
@@ -444,7 +350,6 @@ export const deleteUserByAdmin = async (req, res) => {
|
||||
const user = await userQuery.first({ useMasterKey: true });
|
||||
if (!user) {
|
||||
const errorMessage = 'User not found.';
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(400).json({ message: errorMessage });
|
||||
}
|
||||
const response = await deleteUser(userId, adminId);
|
||||
@@ -458,7 +363,6 @@ export const deleteUserByAdmin = async (req, res) => {
|
||||
error?.message ||
|
||||
'An error occurred while deleting your account.';
|
||||
console.error(`Account deletion error:`, errorMessage);
|
||||
sendmailtoSupport(userDetails, errorMessage);
|
||||
return res.status(code).json({ message: errorMessage });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import { OTP_LENGTH, RESEND_COOLDOWN_SEC } from './deleteUtils.js';
|
||||
|
||||
// 1. HTML Password Prompt Page
|
||||
export const deleteUserGet = async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
|
||||
const extUserQuery = new Parse.Query('contracts_Users');
|
||||
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
const extUser = await extUserQuery.first({ useMasterKey: true });
|
||||
if (!extUser) return res.status(404).send('User not found.');
|
||||
|
||||
const routePath = process?.env?.SERVER_URL?.includes?.('api') ? '/api' : '';
|
||||
|
||||
const htmlForm = `
|
||||
<html>
|
||||
<head>
|
||||
<title>Delete Account</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
.container { background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); width: 100%; max-width: 420px; text-align: center; }
|
||||
h2 { color: #dc3545; margin-bottom: 14px; }
|
||||
p.warning { color: #6c757d; font-size: 14px; margin-top: 0; margin-bottom: 18px; }
|
||||
|
||||
label { display: block; margin-bottom: 10px; font-weight: 600; text-align: left; }
|
||||
input[type="text"] { width: 100%; padding: 12px; margin-bottom: 16px; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; letter-spacing: 0.2em; }
|
||||
|
||||
button { background-color: #d9534f; color: #ffffff; border: none; padding: 12px 16px; font-size: 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; }
|
||||
button:hover { background-color: #c9302c; }
|
||||
.secondary { background-color: #6c757d; }
|
||||
.secondary:hover { background-color: #5a6268; }
|
||||
.btn-block { width: 100%; }
|
||||
|
||||
.link-btn { background: transparent; border: none; color: #0d6efd; text-decoration: underline; padding: 0; font-size: 14px; cursor: pointer; }
|
||||
.link-btn[disabled] { color: #6c757d; text-decoration: none; cursor: not-allowed; }
|
||||
|
||||
.muted { color: #6c757d; font-size: 13px; margin-top: 8px; min-height: 18px; }
|
||||
.error { color: #c9302c; font-size: 13px; margin-top: 8px; min-height: 18px; }
|
||||
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Confirm Account Deletion</h2>
|
||||
|
||||
<!-- Initial view: only Send OTP -->
|
||||
<div id="preOtp">
|
||||
<p class="warning">This action is irreversible. A verification mail will be sent to your account to your registered email.</p>
|
||||
<button type="button" id="sendOtpBtn" class="secondary btn-block">Send OTP</button>
|
||||
<div id="preMsg" class="muted"></div>
|
||||
<div id="preErr" class="error"></div>
|
||||
</div>
|
||||
|
||||
<!-- Shown after OTP is sent -->
|
||||
<form id="otpForm" class="hidden" method="POST" action="${routePath}/delete-account/${userId}">
|
||||
<p class="warning">This action is irreversible. Verify with an OTP sent to your registered email.</p>
|
||||
<label for="otp">One-Time Password (OTP)</label>
|
||||
<input type="text" name="otp" id="otp" placeholder="Enter ${OTP_LENGTH}-digit OTP" required maxlength="${OTP_LENGTH}" inputmode="numeric" />
|
||||
|
||||
<div style="text-align:center; margin-top: 10px;">
|
||||
<button type="submit" id="deleteBtn">Delete My Account</button>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center; margin-top: 10px;">
|
||||
<button type="button" id="resendOtpLink" class="link-btn">Resend OTP</button>
|
||||
</div>
|
||||
|
||||
<div id="timer" class="muted"></div>
|
||||
<div id="msg" class="muted"></div>
|
||||
<div id="err" class="error"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const preOtp = document.getElementById('preOtp');
|
||||
const sendBtn = document.getElementById('sendOtpBtn');
|
||||
const preMsg = document.getElementById('preMsg');
|
||||
const preErr = document.getElementById('preErr');
|
||||
|
||||
const form = document.getElementById('otpForm');
|
||||
const otpInput = document.getElementById('otp');
|
||||
const resendLink = document.getElementById('resendOtpLink');
|
||||
const timerEl = document.getElementById('timer');
|
||||
const msgEl = document.getElementById('msg');
|
||||
const errEl = document.getElementById('err');
|
||||
|
||||
const RESEND_WAIT = ${RESEND_COOLDOWN_SEC};
|
||||
let countdown = 0;
|
||||
let iv = null;
|
||||
|
||||
function setText(el, t){ el.textContent = t || ''; }
|
||||
function updateResendState(){
|
||||
resendLink.disabled = countdown>0;
|
||||
if(countdown>0){
|
||||
resendLink.setAttribute('disabled','true');
|
||||
resendLink.style.pointerEvents='none';
|
||||
}else{
|
||||
resendLink.removeAttribute('disabled');
|
||||
resendLink.style.pointerEvents='auto';
|
||||
}
|
||||
}
|
||||
function tick(){
|
||||
if(countdown<=0){ clearInterval(iv); iv=null; setText(timerEl,'You can resend the OTP now.'); updateResendState(); return; }
|
||||
setText(timerEl,'Resend available in '+countdown+'s'); countdown--; updateResendState();
|
||||
}
|
||||
function startTimer(sec){ countdown=sec||RESEND_WAIT; if(iv) clearInterval(iv); tick(); iv=setInterval(tick,1000); }
|
||||
|
||||
async function sendOtp(showForm){
|
||||
setText(preErr,''); setText(preMsg,'Sending OTP...');
|
||||
try {
|
||||
const resp = await fetch('${routePath}/delete-account/${userId}/otp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
|
||||
const data = await resp.json().catch(()=>({}));
|
||||
if(!resp.ok){
|
||||
if(resp.status===429 && data?.retryAfterSec){
|
||||
startTimer(data.retryAfterSec); setText(preErr,'Please wait '+data.retryAfterSec+'s before resending.'); setText(preMsg,'');
|
||||
return;
|
||||
}
|
||||
throw new Error(data?.error || 'Failed to send OTP.');
|
||||
}
|
||||
startTimer(data?.cooldownSec || RESEND_WAIT);
|
||||
|
||||
// Success: hide pre, show form
|
||||
preOtp.classList.add('hidden');
|
||||
form.classList.remove('hidden');
|
||||
setText(msgEl,'OTP sent to your registered email.');
|
||||
otpInput.focus();
|
||||
|
||||
} catch(e){
|
||||
setText(preErr,e.message||'Error sending OTP.');
|
||||
setText(preMsg,'');
|
||||
}
|
||||
}
|
||||
|
||||
sendBtn.addEventListener('click',()=>sendOtp(true));
|
||||
resendLink.addEventListener('click',()=>{ if(countdown<=0) sendOtp(false); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
res.send(htmlForm);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
generateOtp,
|
||||
msUntil,
|
||||
sendDeleteOtpEmail,
|
||||
OTP_EXPIRES_MIN,
|
||||
RESEND_COOLDOWN_SEC,
|
||||
} from './deleteUtils.js';
|
||||
|
||||
export const deleteUserOtp = async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
|
||||
const extUserQuery = new Parse.Query('contracts_Users');
|
||||
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
const extUser = await extUserQuery.first({ useMasterKey: true });
|
||||
if (!extUser) return res.status(404).json({ error: 'User not found' });
|
||||
|
||||
const now = Date.now();
|
||||
const lastSentAt = extUser.get('DeleteOTPSentAt')?.getTime?.() || 0;
|
||||
const cooldownEndsAt = lastSentAt + RESEND_COOLDOWN_SEC * 1000;
|
||||
const remainingMs = msUntil(now, cooldownEndsAt);
|
||||
|
||||
if (remainingMs > 0) {
|
||||
return res
|
||||
.status(429)
|
||||
.json({ error: 'Cooldown not finished', retryAfterSec: Math.ceil(remainingMs / 1000) });
|
||||
}
|
||||
|
||||
const otp = generateOtp();
|
||||
const expiresAt = new Date(now + OTP_EXPIRES_MIN * 60 * 1000);
|
||||
|
||||
try {
|
||||
const resp = await sendDeleteOtpEmail(extUser, otp);
|
||||
extUser.set('DeleteOTP', otp);
|
||||
extUser.set('DeleteOTPExpiry', expiresAt);
|
||||
extUser.set('DeleteOTPSentAt', new Date(now));
|
||||
extUser.set('DeleteOTPTries', 0); // reset tries on resend
|
||||
await extUser.save(null, { useMasterKey: true });
|
||||
return res.json({ ok: true, cooldownSec: RESEND_COOLDOWN_SEC, expiresInMin: OTP_EXPIRES_MIN });
|
||||
} catch (err) {
|
||||
console.log('Error sending delete OTP (POST /otp):', err?.response?.data || err);
|
||||
return res.status(500).json({ error: 'Failed to send OTP' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import axios from 'axios';
|
||||
import { appName, cloudServerUrl, serverAppId } from '../../../Utils.js';
|
||||
|
||||
const serverUrl = cloudServerUrl;
|
||||
const appId = serverAppId;
|
||||
const masterKey = process.env.MASTER_KEY;
|
||||
|
||||
// Constants (adjust to your preference)
|
||||
export const OTP_LENGTH = 6;
|
||||
export const OTP_EXPIRES_MIN = 10; // OTP validity in minutes
|
||||
export const RESEND_COOLDOWN_SEC = 30; // Cooldown between OTP sends
|
||||
export const MAX_ATTEMPTS = 5; // Max allowed wrong attempts
|
||||
|
||||
export function generateOtp(len = OTP_LENGTH) {
|
||||
// 6-digit numeric OTP (000000–999999, padded)
|
||||
const n = Math.floor(Math.random() * Math.pow(10, len));
|
||||
return String(n).padStart(len, '0');
|
||||
}
|
||||
|
||||
export async function sendDeleteOtpEmail(extUser, otp) {
|
||||
const params = {
|
||||
extUserId: extUser.id,
|
||||
from: appName,
|
||||
recipient: extUser?.get('Email'),
|
||||
subject: 'OTP for Deletion account request',
|
||||
html: `
|
||||
<html lang="en">
|
||||
<body style="margin:0;padding:0;background:#f6f7fb;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f7fb;">
|
||||
<tr>
|
||||
<td align="center" style="padding:24px;">
|
||||
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background:#ffffff;border:1px solid #e9ecf1;border-radius:8px;padding:20px;">
|
||||
<tr>
|
||||
<td align="left" style="font-size:16px;color:#0f172a;">
|
||||
<div style="font-weight:bold;margin-bottom:8px;">${appName}</div>
|
||||
<div style="font-size:18px;margin:0 0 12px 0;">Your verification code</div>
|
||||
<div style="display:inline-block;border:1px solid #e9ecf1;border-radius:6px;background:#f8fafc;padding:10px 14px;margin-bottom:10px;">
|
||||
<span style="font-family:Consolas,'Courier New',monospace;font-size:24px;letter-spacing:6px;color:#0f172a;">${otp}</span>
|
||||
</div>
|
||||
<p style="margin:8px 0 0 0;font-size:13px;color:#475569;">
|
||||
This code expires in <strong>${OTP_EXPIRES_MIN}</strong> minutes.
|
||||
</p>
|
||||
|
||||
<hr style="border:none;border-top:1px solid #e9ecf1;margin:18px 0;">
|
||||
<p style="margin:0;font-size:12px;color:#64748b;">
|
||||
If you didn’t request this code, you can ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="font-size:11px;color:#94a3b8;margin-top:12px;">
|
||||
© ${new Date().getFullYear()} ${appName}. All rights reserved.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
};
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': appId,
|
||||
'X-Parse-Master-Key': masterKey,
|
||||
};
|
||||
return axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
}
|
||||
|
||||
export function msUntil(nowMs, futureMs) {
|
||||
return Math.max(0, (futureMs || 0) - nowMs);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { appName, smtpenable } from '../../Utils.js';
|
||||
|
||||
export default async function sendmailtoSupport(userDetails, errorMessage) {
|
||||
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
|
||||
// Render a simple HTML form. In production, consider using a templating engine.
|
||||
try {
|
||||
await Parse.Cloud.sendEmail({
|
||||
sender: appName + ' <' + mailsender + '>',
|
||||
recipient: 'support@opensignlabs.com',
|
||||
subject: `Error while deleting account – ${appName}`,
|
||||
text: `Error while deleting account – ${appName}`,
|
||||
html: `<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #dc3545;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
color: #495057;
|
||||
margin-top: 30px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.details p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #495057;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Error: ${errorMessage}</h1>
|
||||
|
||||
<h2>User Details</h2>
|
||||
<div class="details">
|
||||
<p><span class="label">UserRole:</span> <span class="value">${userDetails?.UserRole}</span></p>
|
||||
<p><span class="label">Name:</span> <span class="value">${userDetails?.Name}</span></p>
|
||||
<p><span class="label">Email:</span> <span class="value">${userDetails?.Email}</span></p>
|
||||
<p><span class="label">UserId:</span> <span class="value">${userDetails?.UserId}</span></p>
|
||||
<p><span class="label">ExtUserId:</span> <span class="value">${userDetails?.objectId}</span></p>
|
||||
<p><span class="label">TenantId:</span> <span class="value">${userDetails?.TenantId}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('err while sending mail to support', err);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import fs from 'node:fs';
|
||||
import dotenv from 'dotenv';
|
||||
import GenerateCertificate from './pdf/GenerateCertificate.js';
|
||||
import { getSecureUrl } from '../../Utils.js';
|
||||
import { parseUploadFile } from '../../utils/fileUtils.js';
|
||||
dotenv.config({ quiet: true });
|
||||
const eSignName = 'OpenSign';
|
||||
const eSigncontact = 'hello@opensignlabs.com';
|
||||
@@ -16,10 +17,14 @@ async function uploadFile(pdfName, filepath) {
|
||||
try {
|
||||
const filedata = fs.readFileSync(filepath);
|
||||
let fileUrl;
|
||||
const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
|
||||
await file.save({ useMasterKey: true });
|
||||
const fileRes = getSecureUrl(file.url());
|
||||
fileUrl = fileRes.url;
|
||||
|
||||
// const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
|
||||
// await file.save({ useMasterKey: true });
|
||||
// const fileRes = getSecureUrl(file.url());
|
||||
// fileUrl = fileRes.url;
|
||||
|
||||
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
|
||||
fileUrl = getSecureUrl(fileRes?.url)?.url;
|
||||
return { imageUrl: fileUrl };
|
||||
} catch (err) {
|
||||
console.log('Err ', err);
|
||||
@@ -52,7 +57,9 @@ export default async function generateCertificatebydocId(req) {
|
||||
const certificatePath = `./exports/certificate_${docId}.pdf`;
|
||||
try {
|
||||
const getDocument = new Parse.Query('contracts_Document');
|
||||
getDocument.include('ExtUserPtr,Signers,AuditTrail.UserPtr,Placeholders,ExtUserPtr.TenantId');
|
||||
getDocument.include(
|
||||
'ExtUserPtr,Signers,AuditTrail.UserPtr,Placeholders,ExtUserPtr.TenantId,ExtUserPtr.UserId'
|
||||
);
|
||||
const docRes = await getDocument.get(docId, { useMasterKey: true });
|
||||
|
||||
if (docRes && docRes?.get('IsCompleted') && !docRes?.get('CertificateUrl')) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export default async function getDrive(request) {
|
||||
return { error: 'Please provide required parameter!' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err', err);
|
||||
console.log('err', err?.response?.data || err);
|
||||
if (err.code == 209) {
|
||||
return { error: 'Invalid session token' };
|
||||
} else {
|
||||
|
||||
@@ -20,22 +20,23 @@ async function getTenantByUserId(userId, contactId) {
|
||||
}
|
||||
} else {
|
||||
const query = new Parse.Query('contracts_Users');
|
||||
query.equalTo('UserId', {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: userId,
|
||||
});
|
||||
query.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
|
||||
const extuser = await query.first({ useMasterKey: true });
|
||||
if (extuser) {
|
||||
const tenantId = extuser?.get('TenantId')?.id || '';
|
||||
const user = extuser?.get('CreatedBy')?.id || userId;
|
||||
const tenantCreditsQuery = new Parse.Query('partners_Tenant');
|
||||
tenantCreditsQuery.equalTo('UserId', {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: user,
|
||||
});
|
||||
tenantCreditsQuery.exclude('FileAdapters,PfxFile');
|
||||
const res = await tenantCreditsQuery.first({ useMasterKey: true });
|
||||
const tenantQuery = new Parse.Query('partners_Tenant');
|
||||
if (tenantId) {
|
||||
tenantQuery.equalTo('objectId', tenantId);
|
||||
} else {
|
||||
tenantQuery.equalTo('UserId', {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: user,
|
||||
});
|
||||
}
|
||||
tenantQuery.exclude('FileAdapters,PfxFile');
|
||||
const res = await tenantQuery.first({ useMasterKey: true });
|
||||
return res;
|
||||
} else {
|
||||
return {};
|
||||
|
||||
@@ -14,13 +14,19 @@ import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
|
||||
import { Placeholder } from './Placeholder.js';
|
||||
import { SignPdf } from '@signpdf/signpdf';
|
||||
import { P12Signer } from '@signpdf/signer-p12';
|
||||
import { buildDownloadFilename } from '../../../utils/fileUtils.js';
|
||||
import { buildDownloadFilename, parseUploadFile } from '../../../utils/fileUtils.js';
|
||||
|
||||
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
const eSignName = 'OpenSign';
|
||||
const eSigncontact = 'hello@opensignlabs.com';
|
||||
const docUrl = `${serverUrl}/classes/contracts_Document`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': APPID,
|
||||
'X-Parse-Master-Key': masterKEY,
|
||||
};
|
||||
|
||||
async function unlinkFile(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
@@ -37,10 +43,14 @@ async function uploadFile(pdfName, filepath) {
|
||||
try {
|
||||
const filedata = fs.readFileSync(filepath);
|
||||
let fileUrl;
|
||||
const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
|
||||
await file.save({ useMasterKey: true });
|
||||
const fileRes = getSecureUrl(file.url());
|
||||
fileUrl = fileRes.url;
|
||||
|
||||
// const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
|
||||
// await file.save({ useMasterKey: true });
|
||||
// const fileRes = getSecureUrl(file.url());
|
||||
// fileUrl = fileRes.url;
|
||||
|
||||
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
|
||||
fileUrl = getSecureUrl(fileRes?.url)?.url;
|
||||
|
||||
return { imageUrl: fileUrl };
|
||||
} catch (err) {
|
||||
@@ -90,13 +100,7 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
|
||||
isCompleted = true;
|
||||
}
|
||||
const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted };
|
||||
const signedRes = await axios.put(serverUrl + '/classes/contracts_Document/' + docId, body, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': APPID,
|
||||
'X-Parse-Master-Key': masterKEY,
|
||||
},
|
||||
});
|
||||
const signedRes = await axios.put(`${docUrl}/${docId}`, body, { headers });
|
||||
return { isCompleted: isCompleted, message: 'success', AuditTrail: updateAuditTrail };
|
||||
} catch (err) {
|
||||
console.log('update doc err ', err);
|
||||
@@ -141,13 +145,7 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
|
||||
html: body,
|
||||
mailProvider: mailProvider,
|
||||
};
|
||||
await axios.post(serverUrl + '/functions/sendmailv3', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': APPID,
|
||||
'X-Parse-Master-Key': masterKEY,
|
||||
},
|
||||
});
|
||||
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err in sendnotifymail', err);
|
||||
@@ -255,13 +253,7 @@ async function sendCompletedMail(obj) {
|
||||
filename: docName,
|
||||
};
|
||||
try {
|
||||
const res = await axios.post(serverUrl + '/functions/sendmailv3', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': APPID,
|
||||
'X-Parse-Master-Key': masterKEY,
|
||||
},
|
||||
});
|
||||
const res = await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
// console.log('res', res.data.result);
|
||||
if (res.data?.result?.status !== 'success') {
|
||||
unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`);
|
||||
@@ -298,13 +290,7 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
|
||||
fs.writeFileSync(certificatePath, signedCertificate);
|
||||
const file = await uploadFile('certificate.pdf', certificatePath);
|
||||
const body = { CertificateUrl: file.imageUrl };
|
||||
await axios.put(serverUrl + '/classes/contracts_Document/' + doc.objectId, body, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': APPID,
|
||||
'X-Parse-Master-Key': masterKEY,
|
||||
},
|
||||
});
|
||||
await axios.put(`${docUrl}/${doc.objectId}`, body, { headers });
|
||||
// used in API only
|
||||
if (doc.IsSendMail === false) {
|
||||
console.log("don't send mail");
|
||||
@@ -313,6 +299,41 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
|
||||
}
|
||||
saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId);
|
||||
unlinkFile(pfx.name);
|
||||
return file.imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a PDF for signing:
|
||||
* - updates audit trail, generates certificate.
|
||||
* - Optionally inserts a signature placeholder (Placeholder()).
|
||||
* - Otherwise (no merge + no placeholder), it flattens forms for finalization.
|
||||
*
|
||||
* @param {Object} _resDoc - Document details (expects AuditTrail, etc.)
|
||||
* @param {Buffer|Uint8Array} pdfBytes - Original PDF bytes
|
||||
* @param {string} [options.reason] - Reason text used in placeholder
|
||||
* @param {string} [options.UserPtr] - user pointer (for audit trail)
|
||||
* @param {string} [options.ipAddress] - IP (for audit trail)
|
||||
* @param {string} [options.Signature] - Signature (for audit trail)
|
||||
* @returns {Promise<Buffer>} merged PDF Buffer
|
||||
*/
|
||||
async function processPdf(_resDoc, PdfBuffer, reason) {
|
||||
// No CC merge; operate directly on the original PDF
|
||||
const pdfDoc = await PDFDocument.load(PdfBuffer);
|
||||
const form = pdfDoc.getForm();
|
||||
// Updates the field appearances to ensure visual changes are reflected.
|
||||
form.updateFieldAppearances();
|
||||
// Flattens the form, converting all form fields into non-editable, static content
|
||||
form.flatten();
|
||||
Placeholder({
|
||||
pdfDoc: pdfDoc,
|
||||
reason: `Digitally signed by ${eSignName} for ${reason}`,
|
||||
location: 'n/a',
|
||||
name: eSignName,
|
||||
contactInfo: eSigncontact,
|
||||
signatureLength: 16000,
|
||||
});
|
||||
const pdfWithPlaceholderBytes = await pdfDoc.save();
|
||||
return Buffer.from(pdfWithPlaceholderBytes);
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -333,7 +354,7 @@ async function PDF(req) {
|
||||
const publicUrl = req.headers.public_url;
|
||||
// below bode is used to get info of docId
|
||||
const docQuery = new Parse.Query('contracts_Document');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,CreatedBy');
|
||||
docQuery.equalTo('objectId', docId);
|
||||
const resDoc = await docQuery.first({ useMasterKey: true });
|
||||
if (!resDoc) {
|
||||
@@ -389,7 +410,11 @@ async function PDF(req) {
|
||||
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
|
||||
let isCompleted = false;
|
||||
if (_resDoc.Signers && _resDoc.Signers.length > 0) {
|
||||
if (auditTrail.length === _resDoc.Signers.length) {
|
||||
const removePrefill =
|
||||
_resDoc?.Placeholders?.length > 0 &&
|
||||
_resDoc?.Placeholders?.filter(x => x?.Role !== 'prefill');
|
||||
if (auditTrail.length === removePrefill?.length) {
|
||||
// if (auditTrail.length === _resDoc.Signers.length) {
|
||||
isCompleted = true;
|
||||
}
|
||||
} else {
|
||||
@@ -408,24 +433,9 @@ async function PDF(req) {
|
||||
signersName && signersName.length > 0
|
||||
? signersName?.join(', ')
|
||||
: username + ' <' + userEmail + '>';
|
||||
const pdfDoc = await PDFDocument.load(PdfBuffer);
|
||||
const form = pdfDoc.getForm();
|
||||
// Updates the field appearances to ensure visual changes are reflected.
|
||||
form.updateFieldAppearances();
|
||||
// Flattens the form, converting all form fields into non-editable, static content
|
||||
form.flatten();
|
||||
const p12Cert = new P12Signer(P12Buffer, { passphrase: passphrase || null });
|
||||
signedFilePath = `./exports/signed_${name}`;
|
||||
Placeholder({
|
||||
pdfDoc: pdfDoc,
|
||||
reason: `Digitally signed by ${eSignName} for ${reason}`,
|
||||
location: 'n/a',
|
||||
name: eSignName,
|
||||
contactInfo: eSigncontact,
|
||||
signatureLength: 16000,
|
||||
});
|
||||
const pdfWithPlaceholderBytes = await pdfDoc.save();
|
||||
PdfBuffer = Buffer.from(pdfWithPlaceholderBytes);
|
||||
PdfBuffer = await processPdf(_resDoc, PdfBuffer, reason, UserPtr, userIP, sign);
|
||||
//`new signPDF` create new instance of pdfBuffer and p12Buffer
|
||||
const OBJ = new SignPdf();
|
||||
// `signedDocs` is used to signpdf digitally
|
||||
@@ -484,9 +494,7 @@ async function PDF(req) {
|
||||
console.log('Err in signpdf', err);
|
||||
const body = { DebugginLog: err?.message };
|
||||
try {
|
||||
await axios.put(serverUrl + '/classes/contracts_Document/' + docId, body, {
|
||||
headers: { 'X-Parse-Application-Id': APPID, 'X-Parse-Master-Key': masterKEY },
|
||||
});
|
||||
await axios.put(`${docUrl}/${docId}`, body, { headers });
|
||||
} catch (err) {
|
||||
console.log('err in saving debugginglog', err);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function reportJson(id, currentUserId) {
|
||||
'RequestSubject',
|
||||
'ExtUserPtr.TenantId.RequestBody',
|
||||
'ExtUserPtr.TenantId.RequestSubject',
|
||||
'DocSentAt',
|
||||
];
|
||||
const filterKeys = [
|
||||
'TimeToCompleteDays',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { flattenPdf, getSecureUrl } from '../../Utils.js';
|
||||
import { parseUploadFile } from '../../utils/fileUtils.js';
|
||||
|
||||
export default async function saveFile(request) {
|
||||
if (!request.params.fileBase64) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Please provide file.');
|
||||
@@ -10,6 +12,7 @@ export default async function saveFile(request) {
|
||||
const extCls = new Parse.Query('contracts_Users');
|
||||
extCls.equalTo('UserId', request.user);
|
||||
extCls.include('TenantId');
|
||||
extCls.include('UserId');
|
||||
const resExt = await extCls.first({ useMasterKey: true });
|
||||
if (resExt) {
|
||||
const _resExt = JSON.parse(JSON.stringify(resExt));
|
||||
@@ -20,17 +23,26 @@ export default async function saveFile(request) {
|
||||
if (ext === 'pdf') {
|
||||
mimeType = 'application/pdf';
|
||||
const flatPdf = await flattenPdf(fileBase64);
|
||||
file = [...flatPdf];
|
||||
// file = [...flatPdf];
|
||||
file = flatPdf;
|
||||
} else if (ext === 'png' || ext === 'jpeg' || ext === 'jpg') {
|
||||
mimeType = `image/${ext}`;
|
||||
file = { base64: fileBase64 };
|
||||
// file = { base64: fileBase64 };
|
||||
file = Buffer.from(fileBase64, 'base64');
|
||||
}
|
||||
// const pdfFile = new Parse.File(fileName, file, mimeType);
|
||||
// // Save the Parse File if needed
|
||||
// const pdfData = await pdfFile.save({ useMasterKey: true });
|
||||
// const presignedUrl = pdfData.url();
|
||||
// const fileRes = getSecureUrl(presignedUrl);
|
||||
// return { url: fileRes.url };
|
||||
try {
|
||||
const fileRes = await parseUploadFile(fileName, file, mimeType);
|
||||
fileUrl = getSecureUrl(fileRes?.url)?.url;
|
||||
return { url: fileUrl };
|
||||
} catch (err) {
|
||||
throw new Parse.Error(400, err?.message);
|
||||
}
|
||||
const pdfFile = new Parse.File(fileName, file, mimeType);
|
||||
// Save the Parse File if needed
|
||||
const pdfData = await pdfFile.save({ useMasterKey: true });
|
||||
const presignedUrl = pdfData.url();
|
||||
const fileRes = getSecureUrl(presignedUrl);
|
||||
return { url: fileRes.url };
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'User not found.');
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export const config = {
|
||||
appId: serverAppId,
|
||||
logLevel: ['error'],
|
||||
maxLimit: 500,
|
||||
maxUploadSize: '30mb',
|
||||
maxUploadSize: '100mb',
|
||||
masterKey: process.env.MASTER_KEY, //Add your master key here. Keep it secret!
|
||||
masterKeyIps: ['0.0.0.0/0', '::/0'], // '::1'
|
||||
serverURL: cloudServerUrl, // Don't forget to change to https if needed
|
||||
@@ -156,8 +156,8 @@ export const config = {
|
||||
|
||||
export const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
app.use(express.urlencoded({ limit: '100mb', extended: true }));
|
||||
app.use(function (req, res, next) {
|
||||
req.headers['x-real-ip'] = getUserIP(req);
|
||||
const publicUrl = 'https://' + req?.get('host');
|
||||
|
||||
Generated
+2648
-2611
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,8 @@
|
||||
"watch": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.879.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.879.0",
|
||||
"@aws-sdk/client-s3": "^3.891.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.891.0",
|
||||
"@parse/fs-files-adapter": "^3.0.0",
|
||||
"@parse/s3-files-adapter": "^4.2.0",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
@@ -27,16 +27,16 @@
|
||||
"@signpdf/signer-p12": "^3.2.4",
|
||||
"@signpdf/signpdf": "^3.2.5",
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.2",
|
||||
"coherentpdf": "^2.5.5",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^5.1.0",
|
||||
"form-data": "^4.0.4",
|
||||
"generate-api-key": "^1.0.2",
|
||||
"googleapis": "^159.0.0",
|
||||
"libreoffice-convert": "^1.6.1",
|
||||
"libreoffice-convert": "^1.7.0",
|
||||
"mailgun.js": "^12.0.3",
|
||||
"mongodb": "^6.19.0",
|
||||
"multer": "^2.0.2",
|
||||
@@ -49,19 +49,19 @@
|
||||
"parse-server": "^8.2.4",
|
||||
"parse-server-api-mail-adapter": "^4.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"posthog-node": "^5.8.1",
|
||||
"posthog-node": "^5.8.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"rate-limiter-flexible": "^7.2.0",
|
||||
"sharp": "^0.34.3",
|
||||
"rate-limiter-flexible": "^7.3.1",
|
||||
"sharp": "^0.34.4",
|
||||
"speakeasy": "^2.0.0",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.28.0",
|
||||
"eslint": "^9.34.0",
|
||||
"@babel/eslint-parser": "^7.28.4",
|
||||
"eslint": "^9.35.0",
|
||||
"jasmine": "^5.10.0",
|
||||
"mongodb-runner": "^5.9.2",
|
||||
"mongodb-runner": "^5.9.3",
|
||||
"nodemon": "^3.1.10",
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "^3.6.2"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../Utils.js';
|
||||
|
||||
function formatFixedDate(date = new Date()) {
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
const months = [
|
||||
@@ -75,3 +78,23 @@ export function buildDownloadFilename(formatId, ctx) {
|
||||
const safeExt = ext.replace(/\.+/g, '').toLowerCase() || 'pdf';
|
||||
return `${stem}.${safeExt}`;
|
||||
}
|
||||
|
||||
export async function parseUploadFile(fileName, fileData, mimeType) {
|
||||
try {
|
||||
const res = await axios.post(`${cloudServerUrl}/files/${fileName}`, fileData, {
|
||||
headers: {
|
||||
'X-Parse-Application-Id': serverAppId,
|
||||
'X-Parse-Master-Key': process.env.MASTER_KEY,
|
||||
'Content-Type': mimeType,
|
||||
},
|
||||
});
|
||||
|
||||
// console.log('File uploaded:', res.data);
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
const errorMessage = err?.response?.data?.error || 'Unknown error';
|
||||
const statusCode = err?.response?.status || 500;
|
||||
console.log('Err in parseUploadFile', errorMessage);
|
||||
throw { message: errorMessage, code: statusCode };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user