Compare commits

..
Author SHA1 Message Date
prafull-opensignlabs 3500883205 v2.21.2
fix: handle subscription error in publicsign
2025-05-21 07:37:15 +00:00
prafull-opensignlabsandGitHub 8f2433fbea Merge pull request #1737 from OpenSignLabs/updates-15043583319
v2.21.1
2025-05-15 16:48:53 +05:30
prafull-opensignlabs b671dbac19 v2.21.1
fix: handle undefined placeholders condition
2025-05-15 11:17:30 +00:00
prafull-opensignlabsandGitHub 79cdbc9c8c Merge pull request #1735 from OpenSignLabs/updates-15024699698
fix: resolve error cannot read undefined props
2025-05-14 20:55:08 +05:30
prafull-opensignlabs e585b7c943 fix: resolve error cannot read undefined props 2025-05-14 15:24:27 +00:00
prafull-opensignlabsandGitHub 5336e2cb7c Merge pull request #1734 from OpenSignLabs/prafull-opensignlabs-patch-7
refactor: remove unnecessary file
2025-05-14 20:26:36 +05:30
prafull-opensignlabsandGitHub 72401291f8 Delete apps/OpenSign/vite.public-template.config.js 2025-05-14 20:24:54 +05:30
prafull-opensignlabsandGitHub 7b34314b9e Merge pull request #1733 from OpenSignLabs/updates-15023728155
fix: correct build commands for selfhosting
2025-05-14 20:13:14 +05:30
prafull-opensignlabs b6f1cceeca Merge pull request #612 from OpenSignLabs/staging
fix: correct build commands for selfhosting
2025-05-14 14:42:30 +00:00
prafull-opensignlabsandGitHub c422cdac06 Merge pull request #1732 from OpenSignLabs/updates-15023371589
fix: edit template option not visible in mobile view
2025-05-14 20:00:02 +05:30
prafull-opensignlabsandGitHub ab08d2615d Update package.json 2025-05-14 19:59:01 +05:30
prafull-opensignlabs 2b0c7488d9 Merge pull request #610 from OpenSignLabs/staging
fix: edit template option not visible in mobile view
2025-05-14 14:27:57 +00:00
prafull-opensignlabsandGitHub 6927540741 Merge pull request #1731 from OpenSignLabs/updates-15022725718
v2.21.0
2025-05-14 19:33:19 +05:30
prafull-opensignlabs 1ceeee36ac v2.21.0
Merge pull request #601 from OpenSignLabs/staging
2025-05-14 14:00:50 +00:00
prafull-opensignlabsandGitHub 81d3969d1f Merge pull request #1719 from OpenSignLabs/updates-14856876951
v2.20.1
2025-05-06 15:35:42 +05:30
prafull-opensignlabs 5819103f57 v2.20.1 2025-05-06 10:01:20 +00:00
prafull-opensignlabsandGitHub 6653b7ae5a Merge pull request #1717 from OpenSignLabs/updates-14829517615
v2.20.0
2025-05-05 10:46:07 +05:30
prafull-opensignlabs b50c1e70ee v2.20.0 2025-05-05 05:12:54 +00:00
prafull-opensignlabsandGitHub 365c2a217e Merge pull request #1709 from OpenSignLabs/prafull-opensignlabs-patch-5
fix: quick send not working
2025-04-29 12:11:39 +05:30
prafull-opensignlabsandGitHub b1b9b20e97 Update Validate.js 2025-04-29 12:10:14 +05:30
prafull-opensignlabsandGitHub 75c0cdb5f9 fix: quick send not working 2025-04-29 12:09:05 +05:30
prafull-opensignlabsandGitHub 740ccca1f6 Merge pull request #1704 from OpenSignLabs/updates-14689408163
Merge pull request #547 from OpenSignLabs/staging
2025-04-27 12:21:00 +05:30
prafull-opensignlabs f3b79cb71f Merge pull request #547 from OpenSignLabs/staging
refactor: remove extra comment
2025-04-27 06:50:12 +00:00
prafull-opensignlabsandGitHub f95ea0b9e9 Merge pull request #1702 from OpenSignLabs/updates-14689261849
fix: update migration script
2025-04-27 12:01:50 +05:30
prafull-opensignlabs 647a9ce5b4 Merge pull request #545 from OpenSignLabs/staging
fix: update migration script
2025-04-27 06:30:15 +00:00
prafull-opensignlabsandGitHub 3f3ca668f5 Merge pull request #1700 from OpenSignLabs/updates-14666117248
fix: return https protocol for signing urls
2025-04-25 19:17:21 +05:30
prafull-opensignlabs 2fd6d1a37a fix: return https protocol for signing urls 2025-04-25 13:46:14 +00:00
prafull-opensignlabsandGitHub 307137f926 Merge pull request #1699 from OpenSignLabs/updates-14660165068
remove non-exist class from migration script
2025-04-25 13:51:06 +05:30
prafull-opensignlabs 2394880dfc Merge pull request #541 from OpenSignLabs/staging
remove non-exist defaultOtp class from migration
2025-04-25 08:19:46 +00:00
AmolandGitHub 414cdcd4b5 Merge pull request #1698 from OpenSignLabs/prafull-opensignlabs-patch-4
fix: Workflow does not contain permissions
2025-04-25 13:29:43 +05:30
prafull-opensignlabsandGitHub 1fa6b25890 fix: Workflow does not contain permissions 2025-04-25 13:29:03 +05:30
prafull-opensignlabsandGitHub ff2fd44b19 Merge pull request #1697 from OpenSignLabs/updates-14659684433
build(deps): update react-router package to fix vulnerabilities
2025-04-25 13:22:28 +05:30
prafull-opensignlabs ad466fb066 build(deps): update react-router package to fix vulnerabilities 2025-04-25 07:51:45 +00:00
AmolandGitHub 0f179fed67 Merge pull request #1696 from OpenSignLabs/updates-14659482113
v2.19.0
2025-04-25 13:13:08 +05:30
nxglabs 3ded6c35a1 Merge pull request #536 from OpenSignLabs/doc_load_drive
refactor: remove protected fields migration
2025-04-25 07:39:21 +00:00
AmolandGitHub 64293db3a1 Merge pull request #1695 from OpenSignLabs/prafull-opensignlabs-patch-3
Update Docker.yml
2025-04-25 13:06:15 +05:30
prafull-opensignlabsandGitHub 18a4c8523a Update Docker.yml 2025-04-25 13:03:50 +05:30
prafull-opensignlabsandGitHub f7b39409a8 Merge pull request #1692 from OpenSignLabs/updates-14590419587
fix: normalize emails, migrate user logic to cloud function
2025-04-22 14:09:18 +05:30
prafull-opensignlabs 6283524d2f Merge pull request #524 from OpenSignLabs/publicurl_adduser
fix: correct public URL, normalize emails, migrate user/orgAdmin logic to cloud function
2025-04-22 08:36:56 +00:00
prafull-opensignlabsandGitHub 216288fd09 Merge pull request #1689 from OpenSignLabs/updates-14509601184
v2.18.0
2025-04-17 12:06:43 +05:30
prafull-opensignlabs 3b1d8a48b7 v2.18.0
New Features:
1. Added support to recreate a new document from declined document without having to redo everything from scratch.
2. Added replace pdf file feature for Templates. PDF underlying a Template can now be replaced while keeping all the fields the same.
3. Introduced the ability to replace pdf file for in UpdateTemplate and CreateDocumentfromtemplate API.
4. Revamped the preferences menu UI.

Bug Fixes:
1. Fixed issue with capital letters in contact emails via API.
2. Better validations & error handling in APIs
2025-04-17 06:34:06 +00:00
prafull-opensignlabsandGitHub ec68770ff0 Merge pull request #1681 from OpenSignLabs/updates-14398222591
v2.17.0
2025-04-11 13:20:43 +05:30
prafull-opensignlabs f5d2027e3a refactor: resolve warnings 2025-04-11 07:45:23 +00:00
prafull-opensignlabsandGitHub 8af0010f55 Merge pull request #1672 from OpenSignLabs/updates-14279056892
v2.16.0
2025-04-05 11:36:59 +05:30
prafull-opensignlabs d7b7ad6285 Merge pull request 2025-04-05 06:01:24 +00:00
prafull-opensignlabsandGitHub fca45ced38 Merge pull request #1663 from OpenSignLabs/updates-14122971328
v2.15.0
2025-03-28 11:37:55 +05:30
prafull-opensignlabs ac8ab70246 v2.15.0
v2.15.0
2025-03-28 06:02:14 +00:00
prafull-opensignlabsandGitHub 8373679f27 Merge pull request #1661 from OpenSignLabs/do_deploy
feat: update proxy file
2025-03-24 13:47:33 +05:30
prafull-opensignlabs 0a23aabbe4 feat: update proxy file 2025-03-24 13:45:45 +05:30
prafull-opensignlabsandGitHub 4a85599703 Merge pull request #1660 from OpenSignLabs/updates-14030275569
feat: changes to implement one click deployment
2025-03-24 13:42:41 +05:30
prafull-opensignlabs 3e9b84de72 feat: changes to implement one click deployment
feat: changes to implement one click deployment
2025-03-24 08:11:52 +00:00
prafull-opensignlabsandGitHub 4313d8d0e4 Merge pull request #1654 from OpenSignLabs/updates-14016837992
refactor: remove unnecessary code
2025-03-23 13:11:28 +05:30
prafull-opensignlabs 3088fdd163 Merge pull request #403 from OpenSignLabs/staging
refactor: remove unnecessary code
2025-03-23 07:40:01 +00:00
ZathielandGitHub 8c86fe27ec Merge pull request #1616 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/i18next-browser-languagedetector-8.0.4
build(deps): bump i18next-browser-languagedetector from 8.0.2 to 8.0.4 in /apps/OpenSign
2025-03-21 21:50:38 +01:00
ZathielandGitHub 01e09998e5 Merge pull request #1650 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/npm_and_yarn-1c007eb283
build(deps): bump axios from 1.8.3 to 1.8.4 in /apps/OpenSignServer in the npm_and_yarn group across 1 directory
2025-03-21 21:49:55 +01:00
ZathielandGitHub 55fdb52bf3 Merge pull request #1651 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/npm_and_yarn-1c007eb283
build(deps): bump axios from 1.8.3 to 1.8.4 in /apps/OpenSign in the npm_and_yarn group across 1 directory
2025-03-21 21:49:17 +01:00
dependabot[bot]andGitHub 08f46a393b build(deps): bump axios
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSign directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.8.3 to 1.8.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.3...v1.8.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 14:41:30 +00:00
dependabot[bot]andGitHub 9345da543a build(deps): bump axios
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSignServer directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.8.3 to 1.8.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.3...v1.8.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 14:41:20 +00:00
dependabot[bot]andGitHub 208891e10d build(deps): bump i18next-browser-languagedetector in /apps/OpenSign
Bumps [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) from 8.0.2 to 8.0.4.
- [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.0.2...v8.0.4)

---
updated-dependencies:
- dependency-name: i18next-browser-languagedetector
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 14:41:17 +00:00
prafull-opensignlabsandGitHub db0fa61b2b Merge pull request #1648 from OpenSignLabs/updates-13994222227
v2.14.0
2025-03-21 20:09:57 +05:30
prafull-opensignlabs 221f098de6 v2.14.0
v2.14.0
2025-03-21 14:38:32 +00:00
176 changed files with 14255 additions and 9958 deletions
+7
View File
@@ -1,8 +1,15 @@
name: ci
# 👇 add this block
permissions:
contents: read # allow checkout & metadata-action to read repo
id-token: write # needed by docker/metadata-action v4
on:
push:
branches:
- 'main'
- 'staging'
jobs:
docker:
@@ -3,11 +3,11 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> -->
<!-- <link rel="apple-touch-icon" href="/logo192.png" /> -->
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
@@ -17,5 +17,6 @@
</head>
<body>
<div id="root" style="touch-action:pan-x pan-y;"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
</html>
+3780 -1903
View File
File diff suppressed because it is too large Load Diff
+51 -38
View File
@@ -4,28 +4,28 @@
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@lottiefiles/dotlottie-react": "^0.13.0",
"@lottiefiles/dotlottie-react": "^0.13.5",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.1.6",
"@react-pdf/renderer": "^4.1.6",
"@reduxjs/toolkit": "^2.5.1",
"axios": "^1.7.9",
"css-minimizer-webpack-plugin": "^7.0.0",
"@reduxjs/toolkit": "^2.7.0",
"axios": "^1.9.0",
"css-minimizer-webpack-plugin": "^7.0.2",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"nth-check": "^2.1.1",
"parse": "^5.3.0",
"parse": "^6.1.1",
"pdf-lib": "^1.17.1",
"print-js": "^1.6.0",
"radix-ui": "^1.0.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.9",
"react-confetti": "^6.2.2",
"react-confetti": "^6.4.0",
"react-cookie": "^7.2.2",
"react-datepicker": "^7.6.0",
"react-dnd": "^16.0.1",
@@ -35,40 +35,42 @@
"react-dom": "^18.2.0",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-i18next": "^15.5.0",
"react-konva": "^18.2.10",
"react-pdf": "^9.2.1",
"react-quill-new": "^3.3.3",
"react-quill-new": "^3.4.6",
"react-redux": "^9.2.0",
"react-rnd": "^10.4.14",
"react-router": "^7.1.5",
"react-rnd": "^10.5.2",
"react-router": "^7.5.3",
"react-scripts": "^5.0.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.0",
"react-select": "^5.10.1",
"react-signature-canvas": "^1.0.7",
"react-syntax-highlighter": "^15.6.1",
"react-timezone-select": "^3.2.8",
"react-tooltip": "^5.28.0",
"react-tooltip": "^5.28.1",
"react-web-share": "^2.0.2",
"reactour": "^1.19.4",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"regex-parser": "^2.3.0",
"regex-parser": "^2.3.1",
"serve": "^14.2.4",
"styled-components": "^5.3.0",
"web-vitals": "^4.2.4",
"ws": "^8.18.0",
"ws": "^8.18.2",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
"scripts": {
"build": "npm run version && react-scripts build",
"start-dev": "react-scripts start",
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
"start-dev": "vite",
"dev": "vite",
"preview": "vite preview",
"start": "serve -s build",
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
"version-win": "powershell -Command \"Invoke-RestMethod -Uri 'https://api.github.com/repos/opensignlabs/opensign/releases/latest' | Select-Object -ExpandProperty tag_name | Out-File -FilePath ./public/version.txt\"",
"build-win": "npm run version-win && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"build-win": "npm run version-win && vite build",
"test": "vitest run",
"test:watch": "vitest",
"release": "standard-version",
"commit": "cz"
},
@@ -96,28 +98,39 @@
}
},
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@babel/runtime-corejs2": "^7.26.9",
"autoprefixer": "^10.4.20",
"babel-loader": "^9.2.1",
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.1",
"@babel/preset-react": "^7.27.1",
"@babel/runtime-corejs2": "^7.27.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.3.21",
"@vitejs/plugin-react": "^4.4.1",
"@vitejs/plugin-react-swc": "^3.9.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.1.2",
"css-loader": "^7.1.2",
"daisyui": "^4.12.23",
"dotenv": "^16.4.7",
"daisyui": "^4.12.24",
"dotenv": "^16.5.0",
"dotenv-webpack": "^8.1.0",
"eslint": "^9.20.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"lint-staged": "^15.4.3",
"eslint": "^8.57.1",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"lint-staged": "^15.5.1",
"mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.5.1",
"prettier": "^3.5.0",
"pretty-quick": "^4.0.0",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"pretty-quick": "^4.1.1",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.17",
"terser-webpack-plugin": "^5.3.11",
"terser-webpack-plugin": "^5.3.14",
"vite": "^6.3.5",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3",
"webpack-cli": "^5.1.4"
},
"overrides": {
@@ -1,4 +1,4 @@
{
{
"header-news": "Neue Funktion: Benutzer des Teams-Plans können jetzt ihre eigenen AWS S3-Buckets für die Dateispeicherung integrieren",
"header-news-btn": "Jetzt einrichten",
"create-account": "Konto erstellen",
@@ -151,7 +151,10 @@
"extend-expiry-date": "Ablaufdatum verlängern",
"Duplicate Template": "Vorlage duplizieren",
"Duplicate": "Duplikat",
"daily-mail-quota": "Tägliches E-Mail-Kontingent"
"daily-mail-quota": "Tägliches E-Mail-Kontingent",
"Save as template": "Als Vorlage speichern",
"Fix & resend": "Korrigieren und erneut senden",
"Kiosk Mode": "Kiosk Modus"
},
"report-heading": {
"Sr.No": "Nr.",
@@ -247,7 +250,7 @@
"generate-token-alert": "Sind Sie sicher, dass Sie das Token neu generieren möchten? Das alte Token wird ablaufen.",
"yes": "Ja",
"copied": "Kopiert",
"something-went-wrong-mssg": "Etwas ist schiefgelaufen. Bitte versuchen Sie es später erneut.",
"something-went-wrong-mssg": "Etwas ist schiefgelaufen, Das Aktualisieren dieser Seite kann das Problem lösen.",
"token-generated": "Token erfolgreich generiert.",
"webhook": "Webhook",
"update-webhook": "Webhook aktualisieren",
@@ -283,6 +286,7 @@
"make-template-public": "Vorlage öffentlich machen",
"make-template-private": "Vorlage privat machen",
"make-template-public-alert": "Sind Sie sicher, dass Sie diese Vorlage öffentlich machen möchten?",
"make-template-private-alert-non": "Sind Sie sicher, dass Sie diese Vorlage privat machen möchten?",
"make-template-private-alert": "Sind Sie sicher, dass Sie diese Vorlage privat machen möchten? Dadurch wird sie aus Ihrem öffentlichen Profil entfernt.",
"public-role": "Öffentliche Rolle",
"public-url": "Öffentliches Profil",
@@ -301,14 +305,14 @@
"send": "Senden",
"quick-send-alert-1": "Alle Rollen in diesem Dokument sind derzeit mit Kontakten verknüpft. Um Kopien dieser Vorlage schnell an mehrere Unterzeichner zu senden, stellen Sie sicher, dass mindestens eine Rolle keinem Kontakt zugeordnet ist.",
"quick-send-alert-2": "Bitte stellen Sie sicher, dass für alle Empfänger mindestens ein Signatur-Widget hinzugefügt wurde.",
"quick-send-alert-3": "Bitte fügen Sie mindestens eine Rolle zu dieser Vorlage hinzu, um Kopien davon schnell an mehrere Unterzeichner zu senden.",
"quick-send-alert-3": "Bitte fügen Sie diesem template mindestens eine Rolle hinzu.",
"quick-send-alert-4": "Das Limit für Schnellsendungen wurde erreicht.",
"copy-link": "Link kopieren",
"copy": "Kopieren",
"revoke-document": "Dokument widerrufen",
"revoke-document-alert": "Sind Sie sicher, dass Sie dieses Dokument widerrufen möchten?",
"resend-mail": "E-Mail erneut senden",
"resend-mail-help": "Sie können folgende Variablen verwenden, die durch ihre tatsächlichen Werte ersetzt werden: {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}.",
"resend-mail-help": "Sie können folgende Variablen verwenden, die durch ihre tatsächlichen Werte ersetzt werden: {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}, {{note}}.",
"subject": "Betreff",
"body": "Inhalt",
"add-contact": "Kontakt hinzufügen",
@@ -338,7 +342,7 @@
"verify-email-1": "E-Mail verifizieren",
"resend": "Erneut senden",
"contact-details": "Kontaktdetails",
"verify-email": "Bitte verifizieren Sie Ihre E-Mail!",
"verify-email": "Bitte bestätigen Sie Ihre E-Mail, um fortzufahren. Dies ist ein einmaliger Schritt, um die mit Ihrem OpenSign-Konto verknüpfte E-Mail zu bestätigen.",
"send-otp": "OTP senden",
"otp-placeholder": "Verifizierungscode aus der E-Mail eingeben",
"loading-doc": "Dokument wird geladen...",
@@ -458,6 +462,7 @@
"placeholder-alert-3": "Sind Sie sicher, dass Sie dieses Dokument zur Unterzeichnung senden möchten?",
"placeholder-alert-4": "Sie haben erfolgreich E-Mails an alle Empfänger gesendet!",
"placeholder-mail-alert": "Sie haben erfolgreich eine E-Mail an {{name}} gesendet. Die nachfolgenden Unterzeichner erhalten eine E-Mail, sobald {{name}} das Dokument unterzeichnet.",
"placeholder-mail-alert-you": "Nachfolgende Unterzeichner erhalten E-Mails, sobald Sie das Dokument unterschreiben.",
"placeholder-alert-5": "Möchten Sie die Dokumente jetzt unterschreiben?",
"placeholder-alert-6": "Bitte richten Sie den E-Mail-Adapter ein, um E-Mails zu senden!",
"placeholder-alert-7": "Bitte wählen Sie einen Unterzeichner aus, um Platzhalter hinzuzufügen!",
@@ -521,7 +526,7 @@
"correct-password": "Bitte korrektes Passwort angeben",
"decrypting-pdf": "PDF wird entschlüsselt, bitte warten...",
"invalid-otp": "Ungültiger OTP",
"user-not-found": "Benutzer nicht gefunden!",
"user-not-found": "Benutzer nicht gefunden",
"enter-otp-alert": "Bitte OTP eingeben!",
"get-verification-code": "Verifizierungscode erhalten",
"get-verification-code-2": "Sie erhalten einen Verifizierungscode per E-Mail",
@@ -831,7 +836,7 @@
"initial-type": "Ihre Initialen",
"redirect-url": "Weiterleitungs-URL",
"bulk-send": "Massenversand",
"select-timezone": "Wählen Sie Ihre Zeitzone",
"select-timezone": "Zeitzone",
"current-time": "Aktuelle Uhrzeit",
"email-help": "Aus Sicherheitsgründen dürfen Sie die E-Mail-Adresse nicht ändern. Bitte erstellen Sie ein weiteres kostenloses Konto mit der neuen E-Mail-Adresse.",
"doc-sent": "Dokument erfolgreich gesendet.",
@@ -856,5 +861,115 @@
"enabled-signature-type-help": "Die Einstellung 'Aktivierte Signaturtypen' bestimmt, welche Signaturoptionen in Ihrer Organisation verfügbar sind. Wenn Sie beispielsweise die Option 'Zeichnen' deaktivieren, wird sie den Mitgliedern Ihrer Organisation im Signatur-Widget nicht angezeigt, während die anderen drei Optionen weiterhin zugänglich bleiben.",
"indexing-public-profile": "Erlaube die Indexierung des öffentlichen Profils durch Suchmaschinen",
"user-created-successfully": "Benutzer erfolgreich erstellt.",
"only-15-reminder-allowed": "Sie können bis zu 15 automatische Erinnerungen festlegen. Wenn zum Beispiel 'TimeToComplete' auf 15 Tage und 'RemindOnceInEvery' auf 1 Tag eingestellt ist, erreichen Sie das maximale Limit von 15 Erinnerungen. Passen Sie Ihre Einstellungen entsprechend an."
"only-15-reminder-allowed": "Sie können bis zu 15 automatische Erinnerungen festlegen. Wenn zum Beispiel 'TimeToComplete' auf 15 Tage und 'RemindOnceInEvery' auf 1 Tag eingestellt ist, erreichen Sie das maximale Limit von 15 Erinnerungen. Passen Sie Ihre Einstellungen entsprechend an.",
"rate-your-experience": "Wie war Ihre Erfahrung mit {{appName}}?",
"thanks-for-feedback": "Danke für Ihr Feedback 🙏",
"share-your-feedback": "Teilen Sie Ihr Feedback",
"share-your-review": "Teilen Sie Ihre Bewertung",
"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?",
"go-to-manage-templates": "Zu 'Vorlagen verwalten' gehen",
"template-created": "Vorlage erstellt",
"how-would-you-like-to-proceed?": "Wie möchten Sie fortfahren?",
"failed-to-load-refresh-page": "Fehler beim Laden des Dokuments. Bitte versuchen Sie, diese Seite zu aktualisieren.",
"document-has-been-signed": "Das Dokument wurde erfolgreich unterschrieben!",
"document-has-been-signed-by-you": "Das Dokument wurde erfolgreich von Ihnen unterschrieben!",
"participant-completed-signing": "Alle Teilnehmer haben den Signaturprozess abgeschlossen.",
"you-will-receive-email-shortly": "✅ Das war's! Sie erhalten in Kürze eine Bestätigungs-E-Mail.",
"please-provide-templateid": "Bitte geben Sie templateid an",
"this-template-is-not-public": "Dieses template ist nicht öffentlich",
"invalid-templateid": "Ungültige templateid",
"contact-billing-at-opensign": "Um weitere Plätze hinzuzufügen, kontaktieren Sie bitte OpenSign™ unter <1>billing@opensignlabs.com</1> für Unterstützung.",
"title-length-alert": "Der Titel darf höchstens 250 Zeichen lang sein.",
"note-length-alert": "Die Notiz darf höchstens 200 Zeichen lang sein.",
"description-length-alert": "Die Beschreibung darf höchstens 500 Zeichen lang sein.",
"fix-&-resend-document": "Dokument korrigieren und erneut senden",
"do-you-want-recreate-document?": "Dadurch wird ein Entwurf aus diesem Dokument mit allen vorhandenen Feldern erstellt. Sind Sie sicher, dass Sie dieses Dokument neu erstellen möchten?",
"start-editing": "Bearbeitung starten",
"unsaved-changes-discard-them?": "Sie haben ungespeicherte Änderungen. Verwerfen?",
"yes-discard": "Ja, verwerfen",
"LTV-enabled-signatures": "LTV-fähige Signaturen",
"BETA": "BETA",
"two-factor-authentication": "Zwei-Faktor-Authentifizierung",
"2fa-help-text": "Die Zwei-Faktor-Authentifizierung fügt Ihrem Konto eine zusätzliche Sicherheitsebene hinzu, indem sie mehr als nur ein Passwort zum Anmelden erfordert.",
"2fa-help-bullet1": "Erhöht die Sicherheit, indem sowohl Ihr Passwort als auch ein Bestätigungscode erforderlich sind.",
"2fa-help-bullet2": "Der Bestätigungscode wird von einer Authentifizierungs-App auf Ihrem Gerät generiert.",
"2fa-help-bullet3": "Schützt Ihr Konto, selbst wenn Ihr Passwort kompromittiert wurde.",
"setup-2fa": "2FA einrichten",
"setup-2fa-again": "2FA erneut einrichten",
"2fa-setup-intro": "Schützen Sie Ihr Konto mit der Zwei-Faktor-Authentifizierung. Wenn aktiviert, müssen Sie bei jeder Anmeldung einen Code aus Ihrer Authentifizierungs-App eingeben.",
"scan-qr-code": "QR-Code scannen",
"scan-qr-instructions": "Verwenden Sie eine Authentifizierungs-App wie Google Authenticator, Microsoft Authenticator oder Authy, um diesen QR-Code zu scannen.",
"manual-setup-instructions": "Können Sie den Code nicht scannen? Sie können Ihre Authentifizierungs-App manuell mit diesem geheimen Schlüssel einrichten:",
"secret-key": "Geheimer Schlüssel",
"copied-to-clipboard": "In die Zwischenablage kopiert",
"copy-to-clipboard": "In die Zwischenablage kopieren",
"recovery-codes": "Wiederherstellungscodes",
"recovery-codes-instructions": "Speichern Sie diese Wiederherstellungscodes an einem sicheren Ort. Wenn Sie den Zugriff auf Ihre Authentifizierungs-App verlieren, können Sie einen dieser Einmal-Codes verwenden, um sich anzumelden.",
"download-recovery-codes": "Wiederherstellungscodes herunterladen",
"verification-code": "Bestätigungscode",
"enter-code-from-authenticator-app": "Geben Sie den 6-stelligen Code aus Ihrer Authentifizierungs-App ein",
"verification-code-required": "Bestätigungscode ist erforderlich",
"verification-code-invalid": "Ungültiger Bestätigungscode. Bitte versuchen Sie es erneut.",
"2fa-enabled": "2FA aktiviert",
"2fa-enabled-successfully": "Zwei-Faktor-Authentifizierung erfolgreich aktiviert",
"2fa-setup-complete": "Einrichtung abgeschlossen!",
"2fa-setup-complete-instructions": "Ihr Konto ist jetzt mit der Zwei-Faktor-Authentifizierung geschützt. Sie müssen bei jeder Anmeldung einen Bestätigungscode eingeben.",
"two-factor-verification": "Zwei-Faktor-Verifizierung",
"enter-verification-code-instructions": "Geben Sie den 6-stelligen Bestätigungscode aus Ihrer Authentifizierungs-App ein, um fortzufahren.",
"recovery-code": "Wiederherstellungscode",
"enter-recovery-code-help": "Geben Sie einen Ihrer Wiederherstellungscodes ein",
"recovery-code-required": "Wiederherstellungscode ist erforderlich",
"use-verification-code-instead": "Stattdessen Bestätigungscode verwenden",
"use-recovery-code-instead": "Stattdessen Wiederherstellungscode verwenden",
"regenerate-2fa-remove-existing": "Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung neu generieren möchten? Diese Aktion entfernt Ihre bestehenden Authentifizierungseinstellungen.",
"use-passkey": "Mit Passkey anmelden",
"security-section": "Sicherheit",
"passkey-authentication": "Passkey-Authentifizierung",
"passkey-not-supported": "Ihr Browser oder Gerät unterstützt keine Passkey-Authentifizierung",
"passkey-description": "Passkeys bieten eine stärkere, phishing-resistente Alternative zu Passwörtern. Sie können Ihren Fingerabdruck, Ihre Gesichtserkennung oder Ihre Geräte-PIN verwenden, um sich sicher anzumelden.",
"passkey-tooltip": "Passkeys sind eine einfachere und sicherere Alternative zu Passwörtern. Sie verwenden biometrische Daten wie Fingerabdrücke oder Gesichtserkennung, die bereits auf Ihrem Gerät gespeichert sind.",
"security-section-help": "Verwalten Sie Sicherheitsoptionen, einschließlich Passkeys und Authentifizierungsmethoden, um Ihr Konto zu schützen.",
"passkey-register": "Passkey registrieren",
"passkey-register-another": "Weiteren Passkey registrieren",
"passkey-registered": "Passkey registriert",
"passkey-registering": "Passkey wird registriert...",
"passkey-registered-success": "Passkey erfolgreich registriert!",
"passkey-registration-failed": "Passkey-Registrierung fehlgeschlagen",
"passkey-auth-failed": "Passkey-Authentifizierung fehlgeschlagen",
"passkey-missing-user-info": "Benutzerinformationen erforderlich",
"passkeys-list": "Ihre Passkeys",
"default-passkey": "Ihr Passkey",
"passkey-rename": "Umbenennen",
"passkey-delete": "Löschen",
"passkey-rename-title": "Passkey umbenennen",
"passkey-delete-title": "Passkey löschen",
"passkey-delete-confirm": "Möchten Sie den Passkey \"{{name}}\" wirklich löschen?",
"passkey-name": "Passkey-Name",
"passkey-name-placeholder": "Geben Sie einen beschreibenden Namen für diesen Passkey ein",
"passkey-renamed-success": "Passkey erfolgreich umbenannt",
"passkey-deleted-success": "Passkey erfolgreich gelöscht",
"passkey-rename-failed": "Passkey konnte nicht umbenannt werden",
"passkey-delete-failed": "Passkey konnte nicht gelöscht werden",
"processing": "Wird verarbeitet...",
"today": "Heute",
"yesterday": "Gestern",
"days-ago": "Vor {{days}} Tagen",
"verify-with-passkey": "Mit Passkey verifizieren",
"verify-with-otp": "Mit OTP verifizieren",
"verify-identity": "Identität verifizieren",
"verify-account": "Verifizieren Sie Ihre Identität",
"verification": "Verifizierung",
"passkey-verification-failed": "Passkey-Verifizierung fehlgeschlagen",
"security-auth-help": {
"p1": "Administre la configuración de seguridad de su cuenta para mantener sus datos seguros. OpenSign admite métodos de autenticación avanzados para mejorar la protección de la cuenta.",
"2fa-auth-help": "Agregue una capa adicional de seguridad activando 2FA. Esto requiere ingresar un código de verificación desde una aplicación autenticadora después de su contraseña.",
"passkey-auth-help": "Use claves de acceso para iniciar sesión sin contraseña con verificación biométrica o basada en el dispositivo, proporcionando una seguridad sólida y comodidad."
},
"signer-already-present": "Unterzeichner bereits vorhanden",
"kiosk-sign": "Kiosk-Unterschrift",
"dont-have-access-to-template": "Das template wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
"kiosk-info": "Kiosk Modus ermöglicht es Ihnen, persönliche Unterschriften schnell und effizient zu erfassen. Ideal für Messen, Veranstaltungen oder Laufkundschaft, bei denen alle Unterzeichner physisch anwesend sind. ",
"learn-more": "Mehr erfahren"
}
+137 -22
View File
@@ -1,5 +1,5 @@
{
"header-news": "New feature: Teams plan users can now integrate their own AWS S3 buckets for file storage",
"header-news": "New feature: Protect your account with Two-Factor Authentication (2FA) and enjoy the future of login with Passkeys — no passwords needed.",
"header-news-btn": "Setup now",
"create-account": "Create account",
"login": "Login",
@@ -151,7 +151,10 @@
"extend-expiry-date": "Extend expiry date",
"Duplicate Template": "Duplicate template",
"Duplicate": "Duplicate",
"daily-mail-quota": "Daily Email Quota"
"daily-mail-quota": "Daily Email Quota",
"Save as template": "Save as template",
"Fix & resend": "Fix & Resend",
"Kiosk Mode": "Kiosk Mode"
},
"report-heading": {
"Sr.No": "Sr.No",
@@ -247,7 +250,7 @@
"generate-token-alert": "Are you sure you want to regenerate token it will expire old token?",
"yes": "Yes",
"copied": "Copied",
"something-went-wrong-mssg": "Something went wrong, please try again later.",
"something-went-wrong-mssg": "Something went wrong, refreshing this page may solve this issue.",
"token-generated": "Token generated successfully.",
"webhook": "Webhook",
"update-webhook": "Update webhook",
@@ -283,11 +286,12 @@
"make-template-public": "Make template public",
"make-template-private": "Make template private",
"make-template-public-alert": "Are you sure you want to make this template public?",
"make-template-private-alert-non": "Are you sure you want to make this template private?",
"make-template-private-alert": "Are you sure you want to make this template private? This will remove it from your public profile.",
"public-role": "Public role",
"public-url": "Public profile",
"embed-template": "Embed template",
"public-url-copy": "Heres your public URL: ",
"public-url-copy": "Here's your public URL: ",
"public-url-copy-mssg": "Copy it or share it with the signer, and you will be able to see all your publicly set templates.",
"add-public-url-alert": "You can generate your {{appName}} public profile",
"share-with-alert": "You cannot share a template if any roles already have contacts assigned. Please remove all contact assignments from the roles before sharing the template.",
@@ -301,19 +305,19 @@
"send": "Send",
"quick-send-alert-1": "All roles in this document are currently linked to contacts. To quick send copies of this template to multiple signers, please ensure that at least one role is not linked to any contact.",
"quick-send-alert-2": "Please ensure there's at least one signature widget added for all recipients.",
"quick-send-alert-3": "Please add at least one role to this template in order to 'quick send' copies of it to multiple signers.",
"quick-send-alert-3": "Please add at least one role to this template.",
"quick-send-alert-4": "Quick send reached limit.",
"copy-link": "Copy link",
"copy": "Copy",
"revoke-document": "Revoke document",
"revoke-document-alert": "Are you sure you want to revoke this document?",
"resend-mail": "Resend mail",
"resend-mail-help": "You can use following variables which will get replaced with their actual values:- {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}.",
"resend-mail-help": "You can use following variables which will get replaced with their actual values:- {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}, {{note}}.",
"subject": "Subject",
"body": "Body",
"add-contact": "Add contact",
"edit-contact": "Edit contact",
"add-signer-alert": "Contact already exist! Please select it from Signers dropdown",
"add-signer-alert": "Contact already exist! Please select it from 'Signers' dropdown",
"record-delete-alert": "Record deleted successfully!",
"record-revoke-alert": "Record revoked successfully!",
"mail-sent-alert": "Mail sent successfully.",
@@ -338,7 +342,7 @@
"verify-email-1": "Verify email",
"resend": "Resend",
"contact-details": "Contact details",
"verify-email": "Please verify your email !",
"verify-email": "Please verify your email to continue. This is a one-time step to confirm the email associated with your OpenSign account.",
"send-otp": " Send OTP",
"otp-placeholder": "Enter verification code received over email",
"loading-doc": "Loading document..",
@@ -458,6 +462,7 @@
"placeholder-alert-3": " Are you sure you want to send out this document for signatures?",
"placeholder-alert-4": "You have successfully sent mails to all recipients!",
"placeholder-mail-alert": "You have successfully sent email to {{name}}. Subsequent signers will get email(s) once {{name}} signs the document",
"placeholder-mail-alert-you": "Subsequent signers will get email(s) once you signs the document.",
"placeholder-alert-5": "Do you want to sign the document right now?",
"placeholder-alert-6": "Please setup mail adapter to send mail!",
"placeholder-alert-7": "Please select signer for add placeholder!",
@@ -521,7 +526,7 @@
"correct-password": "Please provide correct password",
"decrypting-pdf": " Decrypting pdf please wait...",
"invalid-otp": "Invalid otp",
"user-not-found": "User not found!",
"user-not-found": "User not found",
"enter-otp-alert": "Please enter OTP!",
"get-verification-code": "Get verification code",
"get-verification-code-2": "You will get a verification code via Email",
@@ -584,7 +589,7 @@
},
"tour-mssg": {
"home-layout-1": "You have logged in successfully! Let's take a look.",
"home-layout-2": "To upload documents for self-signing or to request others signatures, simply select the respective buttons.",
"home-layout-2": "To upload documents for self-signing or to request others' signatures, simply select the respective buttons.",
"home-layout-3": "You are ready to start using {{appName}}! If you need support feel free to contact us.",
"generate-token": "Upgrade now to generate production API token.",
"opensign-drive-1": "Click on the breadcrumb links to easily navigate through the folder hierarchy and view the documents within each folder.",
@@ -593,7 +598,7 @@
"opensign-drive-4": "Click on this menu to display the documents in list view.",
"opensign-drive-5": "The document list is displayed according to the selected sorting option. Icons next to each document indicate its current status.",
"opensign-drive-6": "Right-click on a document to see options such as Download, Rename, Move, and Delete. Click on the document to open it.",
"opensign-drive-7": "Right-click on any folder to see options. Choose Rename to change the folders name or click on the folder to navigate through its contents.",
"opensign-drive-7": "Right-click on any folder to see options. Choose 'Rename' to change the folder's name or click on the folder to navigate through its contents.",
"pdf-request-file-1": "List of signers who still need to sign the document .",
"pdf-request-file-2": "Click any of the placeholders appearing on the document to sign. You will then see options to draw your signature, type it, or upload an image .",
"pdf-request-file-3": "Click Decline, or Finish buttons to navigate your document. Use the ellipsis menu for additional options, including the Download button .",
@@ -606,24 +611,24 @@
"placeholder-sign-4": "Drag or click on a field to add it to the document.",
"placeholder-sign-5": "The PDF content area already displays the template's existing placeholders. For your convenience, these placeholders will match the color of the recipient's name, making them easily identifiable.",
"placeholder-sign-6": "Clicking 'Next' will save the document. In the next step you can customize the emails to be sent out to the recipients or copy the signing links and share those with the recipients yourself.",
"report-1": "Click the 'Add' button to create a new template. Templates are reusable documents designed to quickly generate new documents with the same structure and varying signers. For example, an HR template for onboarding could have predefined roles like HR Manager and New Employee. Each time you use the template, you can assign the New Employee role to different incoming staff members, while the HR Manager role remains constant, facilitating a seamless onboarding process for each recruit. ",
"report-1": "Click the 'Add' button to create a new template. Templates are reusable documents designed to quickly generate new documents with the same structure and varying signers. For example, an HR template for onboarding could have predefined roles like 'HR Manager' and 'New Employee'. Each time you use the template, you can assign the 'New Employee' role to different incoming staff members, while the 'HR Manager' role remains constant, facilitating a seamless onboarding process for each recruit. ",
"redirect": "Click the 'Use' button to create a new document from an existing template.",
"bulksend": "To quickly send multiple documents using an existing template by just creating the recipient email addresses, click the 'Bulk Send' button.",
"option": "This menu reveals more options such as Edit & Delete. Use the 'Edit' button to add signer roles, modify fields, and update your template. Changes will apply to all future documents created from this template but wont affect existing documents.Use the Delete button you can delete template. ",
"option": "This menu reveals more options such as Edit & Delete. Use the 'Edit' button to add signer roles, modify fields, and update your template. Changes will apply to all future documents created from this template but won't affect existing documents.Use the Delete button you can delete template. ",
"signyour-self-1": "Select and drag your preferred widgets onto the PDF to customize your document before signing. Choose the perfect spots for each modification to tailor the document to your needs.",
"signyour-self-2": "Drag and drop anywhere in this area. You can resize and move it later.",
"template-placeholder-1": "Clicking 'Add role' button will allow you to add various signer roles. You can attach users to each role in subsequent steps.",
"template-placeholder-2": "Once roles are added, select a role from list to add a place-holder where he is supposed to sign. The placeholder will appear in the same colour as the role name once you drop it on the document.",
"template-placeholder-3": "Drag or click on a field to add it to the document.",
"template-placeholder-4": "Drag the placeholder for a role anywhere on the document.Remember, it will appear in the same colour as the name of the recipient for easy reference.",
"template-placeholder-5": "Clicking 'Next' will store the current template. After saving, youll be prompted to create a new document from this template if you wish.",
"template-placeholder-5": "Clicking 'Next' will store the current template. After saving, you'll be prompted to create a new document from this template if you wish.",
"webhook-1": "Upgrade now to set webhook",
"Need your Signature": "Clicking on this card will take you to the list of documents awaiting your review.",
"Out for signatures": "Clicking on this card will take you to a list of documents awaiting signature.",
"Recent signature requests": "This is a list of documents that are waiting for your signature.",
"Recently sent for signatures": "This is a list of documents you've sent to other parties for signature.",
"Drafts": "This are documents you have started but have not finalized for sending.",
"public-template": "This video demonstrates how to set up your personalized public profile, such as https://opensign.me/your-username. Youll also learn how to customize your tagline and make your templates available for public signing.",
"public-template": "This video demonstrates how to set up your personalized public profile, such as 'https://opensign.me/your-username'. You'll also learn how to customize your tagline and make your templates available for public signing.",
"allowModify-widgets": "You can drag and drop any of these fields onto the document, in addition to the fields already designated for you by the document creator."
},
"enter-email-plaholder": "Add an email address and hit enter",
@@ -657,13 +662,13 @@
"bulk-send-subcription-alert": "Please upgrade to Professional or Team plan to use bulk send.",
"generate-test-token": "Generate test token",
"regenerate-test-token": "Regenerate test token",
"help-test-token": "This token can be used to test the APIs at the https://sandbox.opensignlabs.com/api/v1 endpoint, allowing you to conduct unlimited document signatures. Please note that the sandbox API will sign your documents with self-signed certificates, which may not be recognized as valid by Adobe. Once youve completed your testing, you can upgrade to one of our paid plans to generate a production token.",
"help-test-token": "This token can be used to test the APIs at the https://sandbox.opensignlabs.com/api/v1 endpoint, allowing you to conduct unlimited document signatures. Please note that the sandbox API will sign your documents with self-signed certificates, which may not be recognized as valid by Adobe. Once you've completed your testing, you can upgrade to one of our paid plans to generate a production token.",
"help-api-token": "This token can be used to access the production APIs at the {{origin}}/api/v1 endpoint. It can only be generated on one of our paid plans.",
"reason": "Reason",
"decline-by": "Declined/revoked by",
"document-declined": "Document declined",
"public-template-mssg-1": "To integrate OpenSign into your React or Next.js project, simply run the following command:",
"public-template-mssg-2": "Ensure you have npm or yarn set up in your project. If youre using Yarn, you can replace npm install with yarn add @opensign/react.",
"public-template-mssg-2": "Ensure you have npm or yarn set up in your project. If you're using Yarn, you can replace npm install with yarn add @opensign/react.",
"public-template-mssg-3": "Need more details or examples?",
"public-template-mssg-4": "Visit the",
"public-template-mssg-5": " npm for the latest updates, detailed documentation, and version history.",
@@ -791,7 +796,7 @@
"term-cond-p22": "Understand that {{appName}} is a platform facilitating the transaction and is not a party to the agreement.",
"term-cond-h7": "7. Legal Effect",
"term-cond-p23": "Your electronic signature facilitated through {{appName}}:",
"term-cond-p24": "Complies with applicable electronic signature laws, including but not limited to the E-SIGN Act in the United States, the EU eIDAS Regulation, and Indias Information Technology Act.",
"term-cond-p24": "Complies with applicable electronic signature laws, including but not limited to the E-SIGN Act in the United States, the EU eIDAS Regulation, and India's Information Technology Act.",
"term-cond-p25": "Is legally binding between You and the Sender for the signed document(s).",
"term-cond-h8": "8. Platform Role and Limitation of Liability",
"term-cond-p26": "{{appName}} serves as a platform to facilitate electronic transactions. It is not responsible for the content, validity, or enforceability of the documents sent by the Sender. Any disputes or issues related to the document or its signing must be resolved directly between You and the Sender.",
@@ -831,7 +836,7 @@
"initial-type": "Your initials",
"redirect-url": "Redirect url",
"bulk-send": "Bulk send",
"select-timezone": "Select your Timezone",
"select-timezone": "Timezone",
"current-time": "Current time",
"email-help": "You are not allowed to change email address due to security reasons. Please create another free account using the new email address.",
"doc-sent": "Document sent successfully.",
@@ -851,10 +856,120 @@
"draft-template-info-p1": "To make your template public, it must either contain a single role, or, if it includes multiple roles, all additional roles must already be assigned to signers. The unassigned public role should remain empty and must be placed in the first position.",
"visit-below-link": "Visit below link to know more -",
"storage-help": "Enabling BYOC lets you connect your own S3 storage so your files remain entirely under your control—no external copies retained. If data autonomy matters to you, consider upgrading to Teams to unlock this feature.",
"daily-quota-reached": "Youve reached your daily quota. For assistance, please contact quotas@opensignlabs.com.",
"daily-quota-reached": "You've reached your daily quota. For assistance, please contact quotas@opensignlabs.com.",
"enabled-signature-type": "Enabled Signature Types",
"enabled-signature-type-help": "The 'Enabled Signature Types' setting determines which signature options are available across your organization. For example, if you disable the 'Draw' option, members of your organization will not see it in the signature widget, while the other three options will remain accessible.",
"indexing-public-profile": "Allow indexing of public profile by search engines",
"user-created-successfully": "user created successfully.",
"only-15-reminder-allowed": "You can set up to 15 automatic reminders. For example, if 'TimeToComplete' is 15 days and 'RemindOnceInEvery' is 1 day, you'll reach the maximum limit of 15 reminders. Adjust your settings accordingly."
}
"only-15-reminder-allowed": "You can set up to 15 automatic reminders. For example, if 'TimeToComplete' is 15 days and 'RemindOnceInEvery' is 1 day, you'll reach the maximum limit of 15 reminders. Adjust your settings accordingly.",
"rate-your-experience": "How was your experience with {{appName}}?",
"thanks-for-feedback": "Thanks for your feedback 🙏",
"share-your-feedback": "Share your feedback",
"share-your-review": "Share your review",
"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?",
"go-to-manage-templates": "go to 'Manage templates'",
"template-created": "Template Created",
"how-would-you-like-to-proceed?": "How would you like to proceed?",
"failed-to-load-refresh-page": "Failed to load the document. Please try refreshing this page.",
"document-has-been-signed": "The document has been signed successfully!",
"document-has-been-signed-by-you": "The document has been successfully signed by you!",
"participant-completed-signing": "All participants have completed the signing process.",
"you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly.",
"please-provide-templateid": "Please provide templateid",
"this-template-is-not-public": "This template is not public",
"invalid-templateid": "Invaldi templateid",
"contact-billing-at-opensign": "To add more seats, please contact OpenSign™ at <1>billing@opensignlabs.com</1> for assistance",
"title-length-alert": "Title must be at most 250 characters long.",
"note-length-alert": "Note must be at most 200 characters long.",
"description-length-alert": "Description must be at most 500 characters long.",
"fix-&-resend-document": "Fix & Resend Document",
"do-you-want-recreate-document?": "This will create a draft from this document with all fields intact. Are you sure you want to recreate this document?",
"start-editing": "Start Editing",
"unsaved-changes-discard-them?": "You have unsaved changes. Discard them?",
"yes-discard": "Yes, Discard",
"LTV-enabled-signatures": "LTV enabled signatures",
"BETA": "BETA",
"two-factor-authentication": "Two-Factor Authentication",
"2fa-help-text": "Two-factor authentication adds an extra layer of security to your account by requiring more than just a password to sign in.",
"2fa-help-bullet1": "Enhances security by requiring both your password and a verification code.",
"2fa-help-bullet2": "The verification code is generated by an authenticator app on your device.",
"2fa-help-bullet3": "Protects your account even if your password is compromised.",
"setup-2fa": "Setup 2FA",
"setup-2fa-again": "Setup 2FA again",
"2fa-setup-intro": "Protect your account with two-factor authentication. When enabled, you'll need to enter a code from your authenticator app whenever you sign in.",
"scan-qr-code": "Scan QR Code",
"scan-qr-instructions": "Use an authenticator app like Google Authenticator, Microsoft Authenticator, or Authy to scan this QR code.",
"manual-setup-instructions": "Can't scan the code? You can manually set up your authenticator app using this secret key:",
"secret-key": "Secret Key",
"copied-to-clipboard": "Copied to clipboard",
"copy-to-clipboard": "Copy to clipboard",
"recovery-codes": "Recovery Codes",
"recovery-codes-instructions": "Save these recovery codes in a secure location. If you lose access to your authenticator app, you can use one of these one-time codes to sign in.",
"download-recovery-codes": "Download Recovery Codes",
"verification-code": "Verification Code",
"enter-code-from-authenticator-app": "Enter the 6-digit code from your authenticator app",
"verification-code-required": "Verification code is required",
"verification-code-invalid": "Invalid verification code. Please try again.",
"2fa-enabled": "2FA enabled",
"2fa-enabled-successfully": "Two-factor authentication enabled successfully",
"2fa-setup-complete": "Setup Complete!",
"2fa-setup-complete-instructions": "Your account is now protected with two-factor authentication. You'll need to enter a verification code each time you sign in.",
"two-factor-verification": "Two-Factor Verification",
"enter-verification-code-instructions": "Enter the 6-digit verification code from your authenticator app to continue.",
"recovery-code": "Recovery Code",
"enter-recovery-code-help": "Enter one of your recovery codes",
"recovery-code-required": "Recovery code is required",
"use-verification-code-instead": "Use verification code instead",
"use-recovery-code-instead": "Use recovery code instead",
"regenerate-2fa-remove-existing": "Are you sure you want to regenerate two-factor authentication? This action will remove your existing authentication settings.",
"use-passkey": "Sign in with passkey",
"security-section": "Security",
"passkey-authentication": "Passkey Authentication",
"passkey-not-supported": "Your browser or device doesn't support passkey authentication",
"passkey-description": "Passkeys provide a stronger, phishing-resistant alternative to passwords. You can use your fingerprint, face recognition, or device PIN to sign in securely.",
"passkey-tooltip": "Passkeys are a simpler and more secure alternative to passwords. They use biometric data like fingerprints or facial recognition that's already stored on your device.",
"security-section-help": "Manage security options including passkeys and authentication methods to protect your account.",
"passkey-register": "Register passkey",
"passkey-register-another": "Register another passkey",
"passkey-registered": "Passkey registered",
"passkey-registering": "Registering passkey...",
"passkey-registered-success": "Passkey successfully registered!",
"passkey-registration-failed": "Failed to register passkey",
"passkey-auth-failed": "Passkey authentication failed",
"passkey-missing-user-info": "User information is required",
"passkeys-list": "Your Passkeys",
"default-passkey": "Your Passkey",
"passkey-rename": "Rename",
"passkey-delete": "Delete",
"passkey-rename-title": "Rename Passkey",
"passkey-delete-title": "Delete Passkey",
"passkey-delete-confirm": "Are you sure you want to delete passkey \"{{name}}\"?",
"passkey-name": "Passkey Name",
"passkey-name-placeholder": "Enter a descriptive name for this passkey",
"passkey-renamed-success": "Passkey renamed successfully",
"passkey-deleted-success": "Passkey deleted successfully",
"passkey-rename-failed": "Failed to rename passkey",
"passkey-delete-failed": "Failed to delete passkey",
"processing": "Processing...",
"today": "Today",
"yesterday": "Yesterday",
"days-ago": "{{days}} days ago",
"verify-with-passkey": "Verify with passkey",
"verify-with-otp": "Verify with OTP",
"verification": "Verification",
"verify-account": "Verify your account",
"verify-identity": "Verify identity",
"passkey-verification-failed": "Passkey verification failed. Please try again or use OTP.",
"security-auth-help": {
"p1":"Manage your account's security settings to keep your data safe. OpenSign supports advanced authentication methods to enhance account protection.",
"2fa-auth-help":" Add an extra layer of security by enabling 2FA. This requires you to enter a verification code from an authenticator app after your password.",
"passkey-auth-help":"Use passkeys for passwordless sign-in with biometric or device-based verification, providing both strong security and convenience."
},
"signer-already-present": "Signer already present",
"kiosk-sign": "Kiosk Sign",
"dont-have-access-to-template": "The template has been deleted or you don't have access. Please contact the sender.",
"kiosk-info": "Kiosk Mode lets you collect in-person signatures quickly and efficiently. Ideal for trade shows, events, or walk-in scenarios where all signers are physically present. ",
"learn-more": "Learn more"
}
@@ -151,7 +151,10 @@
"extend-expiry-date": "Date d'expiration",
"Duplicate Template": "Plantilla duplicada",
"Duplicate": "Duplicada",
"daily-mail-quota": "Cuota diaria de correos electrónicos"
"daily-mail-quota": "Cuota diaria de correos electrónicos",
"Save as template": "Guardar como plantilla",
"Fix & resend": "Corregir y reenviar",
"Kiosk Mode": "Modo Kiosco"
},
"report-heading": {
"Sr.No": "Nº",
@@ -248,7 +251,7 @@
"generate-token-alert": "¿En definitiva quieres regenerar el token? Esto expirará el token antiguo.",
"yes": "Sí",
"copied": "Copiado",
"something-went-wrong-mssg": "Algo salió mal, por favor, intenta de nuevo más tarde.",
"something-went-wrong-mssg": "Un problème est survenu, Actualiser cette page peut résoudre le problème.",
"token-generated": "Token generado exitosamente.",
"webhook": "Webhook",
"update-webhook": "Actualizar webhook",
@@ -284,6 +287,7 @@
"make-template-public": "Convertir la plantilla en pública",
"make-template-private": "Convertir la plantilla en privada",
"make-template-public-alert": "¿En definitiva quieres convertir esta plantilla en pública?",
"make-template-private-alert-non": "¿Está seguro de que desea hacer este plantilla privado?",
"make-template-private-alert": "¿En definitiva quieres convertir esta plantilla en privada? Esto lo removerá de tu perfil público.",
"public-role": "Rol público",
"public-url": "Perfil publico",
@@ -302,14 +306,14 @@
"send": "Enviar",
"quick-send-alert-1": "Todos los roles de este documento están actualmente vinculados a contactos. Para enviar rápidamente copias de esta plantilla a varios firmantes, por favor, asegúrate de que al menos un rol no esté vinculado a ningún contacto.",
"quick-send-alert-2": "Por favor, asegúrate de que hay al menos un widget de firma añadido para cada destinatario.",
"quick-send-alert-3": "Por favor, añade al menos un rol a esta plantilla para poder hacer un «envío rápido» de copias a varios firmantes.",
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
"quick-send-alert-4": "El envío rápido ha alcanzado el límite.",
"copy-link": "Copiar enlace",
"copy": "Copiar",
"revoke-document": "Revocar documento",
"revoke-document-alert": "¿En definitiva quieres revocar este documento?",
"resend-mail": "Reenviar correo",
"resend-mail-help": "Puedes usar las siguientes variables que serán reemplazadas por sus valores reales:- {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}.",
"resend-mail-help": "Puedes usar las siguientes variables que serán reemplazadas por sus valores reales:- {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}, {{note}}.",
"subject": "Asunto",
"body": "Cuerpo",
"add-contact": "Agregar contacto",
@@ -339,7 +343,7 @@
"verify-email-1": "Verificar correo",
"resend": "Reenviar",
"contact-details": "Detalles del contacto",
"verify-email": "¡Por favor, verifica tu correo!",
"verify-email": "Por favor verifique su correo electrónico para continuar. Este es un paso único para confirmar el correo electrónico asociado a su cuenta de OpenSign.",
"send-otp": " Enviar OTP",
"otp-placeholder": "Ingresa el código de verificación enviado por correo",
"loading-doc": "Cargando documento...",
@@ -458,6 +462,7 @@
"placeholder-alert-3": " ¿En definitiva quieres enviar este documento para ser firmado?",
"placeholder-alert-4": "¡Has enviado exitosamente correos a todos los destinatarios!",
"placeholder-mail-alert": "Has enviado un correo electrónico con éxito a {{name}}. Los siguientes firmantes recibirán un correo electrónico una vez que {{name}} firme el documento.",
"placeholder-mail-alert-you": "Los firmantes posteriores recibirán correos electrónicos una vez que firme el documento.",
"placeholder-alert-5": "¿Quieres firmar documentos ahora mismo?",
"placeholder-alert-6": "¡Por favor, configura el adaptador de correo para enviar correos!",
"placeholder-alert-7": "¡Por favor, selecciona un firmante para agregar un marcador de posición!",
@@ -521,7 +526,7 @@
"correct-password": "Por favor, proporciona la contraseña correcta",
"decrypting-pdf": " Desencriptando PDF, por favor, espera...",
"invalid-otp": "OTP inválido",
"user-not-found": "¡Usuario no encontrado!",
"user-not-found": "Usuario no encontrado",
"enter-otp-alert": "¡Por favor, ingresa el OTP!",
"get-verification-code": "Obtener código de verificación",
"get-verification-code-2": "Obtendrás un código de verificación por correo",
@@ -831,7 +836,7 @@
"initial-type": "Tus iniciales",
"redirect-url": "URL de redireccionamiento",
"bulk-send": "envío masivo",
"select-timezone": "Seleccione su zona horaria",
"select-timezone": "Zona horaria",
"current-time": "Hora actual",
"email-help": "No se permite cambiar la dirección de correo electrónico por razones de seguridad. Por favor, cree otra cuenta gratuita utilizando la nueva dirección de correo electrónico.",
"doc-sent": "Documento enviado con éxito.",
@@ -856,5 +861,115 @@
"enabled-signature-type-help": "La configuración de 'Tipos de firma habilitados' determina qué opciones de firma están disponibles en su organización. Por ejemplo, si desactiva la opción 'Dibujar', los miembros de su organización no la verán en el widget de firma, mientras que las otras tres opciones seguirán siendo accesibles.",
"indexing-public-profile": "Permitir la indexación del perfil público por los motores de búsqueda",
"user-created-successfully": "Usuario creado con éxito.",
"only-15-reminder-allowed": "Puede configurar hasta 15 recordatorios automáticos. Por ejemplo, si 'TimeToComplete' es de 15 días y 'RemindOnceInEvery' es de 1 día, alcanzará el límite máximo de 15 recordatorios. Ajuste su configuración en consecuencia."
"only-15-reminder-allowed": "Puede configurar hasta 15 recordatorios automáticos. Por ejemplo, si 'TimeToComplete' es de 15 días y 'RemindOnceInEvery' es de 1 día, alcanzará el límite máximo de 15 recordatorios. Ajuste su configuración en consecuencia.",
"rate-your-experience": "¿Cómo fue su experiencia con {{appName}}?",
"thanks-for-feedback": "Gracias por su comentario 🙏",
"share-your-feedback": "Comparta sus comentarios",
"share-your-review": "Comparta su reseña",
"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?",
"go-to-manage-templates": "Ir a 'Gestionar plantillas'",
"template-created": "Plantilla creada",
"how-would-you-like-to-proceed?": "¿Cómo le gustaría proceder?",
"failed-to-load-refresh-page": "Error al cargar el documento. Intente actualizar esta página.",
"document-has-been-signed": "¡El documento ha sido firmado con éxito!",
"document-has-been-signed-by-you": "¡El documento ha sido firmado con éxito por usted!",
"participant-completed-signing": "Todos los participantes han completado el proceso de firma.",
"you-will-receive-email-shortly": "✅ ¡Eso es todo! Recibirá un correo de confirmación en breve.",
"please-provide-templateid": "Por favor, proporcione templateid",
"this-template-is-not-public": "Esta template no es pública",
"invalid-templateid": "templateid no válida",
"contact-billing-at-opensign": "Para agregar más asientos, comuníquese con OpenSign™ a <1>billing@opensignlabs.com</1> para obtener ayuda.",
"title-length-alert": "El título debe tener como máximo 250 caracteres.",
"note-length-alert": "La nota debe tener como máximo 200 caracteres.",
"description-length-alert": "La descripción debe tener como máximo 500 caracteres.",
"fix-&-resend-document": "Corregir y reenviar el documento",
"do-you-want-recreate-document?": "Esto creará un borrador a partir de este documento con todos los campos intactos. ¿Está seguro de que desea recrear este documento?",
"start-editing": "Comenzar a editar",
"unsaved-changes-discard-them?": "Tienes cambios sin guardar. ¿Deseas descartarlos?",
"yes-discard": "Sí, descartar",
"LTV-enabled-signatures": "Firmas con LTV habilitado",
"BETA": "BETA",
"two-factor-authentication": "Autenticación de dos factores",
"2fa-help-text": "La autenticación de dos factores agrega una capa adicional de seguridad a su cuenta al requerir más que solo una contraseña para iniciar sesión.",
"2fa-help-bullet1": "Mejora la seguridad al requerir tanto su contraseña como un código de verificación.",
"2fa-help-bullet2": "El código de verificación es generado por una aplicación de autenticación en su dispositivo.",
"2fa-help-bullet3": "Protege su cuenta incluso si su contraseña se ve comprometida.",
"setup-2fa": "Configurar 2FA",
"setup-2fa-again": "Configurar 2FA nuevamente",
"2fa-setup-intro": "Proteja su cuenta con la autenticación de dos factores. Cuando esté habilitada, deberá ingresar un código de su aplicación de autenticación cada vez que inicie sesión.",
"scan-qr-code": "Escanear código QR",
"scan-qr-instructions": "Use una aplicación de autenticación como Google Authenticator, Microsoft Authenticator o Authy para escanear este código QR.",
"manual-setup-instructions": "¿No puede escanear el código? Puede configurar manualmente su aplicación de autenticación utilizando esta clave secreta:",
"secret-key": "Clave secreta",
"copied-to-clipboard": "Copiado al portapapeles",
"copy-to-clipboard": "Copiar al portapapeles",
"recovery-codes": "Códigos de recuperación",
"recovery-codes-instructions": "Guarde estos códigos de recuperación en un lugar seguro. Si pierde el acceso a su aplicación de autenticación, puede usar uno de estos códigos de un solo uso para iniciar sesión.",
"download-recovery-codes": "Descargar códigos de recuperación",
"verification-code": "Código de verificación",
"enter-code-from-authenticator-app": "Ingrese el código de 6 dígitos de su aplicación de autenticación",
"verification-code-required": "Se requiere el código de verificación",
"verification-code-invalid": "Código de verificación inválido. Por favor, inténtelo de nuevo.",
"2fa-enabled": "2FA activado",
"2fa-enabled-successfully": "Autenticación de dos factores habilitada con éxito",
"2fa-setup-complete": "¡Configuración completa!",
"2fa-setup-complete-instructions": "Su cuenta ahora está protegida con autenticación de dos factores. Deberá ingresar un código de verificación cada vez que inicie sesión.",
"two-factor-verification": "Verificación de dos factores",
"enter-verification-code-instructions": "Ingrese el código de verificación de 6 dígitos de su aplicación de autenticación para continuar.",
"recovery-code": "Código de recuperación",
"enter-recovery-code-help": "Ingrese uno de sus códigos de recuperación",
"recovery-code-required": "Se requiere el código de recuperación",
"use-verification-code-instead": "Usar código de verificación en su lugar",
"use-recovery-code-instead": "Usar código de recuperación en su lugar",
"regenerate-2fa-remove-existing": "¿Está seguro de que desea regenerar la autenticación de dos factores? Esta acción eliminará su configuración de autenticación actual.",
"use-passkey": "Iniciar sesión con passkey",
"security-section": "Seguridad",
"passkey-authentication": "Autenticación con passkey",
"passkey-not-supported": "Tu navegador o dispositivo no admite la autenticación con passkey",
"passkey-description": "Las passkeys proporcionan una alternativa más segura y resistente al phishing que las contraseñas. Puedes usar tu huella digital, reconocimiento facial o PIN del dispositivo para iniciar sesión de forma segura.",
"passkey-tooltip": "Las passkeys son una alternativa más simple y segura a las contraseñas. Usan datos biométricos como huellas digitales o reconocimiento facial ya almacenados en tu dispositivo.",
"security-section-help": "Administra las opciones de seguridad, incluidas las passkeys y métodos de autenticación para proteger tu cuenta.",
"passkey-register": "Registrar passkey",
"passkey-register-another": "Registrar otra passkey",
"passkey-registered": "Passkey registrada",
"passkey-registering": "Registrando passkey...",
"passkey-registered-success": "¡Passkey registrada con éxito!",
"passkey-registration-failed": "Fallo al registrar la passkey",
"passkey-auth-failed": "Falló la autenticación con passkey",
"passkey-missing-user-info": "Se requiere información del usuario",
"passkeys-list": "Tus passkeys",
"default-passkey": "Tu passkey",
"passkey-rename": "Renombrar",
"passkey-delete": "Eliminar",
"passkey-rename-title": "Renombrar passkey",
"passkey-delete-title": "Eliminar passkey",
"passkey-delete-confirm": "¿Estás seguro de que deseas eliminar la passkey \"{{name}}\"?",
"passkey-name": "Nombre de la passkey",
"passkey-name-placeholder": "Introduce un nombre descriptivo para esta passkey",
"passkey-renamed-success": "Passkey renombrada con éxito",
"passkey-deleted-success": "Passkey eliminada con éxito",
"passkey-rename-failed": "No se pudo renombrar la passkey",
"passkey-delete-failed": "No se pudo eliminar la passkey",
"processing": "Procesando...",
"today": "Hoy",
"yesterday": "Ayer",
"days-ago": "Hace {{days}} días",
"verify-with-passkey": "Verificar con passkey",
"verify-with-otp": "Verificar con OTP",
"verify-identity": "Verificar identidad",
"verify-account": "Verifique su identidad",
"verification": "Verificación",
"passkey-verification-failed": "La verificación con passkey ha fallado",
"security-auth-help": {
"p1": "Administre la configuración de seguridad de su cuenta para mantener sus datos seguros. OpenSign admite métodos de autenticación avanzados para mejorar la protección de la cuenta.",
"2fa-auth-help": "Agregue una capa adicional de seguridad activando 2FA. Esto requiere ingresar un código de verificación desde una aplicación autenticadora después de su contraseña.",
"passkey-auth-help": "Use claves de acceso para iniciar sesión sin contraseña con verificación biométrica o basada en el dispositivo, proporcionando una seguridad sólida y comodidad."
},
"signer-already-present": "Firmante ya presente",
"kiosk-sign": "Firma en quiosco",
"dont-have-access-to-template": "El template ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
"kiosk-info": "El Modo Kiosco le permite recopilar firmas en persona de forma rápida y eficiente. Ideal para ferias, eventos o situaciones con personas que firman en el lugar. ",
"learn-more": "Más información"
}
+125 -10
View File
@@ -172,7 +172,10 @@
"extend-expiry-date": "Prolonger la date d'expiration",
"Duplicate Template": "dupliquer le modèle",
"Duplicate": "Double",
"daily-mail-quota": "Quota d'e-mails quotidien"
"daily-mail-quota": "Quota d'e-mails quotidien",
"Save as template": "Enregistrer comme modèle",
"Fix & resend": "Corriger et renvoyer",
"Kiosk Mode": "Mode Kiosque"
},
"report-help": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -200,7 +203,7 @@
"send-in-order-help": {
"p1": "Choisissez la manière dont vous souhaitez que les demandes de signature soient envoyées aux signataires du document :",
"p2": "La sélection de cette option enverra initialement la demande de signature au premier signataire. Une fois que le premier signataire a terminé sa partie, le prochain signataire de la séquence recevra la demande. Ce processus se poursuit jusqu'à ce que tous les signataires aient signé le document. Cette méthode garantit que le document est signé dans un ordre spécifique.",
"p3": "La sélection de cette option enverra les liens de signature à tous les signataires simultanément. Chaque signataire peut signer le document à sa convenance, que d'autres signataires aient ou non complété leur signature. Cette méthode est plus rapide mais nimpose aucun ordre de signature entre les participants.",
"p3": "La sélection de cette option enverra les liens de signature à tous les signataires simultanément. Chaque signataire peut signer le document à sa convenance, que d'autres signataires aient ou non complété leur signature. Cette méthode est plus rapide mais n'impose aucun ordre de signature entre les participants.",
"p4": "Sélectionnez l'option qui correspond le mieux aux besoins de votre traitement de documents."
},
"no": "Non",
@@ -226,7 +229,7 @@
"create": "Créer",
"signers": "Signataires",
"signers-help": "Commencez à saisir le nom d'un contact pour voir les signataires suggérés par vos contacts enregistrés ou en ajouter de nouveaux. Organisez l'ordre de signature en ajoutant des signataires dans l'ordre souhaité. Utilisez le bouton « + » pour inclure les signataires et le « x » pour les supprimer. Chaque signataire recevra un e-mail invité à signer le document dans l'ordre indiqué.",
"bcc-help": "Commencez à taper le nom dun contact pour voir les suggestions parmi vos contacts enregistrés ou en ajouter de nouveaux. Utilisez le bouton '+' pour ajouter un utilisateur et le bouton 'x' pour le supprimer. Ladresse e-mail de lutilisateur sélectionné sera ajoutée en Bcc (copie carbone invisible). Chaque utilisateur recevra une notification par e-mail une fois le document terminé.",
"bcc-help": "Commencez à taper le nom d'un contact pour voir les suggestions parmi vos contacts enregistrés ou en ajouter de nouveaux. Utilisez le bouton '+' pour ajouter un utilisateur et le bouton 'x' pour le supprimer. L'adresse e-mail de l'utilisateur sélectionné sera ajoutée en Bcc (copie carbone invisible). Chaque utilisateur recevra une notification par e-mail une fois le document terminé.",
"add-signer": "Ajouter un signataire",
"contact-not-found": "Contact introuvable",
"add-yourself": "Ajoutez-vous",
@@ -283,6 +286,7 @@
"make-template-public": "Rendre le modèle public",
"make-template-private": "Rendre le modèle privé",
"make-template-public-alert": "Êtes-vous sûr de vouloir rendre ce modèle public ?",
"make-template-private-alert-non": "Êtes-vous sûr de vouloir rendre ce modèle privé ?",
"make-template-private-alert": "Êtes-vous sûr de vouloir rendre ce modèle privé ? Cela le supprimera de votre profil public.",
"public-role": "Rôle public",
"public-url": "Profil public",
@@ -301,14 +305,14 @@
"send": "envoyer",
"quick-send-alert-1": "Tous les rôles dans ce document sont actuellement liés à des contacts. Pour envoyer rapidement des copies de ce modèle à plusieurs signataires, veuillez vous assurer qu'au moins un rôle n'est lié à aucun contact.",
"quick-send-alert-2": "Veuillez vous assurer qu'au moins un widget de signature est ajouté pour tous les destinataires.",
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce modèle afin d'en « envoyer rapidement » des copies à plusieurs signataires.",
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
"quick-send-alert-4": "L'envoi rapide a atteint la limite.",
"copy-link": "Copier le lien",
"copy": "Copier",
"revoke-document": "Révoquer le document",
"revoke-document-alert": "Êtes-vous sûr de vouloir révoquer ce document ?",
"resend-mail": "Renvoyer le courrier",
"resend-mail-help": "Vous pouvez utiliser les variables suivantes qui seront remplacées par leurs valeurs réelles : - {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email} }, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}.",
"resend-mail-help": "Vous pouvez utiliser les variables suivantes qui seront remplacées par leurs valeurs réelles : - {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email} }, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}, {{note}}.",
"subject": "Sujet",
"body": "Corps",
"add-contact": "Ajouter le contact",
@@ -338,7 +342,7 @@
"verify-email-1": "Vérifier l'e-mail",
"resend": "Renvoyer",
"contact-details": "Coordonnées",
"verify-email": "Veuillez vérifier votre email!",
"verify-email": "Veuillez vérifier votre e-mail pour continuer. C'est une étape unique pour confirmer l'adresse e-mail associée à votre compte OpenSign.",
"send-otp": "envoyer un code à usage unique",
"otp-placeholder": "Entrez le code de vérification reçu par e-mail",
"loading-doc": "Chargement du document..",
@@ -458,6 +462,7 @@
"placeholder-alert-3": "Etes-vous sûr de vouloir envoyer ce document pour signature ? ",
"placeholder-alert-4": "Vous avez envoyé avec succès des mails à tous les",
"placeholder-mail-alert": "Vous avez envoyé un e-mail avec succès à {{name}}. Les signataires suivants recevront un e-mail une fois que {{name}} aura signé le document.",
"placeholder-mail-alert-you": "Les signataires suivants recevront un e-mail dès que vous signez le document.",
"placeholder-alert-5": "Voulez-vous signer des documents maintenant ?",
"placeholder-alert-6": "Veuillez configurer l'adaptateur de messagerie pour envoyer du courrier !",
"placeholder-alert-7": "Veuillez sélectionner le signataire pour ajouter un espace réservé !",
@@ -521,7 +526,7 @@
"correct-password": "Veuillez fournir un mot de passe correct",
"decrypting-pdf": "Décryptage du pdf, veuillez patienter...",
"invalid-otp": "OTP invalide",
"user-not-found": "Utilisateur non trouvé!",
"user-not-found": "Utilisateur non trouvé",
"enter-otp-alert": "Veuillez saisir OTP !",
"get-verification-code": "Obtenir le code de vérification",
"get-verification-code-2": "Vous recevrez un code de vérification par e-mail",
@@ -657,7 +662,7 @@
"bulk-send-subcription-alert": "Veuillez passer au forfait Professionnel ou Équipe pour utiliser Quicksend.",
"generate-test-token": "Générer jeton de test",
"regenerate-test-token": "Régénérer le jeton de test",
"help-test-token": "Ce jeton peut être utilisé pour tester les API au niveau du point de terminaison https://sandbox.opensignlabs.com/api/v1, vous permettant ainsi d'effectuer un nombre illimité de signatures de documents. Veuillez noter que l'API sandbox signera vos documents avec des certificats auto-signés, qui peuvent ne pas être reconnus comme valides par Adobe. Une fois vos tests terminés, vous pouvez passer à lun de nos forfaits payants pour générer un jeton de production.",
"help-test-token": "Ce jeton peut être utilisé pour tester les API au niveau du point de terminaison https://sandbox.opensignlabs.com/api/v1, vous permettant ainsi d'effectuer un nombre illimité de signatures de documents. Veuillez noter que l'API sandbox signera vos documents avec des certificats auto-signés, qui peuvent ne pas être reconnus comme valides par Adobe. Une fois vos tests terminés, vous pouvez passer à l'un de nos forfaits payants pour générer un jeton de production.",
"help-api-token": "Ce jeton peut être utilisé pour accéder aux API de production au point de terminaison {{origin}}/api/v1. Il ne peut être généré que sur l'un de nos forfaits payants.",
"reason": "Raison",
"decline-by": "Refusé/révoqué par",
@@ -831,7 +836,7 @@
"initial-type": "Vos initiales",
"redirect-url": "URL de redirection",
"bulk-send": "Envoi groupé",
"select-timezone": "Sélectionnez votre fuseau horaire",
"select-timezone": "Fuseau horaire",
"current-time": "Heure actuelle",
"email-help": "Vous n'êtes pas autorisé à changer d'adresse e-mail pour des raisons de sécurité. Veuillez créer un autre compte gratuit en utilisant la nouvelle adresse e-mail.",
"doc-sent": "Document envoyé avec succès.",
@@ -856,5 +861,115 @@
"enabled-signature-type-help": "Le paramètre 'Types de signature activés' détermine quelles options de signature sont disponibles dans votre organisation. Par exemple, si vous désactivez l'option 'Dessiner', les membres de votre organisation ne la verront pas dans le widget de signature, tandis que les trois autres options resteront accessibles.",
"indexing-public-profile": "Autoriser l'indexation du profil public par les moteurs de recherche",
"user-created-successfully": "Utilisateur créé avec succès.",
"only-15-reminder-allowed": "Vous pouvez définir jusqu'à 15 rappels automatiques. Par exemple, si 'TimeToComplete' est de 15 jours et 'RemindOnceInEvery' est de 1 jour, vous atteindrez la limite maximale de 15 rappels. Ajustez vos paramètres en conséquence."
"only-15-reminder-allowed": "Vous pouvez définir jusqu'à 15 rappels automatiques. Par exemple, si 'TimeToComplete' est de 15 jours et 'RemindOnceInEvery' est de 1 jour, vous atteindrez la limite maximale de 15 rappels. Ajustez vos paramètres en conséquence.",
"rate-your-experience": "Comment s'est passée votre expérience avec {{appName}} ?",
"thanks-for-feedback": "Merci pour votre retour 🙏",
"share-your-feedback": "Partagez votre avis",
"share-your-review": "Partagez votre avis",
"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 ?",
"go-to-manage-templates": "Aller à 'Gérer les modèles'",
"template-created": "Modèle créé",
"how-would-you-like-to-proceed?": "Comment souhaitez-vous procéder ?",
"failed-to-load-refresh-page": "Échec du chargement du document. Veuillez essayer d'actualiser cette page.",
"document-has-been-signed": "Le document a été signé avec succès !",
"document-has-been-signed-by-you": "Le document a été signé avec succès par vous !",
"participant-completed-signing": "Tous les participants ont terminé le processus de signature.",
"you-will-receive-email-shortly": "✅ Voilà, c'est fait ! Vous recevrez un e-mail de confirmation sous peu.",
"please-provide-templateid": "Veuillez fournir templateid",
"this-template-is-not-public": "Ce template n'est pas public",
"invalid-templateid": "templateid invalide",
"contact-billing-at-opensign": "Pour ajouter plus de places, veuillez contacter OpenSign™ à l'adresse <1>billing@opensignlabs.com</1> pour obtenir de l'aide.",
"title-length-alert": "Le titre doit comporter au maximum 250 caractères.",
"note-length-alert": "La note doit comporter au maximum 200 caractères.",
"description-length-alert": "La description doit comporter au maximum 500 caractères",
"fix-&-resend-document": "Corriger et renvoyer le document",
"do-you-want-recreate-document?": "Cela créera un brouillon à partir de ce document avec tous les champs intacts. Êtes-vous sûr de vouloir recréer ce document ?",
"start-editing": "Commencer l'édition",
"unsaved-changes-discard-them?": "Vous avez des modifications non enregistrées. Les supprimer ?",
"yes-discard": "Oui, supprimer",
"LTV-enabled-signatures": "Signatures avec LTV activée",
"BETA": "BETA",
"two-factor-authentication": "Authentification à deux facteurs",
"2fa-help-text": "L'authentification à deux facteurs ajoute une couche de sécurité supplémentaire à votre compte en exigeant plus qu'un simple mot de passe pour se connecter.",
"2fa-help-bullet1": "Renforce la sécurité en demandant à la fois votre mot de passe et un code de vérification.",
"2fa-help-bullet2": "Le code de vérification est généré par une application d'authentification sur votre appareil.",
"2fa-help-bullet3": "Protège votre compte même si votre mot de passe est compromis.",
"setup-2fa": "Configurer 2FA",
"setup-2fa-again": "Reconfigurer 2FA",
"2fa-setup-intro": "Protégez votre compte avec l'authentification à deux facteurs. Une fois activée, vous devrez saisir un code provenant de votre application d'authentification à chaque connexion.",
"scan-qr-code": "Scanner le code QR",
"scan-qr-instructions": "Utilisez une application d'authentification comme Google Authenticator, Microsoft Authenticator ou Authy pour scanner ce code QR.",
"manual-setup-instructions": "Vous ne pouvez pas scanner le code ? Configurez manuellement votre application d'authentification avec cette clé secrète :",
"secret-key": "Clé secrète",
"copied-to-clipboard": "Copié dans le presse-papiers",
"copy-to-clipboard": "Copier dans le presse-papiers",
"recovery-codes": "Codes de récupération",
"recovery-codes-instructions": "Enregistrez ces codes de récupération dans un endroit sûr. Si vous perdez l'accès à votre application d'authentification, vous pourrez utiliser l'un de ces codes à usage unique pour vous connecter.",
"download-recovery-codes": "Télécharger les codes de récupération",
"verification-code": "Code de vérification",
"enter-code-from-authenticator-app": "Saisissez le code à 6 chiffres de votre application d'authentification",
"verification-code-required": "Le code de vérification est requis",
"verification-code-invalid": "Code de vérification invalide. Veuillez réessayer.",
"2fa-enabled": "2FA activé",
"2fa-enabled-successfully": "Authentification à deux facteurs activée avec succès",
"2fa-setup-complete": "Configuration terminée !",
"2fa-setup-complete-instructions": "Votre compte est maintenant protégé par l'authentification à deux facteurs. Vous devrez entrer un code de vérification à chaque connexion.",
"two-factor-verification": "Vérification en deux étapes",
"enter-verification-code-instructions": "Saisissez le code de vérification à 6 chiffres de votre application d'authentification pour continuer.",
"recovery-code": "Code de récupération",
"enter-recovery-code-help": "Saisissez l'un de vos codes de récupération",
"recovery-code-required": "Le code de récupération est requis",
"use-verification-code-instead": "Utiliser le code de vérification à la place",
"use-recovery-code-instead": "Utiliser le code de récupération à la place",
"regenerate-2fa-remove-existing": "Êtes-vous sûr de vouloir régénérer l'authentification à deux facteurs ? Cette action supprimera vos paramètres d'authentification existants.",
"use-passkey": "Se connecter avec une clé d'accès",
"security-section": "Sécurité",
"passkey-authentication": "Authentification par clé d'accès",
"passkey-not-supported": "Votre navigateur ou appareil ne prend pas en charge l'authentification par clé d'accès",
"passkey-description": "Les clés d'accès offrent une alternative plus sécurisée et résistante au phishing que les mots de passe. Vous pouvez utiliser votre empreinte digitale, reconnaissance faciale ou code PIN de l'appareil pour vous connecter en toute sécurité.",
"passkey-tooltip": "Les clés d'accès sont une alternative plus simple et plus sécurisée aux mots de passe. Elles utilisent des données biométriques comme les empreintes digitales ou la reconnaissance faciale déjà stockées sur votre appareil.",
"security-section-help": "Gérez les options de sécurité, y compris les clés d'accès et les méthodes d'authentification pour protéger votre compte.",
"passkey-register": "Enregistrer une clé d'accès",
"passkey-register-another": "Enregistrer une autre clé d'accès",
"passkey-registered": "Clé d'accès enregistrée",
"passkey-registering": "Enregistrement de la clé d'accès...",
"passkey-registered-success": "Clé d'accès enregistrée avec succès !",
"passkey-registration-failed": "Échec de l'enregistrement de la clé d'accès",
"passkey-auth-failed": "Échec de l'authentification avec la clé d'accès",
"passkey-missing-user-info": "Informations utilisateur requises",
"passkeys-list": "Vos clés d'accès",
"default-passkey": "Votre clé d'accès",
"passkey-rename": "Renommer",
"passkey-delete": "Supprimer",
"passkey-rename-title": "Renommer la clé d'accès",
"passkey-delete-title": "Supprimer la clé d'accès",
"passkey-delete-confirm": "Êtes-vous sûr de vouloir supprimer la clé d'accès « {{name}} » ?",
"passkey-name": "Nom de la clé d'accès",
"passkey-name-placeholder": "Entrez un nom descriptif pour cette clé d'accès",
"passkey-renamed-success": "Clé d'accès renommée avec succès",
"passkey-deleted-success": "Clé d'accès supprimée avec succès",
"passkey-rename-failed": "Échec du renommage de la clé d'accès",
"passkey-delete-failed": "Échec de la suppression de la clé d'accès",
"processing": "Traitement en cours...",
"today": "Aujourd'hui",
"yesterday": "Hier",
"days-ago": "Il y a {{days}} jours",
"verify-with-passkey": "Vérifier avec une passkey",
"verify-with-otp": "Vérifier avec OTP",
"verify-identity": "Vérifier l'identité",
"verify-account": "Vérifiez votre identité",
"verification": "Vérification",
"passkey-verification-failed": "La vérification par passkey a échoué",
"security-auth-help": {
"p1": "Gérez les paramètres de sécurité de votre compte pour protéger vos données. OpenSign prend en charge des méthodes d'authentification avancées pour renforcer la protection de votre compte.",
"2fa-auth-help": "Ajoutez une couche de sécurité supplémentaire en activant 2FA. Cela vous demandera de saisir un code de vérification provenant dune application dauthentification après votre mot de passe.",
"passkey-auth-help": "Utilisez des passkeys pour une connexion sans mot de passe grâce à une vérification biométrique ou basée sur lappareil, offrant à la fois une sécurité renforcée et une grande commodité."
},
"signer-already-present": "Signataire déjà présent",
"kiosk-sign": "Signature sur kiosque",
"dont-have-access-to-template": "Le template a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.",
"kiosk-info": "Le Mode Kiosque vous permet de recueillir des signatures en personne rapidement et efficacement. Idéal pour les salons, événements ou situations où tous les signataires sont physiquement présents. ",
"learn-more": "En savoir plus"
}
+126 -11
View File
@@ -151,7 +151,10 @@
"extend-expiry-date": "Estendi data di scadenza",
"Duplicate Template": "Duplica modello",
"Duplicate": "Duplica",
"daily-mail-quota": "Quota e-mail giornaliera"
"daily-mail-quota": "Quota e-mail giornaliera",
"Save as template": "Salva come modello",
"Fix & resend": "Correggi e reinvia",
"Kiosk Mode": "Modalità Kiosk"
},
"report-heading": {
"Sr.No": "Nr.",
@@ -247,7 +250,7 @@
"generate-token-alert": "Sei sicuro di voler rigenerare il token? Questo invaliderà il vecchio token.",
"yes": "Sì",
"copied": "Copiato",
"something-went-wrong-mssg": "Qualcosa è andato storto, riprova più tardi.",
"something-went-wrong-mssg": "Si è verificato un errore, L'aggiornamento della pagina potrebbe risolvere il problema.",
"token-generated": "Token generato con successo.",
"webhook": "Webhook",
"update-webhook": "Aggiorna Webhook",
@@ -283,6 +286,7 @@
"make-template-public": "Rendi il modello pubblico",
"make-template-private": "Rendi il modello privato",
"make-template-public-alert": "Sei sicuro di voler rendere pubblico questo modello?",
"make-template-private-alert-non": "Sei sicuro di voler rendere privato questo modello?",
"make-template-private-alert": "Sei sicuro di voler rendere privato questo modello? Questo lo rimuoverà dal tuo profilo pubblico.",
"public-role": "Ruolo pubblico",
"public-url": "Profilo pubblico",
@@ -301,19 +305,19 @@
"send": "Invia",
"quick-send-alert-1": "Tutti i ruoli in questo documento sono attualmente collegati a contatti. Per inviare rapidamente copie di questo modello a più firmatari, assicurati che almeno un ruolo non sia collegato a nessun contatto.",
"quick-send-alert-2": "Assicurati che ci sia almeno un widget firma aggiunto per tutti i destinatari.",
"quick-send-alert-3": "Aggiungi almeno un ruolo a questo modello per 'invio rapido' a più firmatari.",
"quick-send-alert-3": "Si prega di aggiungere almeno un ruolo a questo template.",
"quick-send-alert-4": "Limite di invio rapido raggiunto.",
"copy-link": "Copia link",
"copy": "Copia",
"revoke-document": "Revoca documento",
"revoke-document-alert": "Sei sicuro di voler revocare questo documento?",
"resend-mail": "Reinvia email",
"resend-mail-help": "Puoi usare le seguenti variabili che verranno sostituite con i loro valori effettivi: {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}.",
"resend-mail-help": "Puoi usare le seguenti variabili che verranno sostituite con i loro valori effettivi: {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}, {{note}}.",
"subject": "Oggetto",
"body": "Corpo del messaggio",
"add-contact": "Aggiungi contatto",
"edit-contact": "Modifica contatto",
"add-signer-alert": "Il contatto esiste già! Selezionalo dal menu a tendina Firmatari.",
"add-signer-alert": "Il contatto esiste già! Selezionalo dal menu a tendina 'Firmatari'.",
"record-delete-alert": "Record eliminato con successo!",
"record-revoke-alert": "Record revocato con successo!",
"mail-sent-alert": "Email inviata con successo.",
@@ -338,7 +342,7 @@
"verify-email-1": "Verifica email",
"resend": "Reinvia",
"contact-details": "Dettagli contatto",
"verify-email": "Verifica la tua email!",
"verify-email": "Si prega di verificare l'e-mail per continuare. Questo è un passaggio unico per confermare l'e-mail associata al tuo account OpenSign.",
"send-otp": "Invia OTP",
"otp-placeholder": "Inserisci il codice di verifica ricevuto via email",
"loading-doc": "Caricamento del documento...",
@@ -458,6 +462,7 @@
"placeholder-alert-3": "Sei sicuro di voler inviare questo documento per le firme?",
"placeholder-alert-4": "Hai inviato con successo le mail a tutti i destinatari!",
"placeholder-mail-alert": "Hai inviato con successo un'e-mail a {{name}}. I firmatari successivi riceveranno un'e-mail una volta che {{name}} avrà firmato il documento.",
"placeholder-mail-alert-you": "I firmatari successivi riceveranno un'email non appena firmi il documento.",
"placeholder-alert-5": "Vuoi firmare i documenti ora?",
"placeholder-alert-6": "Configura l'adattatore email per inviare mail!",
"placeholder-alert-7": "Seleziona il firmatario per aggiungere un segnaposto!",
@@ -521,7 +526,7 @@
"correct-password": "Fornisci la password corretta",
"decrypting-pdf": "Decrittazione PDF, attendi...",
"invalid-otp": "OTP non valido",
"user-not-found": "Utente non trovato!",
"user-not-found": "Utente non trovato",
"enter-otp-alert": "Inserisci l'OTP!",
"get-verification-code": "Ottieni codice di verifica",
"get-verification-code-2": "Riceverai un codice di verifica tramite Email",
@@ -606,7 +611,7 @@
"placeholder-sign-4": "Trascina o fai clic su un campo per aggiungerlo al documento.",
"placeholder-sign-5": "L'area del contenuto PDF visualizza già i segnaposti esistenti del modello. Per tua comodità, questi segnaposti corrisponderanno al colore del nome del destinatario, rendendoli facilmente identificabili.",
"placeholder-sign-6": "Facendo clic su 'Invia' il documento verrà salvato. Nel passaggio successivo potrai personalizzare le email da inviare ai destinatari o copiare i link di firma e condividerli direttamente con i destinatari.",
"report-1": "Fai clic sul pulsante 'Aggiungi' per creare un nuovo modello. I modelli sono documenti riutilizzabili progettati per generare rapidamente nuovi documenti con la stessa struttura e firmatari diversi. Ad esempio, un modello HR per l'onboarding potrebbe avere ruoli predefinita come Responsabile HR e Nuovo Dipendente. Ogni volta che usi il modello, puoi assegnare il ruolo Nuovo Dipendente a membri dello staff in arrivo, mentre il ruolo Responsabile HR rimane costante, facilitando un processo di onboarding fluido per ogni nuovo assunto.",
"report-1": "Fai clic sul pulsante 'Aggiungi' per creare un nuovo modello. I modelli sono documenti riutilizzabili progettati per generare rapidamente nuovi documenti con la stessa struttura e firmatari diversi. Ad esempio, un modello HR per l'onboarding potrebbe avere ruoli predefinita come 'Responsabile HR' e 'Nuovo Dipendente'. Ogni volta che usi il modello, puoi assegnare il ruolo 'Nuovo Dipendente' a membri dello staff in arrivo, mentre il ruolo 'Responsabile HR' rimane costante, facilitando un processo di onboarding fluido per ogni nuovo assunto.",
"redirect": "Fai clic sul pulsante 'Usa' per creare un nuovo documento da un modello esistente.",
"bulksend": "Per inviare rapidamente più documenti utilizzando un modello esistente creando semplicemente gli indirizzi e-mail dei destinatari, fai clic sul pulsante 'Invio Multiplo'.",
"option": "Questo menu rivela altre opzioni come Modifica ed Elimina. Usa il pulsante 'Modifica' per aggiungere ruoli di firmatari, modificare i campi e aggiornare il modello. Le modifiche si applicheranno a tutti i futuri documenti creati da questo modello ma non influiranno sui documenti esistenti. Usa il pulsante Elimina per eliminare il modello.",
@@ -623,7 +628,7 @@
"Recent signature requests": "Questo è un elenco di documenti che aspettano la tua firma.",
"Recently sent for signatures": "Questo è un elenco di documenti che hai inviato ad altre parti per la firma.",
"Drafts": "Questi sono documenti che hai iniziato ma non hai finalizzato per l'invio.",
"public-template": "Questo video dimostra come configurare il tuo profilo pubblico personalizzato, come https://opensign.me/tuo-username. Imparerai anche come personalizzare il tuo slogan e rendere i tuoi modelli disponibili per la firma pubblica.",
"public-template": "Questo video dimostra come configurare il tuo profilo pubblico personalizzato, come 'https://opensign.me/tuo-username'. Imparerai anche come personalizzare il tuo slogan e rendere i tuoi modelli disponibili per la firma pubblica.",
"allowModify-widgets": "È possibile trascinare e rilasciare uno qualsiasi di questi campi nel documento, oltre ai campi già designati dal creatore del documento."
},
"enter-email-plaholder": "Aggiungi un indirizzo email e premi invio",
@@ -831,7 +836,7 @@
"initial-type": "Le tue iniziali",
"redirect-url": "URL di reindirizzamento",
"bulk-send": "Invio in blocco",
"select-timezone": "Seleziona il tuo fuso orario",
"select-timezone": "Fuso orario",
"current-time": "Ora corrente",
"email-help": "Non ti è permesso cambiare l'indirizzo email per motivi di sicurezza. Ti preghiamo di creare un altro account gratuito utilizzando il nuovo indirizzo email.",
"doc-sent": "Documento inviato con successo.",
@@ -856,5 +861,115 @@
"enabled-signature-type-help": "L'impostazione 'Tipi di firma abilitati' determina quali opzioni di firma sono disponibili nella tua organizzazione. Ad esempio, se disabiliti l'opzione 'Disegna', i membri della tua organizzazione non la vedranno nel widget della firma, mentre le altre tre opzioni resteranno accessibili.",
"indexing-public-profile": "Consenti l'indicizzazione del profilo pubblico dai motori di ricerca",
"user-created-successfully": "Utente creato con successo.",
"only-15-reminder-allowed": "Puoi impostare fino a 15 promemoria automatici. Ad esempio, se 'TimeToComplete' è di 15 giorni e 'RemindOnceInEvery' è di 1 giorno, raggiungerai il limite massimo di 15 promemoria. Regola le tue impostazioni di conseguenza."
"only-15-reminder-allowed": "Puoi impostare fino a 15 promemoria automatici. Ad esempio, se 'TimeToComplete' è di 15 giorni e 'RemindOnceInEvery' è di 1 giorno, raggiungerai il limite massimo di 15 promemoria. Regola le tue impostazioni di conseguenza.",
"rate-your-experience": "Com'è stata la sua esperienza con {{appName}}?",
"thanks-for-feedback": "Grazie per il tuo feedback 🙏",
"share-your-feedback": "Condividi il tuo feedback",
"share-your-review": "Condividi la tua recensione",
"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?",
"go-to-manage-templates": "Vai a 'Gestisci modelli'",
"template-created": "Modello creato",
"how-would-you-like-to-proceed?": "Come desideri procedere?",
"failed-to-load-refresh-page": "Impossibile caricare il documento. Prova ad aggiornare questa pagina.",
"document-has-been-signed": "Il documento è stato firmato con successo!",
"document-has-been-signed-by-you": "Il documento è stato firmato con successo da lei!",
"participant-completed-signing": "Tutti i partecipanti hanno completato il processo di firma.",
"you-will-receive-email-shortly": "✅ È tutto! Riceverà a breve un'e-mail di conferma.",
"please-provide-templateid": "Si prega di fornire templateid",
"this-template-is-not-public": "Questo template non è pubblico",
"invalid-templateid": "templateid non valido",
"contact-billing-at-opensign": " Per aggiungere altri posti, contattare OpenSign™ all'indirizzo <1>billing@opensignlabs.com</1> per assistenza.",
"title-length-alert": "Il titolo può contenere al massimo 250 caratteri.",
"note-length-alert": "La nota può contenere al massimo 200 caratteri.",
"description-length-alert": " La descrizione può contenere al massimo 500 caratteri.",
"fix-&-resend-document": "Correggi e reinvia il documento",
"do-you-want-recreate-document?": "Questo creerà una bozza da questo documento con tutti i campi intatti. Sei sicuro di voler ricreare questo documento?",
"start-editing": "Inizia a modificare",
"unsaved-changes-discard-them?": "Hai modifiche non salvate. Vuoi scartarle?",
"yes-discard": "Sì, scarta",
"LTV-enabled-signatures": "Firme con LTV abilitato",
"BETA": "BETA",
"two-factor-authentication": "Autenticazione a due fattori",
"2fa-help-text": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account richiedendo più di una semplice password per accedere.",
"2fa-help-bullet1": "Aumenta la sicurezza richiedendo sia la password che un codice di verifica.",
"2fa-help-bullet2": "Il codice di verifica viene generato da un'app di autenticazione sul tuo dispositivo.",
"2fa-help-bullet3": "Protegge il tuo account anche se la password è stata compromessa.",
"setup-2fa": "Configura 2FA",
"setup-2fa-again": "Configura nuovamente 2FA",
"2fa-setup-intro": "Proteggi il tuo account con l'autenticazione a due fattori. Quando è attiva, dovrai inserire un codice dall'app di autenticazione ogni volta che accedi.",
"scan-qr-code": "Scansiona il codice QR",
"scan-qr-instructions": "Usa un'app di autenticazione come Google Authenticator, Microsoft Authenticator o Authy per scansionare questo codice QR.",
"manual-setup-instructions": "Non riesci a scansionare il codice? Puoi configurare manualmente l'app usando questa chiave segreta:",
"secret-key": "Chiave segreta",
"copied-to-clipboard": "Copiato negli appunti",
"copy-to-clipboard": "Copia negli appunti",
"recovery-codes": "Codici di recupero",
"recovery-codes-instructions": "Salva questi codici di recupero in un luogo sicuro. Se perdi l'accesso all'app di autenticazione, puoi usarne uno per accedere.",
"download-recovery-codes": "Scarica i codici di recupero",
"verification-code": "Codice di verifica",
"enter-code-from-authenticator-app": "Inserisci il codice a 6 cifre dalla tua app di autenticazione",
"verification-code-required": "È richiesto il codice di verifica",
"verification-code-invalid": "Codice di verifica non valido. Riprova.",
"2fa-enabled": "2FA abilitato",
"2fa-enabled-successfully": "Autenticazione a due fattori attivata con successo",
"2fa-setup-complete": "Configurazione completata!",
"2fa-setup-complete-instructions": "Il tuo account è ora protetto da autenticazione a due fattori. Dovrai inserire un codice ogni volta che accedi.",
"two-factor-verification": "Verifica a due fattori",
"enter-verification-code-instructions": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione per continuare.",
"recovery-code": "Codice di recupero",
"enter-recovery-code-help": "Inserisci uno dei tuoi codici di recupero",
"recovery-code-required": "È richiesto il codice di recupero",
"use-verification-code-instead": "Usa il codice di verifica invece",
"use-recovery-code-instead": "Usa il codice di recupero invece",
"regenerate-2fa-remove-existing": "Sei sicuro di voler rigenerare l'autenticazione a due fattori? Questa azione rimuoverà le impostazioni di autenticazione esistenti.",
"use-passkey": "Accedi con passkey",
"security-section": "Sicurezza",
"passkey-authentication": "Autenticazione passkey",
"passkey-not-supported": "Il tuo browser o dispositivo non supporta l'autenticazione passkey",
"passkey-description": "Le passkey offrono unalternativa più sicura e resistente al phishing rispetto alle password. Puoi usare limpronta digitale, il riconoscimento facciale o il PIN del dispositivo per accedere in sicurezza.",
"passkey-tooltip": "Le passkey sono unalternativa più semplice e sicura alle password. Usano dati biometrici come impronte digitali o riconoscimento facciale già presenti sul tuo dispositivo.",
"security-section-help": "Gestisci le opzioni di sicurezza, incluse passkey e metodi di autenticazione, per proteggere il tuo account.",
"passkey-register": "Registra passkey",
"passkey-register-another": "Registra un'altra passkey",
"passkey-registered": "Passkey registrata",
"passkey-registering": "Registrazione passkey in corso...",
"passkey-registered-success": "Passkey registrata con successo!",
"passkey-registration-failed": "Registrazione della passkey non riuscita",
"passkey-auth-failed": "Autenticazione passkey fallita",
"passkey-missing-user-info": "Informazioni utente richieste",
"passkeys-list": "Le tue passkey",
"default-passkey": "La tua passkey",
"passkey-rename": "Rinomina",
"passkey-delete": "Elimina",
"passkey-rename-title": "Rinomina passkey",
"passkey-delete-title": "Elimina passkey",
"passkey-delete-confirm": "Sei sicuro di voler eliminare la passkey \"{{name}}\"?",
"passkey-name": "Nome della passkey",
"passkey-name-placeholder": "Inserisci un nome descrittivo per questa passkey",
"passkey-renamed-success": "Passkey rinominata con successo",
"passkey-deleted-success": "Passkey eliminata con successo",
"passkey-rename-failed": "Impossibile rinominare la passkey",
"passkey-delete-failed": "Impossibile eliminare la passkey",
"processing": "Elaborazione...",
"today": "Oggi",
"yesterday": "Ieri",
"days-ago": "{{days}} giorni fa",
"verify-with-passkey": "Verifica con passkey",
"verify-with-otp": "Verifica con OTP",
"verify-identity": "Verifica identità",
"verify-account": "Verifica la tua identità",
"verification": "Verifica",
"passkey-verification-failed": "Verifica con passkey fallita",
"security-auth-help": {
"p1": "Gestisci le impostazioni di sicurezza del tuo account per proteggere i tuoi dati. OpenSign supporta metodi di autenticazione avanzati per migliorare la protezione dellaccount.",
"2fa-auth-help": "Aggiungi un ulteriore livello di sicurezza abilitando 2FA. Ti verrà richiesto di inserire un codice di verifica da un'app di autenticazione dopo la password.",
"passkey-auth-help": "Usa le passkey per accedere senza password con verifica biometrica o basata sul dispositivo, garantendo sicurezza elevata e praticità."
},
"signer-already-present": "Firmatario già presente",
"kiosk-sign": "Firma su chiosco",
"dont-have-access-to-template": "Il template è stato eliminato o non hai accesso. Si prega di contattare il mittente.",
"kiosk-info": "La Modalità Kiosk consente di raccogliere firme in presenza in modo rapido ed efficiente. Ideale per fiere, eventi o situazioni con firmatari fisicamente presenti. ",
"learn-more": "Scopri di più"
}
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
@@ -1,7 +1,6 @@
import React, { useState, useEffect, lazy } from "react";
import { Routes, Route, BrowserRouter } from "react-router";
import { pdfjs } from "react-pdf";
import Login from "./pages/Login";
import Form from "./pages/Form";
import Report from "./pages/Report";
import Dashboard from "./pages/Dashboard";
@@ -30,6 +29,7 @@ const ManageSign = lazy(() => import("./pages/Managesign"));
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
const Preferences = lazy(() => import("./pages/Preferences"));
const Login = lazy(() => import("./pages/Login"));
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
const AppLoader = () => {
@@ -67,7 +67,7 @@ function App() {
<BrowserRouter>
<Routes>
<Route element={<ValidateRoute />}>
<Route exact path="/" element={<Login />} />
<Route exact path="/" element={<LazyPage Page={Login} />} />
<Route
path="/addadmin"
element={<LazyPage Page={AddAdmin} />}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@@ -24,7 +24,7 @@ const AddSigner = (props) => {
if (savedUserDetails && addYourself) {
setName(savedUserDetails.name);
setPhone(savedUserDetails?.phone || "");
setEmail(savedUserDetails.email);
setEmail(savedUserDetails.email?.toLowerCase()?.replace(/\s/g, ""));
}
}, [addYourself]);
-385
View File
@@ -1,385 +0,0 @@
import React, { useEffect, useState } from "react";
import Parse from "parse";
import Title from "./Title";
import Loader from "../primitives/Loader";
import { copytoData, usertimezone } from "../constant/Utils";
import { emailRegex } from "../constant/const";
import { useTranslation } from "react-i18next";
function generatePassword(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const AddUser = (props) => {
const { t } = useTranslation();
const [formdata, setFormdata] = useState({
name: "",
phone: "",
email: "",
team: "",
password: "",
role: ""
});
const [isFormLoader, setIsFormLoader] = useState(false);
const [teamList, setTeamList] = useState([]);
const role = ["OrgAdmin", "Editor", "User"];
useEffect(() => {
getTeamList();
// eslint-disable-next-line
}, []);
const getTeamList = async () => {
setFormdata((prev) => ({ ...prev, password: generatePassword(12) }));
const teamRes = await Parse.Cloud.run("getteams", { active: true });
if (teamRes.length > 0) {
const _teamRes = JSON.parse(JSON.stringify(teamRes));
setTeamList(_teamRes);
const allUserId =
_teamRes.find((x) => x.Name === "All Users")?.objectId || "";
setFormdata((prev) => ({ ...prev, team: allUserId }));
}
};
const checkUserExist = async () => {
try {
const res = await Parse.Cloud.run("getUserDetails", {
email: formdata.email
});
if (res) {
return true;
} else {
return false;
}
} catch (err) {
console.log("err", err);
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!emailRegex.test(formdata.email)) {
alert("Please enter a valid email address.");
} else {
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
setIsFormLoader(true);
const res = await checkUserExist();
if (res) {
props.showAlert("danger", t("user-already-exist"));
setIsFormLoader(false);
} else {
if (localStorage.getItem("TenantId")) {
try {
const extUser = new Parse.Object("contracts_Users");
extUser.set("Name", formdata.name);
if (formdata.phone) {
extUser.set("Phone", formdata.phone);
}
extUser.set("Email", formdata.email);
extUser.set("UserRole", `contracts_${formdata.role}`);
if (formdata?.team) {
extUser.set("TeamIds", [
{
__type: "Pointer",
className: "contracts_Teams",
objectId: formdata.team
}
]);
}
if (localUser && localUser.OrganizationId) {
extUser.set("OrganizationId", {
__type: "Pointer",
className: "contracts_Organizations",
objectId: localUser.OrganizationId.objectId
});
}
if (localUser && localUser.Company) {
extUser.set("Company", localUser.Company);
}
if (localStorage.getItem("TenantId")) {
extUser.set("TenantId", {
__type: "Pointer",
className: "partners_Tenant",
objectId: localStorage.getItem("TenantId")
});
}
const timezone = usertimezone;
if (timezone) {
extUser.set("Timezone", timezone);
}
try {
const _users = Parse.Object.extend("User");
const _user = new _users();
_user.set("name", formdata.name);
_user.set("username", formdata.email);
_user.set("email", formdata.email);
_user.set("password", formdata.password);
if (formdata.phone) {
_user.set("phone", formdata.phone);
}
const user = await _user.save();
if (user) {
const currentUser = Parse.User.current();
extUser.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
extUser.set("UserId", user);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
extUser.setACL(acl);
const res = await extUser.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find(
(x) => x.objectId === formdata.team
);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.showAlert("success", t("user-created-successfully"));
}
} catch (err) {
console.log("err ", err);
if (err.code === 202) {
const params = { email: formdata.email };
const userRes = await Parse.Cloud.run("getUserId", params);
const currentUser = Parse.User.current();
extUser.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
extUser.set("UserId", {
__type: "Pointer",
className: "_User",
objectId: userRes.id
});
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
extUser.setACL(acl);
const res = await extUser.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find(
(x) => x.objectId === formdata.team
);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.showAlert("success", t("user-created-successfully"));
} else {
setIsFormLoader(false);
props.showAlert("danger", t("something-went-wrong-mssg"));
}
}
} catch (err) {
console.log("err", err);
setIsFormLoader(false);
props.showAlert("danger", t("something-went-wrong-mssg"));
}
} else {
props.showAlert("danger", t("something-went-wrong-mssg"));
}
}
}
};
// Define a function to handle the "add yourself" checkbox
const handleReset = () => {
setFormdata({ name: "", email: "", phone: "", team: "", role: "" });
if (props.closePopup) {
props.closePopup();
}
};
const handleChange = (event) => {
let { name, value } = event.target;
if (name === "email") {
value = value?.toLowerCase()?.replace(/\s/g, "");
}
setFormdata((prev) => ({ ...prev, [name]: value }));
};
const copytoclipboard = (text) => {
copytoData(text);
props.showAlert("success", t("copied"));
};
return (
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
<Title title={t("add-user")} />
{isFormLoader && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
<Loader />
</div>
)}
<div className="w-full mx-auto">
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label
htmlFor="name"
className="block text-xs text-gray-700 font-semibold"
>
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="text"
name="name"
value={formdata.name}
onChange={(e) => handleChange(e)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
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"
/>
</div>
<div className="mb-3">
<label
htmlFor="email"
className="block text-xs text-gray-700 font-semibold"
>
{t("email")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="email"
name="email"
value={formdata.email}
onChange={(e) => handleChange(e)}
required
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"
/>
</div>
<div className="mb-3">
<label className="block text-xs text-gray-700 font-semibold">
{t("password")}
</label>
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
<div className="break-all">{formdata?.password}</div>
<i
onClick={() => copytoclipboard(formdata?.password)}
className="fa-light fa-copy rounded-full hover:bg-base-300 p-[8px] cursor-pointer "
></i>
</div>
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
{t("password-generateed")}
</div>
</div>
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
>
{t("phone")}
{/* <span className="text-[red] text-[13px]"> *</span> */}
</label>
<input
type="text"
name="phone"
placeholder={t("phone-optional")}
value={formdata.phone}
onChange={(e) => handleChange(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
>
{t("Role")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<select
value={formdata.role}
onChange={(e) => handleChange(e)}
name="role"
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
>
<option defaultValue={""} value={""}>
{t("Select")}
</option>
{role.length > 0 &&
role.map((x) => (
<option key={x} value={x}>
{x}
</option>
))}
</select>
</div>
<div className="flex items-center mt-3 gap-2 text-white">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
<div
type="button"
onClick={() => handleReset()}
className="op-btn op-btn-secondary"
>
{t("cancel")}
</div>
</div>
</form>
</div>
</div>
);
};
export default AddUser;
+288
View File
@@ -0,0 +1,288 @@
import React, { useEffect, useState } from "react";
import Parse from "parse";
import Title from "./Title";
import Loader from "../primitives/Loader";
import {
copytoData,
usertimezone
} from "../constant/Utils";
import {
emailRegex,
} from "../constant/const";
import {
useTranslation
} from "react-i18next";
function generatePassword(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const AddUser = (props) => {
const { t } = useTranslation();
const [formdata, setFormdata] = useState({
name: "",
phone: "",
email: "",
team: "",
password: "",
role: ""
});
const [isFormLoader, setIsFormLoader] = useState(false);
const [teamList, setTeamList] = useState([]);
const role = ["OrgAdmin", "Editor", "User"];
useEffect(() => {
getTeamList();
// eslint-disable-next-line
}, []);
const getTeamList = async () => {
setFormdata((prev) => ({ ...prev, password: generatePassword(12) }));
const teamRes = await Parse.Cloud.run("getteams", { active: true });
if (teamRes.length > 0) {
const _teamRes = JSON.parse(JSON.stringify(teamRes));
setTeamList(_teamRes);
const allUserId =
_teamRes.find((x) => x.Name === "All Users")?.objectId || "";
setFormdata((prev) => ({ ...prev, team: allUserId }));
}
};
const checkUserExist = async () => {
try {
const res = await Parse.Cloud.run("getUserDetails", {
email: formdata.email
});
if (res) {
return true;
} else {
return false;
}
} catch (err) {
console.log("err", err);
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!emailRegex.test(formdata.email)) {
alert("Please enter a valid email address.");
} else {
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
setIsFormLoader(true);
const res = await checkUserExist();
if (res) {
props.showAlert("danger", t("user-already-exist"));
setIsFormLoader(false);
} else {
if (localStorage.getItem("TenantId")) {
const timezone = usertimezone;
try {
const params = {
name: formdata.name,
email: formdata.email,
phone: formdata.phone,
password: formdata.password,
role: formdata.role,
team: formdata.team,
timezone: timezone,
tenantId: localStorage.getItem("TenantId"),
organization: {
objectId: localUser?.OrganizationId?.objectId,
company: localUser?.Company
},
};
const res = await Parse.Cloud.run("adduser", params);
const parseData = JSON.parse(JSON.stringify(res));
console.log("parseData ", parseData);
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find((x) => x.objectId === formdata.team);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.showAlert("success", t("user-created-successfully"));
} catch (err) {
console.log("err", err);
setIsFormLoader(false);
props.showAlert("danger", t("something-went-wrong-mssg"));
}
} else {
props.showAlert("danger", t("something-went-wrong-mssg"));
}
}
}
};
// Define a function to handle the "add yourself" checkbox
const handleReset = () => {
setFormdata({ name: "", email: "", phone: "", team: "", role: "" });
if (props.closePopup) {
props.closePopup();
}
};
const handleChange = (event) => {
let { name, value } = event.target;
if (name === "email") {
value = value?.toLowerCase()?.replace(/\s/g, "");
}
setFormdata((prev) => ({ ...prev, [name]: value }));
};
const copytoclipboard = (text) => {
copytoData(text);
props.showAlert("success", t("copied"));
};
return (
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
<Title title={t("add-user")} />
{isFormLoader && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
<Loader />
</div>
)}
<div className="w-full mx-auto">
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label
htmlFor="name"
className="block text-xs text-gray-700 font-semibold"
>
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="text"
name="name"
value={formdata.name}
onChange={(e) => handleChange(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
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"
/>
</div>
<div className="mb-3">
<label
htmlFor="email"
className="block text-xs text-gray-700 font-semibold"
>
{t("email")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="email"
name="email"
value={formdata.email}
onChange={(e) => handleChange(e)}
required
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"
/>
</div>
<div className="mb-3">
<label className="block text-xs text-gray-700 font-semibold">
{t("password")}
</label>
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
<div className="break-all">{formdata?.password}</div>
<i
onClick={() => copytoclipboard(formdata?.password)}
className="fa-light fa-copy rounded-full hover:bg-base-300 p-[8px] cursor-pointer "
></i>
</div>
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
{t("password-generateed")}
</div>
</div>
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
>
{t("phone")}
{/* <span className="text-[red] text-[13px]"> *</span> */}
</label>
<input
type="text"
name="phone"
placeholder={t("phone-optional")}
value={formdata.phone}
onChange={(e) => handleChange(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
>
{t("Role")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<select
value={formdata.role}
onChange={(e) => handleChange(e)}
name="role"
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
required
>
<option defaultValue={""} value={""}>
{t("Select")}
</option>
{role.length > 0 &&
role.map((x) => (
<option key={x} value={x}>
{x}
</option>
))}
</select>
</div>
<div className="flex items-center mt-3 gap-2 text-white">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
<div
type="button"
onClick={() => handleReset()}
className="op-btn op-btn-secondary"
>
{t("cancel")}
</div>
</div>
</form>
</div>
</div>
);
};
export default AddUser;
@@ -218,7 +218,9 @@ const BulkSendUi = (props) => {
className="flex flex-col"
key={field.fieldId}
>
<label>{field.label}</label>
<label className="block text-xs font-semibold">
{field.label}
</label>
<SuggestionInput
required
type="email"
@@ -31,6 +31,8 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
initializeHead();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function initializeHead() {
const applogo = await getAppLogo();
if (applogo?.logo) {
@@ -1,7 +1,9 @@
import React from "react";
import { Document, Page } from "react-pdf";
import { Stage, Layer, Rect, Text } from "react-konva";
import { useTranslation } from "react-i18next";
const RenderDebugPdf = (props) => {
const { t } = useTranslation();
return (
<div>
<div className="sticky top-0 p-[10px] z-10 bg-white border-[1px] border-[gray] my-[5px]">
@@ -12,10 +14,9 @@ const RenderDebugPdf = (props) => {
onMouseMove={props.handleMouseMoveDiv}
>
<Document
onLoadError={() => {
props.setPdfLoadFail(false);
}}
loading={"Loading Document.."}
onLoadError={() => props.setPdfLoadFail(false)}
loading={t("loading-doc")}
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadSuccess={props.pageDetails}
ref={props.pdfRef}
file={props.pdfUrl}
@@ -17,8 +17,8 @@ const AddRoleModal = (props) => {
onChange={(e) => props.setRoleName(e.target.value)}
placeholder={
props.signersdata.length > 0
? "User " + (props.signersdata.length + 1)
: "User 1"
? "Role " + (props.signersdata.length + 1)
: "Role 1"
}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs mt-1"
/>
@@ -1,276 +0,0 @@
import React, { useEffect, useState } from "react";
import opensignLogo from "../../assets/images/logo.png";
import {
Page,
Text,
View,
Document,
StyleSheet,
Image
} from "@react-pdf/renderer";
function Certificate({ pdfData }) {
const [isMultiSigners, setIsMultiSigners] = useState();
const [multiSigner, setMultiSigners] = useState([]);
const [isLoad, setIsLoad] = useState(false);
useEffect(() => {
handleSignerData();
// eslint-disable-next-line
}, []);
const handleSignerData = () => {
const checkSigners = pdfData.filter((data) => data.Signers);
if (checkSigners && checkSigners.length > 0) {
setIsMultiSigners(true);
const checkSignSigners =
pdfData[0].AuditTrail &&
pdfData[0].AuditTrail.length > 0 &&
pdfData[0].AuditTrail.filter((data) => data.Activity === "Signed");
setMultiSigners(checkSignSigners);
} else {
setIsMultiSigners(false);
}
setIsLoad(true);
};
const styles = StyleSheet.create({
page: {
borderRadius: "5px",
padding: "10px",
backgroundColor: "white"
},
section1: {
border: "1px solid rgb(177, 174, 174)",
padding: "20px"
},
textStyle: {
fontWeight: "bold",
fontSize: "11px",
marginBottom: "10px"
},
textStyle2: {
fontWeight: "600",
fontSize: "11px",
marginBottom: "10px",
color: "gray"
},
image: {
width: "71px",
height: "17px"
}
});
const generatedDate = () => {
const newDate = new Date();
const utcTime = newDate.toUTCString();
return (
<Text
style={{
color: "gray",
fontSize: "10px"
}}
>
Generated On {utcTime}
</Text>
);
};
const changeCompletedDate = () => {
const completedOn = pdfData[0].updatedAt;
const newDate = new Date(completedOn);
const utcTime = newDate.toUTCString();
return <Text style={styles.textStyle2}>{utcTime}</Text>;
};
const signerName = (data) => {
const getSignerName = pdfData[0].Signers.filter(
(sign) => sign.objectId === data.UserPtr.objectId
);
return (
getSignerName[0] &&
getSignerName.length > 0 && (
<>
<Text style={styles.textStyle}>
Name : &nbsp;
<Text style={styles.textStyle2}>{getSignerName[0].Name}</Text>
</Text>
<Text style={styles.textStyle}>
Email : &nbsp;
<Text style={styles.textStyle2}>{getSignerName[0].Email}</Text>
</Text>
</>
)
);
};
return (
isLoad && (
<Document>
{/** Page defines a single page of content. */}
<Page size="A4" style={styles.page}>
<View style={styles.section1}>
<View
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "30px"
}}
>
<Image src={opensignLogo} style={styles.image} />
{generatedDate()}
</View>
<View style={{ justifyContent: "center" }}>
<Text
style={{
textAlign: "center",
fontSize: "20px",
fontWeight: "bold",
color: "#31bceb",
marginBottom: "10px"
}}
>
{" "}
Certificate of Completion
</Text>
<View style={{ border: "1px solid #bdbbbb" }}></View>
<View>
<View>
<Text
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#31bceb",
margin: "10px 0px 10px 0px"
}}
>
Summary
</Text>
</View>
<View style={{ display: "flex", flexDirection: "column" }}>
<Text style={styles.textStyle}>
Document ID : &nbsp;
<Text style={styles.textStyle2}>{pdfData[0].objectId}</Text>
</Text>
<Text style={styles.textStyle}>
Document Name : &nbsp;
<Text style={styles.textStyle2}>{pdfData[0].Name}</Text>
</Text>
<Text style={styles.textStyle}>
Organization : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].ExtUserPtr.Company}
</Text>
</Text>
<Text style={styles.textStyle}>
Completed on : &nbsp;{changeCompletedDate()}
</Text>
{multiSigner && multiSigner.length > 0 && (
<Text style={styles.textStyle}>
Signers : &nbsp;
<Text style={styles.textStyle2}>
{multiSigner.length}
</Text>
</Text>
)}
</View>
{isMultiSigners ? (
<View style={{ display: "flex", flexDirection: "column" }}>
<Text
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#31bceb",
margin: "10px 0px 10px 0px"
}}
>
Recipients
</Text>
<View>
{multiSigner &&
multiSigner.map((data, ind) => {
return (
<View
key={ind}
style={{
display: "flex",
flexDirection: "column"
}}
>
<View
style={{
border: "0.4px solid #bdbbbb",
marginBottom: "10px"
}}
></View>
{signerName(data)}
<Text style={styles.textStyle}>
Accessed from : &nbsp;
<Text style={styles.textStyle2}>
{data.ipAddress}
</Text>
</Text>
</View>
);
})}
</View>
</View>
) : (
<View style={{ display: "flex", flexDirection: "column" }}>
<Text
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#31bceb",
margin: "10px 0px 10px 0px"
}}
>
Recipients
</Text>
<Text style={styles.textStyle}>
Signers : &nbsp;<Text style={styles.textStyle2}>1</Text>
</Text>
<Text style={styles.textStyle}>
Name : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].ExtUserPtr.Name}
</Text>
</Text>
<Text style={styles.textStyle}>
Email : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].ExtUserPtr.Email}
</Text>
</Text>
<Text style={styles.textStyle}>
Accessed from : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].AuditTrail &&
pdfData[0].AuditTrail[0].ipAddress}
</Text>
</Text>
<Text style={styles.textStyle}>
Signed on : &nbsp;{changeCompletedDate()}
</Text>
</View>
)}
</View>
</View>
</View>
</Page>
</Document>
)
);
}
export default Certificate;
@@ -30,15 +30,19 @@ function DraftDocument() {
documentData === "Error: Something went wrong!" ||
(documentData.result && documentData.result.error)
) {
setIsLoading({
isLoader: false,
message: "Error: Something went wrong!"
});
if (documentData?.result?.error?.includes("deleted")) {
setIsLoading({
isLoader: false,
message: t("document-deleted")
});
} else {
setIsLoading({
isLoader: false,
message: t("something-went-wrong-mssg")
});
}
} else {
setIsLoading({
isLoader: false,
message: "No data found!"
});
setIsLoading({ isLoader: false, message: t("no-data") });
}
};
@@ -1,317 +0,0 @@
import React, { useState } from "react";
import { getFileName } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
import SignersInput from "../shared/fields/SignersInput";
const EditTemplate = ({ template, onSuccess }) => {
const appName = "OpenSign™";
const { t } = useTranslation();
const [formData, setFormData] = useState({
Name: template?.Name || "",
Note: template?.Note || "",
Description: template?.Description || "",
SendinOrder: template?.SendinOrder ? `${template?.SendinOrder}` : "false",
AutomaticReminders: template?.AutomaticReminders || false,
RemindOnceInEvery: template?.RemindOnceInEvery || 5,
IsEnableOTP: template?.IsEnableOTP ? `${template?.IsEnableOTP}` : "false",
IsTourEnabled: template?.IsTourEnabled
? `${template?.IsTourEnabled}`
: "false",
NotifyOnSignatures:
template?.NotifyOnSignatures !== undefined
? template?.NotifyOnSignatures
: false,
Bcc: template?.Bcc,
RedirectUrl: template?.RedirectUrl || "",
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15
});
// `isValidURL` is used to check valid webhook url
function isValidURL(value) {
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:";
} catch (error) {
return false;
}
}
const handleStrInput = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (formData.RedirectUrl && !isValidURL(formData?.RedirectUrl)) {
alert(t("invalid-redirect-url"));
return;
}
const isChecked = formData.SendinOrder === "true" ? true : false;
const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true;
const AutoReminder = formData?.AutomaticReminders || false;
const IsEnableOTP = formData.IsEnableOTP === "true" ? true : false;
const allowModify = formData?.AllowModifications || false;
let reminderDate = {};
const remindOnceInEvery = formData?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(formData?.TimeToCompleteDays);
const reminderCount = TimeToCompleteDays / remindOnceInEvery;
if (AutoReminder && reminderCount > 15) {
alert(t("only-15-reminder-allowed"));
return;
}
if (AutoReminder) {
const RemindOnceInEvery = parseInt(formData?.RemindOnceInEvery);
const ReminderDate = new Date(template?.createdAt);
ReminderDate.setDate(ReminderDate.getDate() + RemindOnceInEvery);
reminderDate = { NextReminderDate: ReminderDate };
}
const data = {
...formData,
SendinOrder: isChecked,
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
AllowModifications: allowModify,
...reminderDate
};
onSuccess(data);
};
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
const handleBcc = (data) => {
if (data && data.length > 0) {
const trimEmail = data.map((item) => ({
objectId: item?.value,
Name: item?.label,
Email: item?.email
}));
setFormData((prev) => ({ ...prev, Bcc: trimEmail }));
}
};
return (
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
<div className="text-base-content">
<form onSubmit={handleSubmit}>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("report-heading.File")}
</label>
<div className="op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs">
{getFileName(template.URL)}
</div>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("Title")}
<span className="text-[13px] text-[red]"> *</span>
</label>
<input
type="text"
name="Name"
value={formData.Name}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
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"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Note" className="text-[13px]">
{t("report-heading.Note")}
</label>
<input
type="text"
name="Note"
id="Note"
value={formData.Note}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Description" className="text-[13px]">
{t("description")}
</label>
<input
type="text"
name="Description"
id="Description"
value={formData.Description}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label className="text-[13px]">{t("send-in-order")}</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label className="block">
<span>
{t("enable-tour")}
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</span>
<Tooltip id="istourenabled-tooltip" className="z-50">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<p className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">{t("yes")}: </span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
</p>
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
helpText={t("bcc-help")}
isCaptureAllData
/>
</div>
<div className="text-xs mt-2">
<label className="block">Redirect Url</label>
<input
name="RedirectUrl"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.RedirectUrl}
onChange={handleStrInput}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
<div className="text-xs mt-2">
<label className="block">
{t("time-to-complete")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
type="number"
name="TimeToCompleteDays"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.TimeToCompleteDays}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
<div className="mt-[1rem] flex justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
</div>
</form>
</div>
</div>
);
};
export default EditTemplate;
@@ -0,0 +1,538 @@
import React, {
useState,
useRef,
} from "react";
import {
base64ToArrayBuffer,
convertBase64ToFile,
generatePdfName,
getFileName
} from "../../constant/Utils";
import {
maxDescriptionLength,
maxNoteLength,
maxTitleLength
} from "../../constant/const";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
import SignersInput from "../shared/fields/SignersInput";
import { PDFDocument } from "pdf-lib";
import ModalUi from "../../primitives/ModalUi";
import { SaveFileSize } from "../../constant/saveFileSize";
const EditTemplate = ({
title,
handleClose,
pdfbase64,
template,
onSuccess,
setPdfArrayBuffer,
setPdfBase64Url,
}) => {
const appName =
"OpenSign™";
const { t } = useTranslation();
const inputFileRef = useRef(null);
const [formData, setFormData] = useState({
Name: template?.Name || "",
Note: template?.Note || "",
Description: template?.Description || "",
SendinOrder: template?.SendinOrder ? `${template?.SendinOrder}` : "false",
AutomaticReminders: template?.AutomaticReminders || false,
RemindOnceInEvery: template?.RemindOnceInEvery || 5,
IsEnableOTP: template?.IsEnableOTP ? `${template?.IsEnableOTP}` : "false",
IsTourEnabled: template?.IsTourEnabled
? `${template?.IsTourEnabled}`
: "false",
NotifyOnSignatures:
template?.NotifyOnSignatures !== undefined
? template?.NotifyOnSignatures
: false,
Bcc: template?.Bcc,
RedirectUrl: template?.RedirectUrl || "",
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15
});
const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [uploadPdf, setUploadPdf] = useState({
name: "",
base64: "",
url: ""
});
const handleDrop = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
handleFile(file);
};
const handleFile = (file) => {
if (file && file.type === "application/pdf") {
handleReplaceFileValdition(file);
// You can handle the file here
} else {
alert("Only pdf files are allowed.");
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
const handleDragOver = (e) => {
e.preventDefault();
};
// `isValidURL` is used to check valid webhook url
function isValidURL(value) {
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:";
} catch (error) {
return false;
}
}
const handleStrInput = (e) => {
setIsUpdate(true);
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const getPdfMetadataHash = async (pdfBytes) => {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pages = pdfDoc.getPages();
const metaString = pages
.map((page, index) => {
const { width, height } = page.getSize();
return `${index + 1}:${Math.round(width)}x${Math.round(height)}`;
})
.join("|");
const encoder = new TextEncoder();
const data = encoder.encode(metaString);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
const handleFileInput = async (e) => {
const file = e.target.files[0];
if (!file) return;
handleReplaceFileValdition(file);
};
const handleReplaceFileValdition = async (file) => {
try {
const basePdfBytes = base64ToArrayBuffer(pdfbase64);
const expectedHash = await getPdfMetadataHash(basePdfBytes);
const fileReader = new FileReader();
fileReader.onload = async (event) => {
const uploadedPdfBytes = event.target.result;
const uploadedHash = await getPdfMetadataHash(uploadedPdfBytes);
if (expectedHash === uploadedHash) {
const arrayBuffer = uploadedPdfBytes;
const uint8Array = new Uint8Array(arrayBuffer);
const binaryString = Array.from(uint8Array)
.map((b) => String.fromCharCode(b))
.join("");
const base64 = btoa(binaryString);
const pdfName = generatePdfName(16);
setIsUpdate(true);
setUploadPdf((prev) => ({ ...prev, name: pdfName, base64: base64 }));
// alert("✅ PDFs match (based on page number, width, height)");
} else {
alert("❌ PDF do NOT match based on page number, width, height");
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
fileReader.readAsArrayBuffer(file);
} catch (err) {
alert("Error: " + err.message);
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (formData.RedirectUrl && !isValidURL(formData?.RedirectUrl)) {
alert(t("invalid-redirect-url"));
return;
}
if (formData?.Name?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
if (formData?.Note?.length > maxNoteLength) {
alert(t("note-length-alert"));
return;
}
if (formData?.Description?.length > maxDescriptionLength) {
alert(t("description-length-alert"));
return;
}
let pdfUrl;
if (uploadPdf?.base64) {
pdfUrl = await convertBase64ToFile(
uploadPdf.name,
uploadPdf.base64,
);
setUploadPdf((prev) => ({ ...prev, url: pdfUrl }));
const pdfBuffer = base64ToArrayBuffer(uploadPdf.base64);
setPdfArrayBuffer && setPdfArrayBuffer(pdfBuffer);
setPdfBase64Url && setPdfBase64Url(uploadPdf.base64);
const tenantId =
localStorage.getItem("TenantId") ||
template?.ExtUserPtr?.TenantId?.objectId;
const buffer = atob(uploadPdf.base64);
SaveFileSize(buffer.length, pdfUrl, tenantId);
}
const isChecked = formData.SendinOrder === "true" ? true : false;
const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true;
const AutoReminder = formData?.AutomaticReminders || false;
const IsEnableOTP = formData.IsEnableOTP === "true" ? true : false;
const allowModify = formData?.AllowModifications || false;
let reminderDate = {};
const remindOnceInEvery = formData?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(formData?.TimeToCompleteDays);
const reminderCount = TimeToCompleteDays / remindOnceInEvery;
if (AutoReminder && reminderCount > 15) {
alert(t("only-15-reminder-allowed"));
return;
}
if (AutoReminder) {
const RemindOnceInEvery = parseInt(formData?.RemindOnceInEvery);
const ReminderDate = new Date(template?.createdAt);
ReminderDate.setDate(ReminderDate.getDate() + RemindOnceInEvery);
reminderDate = { NextReminderDate: ReminderDate };
}
const data = {
...formData,
...(pdfUrl ? { URL: pdfUrl } : {}),
SendinOrder: isChecked,
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
AllowModifications: allowModify,
...reminderDate
};
onSuccess(data);
};
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setIsUpdate(true);
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
const handleBcc = (data) => {
if (data && data.length > 0) {
const trimEmail = data.map((item) => ({
objectId: item?.value,
Name: item?.label,
Email: item?.email
}));
setIsUpdate(true);
setFormData((prev) => ({ ...prev, Bcc: trimEmail }));
}
};
const handleEditTemplateClose = () => {
if (isUpdate) {
setShowConfirm(true);
} else {
handleClose();
}
};
const discardChanges = () => {
setShowConfirm(false);
handleClose();
};
return (
<ModalUi
isOpen
title={isUpdate ? `${title} (unsaved)` : title}
handleClose={handleEditTemplateClose}
>
<ModalUi isOpen={showConfirm} showClose={false}>
<div className="p-[20px]">
<p className="text-base font-normal text-base-content py-[5px] md:py-[6px] px-[5px]">
{t("unsaved-changes-discard-them?")}
</p>
<div className="flex items-center mt-2.5 gap-2 md:gap-3 text-white">
<button
className="op-btn op-btn-primary px-6"
onClick={discardChanges}
>
{t("yes-discard")}
</button>
<button
className="op-btn op-btn-secondary px-4 md:px-6"
onClick={() => setShowConfirm(false)}
>
{t("cancel")}
</button>
</div>
</div>
</ModalUi>
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
<div className="text-base-content">
<form onSubmit={handleSubmit}>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("report-heading.File")}
</label>
<div
className="border-[1.5px] border-dashed border-gray-300 rounded-lg px-4 py-6 text-center text-gray-500 bg-white cursor-pointer hover:border-base-content transition"
onDrop={handleDrop}
onDragOver={handleDragOver}
onClick={() => inputFileRef?.current?.click()}
>
<label
htmlFor="fileUpload"
className="cursor-pointer text-center mb-0"
>
Browse or drag & drop a new file to replace the existng one
</label>
</div>
<input
ref={inputFileRef}
type="file"
className="hidden"
accept="application/pdf"
onChange={(e) => handleFileInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
/>
{uploadPdf?.name && (
<div
onClick={() => inputFileRef?.current?.click()}
className="mt-2 cursor-pointer op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs"
>
selected:{" "}
{uploadPdf?.url
? `${uploadPdf?.name}.pdf`
: getFileName(template.URL)}
</div>
)}
</div>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("Title")}
<span className="text-[13px] text-[red]"> *</span>
</label>
<input
type="text"
name="Name"
value={formData.Name}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
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"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Note" className="text-[13px]">
{t("report-heading.Note")}
</label>
<input
type="text"
name="Note"
id="Note"
value={formData.Note}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Description" className="text-[13px]">
{t("description")}
</label>
<input
type="text"
name="Description"
id="Description"
value={formData.Description}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label className="text-[13px]">{t("send-in-order")}</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label className="block">
<span>
{t("enable-tour")}
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</span>
<Tooltip id="istourenabled-tooltip" className="z-50">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">{t("yes")}: </span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
</div>
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
helpText={t("bcc-help")}
isCaptureAllData
/>
</div>
<div className="text-xs mt-2">
<label className="block">Redirect Url</label>
<input
name="RedirectUrl"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.RedirectUrl}
onChange={handleStrInput}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
<div className="text-xs mt-2">
<label className="block">
{t("time-to-complete")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
type="number"
name="TimeToCompleteDays"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.TimeToCompleteDays}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
<div className="mt-[1rem] flex justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
</div>
</form>
</div>
</div>
</ModalUi>
);
};
export default EditTemplate;
@@ -1,10 +1,15 @@
import React, { useState } from "react";
import { handleToPrint } from "../../constant/Utils";
import { emailRegex } from "../../constant/const";
import {
handleToPrint,
} from "../../constant/Utils";
import {
emailRegex,
} from "../../constant/const";
import Loader from "../../primitives/Loader";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
import Parse from "parse";
function EmailComponent({
isEmail,
setIsEmail,
@@ -20,6 +25,7 @@ function EmailComponent({
const [emailErr, setEmailErr] = useState(false);
const [isDownloading, setIsDownloading] = useState("");
const isAndroid = /Android/i.test(navigator.userAgent);
//function for send email
const sendEmail = async () => {
setIsLoading(true);
@@ -35,7 +41,8 @@ function EmailComponent({
setEmailList([]);
}, 1500);
setIsLoading(false);
} else {
}
else {
setIsLoading(false);
setIsEmail(false);
setIsAlert({
@@ -105,7 +112,7 @@ function EmailComponent({
</div>
)}
<div className="flex justify-between items-center py-[10px] px-[20px] border-b-[1px] border-base-content">
<span className="text-base-content font-semibold">
<span className="text-base-content font-bold text-sm md:text-lg">
{t("successfully-signed")}
</span>
<div className="flex flex-row">
@@ -114,14 +121,14 @@ function EmailComponent({
onClick={(e) =>
handleToPrint(e, setIsDownloading, pdfDetails)
}
className="op-btn op-btn-neutral op-btn-sm text-[15px]"
className="op-btn op-btn-neutral op-btn-sm text-xs md:text-[15px]"
>
<i className="fa-light fa-print" aria-hidden="true"></i>
{t("print")}
</button>
)}
<button
className="op-btn op-btn-primary op-btn-sm text-[15px] ml-2"
className="op-btn op-btn-primary op-btn-sm text-xs md:text-[15px] ml-2"
onClick={() => {
handleClose();
setIsDownloadModal(true);
@@ -137,12 +144,12 @@ function EmailComponent({
{t("email-mssg")}
</p>
{emailList.length > 0 ? (
<div className="p-0 border-[1.5px] op-border-primary rounded w-full text-[15px]">
<div className="p-0 border-[1px] op-border-primary w-full rounded-md text-[15px] overflow-hidden">
<div className="flex flex-row flex-wrap">
{emailList.map((data, ind) => {
return (
<div
className="flex flex-row items-center op-bg-primary m-[4px] rounded-md py-[5px] px-[10px]"
className="flex flex-row items-center op-bg-primary mx-[2px] mt-[2px] rounded-md py-[5px] px-[10px]"
key={ind}
>
<span className="text-base-100 text-[13px]">
@@ -162,7 +169,7 @@ function EmailComponent({
<input
type="email"
value={emailValue}
className="p-[10px] pb-[20px] rounded w-full text-[15px] bg-transparent outline-none"
className="p-[10px] rounded-md w-full text-[15px] bg-transparent outline-none"
onChange={handleEmailValue}
onKeyDown={handleEnterPress}
onBlur={() => emailValue && handleEnterPress("add")}
@@ -179,7 +186,7 @@ function EmailComponent({
<input
type="email"
value={emailValue}
className="p-[10px] pb-[20px] rounded w-full text-[15px] outline-none bg-transparent border-[1.5px] op-border-primary"
className="p-[10px] pb-[20px] rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
onChange={handleEmailValue}
onKeyDown={handleEnterPress}
placeholder={t("enter-email-plaholder")}
@@ -197,7 +204,7 @@ function EmailComponent({
{t("email-error-1")}
</p>
)}
<button
{/* <button
className={`${
emailValue ? "cursor-pointer" : "cursor-default"
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
@@ -205,26 +212,27 @@ function EmailComponent({
>
<i className="fa-light fa-plus" aria-hidden="true"></i>
</button>
<div className="bg-[#e3e2e1] mt-[10px] p-[5px] rounded">
<span className="font-bold">{t("report-heading.Note")}: </span>
<span className="text-[15px]">{t("email-error-2")}</span>
</div>
<hr className="w-full my-[15px] bg-base-content" />
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => emailList.length > 0 && sendEmail()}
>
{t("send")}
</button>
<button
type="button"
className="op-btn op-btn-ghost ml-2"
onClick={() => handleClose()}
>
{t("close")}
</button>
<hr className="w-full my-[15px] bg-base-content" /> */}
<div className="mt-2">
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => emailList.length > 0 && sendEmail()}
>
{t("send")}
</button>
<button
type="button"
className="op-btn op-btn-ghost ml-2"
onClick={() => handleClose()}
>
{t("close")}
</button>
</div>
</div>
</ModalUi>
)}
@@ -1,305 +0,0 @@
import React, { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/prism";
import { copytoData } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
function EmbedTab(props) {
const { t } = useTranslation();
const navigate = useNavigate();
const tabName = [
{ title: "React/Next.js", icon: "fa-brands fa-react", color: "#61dafb" },
{ title: "JavaScript", icon: "fa-brands fa-js", color: "#ffd43b" },
{ title: "Angular", icon: "fa-brands fa-angular", color: "#ff5733" }
];
const [activeTab, setActiveTab] = useState(0);
// State to track if the code has been copied
const [isCopied, setIsCopied] = useState(false);
const reactCode = [
{
id: 0,
title: "Installation",
codeString: `npm install @opensign/react`
},
{
id: 1,
title: "Usage",
codeString: `
import React from "react";
import Opensign from "@opensign/react";
export default function App() {
return (
<div className="app">
<Opensign
onLoad={() => console.log("success")}
onLoadError={(error) => console.log(error)}
templateId= "${props.templateId ? props.templateId : "#templateId"}"
/>
</div>
);
}
`
}
];
const angularCode = [
{
id: 0,
title: "Installation",
codeString: `npm install @opensign/angular`
},
{
id: 1,
title: "Usage",
codeString: `
import { Component } from '@angular/core';
import { OpensignComponent } from "@opensign/angular"
@Component({
selector:'app-root',
standalone: true,
imports: [OpensignComponent],
template:\`<opensign templateId="${props.templateId ? props.templateId : "#templateId"}"
(onLoad)="handleLoad()"
(onLoadError)="handleError($event)"
></opensign>\`,
})
export class AppComponent {
handleLoad() {
console.log("success");
}
handleError(error: string) {
console.log(error);
}
}
`
}
];
const jsCodeString = `
<script
src= "${window.location.origin}/static/js/public-template.bundle.js"
id="opensign-script"
templateId=${props.templateId ? props.templateId : "#templateId"}
></script>
`;
const handleCopy = (code, ind) => {
copytoData(code);
setIsCopied({ ...isCopied, [ind]: true });
setTimeout(() => setIsCopied(false), 3000);
};
return (
<div className={`${props.templateId && "border-t-[1px] mt-4"}`}>
{props.templateId && (
<h3 className="text-base-content font-bold text-lg pt-[15px] pb-[5px]">
{t("embed-template")}
</h3>
)}
<div className="flex justify-center items-center mt-2">
<div role="tablist" className="op-tabs op-tabs-bordered">
{tabName.map((tabData, ind) => (
<div
onClick={() => setActiveTab(ind)}
key={ind}
role="tab"
className={`${
activeTab === ind ? "op-tab-active" : ""
} op-tab flex items-center pb-10 md:pb-0`}
>
<i
className={`${tabData.icon}`}
style={{ color: tabData.color }}
></i>
<span className="ml-1 text-[10px] font-medium md:font-normal md:text-[15px]">
{tabData.title}
</span>
</div>
))}
</div>
</div>
<div>
{activeTab === 0 ? (
<div className="mt-4">
{reactCode.map((data, ind) => {
return (
<div key={ind}>
<p className="font-medium text-[18px]">
{t(`${data.title}`)}
</p>
{ind === 0 && (
<p className="text-[15px] mt-2">
{t("public-template-mssg-1")}
</p>
)}
<div className="relative p-1">
<div
onClick={() => handleCopy(data.codeString, ind)}
className="absolute top-[20px] right-[20px] cursor-pointer"
>
<i className="fa-light fa-copy text-white mr-[2px]" />
<span className=" text-white">
{isCopied[ind] ? t("copied-code") : t("copy-code")}
</span>
</div>
<SyntaxHighlighter
customStyle={{
borderRadius: "15px"
}}
language="javascript"
style={tomorrow}
>
{data.codeString}
</SyntaxHighlighter>
</div>
</div>
);
})}
{props.isEmbedPage && (
<p className="text-[15px] my-2">
{t("js-snippet-msg-1")}
<span
className="text-blue-600 cursor-pointer px-1"
onClick={() => navigate("/report/6TeaPr321t")}
>
{t("js-snippet-msg-2")}
</span>
<span>{t("js-snippet-msg-3")}</span>
</p>
)}
<p className="font-medium mt-3 text-[15px]">
{t("public-template-mssg-3")}
</p>
<p className="my-[6px]">
{" "}
{t("public-template-mssg-4")}
<a
href="https://www.npmjs.com/package/@opensign/react"
target="_blank"
rel="noreferrer"
className="cursor-pointer text-blue-700 "
>
{" "}
OpenSign React package{" "}
</a>
{t("public-template-mssg-5")}
</p>
</div>
) : activeTab === 1 ? (
<div className="mt-4">
<div>
<p className="font-medium text-[18px]">{t(`Usage`)}</p>
<p className="text-[15px] my-2">{t("js-snippet-msg")}</p>
<div className="relative p-1">
<div
onClick={() => handleCopy(jsCodeString, 0)}
className="absolute top-[20px] right-[20px] cursor-pointer"
>
<i className="fa-light fa-copy text-white mr-[2px]" />
<span className=" text-white">
{isCopied[0] ? t("copied-code") : t("copy-code")}
</span>
</div>
<SyntaxHighlighter
customStyle={{
borderRadius: "15px"
}}
language="javascript"
style={tomorrow}
>
{jsCodeString}
</SyntaxHighlighter>
</div>
{props.isEmbedPage && (
<p className="text-[15px] my-2">
{t("js-snippet-msg-1")}
<span
className="text-blue-600 cursor-pointer px-1"
onClick={() => navigate("/report/6TeaPr321t")}
>
{t("js-snippet-msg-2")}
</span>
<span>{t("js-snippet-msg-3")}</span>
</p>
)}
</div>
</div>
) : (
activeTab === 2 && (
<div className="mt-4">
{angularCode.map((data, ind) => {
return (
<div key={ind}>
<p className="font-medium text-[18px]">
{t(`${data.title}`)}
</p>
{ind === 0 && (
<p className="text-[15px] mt-2">
{t("angular-npm-mssg-1")}
</p>
)}
<div className="relative p-1">
<div
onClick={() => handleCopy(data.codeString, ind)}
className="absolute top-[20px] right-[20px] cursor-pointer"
>
<i className="fa-light fa-copy text-white mr-[2px]" />
<span className=" text-white">
{isCopied[ind] ? t("copied-code") : t("copy-code")}
</span>
</div>
<SyntaxHighlighter
customStyle={{
borderRadius: "15px"
}}
language="javascript"
style={tomorrow}
>
{data.codeString}
</SyntaxHighlighter>
</div>
</div>
);
})}
{props.isEmbedPage && (
<p className="text-[15px] my-2">
{t("js-snippet-msg-1")}
<span
className="text-blue-600 cursor-pointer px-1"
onClick={() => navigate("/report/6TeaPr321t")}
>
{t("js-snippet-msg-2")}
</span>
<span>{t("js-snippet-msg-3")}</span>
</p>
)}
<p className="font-medium mt-3 text-[15px]">
{t("public-template-mssg-3")}
</p>
<p className="my-[6px]">
{" "}
{t("public-template-mssg-4")}
<a
href="https://www.npmjs.com/package/@opensign/angular"
target="_blank"
rel="noreferrer"
className="cursor-pointer text-blue-700 "
>
{" "}
OpenSign Angular package{" "}
</a>
{t("public-template-mssg-5")}
</p>
</div>
)
)}
</div>
</div>
);
}
export default EmbedTab;
@@ -14,6 +14,7 @@ import ModalUi from "../../primitives/ModalUi";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
function Header(props) {
const { t } = useTranslation();
@@ -27,8 +28,12 @@ function Header(props) {
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
//function for show decline alert
const handleDeclinePdfAlert = async () => {
const currentDecline = { currnt: "Sure", isDeclined: true };
props?.setIsDecline(currentDecline);
if (props?.handleDecline) {
props.handleDecline();
} else {
const currentDecline = { currnt: "Sure", isDeclined: true };
props?.setIsDecline(currentDecline);
}
};
const handleDetelePage = async () => {
props?.setIsUploadPdf && props?.setIsUploadPdf(true);
@@ -50,6 +55,13 @@ function Header(props) {
}
};
// `removeFile` is used to remove file if exists
const removeFile = (e) => {
if (e) {
e.target.value = "";
}
};
const handleFileUpload = async (e) => {
const file = e.target.files[0];
if (!file) {
@@ -60,6 +72,13 @@ function Header(props) {
alert("Only PDF files are allowed.");
return;
}
const mb = Math.round(file?.size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
removeFile(e);
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
@@ -87,7 +106,6 @@ function Header(props) {
console.error("Error merging PDF:", error);
}
};
return (
<div className="flex py-[5px]">
{isMobile && props?.isShowHeader ? (
@@ -258,6 +276,20 @@ function Header(props) {
className="bg-white shadow-md rounded-md px-3 py-2"
sideOffset={5}
>
{props?.setIsEditTemplate && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => props?.setIsEditTemplate(true)}
>
<div className="flex flex-row">
<i
className="fa-light fa-gear mr-[3px]"
aria-hidden="true"
></i>
<span className="font-[500]">{t("Edit")}</span>
</div>
</DropdownMenu.Item>
)}
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
@@ -487,16 +519,6 @@ function Header(props) {
</div>
) : (
<div className="flex" data-tut="reactourFifth">
{(!props?.templateId && !props?.isSelfSign) ||
(!props.isGuestSignFlow && (
<button
onClick={() => window.history.go(-2)}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
>
{t("back")}
</button>
))}
{props?.currentSigner && (
<>
{props?.templateId && (
@@ -7,6 +7,7 @@ import {
} from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
function PdfZoom(props) {
const { t } = useTranslation();
@@ -41,6 +42,14 @@ function PdfZoom(props) {
console.log("error in delete pdf page", e);
}
};
// `removeFile` is used to remove file if exists
const removeFile = (e) => {
if (e) {
e.target.value = "";
}
};
const handleFileUpload = async (e) => {
const file = e.target.files[0];
if (!file) {
@@ -51,6 +60,12 @@ function PdfZoom(props) {
alert("Only PDF files are allowed.");
return;
}
const mb = Math.round(file?.size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
removeFile(e);
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
@@ -3,6 +3,7 @@ import BorderResize from "./BorderResize";
import PlaceholderBorder from "./PlaceholderBorder";
import { Rnd } from "react-rnd";
import {
changeDateToMomentFormat,
defaultWidthHeight,
fontColorArr,
fontsizeArr,
@@ -25,6 +26,8 @@ const selectFormat = (data) => {
switch (data) {
case "L":
return "MM/dd/yyyy";
case "MM/DD/YYYY":
return "MM/dd/yyyy";
case "DD-MM-YYYY":
return "dd-MM-yyyy";
case "DD/MM/YYYY":
@@ -41,6 +44,8 @@ const selectFormat = (data) => {
return "MM.dd.yyyy";
case "MMM DD, YYYY":
return "MMM dd, yyyy";
case "MMMM DD, YYYY":
return "MMMM dd, yyyy";
case "DD MMMM, YYYY":
return "dd MMMM, yyyy";
default:
@@ -48,32 +53,6 @@ const selectFormat = (data) => {
}
};
const changeDateToMomentFormat = (format) => {
switch (format) {
case "MM/dd/yyyy":
return "L";
case "dd-MM-yyyy":
return "DD-MM-YYYY";
case "dd/MM/yyyy":
return "DD/MM/YYYY";
case "MMMM dd, yyyy":
return "LL";
case "dd MMM, yyyy":
return "DD MMM, YYYY";
case "yyyy-MM-dd":
return "YYYY-MM-DD";
case "MM-dd-yyyy":
return "MM-DD-YYYY";
case "MM.dd.yyyy":
return "MM.DD.YYYY";
case "MMM dd, yyyy":
return "MMM DD, YYYY";
case "dd MMMM, yyyy":
return "DD MMMM, YYYY";
default:
return "L";
}
};
//function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
@@ -195,7 +174,7 @@ function Placeholder(props) {
}, [selectDate]);
//`handleWidgetIdandPopup` is used to set current widget id and open relative popup
const handleWidgetIdandPopup = () => {
const handleWidgetIdandPopup = async () => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId(props.pos.key);
}
@@ -218,31 +197,31 @@ function Placeholder(props) {
if (props?.ispublicTemplate) {
props.handleUserDetails();
} else {
if (props?.isNeedSign) {
//funcion is used to height widgets on top if two widgets on overlap
const getCurrentSignerPos = props.xyPosition.find(
(x) => x.Id === props.uniqueId
);
const updateZindex = handleHeighlightWidget(
getCurrentSignerPos,
props.pos.key,
props.pageNumber
);
const updatesignerPos = props.xyPosition.map((x) =>
x.Id === props.uniqueId ? { ...x, placeHolder: updateZindex } : x
);
props.setXyPosition(updatesignerPos);
}
if (
["signature", "stamp", "image", "initials"].includes(props.pos.type)
) {
props.setIsSignPad(true);
props.setSignKey(props.pos.key);
props.setIsStamp(props.pos.isStamp);
}
if (props.pos.type === "initials") {
props.setIsInitial(true);
}
if (props?.isNeedSign) {
//funcion is used to heightlight widgets on top if two widgets on overlap
const getCurrentSignerPos = props.xyPosition.find(
(x) => x.Id === props.uniqueId
);
const updateZindex = handleHeighlightWidget(
getCurrentSignerPos,
props.pos.key,
props.pageNumber
);
const updatesignerPos = props.xyPosition.map((x) =>
x.Id === props.uniqueId ? { ...x, placeHolder: updateZindex } : x
);
props.setXyPosition(updatesignerPos);
}
if (
["signature", "stamp", "image", "initials"].includes(props.pos.type)
) {
props.setIsSignPad(true);
props.setSignKey(props.pos.key);
props.setIsStamp(props.pos.isStamp);
}
if (props.pos.type === "initials") {
props.setIsInitial(true);
}
}
} else if (
props.isPlaceholder &&
@@ -363,6 +342,8 @@ function Placeholder(props) {
} else {
widgetClickHandler();
}
// }
// }
} else {
//The else condition is used to handle the case when the user clicks on a widget and open signature pad to draw sign
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
@@ -7,7 +7,8 @@ import {
radioButtonWidget,
range,
textInputWidget,
textWidget
textWidget,
widgetDataValue
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
@@ -24,7 +25,9 @@ const widgetCls =
function PlaceholderType(props) {
const { t } = useTranslation();
const type = props?.pos?.type;
const widgetTypeTraslation = t(`widgets-name.${props?.pos?.type}`);
const widgetData =
props.pos?.options?.defaultValue || props.pos?.options?.response;
const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
const [selectOption, setSelectOption] = useState("");
const [validatePlaceholder, setValidatePlaceholder] = useState("");
const inputRef = useRef(null);
@@ -141,29 +144,6 @@ function PlaceholderType(props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (
["name", "email", "job title", "company"].includes(props.pos?.type) &&
props.isNeedSign &&
props.data?.signerObjId === props?.signerObjId
) {
const defaultData = props.pos?.options?.defaultValue;
if (defaultData) {
setTextValue(defaultData);
}
if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint);
} else {
setHint(props.pos?.type);
}
} else if ([textInputWidget].includes(props.pos?.type)) {
const defaultData = props.pos?.options?.defaultValue;
if (defaultData) {
setTextValue(defaultData);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos?.options?.defaultValue]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{
@@ -176,32 +156,30 @@ function PlaceholderType(props) {
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
useEffect(() => {
if (
["name", "email", "job title", "company"].includes(type) &&
["name", "email", "job title", "company", textInputWidget].includes(
type
) &&
props.isNeedSign &&
props.data?.signerObjId === props.signerObjId
) {
const isDefault = true;
const senderUser = localStorage.getItem(`Extand_Class`);
const jsonSender = JSON.parse(senderUser);
onChangeInput(
jsonSender && jsonSender[0],
null,
props.xyPosition,
null,
props.setXyPosition,
props.data.Id,
isDefault
);
if (widgetData) {
setTextValue(widgetData);
}
}
if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint);
} else {
setHint(props.pos?.type);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type]);
}, [type, widgetData]);
//function for show checked checkbox
const selectCheckbox = (ind) => {
const res = props.pos.options?.response;
@@ -342,8 +320,8 @@ function PlaceholderType(props) {
className="font-medium"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTraslation
: widgetTypeTraslation}
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
</div>
)}
</div>
@@ -368,8 +346,8 @@ function PlaceholderType(props) {
className="font-medium"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTraslation
: widgetTypeTraslation}
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
</div>
)}
</div>
@@ -462,10 +440,14 @@ function PlaceholderType(props) {
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
? " bg-black/68 select-none "
? "select-none"
: "" + textWidgetCls
}`}
style={{ fontSize: fontSize, color: fontColor }}
style={{
fontSize: fontSize,
color: fontColor,
background: props.data?.blockColor
}}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
@@ -475,7 +457,7 @@ function PlaceholderType(props) {
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{textValue || widgetTypeTraslation}</span>
<span>{textValue || widgetTypeTranslation}</span>
</div>
);
case "dropdown":
@@ -538,7 +520,7 @@ function PlaceholderType(props) {
>
{props.pos?.options?.name
? props.pos.options.name
: widgetTypeTraslation}
: widgetTypeTranslation}
<i className="fa-light fa-circle-chevron-down mr-1 "></i>
</div>
);
@@ -562,8 +544,8 @@ function PlaceholderType(props) {
className="font-medium text-center"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTraslation
: widgetTypeTraslation}
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
</div>
)}
</div>
@@ -574,7 +556,7 @@ function PlaceholderType(props) {
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={hint}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
@@ -597,7 +579,7 @@ function PlaceholderType(props) {
/>
) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}>
<span>{widgetTypeTraslation}</span>
<span>{widgetTypeTranslation}</span>
</div>
);
case "company":
@@ -606,7 +588,7 @@ function PlaceholderType(props) {
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={hint}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
@@ -628,7 +610,7 @@ function PlaceholderType(props) {
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
<span>{widgetTypeTranslation}</span>
</div>
);
case "job title":
@@ -637,7 +619,7 @@ function PlaceholderType(props) {
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={hint}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
@@ -659,7 +641,7 @@ function PlaceholderType(props) {
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
<span>{widgetTypeTranslation}</span>
</div>
);
case "date":
@@ -748,8 +730,8 @@ function PlaceholderType(props) {
className="font-medium text-center"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTraslation
: widgetTypeTraslation}
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
</div>
)}
</div>
@@ -760,7 +742,7 @@ function PlaceholderType(props) {
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={hint}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={(e) => {
// Prevent new line on Enter key press
@@ -792,7 +774,7 @@ function PlaceholderType(props) {
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
<span>{widgetTypeTranslation}</span>
</div>
);
case radioButtonWidget:
@@ -891,8 +873,8 @@ function PlaceholderType(props) {
className="font-medium"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTraslation
: widgetTypeTraslation}
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
</div>
)}
</div>
@@ -4,6 +4,7 @@ import { Document, Page } from "react-pdf";
import { useSelector } from "react-redux";
import { PDFDocument } from "pdf-lib";
import { base64ToArrayBuffer } from "../../constant/Utils";
import { maxFileSize } from "../../constant/const";
function RenderAllPdfPage(props) {
const { t } = useTranslation();
@@ -62,6 +63,12 @@ function RenderAllPdfPage(props) {
};
const pdfDataBase64 = `data:application/pdf;base64,${props?.pdfBase64Url}`;
// `removeFile` is used to remove file if exists
const removeFile = (e) => {
if (e) {
e.target.value = "";
}
};
// `handleFileUpload` is trigger when user click on add pages btn and is used to merge multiple pdf
const handleFileUpload = async (e) => {
const file = e.target.files[0];
@@ -73,6 +80,12 @@ function RenderAllPdfPage(props) {
alert("Only PDF files are allowed.");
return;
}
const mb = Math.round(file?.size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
removeFile(e);
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
@@ -110,6 +123,7 @@ function RenderAllPdfPage(props) {
autoSignScroll hide-scrollbar max-h-[100vh] `}
>
<Document
error=""
loading={t("loading-doc")}
onLoadSuccess={onDocumentLoad}
file={pdfDataBase64}
@@ -381,17 +381,15 @@ function RenderPdf(props) {
</React.Fragment>
);
}))}
{/* Mobile */}
<Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
// ref={pdfRef}'
onClick={() => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId("");
}
}}
onClick={() =>
props.setSelectWidgetId && props.setSelectWidgetId("")
}
file={pdfDataBase64}
>
<Page
@@ -405,6 +403,7 @@ function RenderPdf(props) {
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
className="select-none touch-callout-none"
/>
</Document>
</div>
@@ -601,17 +600,16 @@ function RenderPdf(props) {
</React.Fragment>
);
}))}
{/* large device */}
{/* this component for render pdf document is in middle of the component */}
<Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
onClick={() => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId("");
}
}}
onClick={() =>
props.setSelectWidgetId && props.setSelectWidgetId("")
}
file={pdfDataBase64}
>
<Page
@@ -71,8 +71,15 @@ function SignPad(props) {
} else if (tab === "typed") {
setIsTab("type");
} else if (tab === "default") {
setIsDefaultSign(true);
setIsTab("mysignature");
if (
(props?.isInitial && props?.myInitial) ||
(!props?.isInitial && props?.defaultSign)
) {
setIsDefaultSign(true);
setIsTab("mysignature");
} else {
setIsTab("draw");
}
} else {
setIsTab(true);
}
@@ -630,26 +637,59 @@ function SignPad(props) {
<div className="p-[20px] h-full">
{isDefaultSign ? (
<>
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
>
<img
alt="stamp img"
className="w-full h-full object-contain bg-white"
draggable="false"
src={
props?.isInitial
? props?.myInitial
: props?.defaultSign
}
/>
</div>
</div>
{props.setIsAutoSign && autoSignAll()}
<div className="flex justify-end">
<SaveBtn />
</div>
{!props?.isInitial &&
props?.defaultSign &&
isTabEnabled("default") && (
<>
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
>
<img
alt="stamp img"
className="w-full h-full object-contain bg-white"
draggable="false"
src={
props?.isInitial
? props?.myInitial
: props?.defaultSign
}
/>
</div>
</div>
{props.setIsAutoSign && autoSignAll()}
<div className="flex justify-end">
<SaveBtn />
</div>
</>
)}
{props?.isInitial &&
props?.myInitial &&
isTabEnabled("default") && (
<>
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
>
<img
alt="stamp img"
className="w-full h-full object-contain bg-white"
draggable="false"
src={
props?.isInitial
? props?.myInitial
: props?.defaultSign
}
/>
</div>
</div>
{props.setIsAutoSign && autoSignAll()}
<div className="flex justify-end">
<SaveBtn />
</div>
</>
)}
</>
) : props?.isImageSelect || props?.isStamp ? (
!props?.image ? (
@@ -117,6 +117,7 @@ const WidgetNameModal = (props) => {
}
}
const handleCheckboxChange = (index) => {
// Update the state with the modified array
setSignatureType((prev) =>
@@ -1,173 +0,0 @@
import React, { useEffect, useState } from "react";
import Parse from "parse";
import Alert from "../../../primitives/Alert";
import Loader from "../../../primitives/Loader";
import { useTranslation } from "react-i18next";
const CreateFolder = ({ parentFolderId, onSuccess, folderCls }) => {
const folderPtr = {
__type: "Pointer",
className: folderCls,
objectId: parentFolderId
};
const { t } = useTranslation();
const [name, setName] = useState("");
const [folderList, setFolderList] = useState([]);
const [isAlert, setIsAlert] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const [selectedParent, setSelectedParent] = useState();
const [alert, setAlert] = useState({ type: "info", message: "" });
useEffect(() => {
fetchFolder();
// eslint-disable-next-line
}, []);
const fetchFolder = async () => {
try {
const FolderQuery = new Parse.Query(folderCls);
if (parentFolderId) {
FolderQuery.equalTo("Folder", folderPtr);
FolderQuery.equalTo("Type", "Folder");
FolderQuery.notEqualTo("IsArchive", true);
FolderQuery.equalTo("CreatedBy", Parse.User.current());
} else {
FolderQuery.doesNotExist("Folder");
FolderQuery.equalTo("Type", "Folder");
FolderQuery.notEqualTo("IsArchive", true);
FolderQuery.equalTo("CreatedBy", Parse.User.current());
}
const res = await FolderQuery.find();
if (res) {
const result = JSON.parse(JSON.stringify(res));
if (result) {
setFolderList(result);
}
}
} catch (error) {
console.log("Err ", error);
}
};
const handleCreateFolder = async (event) => {
event.preventDefault();
handleLoader(true);
if (name) {
const currentUser = Parse.User.current();
const exsitQuery = new Parse.Query(folderCls);
exsitQuery.equalTo("Name", name);
exsitQuery.equalTo("Type", "Folder");
exsitQuery.notEqualTo("IsArchive", true);
if (parentFolderId) {
exsitQuery.equalTo("Folder", folderPtr);
}
const templExist = await exsitQuery.first();
if (templExist) {
setAlert({ type: "danger", message: t("folder-already-exist") });
setIsAlert(true);
setTimeout(() => {
setIsAlert(false);
}, 1000);
} else {
const template = new Parse.Object(folderCls);
template.set("Name", name);
template.set("Type", "Folder");
if (selectedParent) {
template.set("Folder", {
__type: "Pointer",
className: folderCls,
objectId: selectedParent
});
} else if (parentFolderId) {
template.set("Folder", folderPtr);
}
template.set("CreatedBy", Parse.User.createWithoutData(currentUser.id));
const res = await template.save();
if (res) {
handleLoader(false);
setAlert({
type: "success",
message: t("folder-created-successfully")
});
setIsAlert(true);
setTimeout(() => {
setIsAlert(false);
}, 1000);
if (onSuccess) {
onSuccess(res);
}
}
}
} else {
handleLoader(false);
setAlert({ type: "info", message: t("fill-folder-name") });
setIsAlert(true);
setTimeout(() => {
setIsAlert(false);
}, 1000);
}
};
const handleOptions = (e) => {
setSelectedParent(e.target.value);
};
const handleLoader = (status) => {
setIsLoader(status);
};
return (
<div>
{isAlert && <Alert type={alert.type}>{alert.message}</Alert>}
<div id="createFolder" className="relative">
{isLoader && (
<div className="absolute h-full w-full flex justify-center items-center">
<Loader />
</div>
)}
<h1 className="text-base font-semibold mt-[0.4rem]">
{t("create-folder")}
</h1>
<div className="text-xs mt-2">
<label className="block">
{t("name")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={name}
onChange={(e) => setName(e.target.value)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
<div className="text-xs mt-2">
<label className="block">{t("parent-folder")}</label>
<select
value={selectedParent}
onChange={handleOptions}
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
>
<option>select</option>
{folderList.length > 0 &&
folderList.map((x) => (
<option key={x.objectId} value={x.objectId}>
{x.Name}
</option>
))}
</select>
</div>
<div>
<button
onClick={handleCreateFolder}
disabled={isLoader}
className="op-btn op-btn-primary op-btn-sm mt-3"
>
<i className="fa-light fa-plus"></i>
<span>{t("create")}</span>
</button>
</div>
</div>
</div>
);
};
export default CreateFolder;
@@ -0,0 +1,109 @@
import React, { useState } from "react";
import Parse from "parse";
import Alert from "../../../primitives/Alert";
import Loader from "../../../primitives/Loader";
import { useTranslation } from "react-i18next";
const CreateFolder = ({ parentFolderId, onSuccess, folderCls, onBack }) => {
const folderPtr = {
__type: "Pointer",
className: folderCls,
objectId: parentFolderId
};
const { t } = useTranslation();
const [name, setName] = useState("");
const [isLoader, setIsLoader] = useState(false);
const [alert, setAlert] = useState({ type: "info", message: "" });
const showToast = (type, msg) => {
setAlert({ type: type, message: msg });
setTimeout(() => setAlert({ type: type, message: "" }), 1000);
};
const handleCreateFolder = async (event) => {
event.preventDefault();
handleLoader(true);
if (name) {
const currentUser = Parse.User.current();
const exsitQuery = new Parse.Query(folderCls);
exsitQuery.equalTo("Name", name);
exsitQuery.equalTo("Type", "Folder");
exsitQuery.notEqualTo("IsArchive", true);
if (parentFolderId) {
exsitQuery.equalTo("Folder", folderPtr);
}
const templExist = await exsitQuery.first();
if (templExist) {
showToast("danger", t("folder-already-exist"));
} else {
const template = new Parse.Object(folderCls);
template.set("Name", name);
template.set("Type", "Folder");
if (parentFolderId) {
template.set("Folder", folderPtr);
}
template.set("CreatedBy", Parse.User.createWithoutData(currentUser.id));
const res = await template.save();
if (res) {
handleLoader(false);
showToast("success", t("folder-created-successfully"));
onSuccess && onSuccess(res?.toJSON());
}
}
} else {
handleLoader(false);
showToast("info", t("fill-folder-name"));
}
};
const handleLoader = (status) => setIsLoader(status);
return (
<div>
{alert.message && <Alert type={alert.type}>{alert.message}</Alert>}
<div id="createFolder" className="relative">
{isLoader && (
<div className="absolute h-full w-full flex justify-center items-center">
<Loader />
</div>
)}
<h1 className="text-base font-semibold mt-[0.4rem]">
{t("create-folder")}
</h1>
<div className="text-xs mt-2">
<label className="block">
{t("name")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={name}
onChange={(e) => setName(e.target.value)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
<div className="flex justify-between items-center py-[1rem] ">
<button
onClick={handleCreateFolder}
disabled={isLoader}
className="op-btn op-btn-primary op-btn-sm"
>
<i className="fa-light fa-plus"></i>
<span>{t("create")}</span>
</button>
{onBack && (
<div
className="op-btn op-btn-seconday op-btn-sm"
title={t("back")}
onClick={() => onBack()}
>
<i className="fa-light fa-arrow-left" aria-hidden="true"></i>
<span className="text-xs">{t("back")}</span>
</div>
)}
</div>
</div>
</div>
);
};
export default CreateFolder;
@@ -0,0 +1,79 @@
import React, { useState } from "react";
import { formatDateTime } from "../../../constant/Utils";
import { useTranslation } from "react-i18next";
const DateFormatSelector = (props) => {
const { t } = useTranslation();
const date = new Date();
const [selectedFormat, setSelectedFormat] = useState(props.dateFormat);
const [is12Hour, setIs12Hour] = useState(props?.is12HourTime);
const dateFormats = [
"MM/DD/YYYY",
"MMMM DD, YYYY",
"DD MMMM, YYYY",
"DD-MM-YYYY",
"DD MMM, YYYY",
"YYYY-MM-DD",
"MM-DD-YYYY",
"MM.DD.YYYY",
"MMM DD, YYYY"
];
// Handle format change
const handleFormatChange = (event) => {
setSelectedFormat(event.target.value);
props.setDateFormat && props.setDateFormat(event.target.value);
};
const handleHrInput = () => {
setIs12Hour(!is12Hour);
props.setIs12HourTime && props.setIs12HourTime(!is12Hour);
};
return (
<div className="max-w-[400px] pr-[20px]">
<label className="text-[14px] mb-[0.7rem] font-medium">
{t("date-format")}
</label>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full h-full text-[11px]"
value={selectedFormat}
onChange={handleFormatChange}
>
{dateFormats.map((format) => (
<option key={format} value={format}>
{format}
</option>
))}
</select>
<div className="flex flex-row gap-4 mt-[0.75rem] text-[12px]">
<div className="flex items-center gap-2 ml-2">
<input
type="radio"
value={true}
className="op-radio op-radio-xs"
checked={is12Hour}
onChange={handleHrInput}
/>
<div className="text-center">12 hr</div>
</div>
<div className="flex items-center gap-2 ml-2">
<input
type="radio"
value={false}
className="op-radio op-radio-xs"
checked={!is12Hour}
onChange={handleHrInput}
/>
<div className="text-center">24 hr</div>
</div>
</div>
<p className="mt-[12px] ml-[10px] text-[13px] font-medium">
<strong>
{formatDateTime(date, selectedFormat, props?.timezone, is12Hour)}
</strong>
</p>
</div>
);
};
export default DateFormatSelector;
@@ -105,49 +105,45 @@ const FolderModal = (props) => {
// `handleCancel` is call when user click on folder name from path/tab in popup
const removeTabListItem = async (e, i) => {
e.preventDefault();
// setEditable(false);
if (!isAdd) {
setIsLoader(true);
let folderPtr;
if (i) {
setFolderList([]);
let list = tabList.filter((itm, j) => {
if (j <= i) {
return itm;
}
});
let _len = list.length - 1;
folderPtr = {
__type: "Pointer",
className: props.folderCls,
objectId: list[_len].objectId
};
setTabList(list);
} else {
setClickFolder({});
setFolderList([]);
setTabList([]);
}
setIsLoader(true);
setIsAdd(false);
if (i !== undefined) {
setFolderList([]);
const list = tabList.filter((folder, j) => j <= i && folder);
const index = list.length - 1;
const folderPtr = {
__type: "Pointer",
className: props.folderCls,
objectId: list[index].objectId
};
setTabList(list);
fetchFolder(folderPtr);
} else {
setClickFolder({});
setFolderList([]);
setTabList([]);
fetchFolder();
}
};
// `handleCreate` is used to open folder creation form in popup
const handleCreate = () => {
setIsAdd(!isAdd);
};
const handleCreate = () => setIsAdd(true);
const handleBack = () => setIsAdd(false);
// `handleAddFolder` is call when user folder created successfully and it fetch folder list on the basis of folderPtr or without folderPtr
const handleAddFolder = (newFolder) => {
props.setPdfData((prev) => [...prev, newFolder?.toJSON()]);
props.setPdfData((prev) => [...prev, newFolder]);
if (clickFolder && clickFolder.ObjectId) {
fetchFolder({
__type: "Pointer",
className: props.folderCls,
objectId: clickFolder.ObjectId
objectId: newFolder.objectId // clickFolder.ObjectId
});
} else {
fetchFolder();
}
handleCreate();
setClickFolder({ ObjectId: newFolder.objectId, Name: newFolder.Name });
setTabList((prev) => [...prev, newFolder]);
handleBack();
};
return (
<div className="text-xs mt-2">
@@ -180,54 +176,57 @@ const FolderModal = (props) => {
))}
<hr className="bg-[#8a8a8a] mt-[0.750rem]" />
</div>
<div className="mt-2 mb-3">
<div className="max-h-[210px] overflow-auto">
{!isAdd && folderList.length > 0
? folderList.map((folder) => (
<div
key={folder.objectId}
className={`${
folder.Type === "Folder"
? "cursor-pointer"
: "cursor-default"
} border-b-[1px] border-[#8a8a8a] py-2 mb-0.5"`}
onClick={() =>
folder.Type === "Folder" && handleSelect(folder)
}
>
<div className="flex items-center gap-2">
{folder.Type === "Folder" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-secondary"
>
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-primary"
>
<path d="M374.629 150.627L233.371 9.373C227.371 3.371 219.23 0 210.746 0H64C28.652 0 0 28.652 0 64V448C0 483.345 28.652 512 64 512H320C355.348 512 384 483.345 384 448V173.254C384 164.767 380.629 156.629 374.629 150.627ZM224 22.629L361.375 160H248C234.781 160 224 149.234 224 136V22.629ZM368 448C368 474.467 346.469 496 320 496H64C37.531 496 16 474.467 16 448V64C16 37.533 37.531 16 64 16H208V136C208 158.062 225.938 176 248 176H368V448ZM96 264C96 268.406 99.594 272 104 272H280C284.406 272 288 268.406 288 264S284.406 256 280 256H104C99.594 256 96 259.594 96 264ZM280 320H104C99.594 320 96 323.594 96 328S99.594 336 104 336H280C284.406 336 288 332.406 288 328S284.406 320 280 320ZM280 384H104C99.594 384 96 387.594 96 392S99.594 400 104 400H280C284.406 400 288 396.406 288 392S284.406 384 280 384Z" />
</svg>
)}
<span className="font-semibold">{folder.Name}</span>
<div className={`${!isAdd ? "mb-3" : ""} mt-2`}>
{!isAdd && (
<div className="max-h-[210px] overflow-auto">
{folderList.length > 0
? folderList.map((folder) => (
<div
key={folder.objectId}
className={`${
folder.Type === "Folder"
? "cursor-pointer"
: "cursor-default"
} border-b-[1px] border-[#8a8a8a] py-2 mb-0.5"`}
onClick={() =>
folder.Type === "Folder" && handleSelect(folder)
}
>
<div className="flex items-center gap-2">
{folder.Type === "Folder" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-secondary"
>
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-primary"
>
<path d="M374.629 150.627L233.371 9.373C227.371 3.371 219.23 0 210.746 0H64C28.652 0 0 28.652 0 64V448C0 483.345 28.652 512 64 512H320C355.348 512 384 483.345 384 448V173.254C384 164.767 380.629 156.629 374.629 150.627ZM224 22.629L361.375 160H248C234.781 160 224 149.234 224 136V22.629ZM368 448C368 474.467 346.469 496 320 496H64C37.531 496 16 474.467 16 448V64C16 37.533 37.531 16 64 16H208V136C208 158.062 225.938 176 248 176H368V448ZM96 264C96 268.406 99.594 272 104 272H280C284.406 272 288 268.406 288 264S284.406 256 280 256H104C99.594 256 96 259.594 96 264ZM280 320H104C99.594 320 96 323.594 96 328S99.594 336 104 336H280C284.406 336 288 332.406 288 328S284.406 320 280 320ZM280 384H104C99.594 384 96 387.594 96 392S99.594 400 104 400H280C284.406 400 288 396.406 288 392S284.406 384 280 384Z" />
</svg>
)}
<span className="font-semibold">{folder.Name}</span>
</div>
</div>
</div>
))
: !isLoader && (
<div className="text-base-content text-center my-2">
{t("no-data")}
</div>
)}
</div>
))
: !isLoader && (
<div className="text-base-content text-center my-2">
{t("no-data")}
</div>
)}
</div>
)}
{isAdd && (
<CreateFolder
parentFolderId={clickFolder && clickFolder.ObjectId}
folderCls={props.folderCls}
onSuccess={handleAddFolder}
onBack={handleBack}
/>
)}
{isLoader && (
@@ -238,33 +237,26 @@ const FolderModal = (props) => {
</div>
</div>
<hr />
<div className="flex justify-between items-center py-[.75rem] px-[1.25rem]">
<div
className="op-btn op-btn-seconday op-btn-sm"
title={t("save-here")}
onClick={handleCreate}
>
{isAdd ? (
<>
<i className="fa-light fa-arrow-left" aria-hidden="true"></i>
<span className="text-xs">{t("back")}</span>
</>
) : (
<>
<i className="fa-light fa-square-plus" aria-hidden="true"></i>
<span className="">{t("add-folder")}</span>
</>
)}
{!isAdd && (
<div className="flex justify-between items-center py-[.75rem] px-[1.25rem]">
<div
className="op-btn op-btn-primary op-btn-sm"
title={t("save-here")}
onClick={handleSubmit}
>
<i className="fa-light fa-save" aria-hidden="true"></i>
{t("save-here")}
</div>
<div
className="op-btn op-btn-seconday op-btn-sm"
title={t("add-folder")}
onClick={handleCreate}
>
<i className="fa-light fa-square-plus" aria-hidden="true"></i>
<span className="">{t("add-folder")}</span>
</div>
</div>
<div
className="op-btn op-btn-primary op-btn-sm"
title={t("save-here")}
onClick={handleSubmit}
>
<i className="fa-light fa-save" aria-hidden="true"></i>
{t("save-here")}
</div>
</div>
)}
</ModalUi>
</div>
);
@@ -125,50 +125,46 @@ const SelectFolder = ({ required, onSuccess, folderCls, isReset }) => {
// `handleCancel` is call when user click on folder name from path/tab in popup
const removeTabListItem = async (e, i) => {
e.preventDefault();
// setEditable(false);
if (!isAdd) {
setIsLoader(true);
let folderPtr;
if (i) {
setFolderList([]);
let list = tabList.filter((itm, j) => {
if (j <= i) {
return itm;
}
});
let _len = list.length - 1;
folderPtr = {
__type: "Pointer",
className: folderCls,
objectId: list[_len].objectId
};
setTabList(list);
} else {
setClickFolder({});
setSelectedFolder({});
setFolderList([]);
setTabList([]);
}
setIsLoader(true);
setIsAdd(false);
if (i !== undefined) {
setFolderList([]);
const list = tabList.filter((folder, j) => j <= i && folder);
const index = list.length - 1;
const folderPtr = {
__type: "Pointer",
className: folderCls,
objectId: list[index].objectId
};
fetchFolder(folderPtr);
setTabList(list);
} else {
setClickFolder({});
setSelectedFolder({});
setFolderList([]);
setTabList([]);
fetchFolder();
}
};
// `handleCreate` is used to open folder creation form in popup
const handleCreate = () => {
setIsAdd(!isAdd);
};
const handleCreate = () => setIsAdd(true);
const handleBack = () => setIsAdd(false);
// `handleAddFolder` is call when user folder created successfully and it fetch folder list on the basis of folderPtr or without folderPtr
const handleAddFolder = () => {
const handleAddFolder = (newFolder) => {
setFolderList([]);
if (clickFolder && clickFolder.ObjectId) {
fetchFolder({
__type: "Pointer",
className: folderCls,
objectId: clickFolder.ObjectId
objectId: newFolder.objectId // clickFolder.ObjectId
});
} else {
fetchFolder();
}
handleCreate();
setClickFolder({ ObjectId: newFolder.objectId, Name: newFolder.Name });
setTabList((prev) => [...prev, newFolder]);
handleBack();
};
return (
<div className="text-xs mt-2 ">
@@ -246,53 +242,56 @@ const SelectFolder = ({ required, onSuccess, folderCls, isReset }) => {
<hr className="bg-[#8a8a8a] mt-[0.750rem]" />
</div>
<div className="mb-2">
<div className="max-h-[210px] overflow-auto">
{!isAdd && folderList.length > 0
? folderList.map((folder) => (
<div
key={folder.objectId}
className={`${
folder.Type === "Folder"
? "cursor-pointer"
: "cursor-default"
} border-b-[1px] border-[#8a8a8a] py-2 mb-0.5"`}
onClick={() =>
folder.Type === "Folder" && handleSelect(folder)
}
>
<div className="flex items-center gap-2">
{folder.Type === "Folder" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-secondary"
>
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-primary"
>
<path d="M374.629 150.627L233.371 9.373C227.371 3.371 219.23 0 210.746 0H64C28.652 0 0 28.652 0 64V448C0 483.345 28.652 512 64 512H320C355.348 512 384 483.345 384 448V173.254C384 164.767 380.629 156.629 374.629 150.627ZM224 22.629L361.375 160H248C234.781 160 224 149.234 224 136V22.629ZM368 448C368 474.467 346.469 496 320 496H64C37.531 496 16 474.467 16 448V64C16 37.533 37.531 16 64 16H208V136C208 158.062 225.938 176 248 176H368V448ZM96 264C96 268.406 99.594 272 104 272H280C284.406 272 288 268.406 288 264S284.406 256 280 256H104C99.594 256 96 259.594 96 264ZM280 320H104C99.594 320 96 323.594 96 328S99.594 336 104 336H280C284.406 336 288 332.406 288 328S284.406 320 280 320ZM280 384H104C99.594 384 96 387.594 96 392S99.594 400 104 400H280C284.406 400 288 396.406 288 392S284.406 384 280 384Z" />
</svg>
)}
<span className="font-semibold">{folder.Name}</span>
{!isAdd && (
<div className="max-h-[210px] overflow-auto">
{folderList.length > 0
? folderList.map((folder) => (
<div
key={folder.objectId}
className={`${
folder.Type === "Folder"
? "cursor-pointer"
: "cursor-default"
} border-b-[1px] border-[#8a8a8a] py-2 mb-0.5"`}
onClick={() =>
folder.Type === "Folder" && handleSelect(folder)
}
>
<div className="flex items-center gap-2">
{folder.Type === "Folder" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-secondary"
>
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
className="w-[1.4rem] h-[1.4rem] fill-current op-text-primary"
>
<path d="M374.629 150.627L233.371 9.373C227.371 3.371 219.23 0 210.746 0H64C28.652 0 0 28.652 0 64V448C0 483.345 28.652 512 64 512H320C355.348 512 384 483.345 384 448V173.254C384 164.767 380.629 156.629 374.629 150.627ZM224 22.629L361.375 160H248C234.781 160 224 149.234 224 136V22.629ZM368 448C368 474.467 346.469 496 320 496H64C37.531 496 16 474.467 16 448V64C16 37.533 37.531 16 64 16H208V136C208 158.062 225.938 176 248 176H368V448ZM96 264C96 268.406 99.594 272 104 272H280C284.406 272 288 268.406 288 264S284.406 256 280 256H104C99.594 256 96 259.594 96 264ZM280 320H104C99.594 320 96 323.594 96 328S99.594 336 104 336H280C284.406 336 288 332.406 288 328S284.406 320 280 320ZM280 384H104C99.594 384 96 387.594 96 392S99.594 400 104 400H280C284.406 400 288 396.406 288 392S284.406 384 280 384Z" />
</svg>
)}
<span className="font-semibold">{folder.Name}</span>
</div>
</div>
</div>
))
: !isLoader && (
<div className="text-base-content text-center my-2">
{t("no-data")}
</div>
)}
</div>
))
: !isLoader && (
<div className="text-base-content text-center my-2">
{t("no-data")}
</div>
)}
</div>
)}
{isAdd && (
<CreateFolder
parentFolderId={clickFolder && clickFolder.ObjectId}
folderCls={folderCls}
onSuccess={handleAddFolder}
onBack={handleBack}
/>
)}
{isLoader && (
@@ -303,33 +302,26 @@ const SelectFolder = ({ required, onSuccess, folderCls, isReset }) => {
</div>
</div>
<hr />
<div className="flex justify-between items-center py-[.75rem] px-[1.25rem]">
<div
className="op-btn op-btn-seconday op-btn-sm"
title={t("save-here")}
onClick={handleCreate}
>
{isAdd ? (
<>
<i className="fa-light fa-arrow-left" aria-hidden="true"></i>
<span className="text-xs">{t("back")}</span>
</>
) : (
<>
<i className="fa-light fa-square-plus" aria-hidden="true"></i>
<span className="">{t("add-folder")}</span>
</>
)}
{!isAdd && (
<div className="flex justify-between items-center py-[.75rem] px-[1.25rem]">
<div
className="op-btn op-btn-primary op-btn-sm"
title={t("save-here")}
onClick={handleSubmit}
>
<i className="fa-light fa-save" aria-hidden="true"></i>
{t("save-here")}
</div>
<div
className="op-btn op-btn-seconday op-btn-sm"
title={t("add-folder")}
onClick={handleCreate}
>
<i className="fa-light fa-square-plus" aria-hidden="true"></i>
<span className="">{t("add-folder")}</span>
</div>
</div>
<div
className="op-btn op-btn-primary op-btn-sm"
title={t("save-here")}
onClick={handleSubmit}
>
<i className="fa-light fa-save" aria-hidden="true"></i>
{t("save-here")}
</div>
</div>
)}
</ModalUi>
</div>
);
@@ -1,10 +1,10 @@
import React, { useState, useEffect } from "react";
import AsyncSelect from "react-select/async";
import AddSigner from "../../AddSigner";
import Parse from "parse";
import AddContact from "../../../primitives/AddContact";
import Tooltip from "../../../primitives/Tooltip";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { findContact } from "../../../constant/Utils";
function arrayMove(array, from, to) {
array = array.slice();
array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]);
@@ -103,17 +103,23 @@ const SignersInput = (props) => {
// `handleNewDetails` is used to set just save from quick form to selected option in dropdown
const handleNewDetails = (data) => {
setState([...state, data]);
const user = {
value: data["objectId"],
label: data["Name"],
email: data?.Email
};
setState([...state, user]);
if (selected.length > 0) {
setSelected([...selected, data]);
setSelected([...selected, user]);
} else {
setSelected([data]);
setSelected([user]);
}
};
const loadOptions = async (inputValue) => {
try {
const params = { search: inputValue };
const contactRes = await Parse.Cloud.run("getsigners", params);
const contactRes = await findContact(
inputValue,
);
if (contactRes) {
const res = JSON.parse(JSON.stringify(contactRes));
//compareArrays is a function where compare between two array (total signersList and dcument signers list)
@@ -209,9 +215,8 @@ const SignersInput = (props) => {
</button>
{isModal && (
<AddSigner
valueKey={"objectId"}
displayKey={"Name"}
<AddContact
isDisableTitle
details={handleNewDetails}
closePopup={handleModalCloseClick}
/>
@@ -7,27 +7,10 @@ const TimezoneSelector = (props) => {
// Intl.DateTimeFormat().resolvedOptions().timeZone // Default to the user's local timezone
const onChangeTimezone = (timezone) => {
setSelectedTimezone(timezone);
setSelectedTimezone(timezone?.value);
props.setTimezone && props.setTimezone(timezone?.value);
};
// Format date and time for the selected timezone
const formatDate = (date, timezone) => {
return timezone
? new Intl.DateTimeFormat("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZone: timezone,
hour12: false
}).format(date)
: new Date(date).toUTCString();
};
return (
<>
<div className="max-w-[400px] pr-[20px]">
@@ -54,11 +37,6 @@ const TimezoneSelector = (props) => {
}}
/>
</div>
<div className="mt-[12px] ml-[10px] text-[13px]">
<strong>
{formatDate(new Date(), selectedTimezone?.value || selectedTimezone)}
</strong>{" "}
</div>
</>
);
};
@@ -18,13 +18,15 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
className={({ isActive }) =>
`${
isActive ? " bg-base-300 text-base-content" : ""
} flex items-center text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
} flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
tabIndex={isOpen ? 0 : -1}
role="menuitem"
>
<i className={`${item.icon} text-[18px]`} aria-hidden="true"></i>
<span className="w-[20px] h-[20px] flex justify-center">
<i className={`${item.icon} text-[18px]`} aria-hidden="true"></i>
</span>
<span className="ml-3 lg:ml-4">
{t(`sidebar.${item.title}`, { appName: drivename })}
</span>
@@ -12,14 +12,16 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
<li role="none" className="my-0.5">
<button
onClick={() => toggleSubmenu(item.title)}
className="flex items-center text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none "
className="flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none "
aria-expanded={submenuOpen}
aria-haspopup="true"
aria-controls={`submenu-${title}`}
>
<i className={`${icon} text-[18px]`}></i>
<span className="w-[20px] h-[20px] flex justify-center">
<i className={`${icon} text-[18px]`}></i>
</span>
<div className="flex justify-between items-center w-full">
<span className="ml-3 lg:ml-4">
<span className="ml-3 lg:ml-4 text-start">
{t(`sidebar.${item.title}`, { appName })}
</span>
<i
@@ -45,16 +47,18 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
className={({ isActive }) =>
`${
isActive ? "bg-base-300 text-base-content" : ""
} flex items-center text-left pl-6 md:pl-8 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
} flex items-center justify-start text-left pl-6 md:pl-8 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
role="menuitem"
tabIndex={submenuOpen ? 0 : -1}
>
<i
className={`${childItem.icon} text-[18px]`}
aria-hidden="true"
></i>
<span className="w-[15px] h-[15px] flex justify-center">
<i
className={`${childItem.icon} text-[18px]`}
aria-hidden="true"
></i>
</span>
<span className="ml-3 lg:ml-4">
{t(`sidebar.${item.title}-Children.${childItem.title}`, {
appName: drivename
@@ -7,7 +7,10 @@ import { appInfo } from "./appinfo";
import { saveAs } from "file-saver";
import printModule from "print-js";
import fontkit from "@pdf-lib/fontkit";
import { themeColor } from "./const";
import {
themeColor
} from "./const";
import { format, toZonedTime } from "date-fns-tz";
export const fontsizeArr = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28];
export const fontColorArr = ["red", "black", "blue", "yellow"];
@@ -134,12 +137,15 @@ export const pdfNewWidthFun = (divRef) => {
};
//`contractUsers` function is used to get contract_User details
export const contractUsers = async () => {
export const contractUsers = async (
) => {
try {
const url = `${localStorage.getItem("baseUrl")}functions/getUserDetails`;
const parseAppId = localStorage.getItem("parseAppId");
const accesstoken = localStorage.getItem("accesstoken");
const token = { "X-Parse-Session-Token": accesstoken };
const accesstoken =
localStorage.getItem("accesstoken");
const token =
{ "X-Parse-Session-Token": accesstoken };
const headers = {
headers: {
"Content-Type": "application/json",
@@ -232,17 +238,73 @@ export const widgets = [
{ type: "email", icon: "fa-light fa-envelope", iconSize: "20px" }
];
export const getDate = () => {
export const getDate = (dateformat) => {
const format = dateformat || "MM/DD/YYYY";
const date = new Date();
const milliseconds = date.getTime();
const newDate = moment(milliseconds).format("MM/DD/YYYY");
const newDate = moment(milliseconds).format(format);
return newDate;
};
export const addWidgetOptions = (type) => {
const defaultOpt = {
name: type,
status: "required"
};
export const selectFormat = (data) => {
switch (data) {
case "L":
return "MM/dd/yyyy";
case "MM/DD/YYYY":
return "MM/dd/yyyy";
case "DD-MM-YYYY":
return "dd-MM-yyyy";
case "DD/MM/YYYY":
return "dd/MM/yyyy";
case "LL":
return "MMMM dd, yyyy";
case "DD MMM, YYYY":
return "dd MMM, yyyy";
case "YYYY-MM-DD":
return "yyyy-MM-dd";
case "MM-DD-YYYY":
return "MM-dd-yyyy";
case "MM.DD.YYYY":
return "MM.dd.yyyy";
case "MMM DD, YYYY":
return "MMM dd, yyyy";
case "MMMM DD, YYYY":
return "MMMM dd, yyyy";
case "DD MMMM, YYYY":
return "dd MMMM, yyyy";
default:
return "MM/dd/yyyy";
}
};
export const changeDateToMomentFormat = (format) => {
switch (format) {
case "MM/dd/yyyy":
return "L";
case "dd-MM-yyyy":
return "DD-MM-YYYY";
case "dd/MM/yyyy":
return "DD/MM/YYYY";
case "MMMM dd, yyyy":
return "LL";
case "dd MMM, yyyy":
return "DD MMM, YYYY";
case "yyyy-MM-dd":
return "YYYY-MM-DD";
case "MM-dd-yyyy":
return "MM-DD-YYYY";
case "MM.dd.yyyy":
return "MM.DD.YYYY";
case "MMM dd, yyyy":
return "MMM DD, YYYY";
case "dd MMMM, yyyy":
return "DD MMMM, YYYY";
default:
return "L";
}
};
export const addWidgetOptions = (type, signer, widgetValue) => {
const defaultOpt = { name: type, status: "required" };
switch (type) {
case "signature":
return defaultOpt;
@@ -258,21 +320,29 @@ export const addWidgetOptions = (type) => {
case "initials":
return defaultOpt;
case "name":
return { ...defaultOpt };
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
case "company":
return { ...defaultOpt };
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
case "job title":
return { ...defaultOpt };
case "date":
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
case "date": {
const dateFormat = signer?.DateFormat
? selectFormat(signer?.DateFormat)
: "MM/dd/yyyy";
return {
...defaultOpt,
response: getDate(),
validation: { format: "MM/dd/yyyy", type: "date-format" }
response: getDate(signer?.DateFormat),
validation: { format: dateFormat, type: "date-format" }
};
}
case "image":
return defaultOpt;
case "email":
return { ...defaultOpt, validation: { type: "email", pattern: "" } };
return {
...defaultOpt,
validation: { type: "email", pattern: "" },
defaultValue: widgetValue ? widgetValue : ""
};
case "dropdown":
return defaultOpt;
case radioButtonWidget:
@@ -289,7 +359,7 @@ export const addWidgetOptions = (type) => {
}
};
export const addWidgetSelfsignOptions = (type, getWidgetValue) => {
export const addWidgetSelfsignOptions = (type, getWidgetValue, owner) => {
switch (type) {
case "signature":
return { name: "signature" };
@@ -319,12 +389,16 @@ export const addWidgetSelfsignOptions = (type, getWidgetValue) => {
defaultValue: getWidgetValue(type),
validation: { type: "text", pattern: "" }
};
case "date":
case "date": {
const dateFormat = owner?.DateFormat
? selectFormat(owner?.DateFormat)
: "MM/dd/yyyy";
return {
name: "date",
response: getDate(),
validation: { format: "MM/dd/yyyy", type: "date-format" }
response: getDate(owner?.DateFormat),
validation: { format: dateFormat, type: "date-format" }
};
}
case "image":
return { name: "image" };
case "email":
@@ -491,7 +565,7 @@ export const signPdfFun = async (
documentId,
signerObjectId,
objectId,
widgets
widgets,
) => {
let isCustomCompletionMail = false;
try {
@@ -500,7 +574,10 @@ export const signPdfFun = async (
if (tenantDetails && tenantDetails === "user does not exist!") {
return { status: "error", message: "User does not exist." };
} else {
if (tenantDetails?.CompletionBody && tenantDetails?.CompletionSubject) {
if (
tenantDetails?.CompletionBody &&
tenantDetails?.CompletionSubject
) {
isCustomCompletionMail = true;
}
}
@@ -604,6 +681,15 @@ export const createDocument = async (
const RedirectUrl = Doc?.RedirectUrl
? { RedirectUrl: Doc?.RedirectUrl }
: {};
const TemplateId = Doc?.objectId
? {
TemplateId: {
__type: "Pointer",
className: "contracts_Template",
objectId: Doc?.objectId
}
}
: {};
const data = {
Name: Doc.Name,
URL: pdfUrl,
@@ -633,7 +719,8 @@ export const createDocument = async (
...SignatureType,
...NotifyOnSignatures,
...Bcc,
...RedirectUrl
...RedirectUrl,
...TemplateId
};
const remindOnceInEvery = Doc?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(Doc?.TimeToCompleteDays);
@@ -780,7 +867,8 @@ export const onChangeInput = (
...position,
options: {
...position.options,
response: value
response: value,
defaultValue:""
}
};
}
@@ -932,26 +1020,23 @@ export const calculateInitialWidthHeight = (widgetData) => {
const height = span.offsetHeight;
document.body.removeChild(span);
return {
getWidth: width,
getHeight: height
};
return { getWidth: width, getHeight: height };
};
export const widgetDataValue = (type, value) => {
switch (type) {
case "name":
return value?.Name;
case "company":
return value?.Company;
case "job title":
return value?.JobTitle;
case "email":
return value?.Email;
default:
return "";
}
};
export const addInitialData = (signerPos, setXyPosition, value, userId) => {
function widgetDataValue(type) {
switch (type) {
case "name":
return value?.Name;
case "company":
return value?.Company;
case "job title":
return value?.JobTitle;
case "email":
return value?.Email;
default:
return "";
}
}
return signerPos.map((item) => {
if (item.placeHolder && item.placeHolder.length > 0) {
// If there is a nested array, recursively add the field to the last object
@@ -966,9 +1051,7 @@ export const addInitialData = (signerPos, setXyPosition, value, userId) => {
)
};
} else {
return {
...item
};
return item;
}
} else if (item.pos && item.pos.length > 0) {
// If there is no nested array, add the new field
@@ -978,7 +1061,7 @@ export const addInitialData = (signerPos, setXyPosition, value, userId) => {
// Adjust this line to add the desired field
};
} else {
const widgetData = widgetDataValue(item.type);
const widgetData = widgetDataValue(item.type, value);
if (["name", "company", "job title", "email"].includes(item.type)) {
return {
...item,
@@ -986,17 +1069,9 @@ export const addInitialData = (signerPos, setXyPosition, value, userId) => {
...item.options,
defaultValue: item?.options?.defaultValue || widgetData
}
// Width:
// calculateInitialWidthHeight(item.type, widgetData).getWidth ||
// item?.Width,
// Height:
// calculateInitialWidthHeight(item.type, widgetData).getHeight ||
// item?.Height
};
} else {
return {
...item
};
return item;
}
}
});
@@ -1004,7 +1079,8 @@ export const addInitialData = (signerPos, setXyPosition, value, userId) => {
//function for embed document id
export const embedDocId = async (pdfDoc, documentId, allPages) => {
const appName = "OpenSign™";
const appName =
"OpenSign™";
// `fontBytes` is used to embed custom font in pdf
const fontBytes = await fileasbytes(
"https://cdn.opensignlabs.com/webfonts/times.ttf"
@@ -1013,7 +1089,8 @@ export const embedDocId = async (pdfDoc, documentId, allPages) => {
const font = await pdfDoc.embedFont(fontBytes, { subset: true });
for (let i = 0; i < allPages; i++) {
const fontSize = 10;
const textContent = documentId && `${appName} DocumentId: ${documentId} `;
const textContent =
documentId && `${appName} DocumentId: ${documentId} `;
const pages = pdfDoc.getPages();
const page = pages[i];
try {
@@ -1838,7 +1915,9 @@ export const placeholderHeight = (pos) => {
};
//function for getting contracts_contactbook details
export const contactBook = async (objectId) => {
export const contactBook = async (
objectId,
) => {
const result = await axios
.get(
`${localStorage.getItem(
@@ -1848,7 +1927,8 @@ export const contactBook = async (objectId) => {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
"X-Parse-Session-Token":
localStorage.getItem("accesstoken")
}
}
)
@@ -1866,9 +1946,12 @@ export const contactBook = async (objectId) => {
};
//function for getting document details from contract_Documents class
export const contractDocument = async (documentId) => {
export const contractDocument = async (
documentId,
) => {
const data = { docId: documentId };
const token = { sessionToken: localStorage.getItem("accesstoken") };
const token =
{ sessionToken: localStorage.getItem("accesstoken") };
const documentDeatils = await axios
.post(`${localStorage.getItem("baseUrl")}functions/getDocument`, data, {
headers: {
@@ -2052,31 +2135,36 @@ export const getFileName = (fileUrl) => {
//fetch tenant app logo from `partners_Tenant` class by domain name
export const getAppLogo = async () => {
const domain = window.location.host;
try {
const tenant = await Parse.Cloud.run("getlogobydomain", {
domain: domain
});
if (tenant) {
localStorage.setItem("appname", "OpenSign™");
return { logo: tenant?.logo, user: tenant?.user };
const domain = window.location.host;
try {
const tenant = await Parse.Cloud.run("getlogobydomain", {
domain: domain
});
if (tenant) {
localStorage.setItem("appname", "OpenSign™");
return { logo: tenant?.logo, user: tenant?.user };
}
} catch (err) {
console.log("err in getlogo ", err);
if (err?.message?.includes("valid JSON")) {
return { logo: appInfo.applogo, user: "exist", error: "invalid_json" };
} else {
return { logo: appInfo.applogo, user: "exist" };
}
}
} catch (err) {
console.log("err in getlogo ", err);
if (err?.message?.includes("valid JSON")) {
return { logo: appInfo.applogo, user: "exist", error: "invalid_json" };
} else {
return { logo: appInfo.applogo, user: "exist" };
}
}
};
export const getTenantDetails = async (objectId, contactId) => {
export const getTenantDetails = async (
objectId,
contactId
) => {
try {
const url = `${localStorage.getItem("baseUrl")}functions/gettenant`;
const parseAppId = localStorage.getItem("parseAppId");
const accesstoken = localStorage.getItem("accesstoken");
const token = { "X-Parse-Session-Token": accesstoken };
const data = { userId: objectId, contactId: contactId };
const token =
{ "X-Parse-Session-Token": accesstoken };
const data =
{ userId: objectId, contactId: contactId };
const res = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
@@ -2156,28 +2244,35 @@ export const handleSendOTP = async (email) => {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
};
const body = { email: email };
const body = {
email: email,
};
await axios.post(url, body, { headers: headers });
} catch (error) {
alert(error.message);
}
};
export const fetchUrl = async (url, pdfName) => {
const appName = "OpenSign™";
const appName =
"OpenSign™";
try {
const response = await fetch(url);
if (!response.ok) {
alert("something went wrong, please try again later.");
alert("something went wrong, refreshing this page may solve this issue.");
throw new Error("Network response was not ok");
}
const blob = await response.blob();
saveAs(blob, `${sanitizeFileName(pdfName)}_signed_by_${appName}.pdf`);
} catch (error) {
alert("something went wrong, please try again later.");
alert("something went wrong, refreshing this page may solve this issue.");
console.error("Error downloading the file:", error);
}
};
export const getSignedUrl = async (pdfUrl, docId, templateId) => {
export const getSignedUrl = async (
pdfUrl,
docId,
templateId
) => {
//use only axios here due to public template sign
const axiosRes = await axios.post(
`${localStorage.getItem("baseUrl")}/functions/getsignedurl`,
@@ -2238,13 +2333,16 @@ export const handleDownloadPdf = async (
setIsDownloading && setIsDownloading("pdf");
const docId = pdfDetails?.[0]?.objectId || "";
try {
const url = await getSignedUrl(pdfUrl, docId);
const url = await getSignedUrl(
pdfUrl,
docId,
);
await fetchUrl(url, pdfName);
setIsDownloading && setIsDownloading("");
} catch (err) {
console.log("err in getsignedurl", err);
setIsDownloading("");
alert("something went wrong, please try again later.");
alert("something went wrong, refreshing this page may solve this issue.");
}
}
};
@@ -2268,7 +2366,7 @@ export const handleToPrint = async (event, setIsDownloading, pdfDetails) => {
`${localStorage.getItem("baseUrl")}/functions/getsignedurl`,
{
url: pdfUrl,
docId: docId
docId: docId,
},
{
headers: {
@@ -2302,7 +2400,7 @@ export const handleToPrint = async (event, setIsDownloading, pdfDetails) => {
} catch (err) {
setIsDownloading("");
console.log("err in getsignedurl", err);
alert("something went wrong, please try again later.");
alert("something went wrong, refreshing this page may solve this issue.");
}
};
@@ -2312,7 +2410,8 @@ export const handleDownloadCertificate = async (
setIsDownloading,
isZip
) => {
const appName = "OpenSign™";
const appName =
"OpenSign™";
if (pdfDetails?.length > 0 && pdfDetails[0]?.CertificateUrl) {
try {
await fetch(pdfDetails[0] && pdfDetails[0]?.CertificateUrl);
@@ -2388,7 +2487,7 @@ export const handleDownloadCertificate = async (
} catch (err) {
setIsDownloading("certificate_err");
console.log("err in download in certificate", err);
alert("something went wrong, please try again later.");
alert("something went wrong, refreshing this page may solve this issue.");
}
}
};
@@ -2396,13 +2495,14 @@ export const handleDownloadCertificate = async (
export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // Escape special characters
}
export async function findContact(value) {
export async function findContact(
value,
) {
try {
const baseURL = localStorage.getItem("baseUrl");
const url = `${baseURL}functions/getsigners`;
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
@@ -2614,20 +2714,23 @@ export function base64ToArrayBuffer(base64) {
return bytes.buffer;
}
export const convertBase64ToFile = async (pdfName, pdfBase64) => {
export const convertBase64ToFile = async (
pdfName,
pdfBase64,
) => {
const fileName = sanitizeFileName(pdfName) + ".pdf";
try {
const pdfFile = new Parse.File(fileName, { base64: pdfBase64 });
// Save the Parse File if needed
const pdfData = await pdfFile.save();
const pdfUrl = pdfData.url();
const fileRes = await getSecureUrl(pdfUrl);
if (fileRes?.url) {
return fileRes.url;
try {
const pdfFile = new Parse.File(fileName, { base64: pdfBase64 });
// Save the Parse File if needed
const pdfData = await pdfFile.save();
const pdfUrl = pdfData.url();
const fileRes = await getSecureUrl(pdfUrl);
if (fileRes?.url) {
return fileRes.url;
}
} catch (e) {
console.log("error in convertbase64tofile", e);
}
} catch (e) {
console.log("error in convertbase64tofile", e);
}
};
export const onClickZoomIn = (scale, zoomPercent, setScale, setZoomPercent) => {
setScale(scale + 0.1 * scale);
@@ -2845,20 +2948,11 @@ export function generatePdfName(length) {
// Format date and time for the selected timezone
export const formatTimeInTimezone = (date, timezone) => {
return timezone
? new Intl.DateTimeFormat("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZone: timezone,
timeZoneName: "short",
hour12: false
}).format(date)
const nyDate = timezone && toZonedTime(date, timezone);
const generatedDate = timezone
? format(nyDate, "EEE, dd MMM yyyy HH:mm:ss zzz", { timeZone: timezone })
: new Date(date).toUTCString();
return generatedDate;
};
// `usertimezone` is used to get timezone of current user
@@ -2926,10 +3020,13 @@ export const flattenPdf = async (pdfFile) => {
};
export const mailTemplate = (param) => {
const appName = "OpenSign™";
const logo = `<div style='padding:10px'><img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' /></div>`;
const appName =
"OpenSign™";
const logo =
`<div style='padding:10px'><img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' /></div>`;
const opurl = ` <a href='https://www.opensignlabs.com' target=_blank>here</a>.</p></div></div></body></html>`;
const opurl =
` <a href='https://www.opensignlabs.com' target=_blank>here</a>.</p></div></div></body></html>`;
const subject = `${param.senderName} has requested you to sign "${param.title}"`;
const body =
@@ -2943,8 +3040,10 @@ export const mailTemplate = (param) => {
param.senderMail +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Organization</td><td></td><td style='color:#626363;font-weight:bold'> " +
param.organization +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Expire on</td><td></td><td style='color:#626363;font-weight:bold'>" +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Expires on</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.localExpireDate +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Note</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.note +
"</td></tr><tr><td></td><td></td></tr></table></div> <div style='margin-left:70px'><a target=_blank href=" +
param.sigingUrl +
"><button style='padding:12px;background-color:#d46b0f;color:white;border:0px;font-weight:bold;margin-top:30px'>Sign here</button></a></div><div style='display:flex;justify-content:center;margin-top:10px'></div></div></div><div><p> This is an automated email from " +
@@ -2957,3 +3056,64 @@ export const mailTemplate = (param) => {
return { subject, body };
};
export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
const zonedDate = toZonedTime(date, timeZone); // Convert date to the given timezone
const timeFormat = is12Hour ? "hh:mm:ss a" : "HH:mm:ss";
return dateFormat
? format(
zonedDate,
`${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`,
{ timeZone }
)
: formatTimeInTimezone(date, timeZone);
}
export const updateDateWidgetsRes = (documentData, signerId, journey) => {
const extUser =
localStorage.getItem("Extand_Class");
const contactUser = documentData?.Signers.find(
(data) => data.objectId === signerId
);
const placeHolders = documentData?.Placeholders;
const userDetails = extUser ? JSON.parse(extUser)[0] : contactUser;
return placeHolders?.map((item) => {
if (item?.signerObjId === signerId) {
return {
...item,
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 (
["name", "email", "job title", "company"].includes(widget.type) &&
!widget.options.defaultValue &&
!widget.options.response
) {
return {
...widget,
options: {
...widget.options,
response: widgetDataValue(widget.type, userDetails)
}
};
}
return widget;
})
}))
};
} else {
return item;
}
});
};
+3 -8
View File
@@ -1,11 +1,9 @@
import logo from "../assets/images/logo.png";
export function serverUrl_fn() {
let baseUrl;
baseUrl = process.env.REACT_APP_SERVERURL
? process.env.REACT_APP_SERVERURL
: window.location.origin + "/api/app";
let baseUrl = process.env.REACT_APP_SERVERURL
? process.env.REACT_APP_SERVERURL
: window.location.origin + "/api/app";
return baseUrl;
}
export const appInfo = {
@@ -13,9 +11,6 @@ export const appInfo = {
appId: process.env.REACT_APP_APPID ? process.env.REACT_APP_APPID : "opensign",
baseUrl: serverUrl_fn(),
defaultRole: "contracts_User",
fbAppId: process.env.REACT_APP_FBAPPID
? `${process.env.REACT_APP_FBAPPID}`
: "",
fev_Icon:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAALlJREFUaEPtmN0NwjAMBpNxYDKYiM1Yp90g93CKStH1NbIdfz921Dker2Pc+Js1cDF7MXAxASMGYkAigBI6vh9ZwoXP53uZoAYcvhwdA3mAVbI2aSb+9zFKU4IURB6j/HoPUIEa2G3iGIAhQQDlAUIoE2diabIklISS0NoFPebo77RF6OenEF3QntOm128he0GKrwHyACFoz2MgBqSGkpAEcHs47oHtN5AFakACqMNjQEMoE8SABFCHn4HE2zGHSLeEAAAAAElFTkSuQmCC",
googleClietId: process.env.REACT_APP_GOOGLECLIENTID
+4
View File
@@ -4,3 +4,7 @@ export const documentCls = "contracts_Document";
export const themeColor = "#47a3ad";
export const iconColor = "#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
-4
View File
@@ -1,4 +0,0 @@
export default function linkURL(url) {
let newURL = (url && decodeURIComponent(url)) || "";
return newURL.substring(newURL.indexOf(".com/") + 5, newURL.indexOf("_"));
}
@@ -13,16 +13,13 @@ import {
MouseTransition,
Preview
} from "react-dnd-multi-backend";
import DragElement from "./components/pdf/DragElement";
import TagManager from "react-gtm-module";
import DragElement from "./components/pdf/DragElement.jsx";
import Parse from "parse";
import "./polyfills";
import { serverUrl_fn } from "./constant/appinfo";
import "./i18n";
const appId = process.env.REACT_APP_APPID
? process.env.REACT_APP_APPID
: "opensign";
const appId = import.meta.env.VITE_APPID || process.env.REACT_APP_APPID || "opensign";
const serverUrl = serverUrl_fn();
Parse.initialize(appId);
Parse.serverURL = serverUrl;
@@ -56,12 +53,6 @@ const generatePreview = (props) => {
);
};
if (process.env.REACT_APP_GTM) {
const tagManagerArgs = {
gtmId: process.env.REACT_APP_GTM
};
TagManager.initialize(tagManagerArgs);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
+108 -1
View File
@@ -1,4 +1,3 @@
export default function reportJson(id) {
// console.log("json ", json);
const head = ["Title", "Note", "Folder", "File", "Owner", "Signers"];
@@ -29,6 +28,25 @@ export default function reportJson(id) {
btnIcon: "fa-light fa-trash",
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:
@@ -107,6 +125,14 @@ export default function reportJson(id) {
redirectUrl: "",
action: "revoke"
},
{
btnId: "0630",
btnLabel: "Save as template",
hoverLabel: "Save as template",
btnIcon: "fa-light fa-envelope",
redirectUrl: "",
action: "saveastemplate"
},
{
btnId: "1488",
btnLabel: "Delete",
@@ -143,6 +169,25 @@ export default function reportJson(id) {
restrictBtn: true,
redirectUrl: "",
action: "delete"
},
{
btnId: "33534",
hoverLabel: "option",
btnColor: "",
restrictBtn: true,
textColor: "black",
btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
action: "option",
subaction: [
{
btnId: "0930",
btnLabel: "Save as template",
hoverLabel: "Save as template",
btnIcon: "fa-light fa-envelope",
redirectUrl: "",
action: "saveastemplate"
}
]
}
],
helpMsg:
@@ -169,6 +214,33 @@ export default function reportJson(id) {
btnIcon: "fa-light fa-trash",
redirectUrl: "",
action: "delete"
},
{
btnId: "44534",
hoverLabel: "option",
btnColor: "",
textColor: "black",
btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
action: "option",
subaction: [
{
btnId: "0940",
btnLabel: "Save as template",
hoverLabel: "Save as template",
restrictBtn: true,
btnIcon: "fa-light fa-envelope",
redirectUrl: "",
action: "saveastemplate"
},
{
btnId: "8440",
btnLabel: "Fix & resend",
hoverLabel: "Fix & resend",
btnIcon: "fa-light fa-paper-plane",
redirectUrl: "",
action: "recreatedocument"
}
]
}
],
helpMsg:
@@ -212,6 +284,14 @@ export default function reportJson(id) {
btnIcon: "fa-light fa-hourglass-end",
redirectUrl: "",
action: "extendexpiry"
},
{
btnId: "5530",
btnLabel: "Save as template",
hoverLabel: "Save as template",
btnIcon: "fa-light fa-envelope",
redirectUrl: "",
action: "saveastemplate"
}
]
}
@@ -273,6 +353,14 @@ export default function reportJson(id) {
redirectUrl: "",
action: "revoke"
},
{
btnId: "7730",
btnLabel: "Save as template",
hoverLabel: "Save as template",
btnIcon: "fa-light fa-envelope",
redirectUrl: "",
action: "saveastemplate"
},
{
btnId: "2000",
btnLabel: "Delete",
@@ -323,6 +411,25 @@ export default function reportJson(id) {
btnIcon: "fa-light fa-trash",
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"
}
]
}
]
};
+1 -1
View File
@@ -36,7 +36,7 @@ const dashboardJson = [
queryType: "",
class: "contracts_Document",
query:
'where={"Type":{"#*ne":"Folder"},"Signers":{"#*exists":true,"#*ne":[]},"Placeholders":{"#*ne":null},"SignedUrl":{"#*ne":null},"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',
'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',
key: "count",
Redirect_type: "Report",
Redirect_id: "1MwEuxLEkF",
@@ -72,7 +72,7 @@ const HomeLayout = () => {
//function to use save data in cookies storage
const saveCookies = () => {
const main_Domain = window.location.origin;
const domainName = window.location.hostname; //app.opensignlabs.com
const domainName = window.location.hostname;
// Find the index of the first dot in the string
const indexOfFirstDot = domainName.indexOf(".");
// Remove the first dot and get the substring starting from the next character
@@ -96,7 +96,7 @@ const AddAdmin = () => {
setState({ loading: true });
const userDetails = {
name: name,
email: email,
email: email?.toLowerCase()?.replace(/\s/g, ""),
phone: phone,
company: company,
jobTitle: jobTitle
@@ -106,10 +106,10 @@ const AddAdmin = () => {
event.preventDefault();
const user = new Parse.User();
user.set("name", name);
user.set("email", email);
user.set("email", email?.toLowerCase()?.replace(/\s/g, ""));
user.set("password", password);
user.set("phone", phone);
user.set("username", email);
user.set("username", email?.toLowerCase()?.replace(/\s/g, ""));
const userRes = await user.save();
if (userRes) {
const params = {
@@ -117,7 +117,7 @@ const AddAdmin = () => {
jobTitle: jobTitle,
company: company,
name: name,
email: email,
email: email?.toLowerCase()?.replace(/\s/g, ""),
phone: phone,
role: "contracts_Admin",
timezone: usertimezone
@@ -176,7 +176,6 @@ const AddAdmin = () => {
localStorage.setItem("accesstoken", sessionToken);
localStorage.setItem("UserInformation", JSON.stringify(_user));
localStorage.setItem("accesstoken", _user.sessionToken);
localStorage.setItem("scriptId", true);
if (_user.ProfilePic) {
localStorage.setItem("profileImg", _user.ProfilePic);
} else {
-140
View File
@@ -1,140 +0,0 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
getBase64FromUrl,
handleDownloadCertificate,
handleDownloadPdf,
handleToPrint
} from "../constant/Utils";
import ModalUi from "../primitives/ModalUi";
import Loader from "../primitives/Loader";
import DownloadPdfZip from "../primitives/DownloadPdfZip";
import Title from "../components/Title";
const DocSuccessPage = () => {
const signed = window.location?.search?.includes("docid");
const sent = window.location?.search?.includes("message");
const [isDownloading, setIsDownloading] = useState(false);
const [isDownloadModal, setIsDownloadModal] = useState(false);
const [pdfDetails, setPdfDetails] = useState([]);
const [pdfBase64Url, setPdfBase64Url] = useState("");
const { t } = useTranslation();
useEffect(() => {
initialsetup();
}, []);
const initialsetup = async () => {
const search = window.location.search.split("?")[1];
if (search) {
const urlParams = new URLSearchParams(search);
const docId = urlParams.get("docid");
const docUrl = urlParams.get("docurl");
const certificate = urlParams.get("certificate");
const completed = urlParams?.get("completed") || false;
const details = {
objectId: docId,
SignedUrl: docUrl,
CertificateUrl: certificate,
IsCompleted: completed
};
setPdfDetails([details]);
const base64Pdf = await getBase64FromUrl(docUrl);
if (base64Pdf) {
setPdfBase64Url(base64Pdf);
}
}
};
const handleDownload = () => {
if (pdfDetails?.[0]?.IsCompleted) {
setIsDownloadModal(true);
} else {
handleDownloadPdf(pdfDetails, setIsDownloading, pdfBase64Url);
}
};
return (
<div className="flex h-screen justify-center items-center text-sm md:text-base">
<Title title="Success" />
{sent ? (
<div>{t("doc-sent")}</div>
) : signed ? (
<div className="text-center">
<p>
{pdfDetails?.[0]?.IsCompleted
? t("document-signed-alert-4")
: t("document-signed-alert")}
</p>
<div className="m-2">
<button
onClick={(e) => handleToPrint(e, setIsDownloading, pdfDetails)}
type="button"
className="font-[500] text-[13px] mr-[5px] op-btn op-btn-neutral"
>
<i className="fa-light fa-print" aria-hidden="true"></i>
<span className="hidden lg:block">{t("print")}</span>
</button>
{pdfDetails?.[0]?.IsCompleted && (
<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={() => handleDownload()}
>
<i className="fa-light fa-download" aria-hidden="true"></i>
<span className="hidden lg:block">{t("download")}</span>
</button>
</div>
{isDownloading === "pdf" && (
<div className="fixed z-[1000] inset-0 flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
<ModalUi
isOpen={
isDownloading === "certificate" ||
isDownloading === "certificate_err"
}
title={
isDownloading === "certificate" ||
isDownloading === "certificate_err"
? t("generating-certificate")
: t("pdf-download")
}
handleClose={() => setIsDownloading("")}
>
<div className="p-3 md:p-5 text-[13px] md:text-base text-center text-base-content">
{isDownloading === "certificate" ? (
<p>{t("generate-certificate-alert")}</p>
) : (
<p>{t("generate-certificate-err")}</p>
)}
</div>
</ModalUi>
<DownloadPdfZip
setIsDownloadModal={setIsDownloadModal}
isDownloadModal={isDownloadModal}
pdfDetails={pdfDetails}
isDocId={true}
pdfBase64={pdfBase64Url}
/>
</div>
) : (
<></>
)}
</div>
);
};
export default DocSuccessPage;
+177
View File
@@ -0,0 +1,177 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Confetti from "react-confetti"; // Import the confetti library
import {
getBase64FromUrl,
handleDownloadCertificate,
handleDownloadPdf,
handleToPrint,
} from "../constant/Utils";
import ModalUi from "../primitives/ModalUi";
import Loader from "../primitives/Loader";
import DownloadPdfZip from "../primitives/DownloadPdfZip";
import Title from "../components/Title";
import CheckCircle from "../primitives/CheckCircle";
const DocSuccessPage = () => {
const { t } = useTranslation();
const signed = window.location?.search?.includes("docid");
const sent = window.location?.search?.includes("message");
const [isDownloading, setIsDownloading] = useState(false);
const [isDownloadModal, setIsDownloadModal] = useState(false);
const [pdfDetails, setPdfDetails] = useState([]);
const [pdfBase64Url, setPdfBase64Url] = useState("");
const [showConfetti, setShowConfetti] = useState(true); // State to control confetti
useEffect(() => {
initialsetup();
// Stop confetti after 5 seconds
const timer = setTimeout(() => setShowConfetti(false), 5000);
return () => clearTimeout(timer);
}, []);
const initialsetup = async () => {
const search = window.location.search.split("?")[1];
if (search) {
const urlParams = new URLSearchParams(search);
const docId = urlParams.get("docid");
const docUrl = urlParams.get("docurl");
const certificate = urlParams.get("certificate");
const completed = urlParams?.get("completed") || false;
const details = {
objectId: docId,
SignedUrl: docUrl,
CertificateUrl: certificate,
IsCompleted: completed
};
setPdfDetails([details]);
const base64Pdf = await getBase64FromUrl(docUrl);
if (base64Pdf) {
setPdfBase64Url(base64Pdf);
}
}
};
const handleDownload = () => {
if (pdfDetails?.[0]?.IsCompleted) {
setIsDownloadModal(true);
} else {
handleDownloadPdf(pdfDetails, setIsDownloading, pdfBase64Url);
}
};
return (
<>
<Title title="Success" />
{/* Confetti Effect */}
{showConfetti && (
<Confetti width={window.innerWidth} height={window.innerHeight} />
)}
{sent ? (
<div className="min-h-screen flex flex-col items-center justify-center p-3 md:p-8 text-center">
<div className="max-w-lg md:max-w-2xl bg-white rounded-lg shadow-lg p-3 md:p-10">
{t("doc-sent")}
</div>
</div>
) : signed ? (
<>
<div className="min-h-screen flex flex-col items-center justify-center p-3 md:p-8 text-center">
<div className="max-w-lg md:max-w-2xl bg-white rounded-lg shadow-lg p-3 md:p-10">
<div className="flex flex-col items-center space-y-4 ">
<CheckCircle className="text-green-500 w-12 h-12 md:w-14 md:h-14" />
<h1 className="text-xl md:text-2xl font-semibold text-gray-800">
{pdfDetails?.[0]?.IsCompleted
? t("document-has-been-signed")
: t("document-has-been-signed-by-you")}
</h1>
{pdfDetails?.[0]?.IsCompleted && (
<p className="text-sm md:text-base text-gray-600">
{t("participant-completed-signing")}
</p>
)}
</div>
{/* Action Buttons */}
<div className="mt-6 flex flex-wrap justify-center gap-2">
<button
type="button"
className="font-medium text-sm md:text-[13px] md:px-4 py-2 op-btn op-btn-primary"
onClick={() => handleDownload()}
>
<i className="fa-light fa-download" aria-hidden="true"></i>
<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>
)}
<button
onClick={(e) =>
handleToPrint(e, setIsDownloading, pdfDetails)
}
type="button"
className="font-medium text-sm md:text-[13px] px-4 py-2 op-btn op-btn-neutral"
>
<i className="fa-light fa-print" aria-hidden="true"></i>
<span>{t("print")}</span>
</button>
</div>
{/* Footer Message */}
<p className="mt-4 md:mt-6 text-xs md:text-sm text-gray-500">
{t("you-will-receive-email-shortly")}
</p>
</div>
</div>
{isDownloading === "pdf" && (
<div className="fixed z-[1000] inset-0 flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
<ModalUi
isOpen={
isDownloading === "certificate" ||
isDownloading === "certificate_err"
}
title={
isDownloading === "certificate" ||
isDownloading === "certificate_err"
? t("generating-certificate")
: t("pdf-download")
}
handleClose={() => setIsDownloading("")}
>
<div className="p-3 md:p-5 text-sm md:text-base text-center text-base-content">
{isDownloading === "certificate" ? (
<p>{t("generate-certificate-alert")}</p>
) : (
<p>{t("generate-certificate-err")}</p>
)}
</div>
</ModalUi>
<DownloadPdfZip
setIsDownloadModal={setIsDownloadModal}
isDownloadModal={isDownloadModal}
pdfDetails={pdfDetails}
isDocId={true}
pdfBase64={pdfBase64Url}
/>
</>
) : (
<></>
)}
</>
);
};
export default DocSuccessPage;
@@ -11,13 +11,15 @@ import {
emailRegex,
} from "../constant/const";
import { useTranslation } from "react-i18next";
import Loader from "../primitives/Loader";
function ForgotPassword() {
const { t } = useTranslation();
const dispatch = useDispatch();
const navigate = useNavigate();
const [state, setState] = useState({ email: "", password: "", hideNav: "" });
const [sentStatus, setSentStatus] = useState("");
const [toast, setToast] = useState({ type: "", message: "" });
const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState();
const handleChange = (event) => {
@@ -40,18 +42,23 @@ function ForgotPassword() {
if (!emailRegex.test(state.email)) {
alert("Please enter a valid email address.");
} else {
setIsLoading(true);
localStorage.setItem("appLogo", appInfo.applogo);
localStorage.setItem("userSettings", JSON.stringify(appInfo.settings));
if (state.email) {
const username = state.email;
try {
await Parse.User.requestPasswordReset(username);
setSentStatus("success");
await Parse.User.requestPasswordReset(username);
setToast({ type: "success", message: t("reset-password-alert-1") });
} catch (err) {
console.log("err ", err.code);
setSentStatus("failed");
setToast({
type: "danger",
message: err.message || t("reset-password-alert-2")
});
} finally {
setTimeout(() => setSentStatus(""), 1000);
setIsLoading(false);
setTimeout(() => setToast({ type: "", message: "" }), 1000);
}
}
}
@@ -75,13 +82,13 @@ function ForgotPassword() {
};
return (
<div>
{isLoading && (
<div className="fixed w-full h-full flex justify-center items-center bg-black bg-opacity-30 z-50">
<Loader />
</div>
)}
<Title title="Forgot password" />
{sentStatus === "success" && (
<Alert type="success">{t("reset-password-alert-1")}</Alert>
)}
{sentStatus === "failed" && (
<Alert type={"danger"}>{t("reset-password-alert-2")}</Alert>
)}
{toast?.message && <Alert type={toast.type}>{toast.message}</Alert>}
<div className="md:p-10 lg:p-16">
<div className="md:p-4 lg:p-10 p-4 bg-base-100 text-base-content op-card">
<div className="w-[250px] h-[66px] inline-block overflow-hidden">
@@ -18,6 +18,12 @@ import {
} from "../constant/Utils";
import { PDFDocument } from "pdf-lib";
import axios from "axios";
import {
maxFileSize,
maxDescriptionLength,
maxNoteLength,
maxTitleLength
} from "../constant/const";
import ModalUi from "../primitives/ModalUi";
import { Tooltip } from "react-tooltip";
import Loader from "../primitives/Loader";
@@ -36,9 +42,9 @@ function Form() {
}
const Forms = (props) => {
const appName = "OpenSign™";
const appName =
"OpenSign™";
const { t } = useTranslation();
const maxFileSize = 20;
const abortController = new AbortController();
const inputFileRef = useRef(null);
const navigate = useNavigate();
@@ -100,12 +106,12 @@ const Forms = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initializeValues = async () => {
setFormData((obj) => ({
...obj,
NotifyOnSignatures: true,
SendinOrder: sendinorder,
IsTourEnabled: istourenabled
}));
setFormData((obj) => ({
...obj,
NotifyOnSignatures: true,
SendinOrder: sendinorder,
IsTourEnabled: istourenabled
}));
};
function getFileAsArrayBuffer(file) {
@@ -143,88 +149,17 @@ const Forms = (props) => {
const name = generatePdfName(16);
const pdfName = `${name?.split(".")[0]}.pdf`;
setfileload(true);
try {
const res = await getFileAsArrayBuffer(files[0]);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(
pdfName,
[...flatPdf],
"application/pdf"
);
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
}
} else {
removeFile(e);
}
} catch (error) {
removeFile(e);
console.error("Error uploading file:", error);
}
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
await Parse.Cloud.run("encryptedpdf", {
email: Parse.User.current().getEmail()
});
} catch (err) {
console.log("err in sending posthog encryptedpdf", err);
}
try {
setIsDecrypting(true);
const size = files?.[0].size;
const name = generatePdfName(16);
const url = "https://ai.nxglabs.in/decryptpdf"; //
let formData = new FormData();
formData.append("file", files[0]);
formData.append("password", "");
const config = {
headers: { "content-type": "multipart/form-data" },
responseType: "blob"
};
const response = await axios.post(url, formData, config);
const pdfBlob = new Blob([response.data], {
type: "application/pdf"
});
const pdfFile = new File([pdfBlob], name, {
type: "application/pdf"
});
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
const parseFile = new Parse.File(
name,
[...flatPdf],
"application/pdf"
);
const res = await getFileAsArrayBuffer(files[0]);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(
pdfName,
[...flatPdf],
"application/pdf"
);
await parseFile.save({
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
@@ -234,16 +169,16 @@ const Forms = (props) => {
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
@@ -252,22 +187,88 @@ const Forms = (props) => {
} else {
removeFile(e);
}
} catch (err) {
removeFile();
if (err?.response?.status === 401) {
setIsPassword(true);
} else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
}
} catch (error) {
removeFile(e);
console.error("Error uploading file:", error);
}
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
setIsDecrypting(true);
const size = files?.[0].size;
const name = generatePdfName(16);
const url = "https://ai.nxglabs.in/decryptpdf"; //
let formData = new FormData();
formData.append("file", files[0]);
formData.append("password", "");
const config = {
headers: { "content-type": "multipart/form-data" },
responseType: "blob"
};
const response = await axios.post(url, formData, config);
const pdfBlob = new Blob([response.data], {
type: "application/pdf"
});
const pdfFile = new File([pdfBlob], name, {
type: "application/pdf"
});
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
const parseFile = new Parse.File(
name,
[...flatPdf],
"application/pdf"
);
await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(
files?.[0]?.name
);
setFormData((obj) => ({ ...obj, Name: title }));
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
}
} else {
removeFile(e);
}
} catch (err) {
removeFile();
if (err?.response?.status === 401) {
setIsPassword(true);
} else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
}
}
} else {
console.log("err ", err);
setFileUpload("");
removeFile(e);
}
} else {
console.log("err ", err);
setFileUpload("");
removeFile(e);
}
}
} else {
const isImage = files?.[0]?.type.includes("image/");
if (isImage) {
@@ -292,50 +293,50 @@ const Forms = (props) => {
});
const size = files?.[0]?.size;
const name = generatePdfName(16);
const getFile = await pdfDoc.save({
useObjectStreams: false
});
setfileload(true);
const pdfName = `${name?.split(".")[0]}.pdf`;
const parseFile = new Parse.File(
pdfName,
[...getFile],
"application/pdf"
);
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
const getFile = await pdfDoc.save({
useObjectStreams: false
});
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
setfileload(true);
const pdfName = `${name?.split(".")[0]}.pdf`;
const parseFile = new Parse.File(
pdfName,
[...getFile],
"application/pdf"
);
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
}
} else {
removeFile(e);
}
} else {
} catch (error) {
removeFile(e);
console.error("Error uploading file:", error);
}
} catch (error) {
removeFile(e);
console.error("Error uploading file:", error);
}
}
}
}
@@ -361,6 +362,18 @@ const Forms = (props) => {
e.preventDefault();
e.stopPropagation();
if (fileupload) {
if (formData?.Name?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
if (formData?.Note?.length > maxNoteLength) {
alert(t("note-length-alert"));
return;
}
if (formData?.Description?.length > maxDescriptionLength) {
alert(t("description-length-alert"));
return;
}
if (formData.RedirectUrl && !isValidURL(formData?.RedirectUrl)) {
alert(t("invalid-redirect-url"));
return;
@@ -398,11 +411,11 @@ const Forms = (props) => {
object.set("RemindOnceInEvery", remindOnceInEvery);
object.set("IsTourEnabled", isTourEnabled);
object.set("TimeToCompleteDays", TimeToCompleteDays);
object.set("AllowModifications", false);
object.set("IsEnableOTP", false);
if (formData.NotifyOnSignatures !== undefined) {
object.set("NotifyOnSignatures", formData.NotifyOnSignatures);
}
object.set("AllowModifications", false);
object.set("IsEnableOTP", false);
if (formData.NotifyOnSignatures !== undefined) {
object.set("NotifyOnSignatures", formData.NotifyOnSignatures);
}
if (formData?.RedirectUrl) {
object.set("RedirectUrl", formData.RedirectUrl);
}
@@ -441,9 +454,10 @@ const Forms = (props) => {
setSigners([]);
setBcc([]);
setFolder({ ObjectId: "", Name: "" });
const notifySign = extUserData?.NotifyOnSignatures
? extUserData?.NotifyOnSignatures
: true;
const notifySign =
extUserData?.NotifyOnSignatures
? extUserData?.NotifyOnSignatures
: true;
setFormData({
Name: "",
Description: "",
@@ -519,9 +533,10 @@ const Forms = (props) => {
setSigners([]);
setBcc([]);
setFolder({ ObjectId: "", Name: "" });
const notifySign = extUserData?.NotifyOnSignatures
? extUserData?.NotifyOnSignatures
: true;
const notifySign =
extUserData?.NotifyOnSignatures
? extUserData?.NotifyOnSignatures
: true;
let obj = {
Name: "",
Description: "",
@@ -575,28 +590,36 @@ const Forms = (props) => {
type: "application/pdf"
});
setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(name, [...flatPdf], "application/pdf");
await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(name, [...flatPdf], "application/pdf");
await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(formData?.file?.name);
setFormData((obj) => ({ ...obj, password: "", Name: title }));
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
setIsDecrypting(false);
if (inputFileRef.current) {
inputFileRef.current.value = ""; // Set file input value to empty string
}
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(formData?.file?.name);
setFormData((obj) => ({ ...obj, password: "", Name: title }));
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
@@ -605,14 +628,6 @@ const Forms = (props) => {
inputFileRef.current.value = ""; // Set file input value to empty string
}
}
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
setIsDecrypting(false);
if (inputFileRef.current) {
inputFileRef.current.value = ""; // Set file input value to empty string
}
}
} catch (err) {
removeFile();
if (err?.response?.status === 401) {
@@ -734,7 +749,9 @@ const Forms = (props) => {
)}
<div className="text-xs">
<label className="block">
{`${`${t("report-heading.File")} (${t("file-type")}`}${")"}`}
{`${`${t("report-heading.File")} (${t("file-type")}`}${
")"
}`}
<span className="text-red-500 text-[13px]">*</span>
</label>
{fileupload.length > 0 ? (
@@ -758,7 +775,9 @@ const Forms = (props) => {
className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs"
onChange={(e) => handleFileInput(e)}
ref={inputFileRef}
accept={"application/pdf,image/png,image/jpeg"}
accept={
"application/pdf,image/png,image/jpeg"
}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
@@ -859,7 +878,7 @@ const Forms = (props) => {
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("send-in-order")}</p>
<p>{t("send-in-order-help.p1")}</p>
<p className="p-[5px]">
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">
@@ -874,7 +893,7 @@ const Forms = (props) => {
<span>{t("send-in-order-help.p3")}</span>
</li>
</ol>
</p>
</div>
<p>{t("send-in-order-help.p4")}</p>
</div>
</Tooltip>
@@ -956,7 +975,7 @@ const Forms = (props) => {
{t("send-in-order")}
</p>
<p>{t("send-in-order-help.p1")}</p>
<p className="p-[5px]">
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">
@@ -975,7 +994,7 @@ const Forms = (props) => {
</span>
</li>
</ol>
</p>
</div>
<p>{t("send-in-order-help.p4")}</p>
</div>
</Tooltip>
@@ -1048,7 +1067,10 @@ const Forms = (props) => {
{isAdvanceOpt && (
<div
style={{
height: props.title === "New Template" ? "100px" : "280px"
height:
props.title === "New Template"
? "100px"
: "280px"
}}
className="w-[1px] bg-gray-300 m-auto hidden md:inline-block"
></div>
@@ -1093,7 +1115,7 @@ const Forms = (props) => {
<Tooltip id="istourenabled-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<p className="p-[5px]">
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">
@@ -1106,7 +1128,7 @@ const Forms = (props) => {
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
</p>
</div>
<p>
{t("istourenabled-help.p3", { appName: appName })}
</p>
@@ -1157,7 +1179,11 @@ const Forms = (props) => {
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
@@ -1166,7 +1192,11 @@ const Forms = (props) => {
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
@@ -15,12 +15,16 @@ import { useTranslation } from "react-i18next";
import SelectLanguage from "../components/pdf/SelectLanguage";
import LoaderWithMsg from "../primitives/LoaderWithMsg";
import Title from "../components/Title";
import ModalUi from "../primitives/ModalUi";
import Loader from "../primitives/Loader";
function GuestLogin() {
const { t, i18n } = useTranslation();
const { id, userMail, contactBookId, base64url } = useParams();
const navigate = useNavigate();
const [email, setEmail] = useState(userMail);
const [email, setEmail] = useState(
userMail?.toLowerCase()?.replace(/\s/g, "")
);
const [OTP, setOTP] = useState("");
const [EnterOTP, setEnterOtp] = useState(false);
const [loading, setLoading] = useState(false);
@@ -33,6 +37,8 @@ function GuestLogin() {
const [contactId, setContactId] = useState(contactBookId);
const [sendmail, setSendmail] = useState();
const [contact, setContact] = useState({ name: "", phone: "", email: "" });
const navigateToDoc = async (docId, contactId) => {
try {
const docDetails = await Parse.Cloud.run("getDocument", {
@@ -56,11 +62,13 @@ function GuestLogin() {
return false;
}
};
useEffect(() => {
handleServerUrl();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//function generate serverUrl and parseAppId from url and save it in local storage
const handleServerUrl = async () => {
setAppLogo(logo);
@@ -84,12 +92,18 @@ function GuestLogin() {
//split url in array from '/'
const checkSplit = decodebase64.split("/");
setDocumentId(checkSplit[0]);
setContact((prev) => ({ ...prev, email: checkSplit[1] }));
setEmail(checkSplit[1]);
setContact((prev) => ({
...prev,
email: checkSplit[1]?.toLowerCase()?.replace(/\s/g, "")
}));
setEmail(checkSplit[1]?.toLowerCase()?.replace(/\s/g, ""));
const contactId = checkSplit?.[2];
setSendmail(checkSplit[3]);
if (!contactId) {
const params = { email: checkSplit[1], docId: checkSplit[0] };
const params = {
email: checkSplit[1]?.toLowerCase()?.replace(/\s/g, ""),
docId: checkSplit[0]
};
try {
const linkContactRes = await Parse.Cloud.run(
"linkcontacttodoc",
@@ -111,9 +125,12 @@ function GuestLogin() {
//send email OTP function
const SendOtp = async () => {
setLoading(true);
setEmail(email);
setEmail(email?.toLowerCase()?.replace(/\s/g, ""));
try {
const params = { email: email.toString(), docId: documentId };
const params = {
email: email?.toLowerCase()?.replace(/\s/g, "")?.toString(),
docId: documentId,
};
const Otp = await Parse.Cloud.run("SendOTPMailV1", params);
if (Otp) {
setLoading(false);
@@ -121,6 +138,7 @@ function GuestLogin() {
}
} catch (error) {
alert(t("something-went-wrong-mssg"));
setLoading(false);
}
};
@@ -144,7 +162,10 @@ function GuestLogin() {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseId
};
let body = { email: email, otp: OTP };
let body = {
email: email?.toLowerCase()?.replace(/\s/g, ""),
otp: OTP
};
let user = await axios.post(url, body, { headers: headers });
if (user.data.result === "Invalid Otp") {
alert(t("invalid-otp"));
@@ -182,14 +203,17 @@ function GuestLogin() {
}
} catch (error) {
console.log("err ", error);
setLoading(false);
}
} else {
alert(t("enter-otp-alert"));
}
};
const handleUserData = async (e) => {
e.preventDefault();
if (!emailRegex.test(contact.email)) {
if (!emailRegex.test(contact.email?.toLowerCase()?.replace(/\s/g, ""))) {
alert("Please enter a valid email address.");
} else {
const params = { ...contact, docId: documentId };
@@ -215,12 +239,67 @@ function GuestLogin() {
}
}
};
const handleInputChange = (e) => {
setContact((prev) => ({ ...prev, [e.target.name]: e.target.value }));
if (e.target.name === "email") {
setContact((prev) => ({
...prev,
[e.target.name]: e.target.value?.toLowerCase()?.replace(/\s/g, "")
}));
} else {
setContact((prev) => ({ ...prev, [e.target.name]: e.target.value }));
}
};
return (
<div>
<Title title="Request Sign" />
{/* OTP Verification Modal */}
{EnterOTP && (
<ModalUi
isOpen
title={t("otp-verification")}
handleClose={() => setEnterOtp(false)}
>
{loading ? (
<div className="h-[150px] flex justify-center items-center">
<Loader />
</div>
) : (
<form onSubmit={(e) => VerifyOTP(e)}>
<div className="px-6 py-3 text-base-content">
<label className="mb-2">{t("enter-otp")}</label>
<input
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
required
type="tel"
pattern="[0-9]{4}"
className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs"
placeholder={t("otp-placeholder")}
value={OTP}
onChange={(e) => setOTP(e.target.value)}
/>
</div>
<div className="px-6 mb-3">
<button type="submit" className="op-btn op-btn-primary">
{t("verify")}
</button>
<button
className="op-btn op-btn-secondary ml-2"
onClick={(e) => handleSendOTPBtn(e)}
>
{t("resend")}
</button>
</div>
</form>
)}
</ModalUi>
)}
{isLoading.isLoad ? (
<LoaderWithMsg isLoading={isLoading} />
) : (
@@ -236,63 +315,34 @@ function GuestLogin() {
)}
</div>
{contactId ? (
<>
{!EnterOTP ? (
<div className="w-full md:w-[50%] text-base-content">
<h1 className="text-2xl md:text-[30px]">{t("welcome")}</h1>
<legend className="text-[12px] text-[#878787] mt-2 mb-1">
{t("get-otp-alert")}
</legend>
<div className="p-[20px] outline outline-1 outline-slate-300/50 my-2 op-card shadow-md">
<input
type="email"
name="email"
value={email}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full disabled:text-[#5c5c5c] text-xs"
disabled
/>
</div>
<div className="mt-3">
<button
className="op-btn op-btn-primary"
onClick={(e) => handleSendOTPBtn(e)}
disabled={loading}
>
{loading ? t("loading") : t("get-verification-code")}
</button>
</div>
</div>
) : (
<form
className="w-full md:w-[50%] text-base-content"
onSubmit={VerifyOTP}
<div className="w-full md:w-[50%] text-base-content">
<h1 className="text-2xl md:text-[30px]">{t("welcome")}</h1>
<legend className="text-[12px] text-[#878787] mt-2 mb-1">
{t("get-otp-alert")}
</legend>
<div className="p-[20px] outline outline-1 outline-slate-300/50 my-2 op-card shadow-md">
<input
type="email"
name="email"
value={email}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full disabled:text-[#5c5c5c] text-xs"
disabled
/>
</div>
<div className="mt-3">
<button
className="op-btn op-btn-primary flex items-center"
onClick={(e) => {
e.preventDefault();
SendOtp();
}}
disabled={loading}
>
<h1 className="text-2xl md:text-[30px]">{t("welcome")}</h1>
<legend className="text-[12px] text-[#878787] mt-2">
{t("guest-email-alert")}
</legend>
<div className="p-[20px] pt-[15px] outline outline-1 outline-slate-300/50 op-card my-2 shadow-md">
<p className="text-sm">{t("enter-verification-code")}</p>
<input
type="number"
className="mt-2 op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
name="OTP"
value={OTP}
onChange={(e) => setOTP(e.target.value)}
/>
</div>
<div className="mt-2.5">
<button
className="op-btn op-btn-primary"
type="submit"
disabled={loading}
>
{loading ? t("loading") : t("verify")}
</button>
</div>
</form>
)}
</>
<i className="fa-light fa-message-sms mr-2"></i>
{loading ? t("loading") : t("get-verification-code")}
</button>
</div>
</div>
) : (
<div className="w-full md:w-[50%] text-base-content">
<h1 className="text-2xl md:text-[30px]">{t("welcome")}</h1>
@@ -33,15 +33,10 @@ function Login() {
const { width } = useWindowSize();
const [state, setState] = useState({
email: "",
password: "",
alertType: "success",
alertMsg: "",
password: "",
passwordVisible: false,
mobile: "",
phone: "",
scanResult: "",
baseUrl: localStorage.getItem("baseUrl"),
parseAppId: localStorage.getItem("parseAppId"),
loading: false,
thirdpartyLoader: false,
});
@@ -57,6 +52,22 @@ function Login() {
// eslint-disable-next-line
}, []);
const setLocalVar = (user) => {
localStorage.setItem("accesstoken", user.sessionToken);
localStorage.setItem("UserInformation", JSON.stringify(user));
localStorage.setItem("userEmail", user.email);
if (user.ProfilePic) {
localStorage.setItem("profileImg", user.ProfilePic);
} else {
localStorage.setItem("profileImg", "");
}
};
const showToast = (type, msg) => {
setState({ ...state, loading: false, alertType: type, alertMsg: msg });
setTimeout(() => setState({ ...state, alertMsg: "" }), 2000);
};
const checkUserExt = async () => {
const app = await getAppLogo();
if (app?.error === "invalid_json") {
@@ -88,141 +99,44 @@ function Login() {
const handleSubmit = async (event) => {
localStorage.removeItem("accesstoken");
event.preventDefault();
if (!emailRegex.test(state.email)) {
alert("Please enter a valid email address.");
} else {
const { email, password } = state;
if (email && password) {
try {
setState({ ...state, loading: true });
localStorage.setItem("appLogo", appInfo.applogo);
// Pass the username and password to logIn function
const user = await Parse.User.logIn(email, password);
if (user) {
let _user = user.toJSON();
localStorage.setItem("UserInformation", JSON.stringify(_user));
localStorage.setItem("userEmail", email);
localStorage.setItem("accesstoken", _user.sessionToken);
localStorage.setItem("scriptId", true);
if (_user.ProfilePic) {
localStorage.setItem("profileImg", _user.ProfilePic);
} else {
localStorage.setItem("profileImg", "");
}
// Check extended class user role and tenentId
try {
const userSettings = appInfo.settings;
await Parse.Cloud.run("getUserDetails")
.then(async (extUser) => {
if (extUser) {
// console.log("extUser", extUser, extUser?.get("IsDisabled"));
const IsDisabled = extUser?.get("IsDisabled") || false;
if (!IsDisabled) {
const userRole = extUser?.get("UserRole");
const menu =
userRole &&
userSettings.find((menu) => menu.role === userRole);
if (menu) {
const _currentRole = userRole;
const redirectUrl =
location?.state?.from ||
`/${menu.pageType}/${menu.pageId}`;
let _role = _currentRole.replace("contracts_", "");
localStorage.setItem("_user_role", _role);
const checkLanguage = extUser?.get("Language");
if (checkLanguage) {
checkLanguage && i18n.changeLanguage(checkLanguage);
}
return;
}
const results = [extUser];
const extUser_str = JSON.stringify(results);
const { email, password } = state;
if (!email || !password) {
return;
}
localStorage.setItem("Extand_Class", extUser_str);
const extInfo = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("userEmail", extInfo.Email);
localStorage.setItem("username", extInfo.Name);
if (extInfo?.TenantId) {
const tenant = {
Id: extInfo?.TenantId?.objectId || "",
Name: extInfo?.TenantId?.TenantName || ""
};
localStorage.setItem("TenantId", tenant?.Id);
dispatch(showTenant(tenant?.Name));
localStorage.setItem("TenantName", tenant?.Name);
}
localStorage.setItem("PageLanding", menu.pageId);
localStorage.setItem("defaultmenuid", menu.menuId);
localStorage.setItem("pageType", menu.pageType);
setState({ ...state, loading: false });
// Redirect to the appropriate URL after successful login
navigate(redirectUrl);
} else {
setState({ ...state, loading: false });
setIsModal(true);
}
} else {
setState({
...state,
loading: false,
alertType: "danger",
alertMsg:
"You don't have access, please contact the admin."
});
logOutUser();
}
} else {
setState({ ...state, loading: false });
setState({
...state,
loading: false,
alertType: "danger",
alertMsg: "User not found."
});
logOutUser();
}
})
.catch((error) => {
setState({
...state,
loading: false,
alertType: "danger",
alertMsg: `Something went wrong.`
});
setTimeout(() => setState({ ...state, alertMsg: "" }), 2000);
console.error("Error while fetching Follow", error);
});
} catch (error) {
setState({
...state,
loading: false,
alertType: "danger",
alertMsg: `${error.message}`
});
console.log(error);
setTimeout(() => setState({ ...state, alertMsg: "" }), 2000);
}
}
} catch (error) {
setState({
...state,
loading: false,
alertType: "danger",
alertMsg: "Invalid username/password or region"
});
console.error("Error while logging in user", error);
} finally {
setTimeout(
() => setState((prev) => ({ ...prev, alertMsg: "" })),
2000
);
}
try {
setState({ ...state, loading: true });
localStorage.setItem("appLogo", appInfo.applogo);
const _user = await Parse.Cloud.run("loginuser", { email, password });
if (!_user) {
setState({ ...state, loading: false });
return;
}
// Get extended user data (including 2FA status) using cloud function
try {
await Parse.User.become(_user.sessionToken);
setLocalVar(_user);
await continueLoginFlow();
} catch (error) {
console.error("Error checking 2FA status:", error);
showToast("danger", t("something-went-wrong-mssg"));
}
} catch (error) {
console.error("Error while logging in user", error);
showToast("danger", "Invalid username/password or region");
}
};
const setThirdpartyLoader = (value) => {
setState({ ...state, thirdpartyLoader: value });
};
const thirdpartyLoginfn = async (sessionToken) => {
const baseUrl = localStorage.getItem("baseUrl");
const parseAppId = localStorage.getItem("parseAppId");
@@ -237,137 +151,27 @@ function Login() {
});
if (res.data) {
let _user = res.data;
localStorage.setItem("UserInformation", JSON.stringify(_user));
localStorage.setItem("userEmail", _user.email);
localStorage.setItem("accesstoken", _user.sessionToken);
localStorage.setItem("scriptId", true);
if (_user.ProfilePic) {
localStorage.setItem("profileImg", _user.ProfilePic);
} else {
localStorage.setItem("profileImg", "");
}
setLocalVar(_user);
// Check extended class user role and tenentId
try {
const userSettings = appInfo.settings;
await Parse.Cloud.run("getUserDetails")
.then(async (extUser) => {
if (extUser) {
const IsDisabled = extUser?.get("IsDisabled") || false;
if (!IsDisabled) {
const userRole = extUser?.get("UserRole");
const menu =
userRole &&
userSettings.find((menu) => menu.role === userRole);
if (menu) {
const _currentRole = userRole;
const redirectUrl =
location?.state?.from || `/${menu.pageType}/${menu.pageId}`;
const _role = _currentRole.replace("contracts_", "");
localStorage.setItem("_user_role", _role);
const results = [extUser];
const extUser_stringify = JSON.stringify(results);
localStorage.setItem("Extand_Class", extUser_stringify);
const extInfo = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("userEmail", extInfo?.Email);
localStorage.setItem("username", extInfo?.Name);
if (extInfo?.TenantId) {
const tenant = {
Id: extInfo?.TenantId?.objectId || "",
Name: extInfo?.TenantId?.TenantName || ""
};
localStorage.setItem("TenantId", tenant?.Id);
dispatch(showTenant(tenant?.Name));
localStorage.setItem("TenantName", tenant?.Name);
}
localStorage.setItem("PageLanding", menu.pageId);
localStorage.setItem("defaultmenuid", menu.menuId);
localStorage.setItem("pageType", menu.pageType);
navigate(redirectUrl);
} else {
setState({
...state,
loading: false,
alertType: "danger",
alertMsg: "Role not found."
});
logOutUser();
}
} else {
setState({
...state,
loading: false,
alertType: "danger",
alertMsg: "You don't have access, please contact the admin."
});
logOutUser();
}
} else {
setState({
...state,
alertType: "danger",
alertMsg: "User not found."
});
logOutUser();
}
})
.catch((err) => {
console.error("err in fetching extUser", err);
setState({
...state,
alertType: "danger",
alertMsg: `${err.message}`
});
const payload = { sessionToken: sessionToken };
handleSubmitbtn(payload);
});
} catch (error) {
setState({
...state,
alertType: "danger",
alertMsg: `${error.message}`
});
console.log(error);
} finally {
setThirdpartyLoader(false);
setState({ ...state, loading: false });
setTimeout(() => setState({ ...state, alertMsg: "" }), 2000);
}
}
};
const GetLoginData = async () => {
setState({ ...state, loading: true });
try {
const user = await Parse.User.become(localStorage.getItem("accesstoken"));
const _user = user.toJSON();
localStorage.setItem("UserInformation", JSON.stringify(_user));
localStorage.setItem("accesstoken", _user.sessionToken);
localStorage.setItem("scriptId", true);
if (_user.ProfilePic) {
localStorage.setItem("profileImg", _user.ProfilePic);
} else {
localStorage.setItem("profileImg", "");
}
const userSettings = appInfo.settings;
await Parse.Cloud.run("getUserDetails").then(async (extUser) => {
const extUser = await Parse.Cloud.run("getUserDetails");
if (extUser) {
const IsDisabled = extUser?.get("IsDisabled") || false;
if (!IsDisabled) {
const userRole = extUser.get("UserRole");
const _currentRole = userRole;
const userRole = extUser?.get("UserRole");
const menu =
userRole && userSettings.find((menu) => menu.role === userRole);
if (menu) {
const _role = _currentRole.replace("contracts_", "");
localStorage.setItem("_user_role", _role);
const _currentRole = userRole;
const redirectUrl =
location?.state?.from || `/${menu.pageType}/${menu.pageId}`;
const results = [extUser];
const extendedInfo_stringify = JSON.stringify(results);
localStorage.setItem("Extand_Class", extendedInfo_stringify);
const _role = _currentRole.replace("contracts_", "");
const extInfo = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("userEmail", extInfo.Email);
localStorage.setItem("username", extInfo.Name);
localStorage.setItem("_user_role", _role);
localStorage.setItem("Extand_Class", JSON.stringify([extUser]));
localStorage.setItem("userEmail", extInfo?.Email);
localStorage.setItem("username", extInfo?.Name);
if (extInfo?.TenantId) {
const tenant = {
Id: extInfo?.TenantId?.objectId || "",
@@ -380,40 +184,82 @@ function Login() {
localStorage.setItem("PageLanding", menu.pageId);
localStorage.setItem("defaultmenuid", menu.menuId);
localStorage.setItem("pageType", menu.pageType);
// Redirect to the appropriate URL after successful login
navigate(redirectUrl);
} else {
setState({ ...state, loading: false });
showToast("danger", t("role-not-found"));
logOutUser();
}
} else {
setState({
...state,
loading: false,
alertType: "danger",
alertMsg: "You don't have access, please contact the admin."
});
showToast("danger", t("do-not-access-contact-admin"));
logOutUser();
}
} else {
setState({
...state,
alertType: "danger",
alertMsg: "User not found."
});
showToast("danger", t("user-not-found"));
logOutUser();
}
});
} catch (error) {
console.error("err in fetching extUser", err);
showToast("danger", `${err.message}`);
const payload = { sessionToken: _user.sessionToken };
handleSubmitbtn(payload);
} finally {
setThirdpartyLoader(false);
}
}
};
const GetLoginData = async () => {
setState({ ...state, loading: true });
try {
const user = await Parse.User.become(localStorage.getItem("accesstoken"));
const _user = user.toJSON();
setLocalVar(_user);
const userSettings = appInfo.settings;
const extUser = await Parse.Cloud.run("getUserDetails");
if (extUser) {
const IsDisabled = extUser?.get("IsDisabled") || false;
if (!IsDisabled) {
const userRole = extUser.get("UserRole");
const _currentRole = userRole;
const menu =
userRole && userSettings.find((menu) => menu.role === userRole);
if (menu) {
const extInfo = JSON.parse(JSON.stringify(extUser));
const _role = _currentRole.replace("contracts_", "");
localStorage.setItem("_user_role", _role);
const redirectUrl =
location?.state?.from || `/${menu.pageType}/${menu.pageId}`;
localStorage.setItem("Extand_Class", JSON.stringify([extUser]));
localStorage.setItem("userEmail", extInfo.Email);
localStorage.setItem("username", extInfo.Name);
if (extInfo?.TenantId) {
const tenant = {
Id: extInfo?.TenantId?.objectId || "",
Name: extInfo?.TenantId?.TenantName || ""
};
localStorage.setItem("TenantId", tenant?.Id);
dispatch(showTenant(tenant?.Name));
localStorage.setItem("TenantName", tenant?.Name);
}
localStorage.setItem("PageLanding", menu.pageId);
localStorage.setItem("defaultmenuid", menu.menuId);
localStorage.setItem("pageType", menu.pageType);
navigate(redirectUrl);
} else {
setState({ ...state, loading: false });
logOutUser();
}
} else {
showToast("danger", t("do-not-access-contact-admin"));
logOutUser();
}
} else {
showToast("danger", t("user-not-found"));
logOutUser();
}
} catch (error) {
setState({
...state,
alertType: "danger",
alertMsg: "Something went wrong, please try again later."
});
showToast("danger", t("something-went-wrong-mssg"));
console.log("err", error);
} finally {
setState({ ...state, loading: false });
setTimeout(() => setState({ ...state, alertMsg: "" }), 2000);
}
};
@@ -425,8 +271,6 @@ function Login() {
e.preventDefault();
if (userDetails.Destination && userDetails.Company) {
setThirdpartyLoader(true);
// console.log("handelSubmit", userDetails);
// const payload = await Parse.User.logIn(state.email, state.password);
const payload = { sessionToken: localStorage.getItem("accesstoken") };
const userInformation = JSON.parse(
localStorage.getItem("UserInformation")
@@ -466,13 +310,7 @@ function Login() {
alert(t("server-error"));
}
} else {
setState({
...state,
loading: false,
alertType: "warning",
alertMsg: "Please fill required details."
});
setTimeout(() => setState((prev) => ({ ...prev, alertMsg: "" })), 2000);
showToast("warning", t("fill-required-details!"));
}
};
@@ -501,6 +339,62 @@ function Login() {
localStorage.setItem("parseAppId", appid);
};
const continueLoginFlow = async () => {
try {
const userSettings = appInfo.settings;
const extUser = await Parse.Cloud.run("getUserDetails");
if (extUser) {
const IsDisabled = extUser?.get("IsDisabled") || false;
if (!IsDisabled) {
const userRole = extUser?.get("UserRole");
const menu =
userRole && userSettings?.find((menu) => menu.role === userRole);
if (menu) {
const _currentRole = userRole;
const redirectUrl =
location?.state?.from || `/${menu.pageType}/${menu.pageId}`;
const _role = _currentRole.replace("contracts_", "");
localStorage.setItem("_user_role", _role);
const checkLanguage = extUser?.get("Language");
if (checkLanguage) {
checkLanguage && i18n.changeLanguage(checkLanguage);
}
const extInfo = JSON.parse(JSON.stringify(extUser));
// Continue with storing user data and redirecting
localStorage.setItem("Extand_Class", JSON.stringify([extUser]));
localStorage.setItem("userEmail", extInfo.Email);
localStorage.setItem("username", extInfo.Name);
if (extInfo?.TenantId) {
const tenant = {
Id: extInfo?.TenantId?.objectId || "",
Name: extInfo?.TenantId?.TenantName || ""
};
localStorage.setItem("TenantId", tenant?.Id);
dispatch(showTenant(tenant?.Name));
localStorage.setItem("TenantName", tenant?.Name);
}
localStorage.setItem("PageLanding", menu.pageId);
localStorage.setItem("defaultmenuid", menu.menuId);
localStorage.setItem("pageType", menu.pageType);
setState({ ...state, loading: false });
navigate(redirectUrl);
} else {
setState({ ...state, loading: false });
setIsModal(true);
}
} else {
showToast("danger", t("do-not-access-contact-admin"));
logOutUser();
}
} else {
showToast("danger", t("user-not-found"));
logOutUser();
}
} catch (error) {
console.error("Error during login flow", error);
showToast("danger", error.message || t("something-went-wrong-mssg"));
}
};
return errMsg ? (
<div className="h-screen flex justify-center text-center items-center p-4 text-gray-500 text-base">
@@ -522,7 +416,7 @@ function Login() {
<div
aria-labelledby="loginHeading"
role="region"
className="pb-1 md:pb-4 pt-10 md:px-10 lg:px-16"
className="pb-1 md:pb-4 pt-10 md:px-10 lg:px-16 h-full"
>
<div className="md:p-4 lg:p-10 p-4 bg-base-100 text-base-content op-card">
<div className="w-[250px] h-[66px] inline-block overflow-hidden">
@@ -63,6 +63,9 @@ function Opensigndrive() {
const [isDontShow, setIsDontShow] = useState(false);
const [tourData, setTourData] = useState();
const [showTourFirstTIme, setShowTourFirstTime] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const debounceTimer = useRef(null);
const orderName = {
Ascending: "Ascending",
Descending: "Descending",
@@ -144,7 +147,7 @@ function Opensigndrive() {
try {
const driveDetails = await getDrive(docId, skip, limit);
if (driveDetails && driveDetails === "Error: Something went wrong!") {
setHandleError("Error: Something went wrong!");
setHandleError(t("something-went-wrong-mssg"));
} else if (driveDetails && driveDetails.length > 0) {
const addMoreTour = [
{
@@ -214,9 +217,7 @@ function Opensigndrive() {
});
} finally {
setLoading(false);
setIsLoading({
isLoad: false
});
setIsLoading({ isLoad: false });
}
};
@@ -224,15 +225,16 @@ function Opensigndrive() {
const handleScroll = () => {
//get document of render openSign-drive component using id
const documentList = document.getElementById("renderList");
//documentList.clientHeight property returns the height of an element's content area, including padding but not including borders, margins, or scrollbars.
//documentList.scrollHeight property returns the entire height of an element,including the parts that are not visible due to overflow..
// documentList.scrollTop property show height of element, how much the content has been scrolled from the top.
const { scrollTop, clientHeight, scrollHeight } = documentList;
const scrolled = Math.ceil(scrollTop + clientHeight); // ceil return e.g 3.14 => 4
const totalHeight = Math.floor(scrollHeight); // floor return e.g 3.14 => 3
// clientHeight property returns the height of an element's content area, including padding but not including borders, margins, or scrollbars.
// scrollHeight property returns the entire height of an element,including the parts that are not visible due to overflow..
// scrollTop property show height of element, how much the content has been scrolled from the top.
// When the sum of scrollTop and clientHeight is equal to scrollHeight, it means that the user has scrolled to the bottom of the div.
if (
documentList &&
documentList.scrollTop + documentList.clientHeight >=
documentList.scrollHeight
) {
if (scrolled >= totalHeight) {
//disableLoading is used disable initial loader
const disableLoading = true;
// If the fetched data length is less than the limit, it means there's no more data to fetch
@@ -257,11 +259,7 @@ function Opensigndrive() {
const handleRoute = (index, folderData) => {
setSkip(0);
// after onclick on route filter route from that index
const updateFolderName = folderName.filter((x, i) => {
if (i <= index) {
return x;
}
});
const updateFolderName = folderName.filter((_, i) => i <= index);
setFolderName(updateFolderName);
//get route details after onclick path of folder name
const getCurrentId = folderData[index];
@@ -522,6 +520,34 @@ function Opensigndrive() {
setIsTour(true);
}
}
const handleSearchChange = async (e) => {
const name = e.target.value.toLowerCase();
setSearchTerm(name);
// Clear previous timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// Start new debounce timer
debounceTimer.current = setTimeout(async () => {
try {
const res = await Parse.Cloud.run("filterdocs", { searchTerm: name });
setPdfData(JSON.parse(JSON.stringify(res)));
} catch (err) {
console.error("Search error:", err);
alert(`Error: ${err.message}`);
}
}, 300);
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
return (
<div className="bg-base-100 text-base-content rounded-box w-full shadow-md">
<Title title={`${drivename} Drive`} drive={true} />
@@ -529,22 +555,14 @@ function Opensigndrive() {
isOpen={isAlert.isShow}
title={t("alert")}
handleClose={() => {
setIsAlert({
isShow: false,
alertMessage: ""
});
setIsAlert({ isShow: false, alertMessage: "" });
}}
>
<div className="h-full p-[20px] pb-[15px]">
<p>{isAlert.alertMessage}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() =>
setIsAlert({
isShow: false,
alertMessage: ""
})
}
onClick={() => setIsAlert({ isShow: false, alertMessage: "" })}
type="button"
className="op-btn op-btn-neutral op-btn-sm"
>
@@ -613,7 +631,7 @@ function Opensigndrive() {
</div>
) : (
<>
<div className="flex flex-row justify-between items-center px-[15px] md:px-[25px] pt-[20px]">
<div className="flex flex-row justify-between items-center px-[15px] md:px-[25px] pt-2 md:pt-[20px]">
{tourData && (
<Tour
onRequestClose={closeTour}
@@ -632,22 +650,42 @@ function Opensigndrive() {
>
{handleFolderTab(folderName)}
</div>
<div className="flex flex-row items-center">
<div className="flex flex-row items-center justify-center md:gap-1">
{/* Desktop search input */}
<div className="hidden md:block p-2">
<input
type="search"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search documents…"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
/>
</div>
{/* Mobile search toggle */}
<button
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-gray-200 text-[18px]"
aria-label="Search"
onClick={() => setMobileSearchOpen((open) => !open)}
>
<i
style={{ color: `${iconColor}` }}
className="fa-solid fa-magnifying-glass"
></i>
</button>
<div
id="folder-menu"
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block`}
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block cursor-pointer hover:bg-gray-200 p-2 rounded-md`}
onClick={() => setIsOptions(!isOptions)}
>
<div className="sort" data-tut="reactourSecond">
<div data-tut="reactourSecond">
<i
className="fa-light fa-plus-square"
className="fa-light fa-plus-square text-[24px]"
aria-hidden="true"
style={{ fontSize: "25px", color: `${iconColor}` }}
style={{ color: `${iconColor}` }}
></i>
</div>
<div
className={`${isOptions ? "block" : "hidden"} ${dropdowncss}`}
// className={isOptions ? "dropdown-menu show" : "dropdown-menu"}
aria-labelledby="dropdownMenuButton"
aria-expanded={isOptions ? "true" : "false"}
>
@@ -686,11 +724,11 @@ function Opensigndrive() {
>
<div
data-tut="reactourThird"
className="sort "
className="cursor-pointer flex flex-row items-center justify-center p-2 hover:bg-gray-200 rounded-md"
data-toggle="dropdown"
>
<i
className="fa-light fa-sort-amount-asc mr-[5px] text-[14px]"
className="fa-light fa-sort-amount-asc mr-[5px] text-[19px]"
aria-hidden="true"
style={{ color: `${iconColor}` }}
></i>
@@ -705,69 +743,57 @@ function Opensigndrive() {
aria-labelledby="dropdownMenuButton"
aria-expanded={isShowSort ? "true" : "false"}
>
{sortingValue.map((value, ind) => {
return (
<span
key={ind}
onClick={() => {
setSelectedSort(value);
sortingData(value, null, pdfData);
}}
className="dropdown-item text-[10px] md:text-[13px]"
style={{
paddingLeft: selectedSort !== value && "31px"
}}
>
{selectedSort === value && (
<i
className="fa-light fa-check"
aria-hidden="true"
></i>
)}
<span className="ml-[5px]">
{t(`sort-order.${value}`)}
</span>
{sortingValue.map((value, ind) => (
<span
key={ind}
onClick={() => {
setSelectedSort(value);
sortingData(value, null, pdfData);
}}
className="dropdown-item text-[10px] md:text-[13px]"
style={{
paddingLeft: selectedSort !== value && "31px"
}}
>
{selectedSort === value && (
<i className="fa-light fa-check" aria-hidden="true"></i>
)}
<span className="ml-[5px]">
{t(`sort-order.${value}`)}
</span>
);
})}
</span>
))}
<hr className="hrStyle" />
{sortOrder.map((order, ind) => {
return (
<span
key={ind}
onClick={() => {
setSortingOrder(order);
sortingData(null, order, pdfData);
}}
className="dropdown-item text-[10px] md:text-[13px]"
style={{
paddingLeft: sortingOrder !== order && "31px"
}}
>
{sortingOrder === order && (
<i
className="fa-light fa-check"
aria-hidden="true"
></i>
)}
<span className="ml-[5px]">
{t(`sort-order.${order}`)}
</span>
{sortOrder.map((order, ind) => (
<span
key={ind}
onClick={() => {
setSortingOrder(order);
sortingData(null, order, pdfData);
}}
className="dropdown-item text-[10px] md:text-[13px]"
style={{
paddingLeft: sortingOrder !== order && "31px"
}}
>
{sortingOrder === order && (
<i className="fa-light fa-check" aria-hidden="true"></i>
)}
<span className="ml-[5px]">
{t(`sort-order.${order}`)}
</span>
);
})}
</span>
))}
</div>
</div>
<div
className="cursor-pointer p-2 hover:bg-gray-200 rounded-md flex justify-center items-center"
data-tut="reactourForth"
className="sort"
onClick={() => setIsList(!isList)}
>
<i
className={
isList ? "fa-light fa-th-large" : "fa-light fa-list"
}
style={{ fontSize: "24px", color: `${iconColor}` }}
className={`${isList ? "fa-light fa-th-large" : "fa-light fa-list"} text-[20px]`}
style={{ color: `${iconColor}` }}
aria-hidden="true"
></i>
</div>
@@ -777,7 +803,7 @@ function Opensigndrive() {
onClick={() => setIsOptions(!isOptions)}
>
<div
className="p-[19px] my-2 flex items-center justify-center cursor-pointer hover:bg-[var(--mauve-3)] rounded-[2px] shadow-[0_2px_4px_rgba(168,204,206,0.1)]"
className="p-3 flex items-center justify-center cursor-pointer rounded-md hover:bg-gray-200"
data-tut="reactourSecond"
>
<i
@@ -821,7 +847,18 @@ function Opensigndrive() {
</div>
</div>
</div>
{/* Mobile search overlay */}
{mobileSearchOpen && (
<div className="top-full left-0 w-full bg-white px-4 py-2 shadow-md md:hidden">
<input
type="search"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search documents…"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
)}
{pdfData && pdfData.length === 0 ? (
<div className="flex justify-center items-center w-full h-[50vh]">
<span className="text-base-content font-bold">
@@ -41,7 +41,9 @@ import {
addWidgetOptions,
textWidget,
compressedFileSize,
mailTemplate
mailTemplate,
updateDateWidgetsRes,
widgetDataValue
} from "../constant/Utils";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
@@ -161,6 +163,7 @@ function PdfRequestFiles(
signId: ""
});
const [showSignPagenumber, setShowSignPagenumber] = useState([]);
const [owner, setOwner] = useState({});
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => addPositionOfSignature(item, monitor),
@@ -256,21 +259,23 @@ function PdfRequestFiles(
alert(t("user-not-exist"));
}
};
//function for get document details for perticular signer with signer'object id
const getDocumentDetails = async (
docId,
isNextUser,
isSuccessPage = false
isSuccessPage = false,
) => {
try {
let isUpdatedSubscribe;
const senderUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
);
const jsonSender = JSON.parse(senderUser);
const contactId = jsonSender?.objectId
? ""
: contactBookId || signerObjectId || "";
: contactBookId ||
signerObjectId ||
"";
const tenantSignTypes = await fetchTenantDetails(contactId);
// `currUserId` will be contactId or extUserId
let currUserId;
@@ -301,6 +306,7 @@ function PdfRequestFiles(
setHandleError(t("something-went-wrong-mssg"));
}
setExtUserId(documentData[0]?.ExtUserPtr?.objectId);
setOwner(documentData?.[0]?.ExtUserPtr);
const isCompleted =
documentData[0].IsCompleted && documentData[0].IsCompleted;
const expireDate = documentData[0].ExpiryDate.iso;
@@ -317,7 +323,9 @@ function PdfRequestFiles(
currUserId = getCurrentSigner?.objectId
? getCurrentSigner.objectId
: contactBookId || signerObjectId || ""; //signerObjectId is contactBookId refer from public template flow
: contactBookId ||
signerObjectId ||
""; //signerObjectId is contactBookId refer from public template flow
if (currUserId) {
setSignerObjectId(currUserId);
}
@@ -407,7 +415,6 @@ function PdfRequestFiles(
//condition to check already signed document by someone
if (audittrailData && audittrailData.length > 0) {
setIsDocId(true);
for (const item of placeholdersOrSigners) {
const checkEmail = item?.email;
//if email exist then compare user signed by using email else signers objectId
@@ -433,13 +440,12 @@ function PdfRequestFiles(
}
setSignedSigners(signers);
setUnSignedSigners(unSignedSigner);
setSignerPos(documentData[0].Placeholders);
} else {
//else condition is show there are no details in audit trail then direct push all signers details
//in unsignedsigners array
setUnSignedSigners(placeholdersOrSigners);
setSignerPos(documentData[0].Placeholders);
}
setSignerPos(updateDateWidgetsRes(documentData[0], currUserId));
setPdfDetails(documentData);
//checking if condition current user already sign or owner does not exist as a signer or document has been declined by someone or document has been expired
//then stop to display tour message
@@ -525,6 +531,14 @@ function PdfRequestFiles(
}
);
const contact = resContact?.data?.result;
localStorage.setItem(
"signer",
JSON.stringify({
Name: contact?.Name,
Email: contact?.Email,
UserId: contact?.UserId?.objectId
})
);
setContractName("_Contactbook");
setSignerUserId(contact?.objectId);
const tourData = contact?.TourStatus && contact?.TourStatus;
@@ -543,12 +557,18 @@ function PdfRequestFiles(
}
setIsUiLoading(false);
setIsLoading({ isLoad: false });
return updatedPdfDetails;
return {
updatedPdfDetails,
};
} else if (
documentData === "Error: Something went wrong!" ||
(documentData.result && documentData.result.error)
) {
setHandleError(t("something-went-wrong-mssg"));
if (documentData?.result?.error?.includes("deleted")) {
setHandleError(t("document-deleted"));
} else {
setHandleError(t("something-went-wrong-mssg"));
}
setIsLoading({ isLoad: false });
console.log("err in getDocument cloud function ");
} else {
@@ -559,12 +579,24 @@ function PdfRequestFiles(
setIsLoading({ isLoad: false });
} catch (err) {
console.log("Error: error in getDocumentDetails", err);
setHandleError("Error: Something went wrong!");
setHandleError(t("something-went-wrong-mssg"));
setIsLoading({ isLoad: false });
}
};
//function for embed signature or image url in pdf
async function embedWidgetsData() {
async function embedWidgetsData(
) {
let contactId =
signerObjectId;
let docId =
documentId;
const addExtraDays = pdfDetails[0]?.TimeToCompleteDays
? pdfDetails[0].TimeToCompleteDays
: 15;
let updateExpiryDate;
updateExpiryDate = new Date();
updateExpiryDate.setDate(updateExpiryDate.getDate() + addExtraDays);
const expiry = updateExpiryDate || pdfDetails?.[0].ExpiryDate.iso;
//for emailVerified data checking first in localstorage
const localuser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
@@ -750,7 +782,7 @@ function PdfRequestFiles(
let pdfArrBuffer;
//`contractDocument` function used to get updated SignedUrl
// to resolve issue of widgets get remove automatically when more than 1 signers try to sign doc at a time
const documentData = await contractDocument(documentId);
const documentData = await contractDocument(docId);
if (documentData && documentData.length > 0) {
const url = documentData[0]?.SignedUrl || documentData[0]?.URL;
//convert document url in array buffer format to use embed widgets in pdf using pdf-lib
@@ -764,7 +796,11 @@ function PdfRequestFiles(
documentData === "Error: Something went wrong!" ||
(documentData.result && documentData.result.error)
) {
setHandleError("Error: Something went wrong!");
if (documentData?.result?.error?.includes("deleted")) {
setHandleError(t("document-deleted"));
} else {
setHandleError(t("something-went-wrong-mssg"));
}
} else {
setHandleError("Document not Found!");
}
@@ -778,7 +814,7 @@ function PdfRequestFiles(
//embed document's object id to all pages in pdf document
if (!HeaderDocId) {
if (!isDocId) {
await embedDocId(pdfDoc, documentId, allPages);
await embedDocId(pdfDoc, docId, allPages);
}
}
//embed multi signature in pdf
@@ -795,8 +831,8 @@ function PdfRequestFiles(
//function for call to embed signature in pdf and get digital signature pdf
const resSign = await signPdfFun(
pdfBytes,
documentId,
signerObjectId,
docId,
contactId,
objectId,
widgets
);
@@ -808,10 +844,11 @@ function PdfRequestFiles(
const isSuccessRoute = pdfDetails?.[0]?.RedirectUrl
? false
: window.location?.pathname?.includes("load");
const updateDoc = await getDocumentDetails(
documentId,
const updatedDoc = await getDocumentDetails(
docId,
true,
isSuccessRoute
isSuccessRoute,
contactId
);
const index = pdfDetails?.[0]?.Signers.findIndex(
(x) => x.objectId === signerObjectId
@@ -824,10 +861,12 @@ function PdfRequestFiles(
? usermail
: pdfDetails?.[0]?.Signers[newIndex];
if (sendmail !== "false" && sendInOrder) {
const requestBody = pdfDetails?.[0]?.RequestBody;
const requestSubject = pdfDetails?.[0]?.RequestSubject;
const requestBody =
updatedDoc.updatedPdfDetails?.[0]?.RequestBody;
const requestSubject =
updatedDoc.updatedPdfDetails?.[0]?.RequestSubject;
if (user) {
const expireDate = pdfDetails?.[0].ExpiryDate.iso;
const expireDate = expiry;
const newDate = new Date(expireDate);
const localExpireDate = newDate.toLocaleDateString(
"en-US",
@@ -853,12 +892,10 @@ function PdfRequestFiles(
let encodeBase64;
if (objectId) {
encodeBase64 = btoa(
`${pdfDetails?.[0].objectId}/${user.Email}/${objectId}`
`${docId}/${user.Email}/${objectId}`
);
} else {
encodeBase64 = btoa(
`${pdfDetails?.[0].objectId}/${user.Email}`
);
encodeBase64 = btoa(`${docId}/${user.Email}`);
}
let signPdf =
`${hostUrl}/login/${encodeBase64}`;
@@ -881,6 +918,7 @@ function PdfRequestFiles(
const variables = {
document_title: documentName,
note: pdfDetails?.[0]?.Note,
sender_name: senderName,
sender_mail: senderEmail,
sender_phone: senderPhone,
@@ -898,6 +936,7 @@ function PdfRequestFiles(
);
}
const mailparam = {
note: pdfDetails?.[0]?.Note || "",
senderName: senderName,
senderMail: senderEmail,
title: documentName,
@@ -928,13 +967,15 @@ function PdfRequestFiles(
setIsredirectCanceled(false);
} else {
const url =
updateDoc?.[0]?.SignedUrl || updateDoc?.[0]?.URL;
updatedDoc.updatedPdfDetails?.[0]?.SignedUrl ||
updatedDoc.updatedPdfDetails?.[0]?.URL;
const fileAdapter =
"";
const isCompleted = updateDoc?.[0]?.IsCompleted
const isCompleted = updatedDoc.updatedPdfDetails?.[0]
?.IsCompleted
? `&completed=true`
: "";
const params = `docid=${updateDoc[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 {
@@ -987,6 +1028,9 @@ function PdfRequestFiles(
}
}
const handleSignPdf = async () => {
await embedWidgetsData();
};
//function for save x and y position and show signature tab on that position
const handleTabDrag = (key) => {
setDragKey(key);
@@ -1100,7 +1144,6 @@ function PdfRequestFiles(
return obj;
});
setSignerPos(newUpdateSigner);
setFontSize();
setFontColor();
handleTextSettingModal(false);
@@ -1638,6 +1681,9 @@ function PdfRequestFiles(
defaultWidthHeight(dragTypeValue).width * containerScale;
const widgetHeight =
defaultWidthHeight(dragTypeValue).height * containerScale;
const extUser = localStorage.getItem("Extand_Class");
const parseUser = extUser && JSON.parse(extUser)[0];
const widgetValue = widgetDataValue(dragTypeValue, parseUser);
//adding and updating drop position in array when user drop signature button in div
if (item === "onclick") {
const divHeight = divRef.current.getBoundingClientRect().height;
@@ -1652,7 +1698,7 @@ function PdfRequestFiles(
scale: containerScale,
zIndex: posZIndex,
type: dragTypeValue,
options: addWidgetOptions(dragTypeValue),
options: addWidgetOptions(dragTypeValue, owner, widgetValue),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
};
@@ -1679,7 +1725,7 @@ function PdfRequestFiles(
scale: containerScale,
zIndex: posZIndex,
type: dragTypeValue,
options: addWidgetOptions(dragTypeValue),
options: addWidgetOptions(dragTypeValue, owner, widgetValue),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
};
@@ -1864,14 +1910,6 @@ function PdfRequestFiles(
closeWithMask={false}
/>
{isUiLoading && (
<div className="absolute h-[100vh] w-full flex flex-col justify-center items-center z-[999] bg-[#e6f2f2] bg-opacity-80">
<Loader />
<span className="text-[13px] text-base-content">
{t("loading-mssg")}
</span>
</div>
)}
{isUiLoading && (
<div className="absolute h-[100vh] w-full flex flex-col justify-center items-center z-[999] bg-[#e6f2f2] bg-opacity-80">
<Loader />
@@ -2159,7 +2197,7 @@ function PdfRequestFiles(
</div>
</ModalUi>
{/* this component is used for signature pad modal */}
{documentId && isSignPad && (
{currentSigner && isSignPad && (
<SignPad
saveSignCheckbox={saveSignCheckbox}
setSaveSignCheckbox={setSaveSignCheckbox}
@@ -2202,7 +2240,7 @@ function PdfRequestFiles(
isSigned={isSigned}
isCompleted={isCompleted.isCertificate}
embedWidgetsData={
embedWidgetsData
handleSignPdf
}
isShowHeader={true}
setIsDecline={setIsDecline}
@@ -3,6 +3,9 @@ import axios from "axios";
import Parse from "parse";
import "../styles/signature.css";
import { PDFDocument } from "pdf-lib";
import {
maxTitleLength
} from "../constant/const";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useDrop } from "react-dnd";
@@ -69,7 +72,8 @@ import AddContact from "../primitives/AddContact";
function PlaceHolderSign() {
const { t } = useTranslation();
const copyUrlRef = useRef(null);
const appName = "OpenSign™";
const appName =
"OpenSign™";
const editorRef = useRef();
const { state } = useLocation();
const navigate = useNavigate();
@@ -150,7 +154,6 @@ function PlaceHolderSign() {
status: false,
message: ""
});
const [extUserId, setExtUserId] = useState("");
const [isCustomize, setIsCustomize] = useState(false);
const [zoomPercent, setZoomPercent] = useState(0);
const [scale, setScale] = useState(1);
@@ -164,13 +167,14 @@ function PlaceHolderSign() {
const [userList, setUserList] = useState([]);
const [isAttchSignerModal, setIsAttchSignerModal] = useState(false);
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [owner, setOwner] = useState({});
const [docTitle, setDocTitle] = useState("");
const isMobile = window.innerWidth < 767;
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => addPositionOfSignature(item, monitor),
collect: (monitor) => ({ isOver: !!monitor.isOver() })
});
const documentId = docId;
useEffect(() => {
if (documentId) {
@@ -244,6 +248,7 @@ function PlaceHolderSign() {
//getting document details
const documentData = await contractDocument(documentId);
if (documentData && documentData.length > 0) {
setDocTitle(documentData?.[0]?.Name);
if (documentData[0]?.Placeholders?.length > 0) {
const signerNotExist = documentData[0]?.Placeholders.some(
(data) => !data.signerObjId && data.Role !== "prefill"
@@ -286,7 +291,7 @@ function PlaceHolderSign() {
setPdfArrayBuffer(arrayBuffer);
setPdfBase64Url(base64Pdf);
}
setExtUserId(documentData[0]?.ExtUserPtr?.objectId);
setOwner(documentData?.[0]?.ExtUserPtr);
const alreadyPlaceholder = documentData[0]?.SignedUrl;
// Check if document is sent for signing
if (alreadyPlaceholder) {
@@ -457,7 +462,11 @@ function PlaceHolderSign() {
documentData === "Error: Something went wrong!" ||
(documentData.result && documentData.result.error)
) {
setHandleError(t("something-went-wrong-mssg"));
if (documentData?.result?.error?.includes("deleted")) {
setHandleError(t("document-deleted"));
} else {
setHandleError(t("something-went-wrong-mssg"));
}
setIsLoading({ isLoad: false });
} else {
setHandleError(t("no-data-avaliable"));
@@ -525,7 +534,7 @@ function PlaceHolderSign() {
scale: containerScale,
zIndex: posZIndex,
type: dragTypeValue,
options: addWidgetOptions(dragTypeValue),
options: addWidgetOptions(dragTypeValue, owner),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
};
@@ -556,7 +565,7 @@ function PlaceHolderSign() {
scale: containerScale,
zIndex: posZIndex,
type: dragTypeValue,
options: addWidgetOptions(dragTypeValue),
options: addWidgetOptions(dragTypeValue, owner),
Width: widgetWidth / (containerScale * scale),
Height: widgetHeight / (containerScale * scale)
};
@@ -863,7 +872,11 @@ function PlaceHolderSign() {
scale
);
const pdfName = generatePdfName(16);
const pdfUrl = await convertBase64ToFile(pdfName, pdfBase64, "");
const pdfUrl = await convertBase64ToFile(
pdfName,
pdfBase64,
"",
);
const tenantId = localStorage.getItem("TenantId");
const buffer = atob(pdfBase64);
SaveFileSize(buffer.length, pdfUrl, tenantId);
@@ -875,7 +888,11 @@ function PlaceHolderSign() {
} else if (pdfBase64Url) {
try {
const pdfName = generatePdfName(16);
const pdfUrl = await convertBase64ToFile(pdfName, pdfBase64Url, "");
const pdfUrl = await convertBase64ToFile(
pdfName,
pdfBase64Url,
"",
);
return pdfUrl;
} catch (err) {
console.log("error to convertBase64ToFile in placeholder flow", err);
@@ -981,7 +998,11 @@ function PlaceHolderSign() {
let pdfUrl;
if (isUploadPdf) {
const pdfName = generatePdfName(16);
pdfUrl = await convertBase64ToFile(pdfName, pdfBase64Url, "");
pdfUrl = await convertBase64ToFile(
pdfName,
pdfBase64Url,
"",
);
}
try {
const docCls = new Parse.Object("contracts_Document");
@@ -1007,6 +1028,7 @@ function PlaceHolderSign() {
const saveDocumentDetails = async () => {
setIsUiLoading(true);
let signerMail = signersdata.slice();
// For "Send in order", only consider the first signer
if (pdfDetails?.[0]?.SendinOrder && pdfDetails?.[0]?.SendinOrder === true) {
signerMail.splice(1);
}
@@ -1019,7 +1041,7 @@ function PlaceHolderSign() {
objectId: x.objectId
};
});
const addExtraDays = pdfDetails[0]?.TimeToCompleteDays
const addExtraDays = pdfDetails?.[0]?.TimeToCompleteDays
? pdfDetails[0].TimeToCompleteDays
: 15;
const currentUser = signersdata.find((x) => x.Email === currentId);
@@ -1034,26 +1056,24 @@ function PlaceHolderSign() {
} else {
setIsCurrUser(currentUser?.objectId ? true : false);
}
let updateExpiryDate, data;
updateExpiryDate = new Date();
// Compute expiry date with extra days
let updateExpiryDate = new Date();
updateExpiryDate.setDate(updateExpiryDate.getDate() + addExtraDays);
//filter label widgets after add label widgets data on pdf
// Filter out prefill roles
const filterPrefill = signerPos.filter((data) => data.Role !== "prefill");
try {
data = {
const data = {
Name: docTitle || pdfDetails?.[0]?.Name,
Placeholders: filterPrefill,
SignedUrl: pdfUrl,
Signers: signers,
SentToOthers: true,
SignatureType: pdfDetails?.[0]?.SignatureType
SignatureType: pdfDetails?.[0]?.SignatureType,
ExpiryDate: { iso: updateExpiryDate, __type: "Date" }
};
if (updateExpiryDate) {
data["ExpiryDate"] = { iso: updateExpiryDate, __type: "Date" };
}
await axios.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${documentId}`,
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${documentId}`,
data,
{
headers: {
@@ -1068,6 +1088,11 @@ function PlaceHolderSign() {
setIsUiLoading(false);
setSignerPos([]);
setIsSendAlert({ mssg: "confirm", alert: true });
if (docTitle) {
const updatedPdfDetails = [...pdfDetails];
updatedPdfDetails[0].Name = docTitle;
setPdfDetails(updatedPdfDetails);
}
} catch (e) {
console.log("error", e);
alert(t("something-went-wrong-mssg"));
@@ -1140,7 +1165,8 @@ function PlaceHolderSign() {
year: "numeric"
});
let senderEmail = pdfDetails?.[0]?.ExtUserPtr?.Email;
let senderEmail =
pdfDetails?.[0]?.ExtUserPtr?.Email;
let senderPhone = pdfDetails?.[0]?.ExtUserPtr?.Phone;
let signerMail = signersdata.slice();
@@ -1166,11 +1192,16 @@ function PlaceHolderSign() {
const orgName = pdfDetails[0]?.ExtUserPtr.Company
? pdfDetails[0].ExtUserPtr.Company
: "";
const senderName = pdfDetails?.[0].ExtUserPtr.Name;
const senderName =
pdfDetails?.[0].ExtUserPtr.Name;
const documentName = `${pdfDetails?.[0].Name}`;
let replaceVar;
if (requestBody && requestSubject && isCustomize) {
if (
requestBody &&
requestSubject &&
isCustomize
) {
const replacedRequestBody = requestBody.replace(/"/g, "'");
htmlReqBody =
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" +
@@ -1179,6 +1210,7 @@ function PlaceHolderSign() {
const variables = {
document_title: documentName,
note: pdfDetails?.[0]?.Note,
sender_name: senderName,
sender_mail: senderEmail,
sender_phone: senderPhone || "",
@@ -1194,7 +1226,10 @@ function PlaceHolderSign() {
htmlReqBody,
variables
);
} else if (tenantMailTemplate?.body && tenantMailTemplate?.subject) {
} else if (
tenantMailTemplate?.body &&
tenantMailTemplate?.subject
) {
const mailBody = tenantMailTemplate?.body;
const mailSubject = tenantMailTemplate?.subject;
const replacedRequestBody = mailBody.replace(/"/g, "'");
@@ -1204,6 +1239,7 @@ function PlaceHolderSign() {
"</body> </html>";
const variables = {
document_title: documentName,
note: pdfDetails?.[0]?.Note,
sender_name: senderName,
sender_mail: senderEmail,
sender_phone: senderPhone || "",
@@ -1218,6 +1254,7 @@ function PlaceHolderSign() {
}
const mailparam = {
senderName: senderName,
note: pdfDetails?.[0]?.Note || "",
senderMail: senderEmail,
title: documentName,
organization: orgName,
@@ -1225,13 +1262,14 @@ function PlaceHolderSign() {
sigingUrl: signPdf
};
let params = {
extUserId: extUserId,
extUserId: owner?.objectId,
recipient: signerMail[i].Email,
subject: replaceVar?.subject
? replaceVar?.subject
: mailTemplate(mailparam).subject,
replyto: senderEmail,
from: senderEmail,
from:
senderEmail,
html: replaceVar?.body
? replaceVar?.body
: mailTemplate(mailparam).body
@@ -1246,13 +1284,20 @@ function PlaceHolderSign() {
setMailStatus("success");
try {
let data;
if (requestBody && requestSubject && isCustomize) {
if (
requestBody &&
requestSubject &&
isCustomize
) {
data = {
RequestBody: htmlReqBody,
RequestSubject: requestSubject,
SendMail: true
};
} else if (tenantMailTemplate?.body && tenantMailTemplate?.subject) {
} else if (
tenantMailTemplate?.body &&
tenantMailTemplate?.subject
) {
data = {
RequestBody: tenantMailTemplate?.body,
RequestSubject: tenantMailTemplate?.subject,
@@ -1290,7 +1335,8 @@ function PlaceHolderSign() {
setIsSend(true);
setIsMailSend(true);
setIsUiLoading(false);
} else {
}
else {
setMailStatus("failed");
setIsSend(true);
setIsMailSend(true);
@@ -1531,7 +1577,8 @@ function PlaceHolderSign() {
status: defaultdata?.status || "required",
hint: defaultdata?.hint || "",
defaultValue: defaultdata?.defaultValue || "",
validation: {},
validation:
{},
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
@@ -1838,6 +1885,10 @@ function PlaceHolderSign() {
setPdfBase64Url(urlDetails.base64);
};
const handleSendDoc = () => {
if (docTitle?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
setIsAttchSignerModal(false);
setCheckTourStatus(true);
alertSendEmail();
@@ -1900,6 +1951,10 @@ function PlaceHolderSign() {
return isAllSigner;
};
const handleCloseAttachSigner = () => {
if (docTitle?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
setIsAttchSignerModal(false);
};
return (
@@ -2006,26 +2061,30 @@ function PlaceHolderSign() {
{!isCustomize && (
<span>{t("placeholder-alert-3")}</span>
)}
{isCustomize && (
<>
<EmailBody
editorRef={editorRef}
requestBody={requestBody}
requestSubject={requestSubject}
handleOnchangeRequest={handleOnchangeRequest}
setRequestSubject={setRequestSubject}
/>
<div
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
onClick={() => {
setRequestBody(defaultBody);
setRequestSubject(defaultSubject);
}}
>
<span>{t("reset-to-default")}</span>
</div>
</>
)}
{
isCustomize && (
<>
<EmailBody
editorRef={editorRef}
requestBody={requestBody}
requestSubject={requestSubject}
handleOnchangeRequest={
handleOnchangeRequest
}
setRequestSubject={setRequestSubject}
/>
<div
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
onClick={() => {
setRequestBody(defaultBody);
setRequestSubject(defaultSubject);
}}
>
<span>{t("reset-to-default")}</span>
</div>
</>
)
}
<div className="flex flex-row items-center gap-2 md:gap-6 mt-2">
<div className="flex flex-row gap-2">
<button
@@ -2043,14 +2102,16 @@ function PlaceHolderSign() {
</button>
)}
</div>
{!isCustomize && (
<span
className="op-link op-link-accent text-sm"
onClick={() => setIsCustomize(!isCustomize)}
>
{t("cutomize-email")}
</span>
)}
{
!isCustomize && (
<span
className="op-link op-link-accent text-sm"
onClick={() => setIsCustomize(!isCustomize)}
>
{t("cutomize-email")}
</span>
)
}
</div>
</>
)}
@@ -2093,9 +2154,11 @@ function PlaceHolderSign() {
<LottieWithLoader />
{pdfDetails[0].SendinOrder ? (
<p>
{t("placeholder-mail-alert", {
name: signersdata[0]?.Name
})}
{isCurrUser
? t("placeholder-mail-alert-you")
: t("placeholder-mail-alert", {
name: signersdata[0]?.Name
})}
</p>
) : (
<p>{t("placeholder-alert-4")}</p>
@@ -2172,24 +2235,45 @@ function PlaceHolderSign() {
</ModalUi>
<ModalUi
isOpen={isAttchSignerModal}
title={t("add-signer")}
title={t("create-document")}
handleClose={() => handleCloseAttachSigner()}
>
<div className="h-[100%] p-[20px]">
<div className="h-[100%] px-[20px] py-[10px]">
<div>
<label
htmlFor="doctitle"
className="block text-xs font-semibold"
>
Title
</label>
<input
type="text"
name="doctitle"
value={docTitle}
onChange={(e) => setDocTitle(e.target.value)}
required
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-[11px] py-[18px]"
/>
</div>
{pdfDetails[0].Placeholders?.some(
(x) => !x.signerObjId
) && (
<>
{/* grid grid-cols-1 md:grid-cols-2 */}
<div className="min-h-max max-h-[250px] overflow-y-auto">
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 mt-3 md:mx-3 mb-4 bg-base-200 text-base-content flex flex-col gap-2 relative">
<div className="py-2 text-base-content flex flex-col gap-2 relative">
{forms?.map((field, id) => {
return (
<div
className="flex flex-col"
key={field?.value}
>
<label>{field?.role}</label>
<label className="block text-xs font-semibold">
{field?.role}
</label>
<div className="flex justify-between items-center gap-1">
<div className="flex-1">
<AsyncSelect
@@ -2232,7 +2316,7 @@ function PlaceHolderSign() {
onClick={(e) =>
handleCreateNew(e, field.value)
}
className="op-btn op-btn-accent op-btn-outline op-btn-sm "
className="op-btn op-btn-accent op-btn-outline op-btn-sm"
>
<i className="fa-light fa-plus"></i>
</button>
@@ -2242,8 +2326,8 @@ function PlaceHolderSign() {
})}
</div>
</div>
<div className="w-full h-[1px] bg-[#9f9f9f] my-[15px]"></div>
<div className="flex mx-4 mb-4 gap-3">
<div className="w-full h-[0.5px] bg-[#9f9f9f] mt-[8px] mb-[15px]"></div>
<div className="flex mx-2 mb-2 gap-3">
<button
disabled={handleDisable()}
onClick={() => handleSendDoc()}

Some files were not shown because too many files have changed in this diff Show More