Compare commits

...
198 Commits
Author SHA1 Message Date
prafull-opensignlabsandGitHub bce0e4971e Merge pull request #2059 from OpenSignLabs/updates-20428403989
v2.34.0
2025-12-22 15:27:36 +05:30
prafull-opensignlabs 4fb8aa9a16 Delete apps/OpenSign/package-lock.json 2025-12-22 09:56:25 +00:00
prafull-opensignlabsandGitHub 8a2329dd60 Merge pull request #2050 from OpenSignLabs/updates-20027001996
v2.33.0
2025-12-08 17:16:35 +05:30
prafull-opensignlabs 75a40fbf43 Delete apps/OpenSignServer/package-lock.json 2025-12-08 11:45:29 +00:00
prafull-opensignlabsandGitHub 334edb3c0c Merge pull request #2042 from OpenSignLabs/updates-19765002150
v2.32.0
2025-11-28 18:54:17 +05:30
prafull-opensignlabs 6465aa7c2c Delete apps/OpenSignServer/package-lock.json 2025-11-28 13:19:46 +00:00
prafull-opensignlabsandGitHub 4593ce7fec Merge pull request #2039 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/npm_and_yarn-42c243e405
build(deps): bump body-parser from 2.2.0 to 2.2.1 in /apps/OpenSignServer in the npm_and_yarn group across 1 directory
2025-11-27 00:09:48 +05:30
dependabot[bot]andGitHub b4ffdf0dc9 build(deps): bump body-parser
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSignServer directory: [body-parser](https://github.com/expressjs/body-parser).


Updates `body-parser` from 2.2.0 to 2.2.1
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.2.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 18:22:09 +00:00
AmolandGitHub 3c8b8835c7 Merge pull request #2029 from OpenSignLabs/build_deps
fix: remove type parameter from parse.save method
2025-11-20 20:17:26 +05:30
prafull-opensignlabs 20752161d3 fix: remove type parameter from parse.save method 2025-11-20 20:08:06 +05:30
prafull-opensignlabsandGitHub 002b0d5277 Merge pull request #2027 from OpenSignLabs/build_deps
fix: handle password validation in change password page
2025-11-20 17:13:20 +05:30
prafull-opensignlabsandGitHub 47348b990e Merge pull request #1957 from Miracee97/patch-1
Update README.md
2025-11-20 17:09:04 +05:30
prafull-opensignlabs ac86b6ef61 fix: add password validation in change password page 2025-11-20 17:00:39 +05:30
prafull-opensignlabsandGitHub 7894a6edb8 Merge pull request #2025 from OpenSignLabs/build_deps
build(deps): update dependencies
2025-11-20 16:21:33 +05:30
prafull-opensignlabs dac8957904 build(deps): update dependencies 2025-11-20 16:19:19 +05:30
AmolandGitHub ef686aec6a Refactor Docker workflow for multi-arch builds 2025-11-20 12:19:09 +05:30
AmolandGitHub f31155e0b4 Merge pull request #2022 from OpenSignLabs/updates-19528160086
v2.31.0
2025-11-20 12:17:56 +05:30
AmolandGitHub 78e4857d1a Merge pull request #2023 from OpenSignLabs/all-contributors/add-ehowe
docs: add ehowe as a contributor for code
2025-11-20 12:17:29 +05:30
allcontributors[bot]andGitHub c3144366d9 docs: update .all-contributorsrc 2025-11-20 06:47:02 +00:00
allcontributors[bot]andGitHub f00a7c9e50 docs: update README.md 2025-11-20 06:47:01 +00:00
prafull-opensignlabs 803d25e9d1 Fix: Document title changes are not reflected when the document is sent to signers.
Merge pull request #1498 from nxglabs/staging
2025-11-20 06:44:56 +00:00
prafull-opensignlabsandGitHub ca1b87309f Merge pull request #2011 from OpenSignLabs/updates-18996309218
feat: handle undefine username/password for SMTP
2025-11-01 17:24:58 +05:30
prafull-opensignlabs 0b4548130d feat: handle undefine username/password for SMTP 2025-11-01 11:53:04 +00:00
prafull-opensignlabsandGitHub e8b3c7fed8 Merge pull request #1994 from OpenSignLabs/updates-18555669223
v2.30.0
2025-10-16 14:19:28 +05:30
prafull-opensignlabs 2ce43ec2c8 v2.30.0 2025-10-16 08:48:28 +00:00
prafull-opensignlabsandGitHub d5af1e8a17 Merge pull request #1973 from OpenSignLabs/updates-18164868546
v2.29.2
2025-10-01 19:38:24 +05:30
prafull-opensignlabs fab7998e22 refactor: handle session expired modal 2025-10-01 14:07:45 +00:00
prafull-opensignlabsandGitHub 4a4db6851e Merge pull request #1969 from OpenSignLabs/updates-18129802734
v2.29.1
2025-09-30 17:57:55 +05:30
prafull-opensignlabs 870973e7f3 Merge pull request 2025-09-30 12:26:28 +00:00
prafull-opensignlabsandGitHub 4146680c1e Merge pull request #1966 from OpenSignLabs/updates-18086083096
v2.29.0
2025-09-29 10:24:16 +05:30
prafull-opensignlabs d9fcc69ca4 v2.29.0 2025-09-29 04:47:23 +00:00
RaistlinandGitHub a878ca1a96 Update README.md
updated link to api documentation v1.1
2025-09-23 11:33:29 +02:00
prafull-opensignlabsandGitHub d1c405912b Merge pull request #1945 from OpenSignLabs/updates-17798985943
refactor: remove unnecessary code
2025-09-17 18:51:19 +05:30
prafull-opensignlabs 745aca1754 Merge pull request #1298 from nxglabs/sync-to-public_repo-17798472199
Merge pull request #1297 from nxglabs/staging
2025-09-17 13:19:46 +00:00
prafull-opensignlabsandGitHub 672eeea851 Merge pull request #1943 from OpenSignLabs/updates-17788011031
build(deps): build dependencies and dark mode ui
2025-09-17 11:21:02 +05:30
prafull-opensignlabs 98efff65c9 Merge pull request #1295 from nxglabs/sync-to-public_repo-17787708673
fix: show signer name in typed signature instead in selfsign flow
2025-09-17 05:48:01 +00:00
prafull-opensignlabsandGitHub fe21978538 Merge pull request #1941 from OpenSignLabs/updates-17737216200
v2.28.0
2025-09-15 20:23:17 +05:30
prafull-opensignlabs ca4ccfd889 Merge pull request #1292 from nxglabs/sync-to-public_repo-17730712175
Merge pull request #1291 from nxglabs/staging
2025-09-15 14:51:16 +00:00
prafull-opensignlabsandGitHub 6cb2d69566 Merge pull request #1931 from OpenSignLabs/updates-17548337132
fix: getting error 'user not found' while deleting user
2025-09-08 16:22:46 +05:30
prafull-opensignlabs 98b9ebb228 fix: getting error 'user not found' while deleting user 2025-09-08 10:51:52 +00:00
prafull-opensignlabsandGitHub 1c2f453496 Merge pull request #1929 from OpenSignLabs/updates-17540166746
v2.27.2
2025-09-08 10:30:18 +05:30
prafull-opensignlabs b01d8125d9 Merge pull request #1250 from nxglabs/sync-to-public_repo-17512737859
Merge pull request
2025-09-08 04:57:25 +00:00
prafull-opensignlabsandGitHub 85bc2fbbb0 Merge pull request #1902 from OpenSignLabs/updates-16958097538
fix: save sign, initials not working
2025-08-14 12:27:31 +05:30
prafull-opensignlabs e857724502 Merge pull request #1137 from nxglabs/signers_completed_main
fix: not able update public username and save sign, initials not working
2025-08-14 06:53:43 +00:00
prafull-opensignlabsandGitHub 086d8ee5be Merge pull request #1889 from OpenSignLabs/updates-16770567266
fix: managesign report alignment and loader issue
2025-08-06 13:06:40 +05:30
prafull-opensignlabs 04543e826a Merge pull request #1105 from nxglabs/raktima-main-patch-2
fix: managesign report alignment and loader issue
2025-08-06 07:35:09 +00:00
prafull-opensignlabsandGitHub 04f6e38778 Merge pull request #1881 from OpenSignLabs/updates-16679049560
refactor: remove form-name from form title
2025-08-01 21:03:59 +05:30
prafull-opensignlabs 1b5c85fd5f Merge pull request #1085 from nxglabs/signers_completed_main
refactor: remove form-name from form title
2025-08-01 15:33:06 +00:00
prafull-opensignlabsandGitHub 123425d7d1 Merge pull request #1880 from OpenSignLabs/updates-16678047946
feat: allow preferences menu for all user
2025-08-01 20:18:07 +05:30
prafull-opensignlabs a5b9d8178c Merge pull request #1084 from nxglabs/signers_completed_main
feat: allow preferences menu for all user and add smtp setting in it
2025-08-01 14:46:44 +00:00
prafull-opensignlabsandGitHub aefd2ff933 Merge pull request #1868 from OpenSignLabs/updates-16625254839
refactor: correct grammar and normalise font size of signer email in report
2025-07-30 19:54:36 +05:30
nxglabs fe93be8be7 Merge pull request #1070 from nxglabs/signers_completed_main 2025-07-30 14:22:07 +00:00
prafull-opensignlabsandGitHub 369e3660fc Merge pull request #1865 from OpenSignLabs/updates-16598173522
fix: add dynamic truncate for email in list of signers
2025-07-29 19:29:57 +05:30
nxglabs 378c0f057b Merge pull request #1066 from nxglabs/signers_completed_main 2025-07-29 13:54:44 +00:00
prafull-opensignlabsandGitHub 4f3c034cec Merge pull request #1864 from OpenSignLabs/updates-16597572999
Fix: show signers list in completed documents
2025-07-29 19:04:08 +05:30
nxglabs e7d3453744 Merge pull request #1064 from nxglabs/signers_completed_main 2025-07-29 13:30:45 +00:00
prafull-opensignlabsandGitHub 47cc0ef580 Merge pull request #1858 from OpenSignLabs/updates-16525889391
remove checkbox from sign agreement screen
2025-07-25 21:18:33 +05:30
prafull-opensignlabs 04f71be6a2 Merge pull request #1044 from nxglabs/raktima-patch-main-2
fix: remove agreement checkbox and remove agreement checkbox tour
2025-07-25 15:38:38 +00:00
prafull-opensignlabsandGitHub 07603f4bc2 Merge pull request #1856 from OpenSignLabs/imgbot
[ImgBot] Optimize images
2025-07-24 14:40:19 +05:30
ImgBotApp ea16180551 [ImgBot] Optimize images
*Total -- 487.98kb -> 238.96kb (51.03%)

/apps/OpenSign/public/logo512.png -- 10.72kb -> 2.72kb (74.62%)
/apps/OpenSignServer/logo.png -- 86.48kb -> 24.83kb (71.28%)
/apps/OpenSign/src/assets/images/logo.png -- 86.48kb -> 24.83kb (71.28%)
/apps/OpenSign/public/static/js/assets/images/logo.2a7bff0c1189183fafe71d7d5b94e0cd.png -- 86.48kb -> 24.83kb (71.28%)
/apps/OpenSign/public/logo192.png -- 2.13kb -> 0.65kb (69.69%)
/apps/OpenSign/src/assets/images/dp.png -- 12.83kb -> 4.25kb (66.9%)
/apps/OpenSign/src/assets/images/folder.png -- 4.39kb -> 1.93kb (56.05%)
/apps/OpenSign/public/static/js/assets/images/logo-dark.png -- 95.21kb -> 57.21kb (39.91%)
/apps/OpenSignServer/public/assets/images/parse-logo.png -- 5.16kb -> 3.81kb (26.21%)
/apps/OpenSign/src/assets/images/pdf3.png -- 11.66kb -> 9.77kb (16.21%)
/apps/OpenSign/src/assets/images/login_img.svg -- 40.75kb -> 39.10kb (4.04%)
/apps/OpenSign/src/assets/images/recreatedoc.png -- 40.69kb -> 40.08kb (1.48%)
/apps/OpenSign/src/assets/images/pad.svg -- 5.00kb -> 4.94kb (1.07%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-07-24 08:28:02 +00:00
prafull-opensignlabsandGitHub 75ad566d54 Merge pull request #1841 from OpenSignLabs/updates-16144605887
v2.26.1
2025-07-08 19:01:19 +05:30
nxglabs fda9fb534b Merge pull request #963 from nxglabs/staging
v2.26.1
2025-07-08 13:27:37 +00:00
prafull-opensignlabsandGitHub cb3e31ebbf Merge pull request #1838 from OpenSignLabs/updates-16124490027
dark-theme logo
2025-07-07 23:43:31 +05:30
nxglabs 400525debb Merge pull request #960 from nxglabs/fix_customlogo
fix: custom applogo not visible in header
2025-07-07 18:07:24 +00:00
prafull-opensignlabsandGitHub 1cc017bb05 Merge pull request #1829 from OpenSignLabs/updates-16070658635
fix: quicksend screen get black out while sending document
2025-07-04 15:06:16 +05:30
prafull-opensignlabs 531dd11185 Merge pull request #942 from nxglabs/staging
fix: quicksend screen get black out while sending document
2025-07-04 09:32:50 +00:00
prafull-opensignlabsandGitHub 1e80754750 Merge pull request #1827 from OpenSignLabs/updates-16068582405
revert: css of drive
2025-07-04 13:14:27 +05:30
prafull-opensignlabs 0d286938c7 Merge pull request #940 from nxglabs/staging
revert: css of drive
2025-07-04 07:43:32 +00:00
prafull-opensignlabsandGitHub 360c13df25 Merge pull request #1825 from OpenSignLabs/updates-16055306436
v2.26.0
2025-07-04 10:35:33 +05:30
nxglabs 71b9bd6367 Merge pull request #937 from nxglabs/tenant_mailtemplate
fix: if user not provide default value use it from template in createdocument from template API
2025-07-03 16:04:35 +00:00
prafull-opensignlabsandGitHub 4a5b35e147 Merge pull request #1818 from OpenSignLabs/updates-15966812948
fix: handle undefine appId
2025-06-30 13:09:02 +05:30
prafull-opensignlabs 7930a8f439 Merge pull request #917 from nxglabs/handle_appid
fix: handle undefine appId
2025-06-30 07:37:32 +00:00
prafull-opensignlabsandGitHub 7204183542 Merge pull request #1804 from OpenSignLabs/updates-15754544632
fix: resolve issues
2025-06-19 15:03:08 +05:30
nxglabs 5561673fdc Merge pull request #866 from nxglabs/fix_main_issues 2025-06-19 09:31:17 +00:00
prafull-opensignlabsandGitHub 0df44062d6 Merge pull request #1803 from OpenSignLabs/updates-15736278136
fix: correct baseapi of docxtopdf and decryptpdf
2025-06-18 20:25:53 +05:30
nxglabs 8983440763 Merge pull request #864 from nxglabs/staging 2025-06-18 14:54:13 +00:00
prafull-opensignlabsandGitHub 5a28c0ed06 Merge pull request #1802 from OpenSignLabs/updates-15735030679
v2.25.0
2025-06-18 19:43:43 +05:30
nxglabs cbca22c308 Merge pull request #861 from nxglabs/staging
fix: handle password protected pdf
2025-06-18 14:03:05 +00:00
prafull-opensignlabsandGitHub b0e0b38e8f Merge pull request #1789 from OpenSignLabs/updates-15483575034
v2.24.1
2025-06-06 11:17:12 +05:30
prafull-opensignlabs 49c23fb52b Merge pull request #753 from OpenSignLabs/staging
v2.24.1
2025-06-06 05:33:37 +00:00
prafull-opensignlabsandGitHub da4b604710 Merge pull request #1785 from OpenSignLabs/updates-15425322075
handle crashing pattern for text input widgets
2025-06-04 00:17:30 +05:30
prafull-opensignlabs 82d518b3c9 Merge pull request #739 from OpenSignLabs/staging
disable signing request email when send_email flag is false
2025-06-03 18:45:49 +00:00
prafull-opensignlabsandGitHub dcbde2b661 Merge pull request #1784 from OpenSignLabs/updates-15419310132
v2.23.1
2025-06-03 19:28:35 +05:30
prafull-opensignlabs 27b0b426ad v2.23.1
Enable widget placement on click in desktop view
Disable signature types not working
Change icon of agreement sign checkbox
Update dependencies
2025-06-03 13:55:57 +00:00
prafull-opensignlabsandGitHub 8788afaa8e Merge pull request #1766 from OpenSignLabs/updates-15350472621
fix: add verify-document route
2025-05-30 21:19:35 +05:30
prafull-opensignlabs 428aa65b38 Merge pull request #710 from OpenSignLabs/staging
fix: add verify-document route
2025-05-30 15:45:27 +00:00
AmolandGitHub 295d942427 feat: add deploy to digitalocean button 2025-05-30 20:33:43 +05:30
prafull-opensignlabsandGitHub c4e28e6e6e Merge pull request #1764 from OpenSignLabs/updates-15349515719
v2.23.0
2025-05-30 20:26:41 +05:30
prafull-opensignlabs b19fb29b82 Merge pull request #708 from OpenSignLabs/staging
refactor: unnecessary code
2025-05-30 14:54:53 +00:00
prafull-opensignlabsandGitHub 3d223ba343 Merge pull request #1756 from OpenSignLabs/updates-15275255429
v2.22.0
2025-05-27 18:13:12 +05:30
prafull-opensignlabs 98754c776f Merge pull request #675 from OpenSignLabs/staging
v2.22.0
2025-05-27 12:28:40 +00:00
prafull-opensignlabsandGitHub 1550e3a3d0 Merge pull request #1748 from OpenSignLabs/updates-15156338467
v2.21.2
2025-05-21 13:09:14 +05:30
prafull-opensignlabs 3500883205 v2.21.2
fix: handle subscription error in publicsign
2025-05-21 07:37:15 +00:00
prafull-opensignlabsandGitHub d888897b62 Update README.md 2025-05-20 20:10:44 +05:30
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 c4779c14c1 feat: add all three docker command 2025-05-14 19:11:18 +05:30
prafull-opensignlabsandGitHub e48869cb2f feat: add deploy to digitalocean button 2025-05-14 18:36:47 +05:30
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
prafull-opensignlabsandGitHub 9658761ede Merge pull request #1646 from OpenSignLabs/prafull-opensignlabs-patch-2
add deprecated warning for env
2025-03-20 16:03:28 +05:30
prafull-opensignlabsandGitHub 6aad832e37 refacctor: add deprecated warning for env 2025-03-20 11:52:18 +05:30
prafull-opensignlabsandGitHub e4b6be51d9 Merge pull request #1644 from OpenSignLabs/updates-13949958694
Merge pull request #395 from OpenSignLabs
2025-03-19 20:41:10 +05:30
prafull-opensignlabs a3b2c797fb Merge pull request #395 from OpenSignLabs/fix_dailyemailquota
fix: dailyemailquota not updating for user and default value not updating of checkbox in API
2025-03-19 15:08:50 +00:00
ZathielandGitHub 22d27abb76 Merge pull request #1631 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/prettier-3.5.3
build(deps-dev): bump prettier from 3.5.0 to 3.5.3 in /apps/OpenSignServer
2025-03-16 20:06:24 +01:00
dependabot[bot]andGitHub 45e0679ca7 build(deps-dev): bump prettier in /apps/OpenSignServer
Bumps [prettier](https://github.com/prettier/prettier) from 3.5.0 to 3.5.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.0...3.5.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 00:48:25 +00:00
prafull-opensignlabsandGitHub 2b2aa20deb Merge pull request #1627 from OpenSignLabs/updates-13647519163
Merge pull request #357 from OpenSignLabs/revert_mail
2025-03-04 11:50:01 +05:30
prafull-opensignlabs 8d6ed3d2b5 Merge pull request #357 from OpenSignLabs/revert_mail
revert: revert mail sending
2025-03-04 06:19:26 +00:00
prafull-opensignlabsandGitHub 6064f66cdf Merge pull request #1625 from mubix/fix-install-doc
Fix Install Documentation
2025-03-04 11:36:42 +05:30
prafull-opensignlabsandGitHub 0ba29eb799 Merge pull request #1626 from OpenSignLabs/updates-13647292049
Merge pull request #356 from OpenSignLabs/correct_cond
2025-03-04 11:34:02 +05:30
prafull-opensignlabs d0dd571478 Merge pull request #356 from OpenSignLabs/mismatch_attach
fix: correct condition
2025-03-04 06:02:50 +00:00
Rob FullerandGitHub ddb63d81a7 fix install doc 2025-03-03 14:04:43 -05:00
prafull-opensignlabsandGitHub f78451d3f9 Merge pull request #1621 from OpenSignLabs/updates-13637250862
Merge pull request #349 from OpenSignLabs/crash_pdf
2025-03-04 00:08:15 +05:30
ZathielandGitHub 28630e2d23 Merge pull request #1618 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/mongodb-6.14.0
build(deps): bump mongodb from 6.13.0 to 6.14.0 in /apps/OpenSignServer
2025-03-03 19:00:15 +01:00
ZathielandGitHub c5022eaee6 Merge pull request #1620 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/posthog-node-4.8.1
build(deps): bump posthog-node from 4.5.0 to 4.8.1 in /apps/OpenSignServer
2025-03-03 18:58:01 +01:00
prafull-opensignlabs 1b7f07a2c4 Merge pull request #349 from OpenSignLabs/mismatch_attach
feat: restrict emails
2025-03-03 17:57:33 +00:00
dependabot[bot]andGitHub 2484c67406 build(deps): bump posthog-node in /apps/OpenSignServer
Bumps [posthog-node](https://github.com/PostHog/posthog-js-lite/tree/HEAD/posthog-node) from 4.5.0 to 4.8.1.
- [Release notes](https://github.com/PostHog/posthog-js-lite/releases)
- [Changelog](https://github.com/PostHog/posthog-js-lite/blob/main/posthog-node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js-lite/commits/posthog-node-v4.8.1/posthog-node)

---
updated-dependencies:
- dependency-name: posthog-node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 00:42:08 +00:00
dependabot[bot]andGitHub abfc3f32f2 build(deps): bump mongodb from 6.13.0 to 6.14.0 in /apps/OpenSignServer
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 6.13.0 to 6.14.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v6.13.0...v6.14.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 00:41:39 +00:00
prafull-opensignlabsandGitHub 5c06ff4242 Merge pull request #1613 from OpenSignLabs/updates-13494498125
Merge pull request #343 from OpenSignLabs/staging
2025-02-24 14:44:07 +05:30
prafull-opensignlabs 1899acfffc Merge pull request #343 from OpenSignLabs/staging
fix: selected date resets to the current date when navigating between document pages.
2025-02-24 09:12:24 +00:00
ZathielandGitHub 796ff058c8 Merge pull request #1581 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/webpack-cli-6.0.1 2025-02-24 09:57:37 +01:00
ZathielandGitHub c88cb5357c Merge pull request #1609 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/ws-8.18.1 2025-02-24 09:56:17 +01:00
dependabot[bot]andGitHub a6a0325d4b build(deps): bump ws from 8.18.0 to 8.18.1 in /apps/OpenSignServer
Bumps [ws](https://github.com/websockets/ws) from 8.18.0 to 8.18.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.0...8.18.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 08:47:11 +00:00
ZathielandGitHub b18d17c44d Merge pull request #1610 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/babel/eslint-parser-7.26.8 2025-02-24 09:45:44 +01:00
ZathielandGitHub a73779e74b Merge pull request #1612 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/i18next-browser-languagedetector-8.0.4 2025-02-24 09:44:44 +01:00
dependabot[bot]andGitHub 5b943c1965 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-02-24 00:52:19 +00:00
dependabot[bot]andGitHub f73844d730 build(deps-dev): bump @babel/eslint-parser in /apps/OpenSignServer
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.26.5 to 7.26.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.8/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 00:08:02 +00:00
prafull-opensignlabsandGitHub aaadab540a Merge pull request #1607 from OpenSignLabs/updates-13456995803
Merge pull request #339 from OpenSignLabs/staging
2025-02-21 18:11:59 +05:30
prafull-opensignlabsandGitHub 83bc466df4 Merge pull request #1606 from OpenSignLabs/updates-13455918957
Merge pull request #336 from OpenSignLabs/staging
2025-02-21 18:10:43 +05:30
prafull-opensignlabs d85ed954f1 Merge pull request #339 from OpenSignLabs/staging
fix: remove logo from certificate for white labeled user
2025-02-21 12:40:33 +00:00
ZathielandGitHub 369deb6e26 Merge pull request #1584 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/regex-parser-2.3.1 2025-02-21 12:44:54 +01:00
ZathielandGitHub 295d1af910 Merge pull request #1589 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/eslint-9.20.1 2025-02-21 12:41:57 +01:00
dependabot[bot]andGitHub 080d7c08dc build(deps-dev): bump eslint in /apps/OpenSignServer
Bumps [eslint](https://github.com/eslint/eslint) from 9.20.0 to 9.20.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.20.0...v9.20.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 11:41:35 +00:00
ZathielandGitHub c877f9fc5d Merge pull request #1590 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/babel/eslint-parser-7.26.8 2025-02-21 12:40:28 +01:00
prafull-opensignlabs 99c7a58d62 Merge pull request #336 from OpenSignLabs/staging
style: correct logo aspect ratio in guest login
2025-02-21 11:32:27 +00:00
dependabot[bot]andGitHub 97642bf857 build(deps-dev): bump webpack-cli from 5.1.4 to 6.0.1 in /apps/OpenSign
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 5.1.4 to 6.0.1.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@5.1.4...webpack-cli@6.0.1)

---
updated-dependencies:
- dependency-name: webpack-cli
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 07:21:34 +00:00
dependabot[bot]andGitHub 2f67ac7b0a build(deps): bump regex-parser from 2.3.0 to 2.3.1 in /apps/OpenSign
Bumps [regex-parser](https://github.com/IonicaBizau/regex-parser.js) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/IonicaBizau/regex-parser.js/releases)
- [Commits](https://github.com/IonicaBizau/regex-parser.js/compare/2.3.0...2.3.1)

---
updated-dependencies:
- dependency-name: regex-parser
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 07:21:32 +00:00
prafull-opensignlabsandGitHub 23aecb45d9 Merge pull request #1601 from OpenSignLabs/updates-13451970686
v2.13.1
2025-02-21 12:50:13 +05:30
prafull-opensignlabs 92b507e621 v2.13.1
fix: Adapter prototype don't match expected prototype during
fix: drive get crash if signer array is null
2025-02-21 07:18:47 +00:00
prafull-opensignlabsandGitHub 1f6c2c7d38 Merge pull request #1597 from OpenSignLabs/updates-13420682171
Merge pull request #316 from OpenSignLabs/patch
2025-02-21 12:45:33 +05:30
prafull-opensignlabs c1be197346 Merge pull request #316 from OpenSignLabs/fix_emailresetdate
fix: Adapter prototype don't match expected prototype
2025-02-19 19:22:54 +00:00
dependabot[bot]andGitHub 24a95e8852 build(deps-dev): bump @babel/eslint-parser in /apps/OpenSignServer
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.26.5 to 7.26.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.8/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 00:56:20 +00:00
320 changed files with 43454 additions and 58595 deletions
+9
View File
@@ -241,6 +241,15 @@
"contributions": [
"translation"
]
},
{
"login": "ehowe",
"name": "Eugene Howe",
"avatar_url": "https://avatars.githubusercontent.com/u/756984?v=4",
"profile": "https://github.com/ehowe",
"contributions": [
"code"
]
}
]
}
+4 -3
View File
@@ -8,13 +8,11 @@ PUBLIC_URL=https://localhost:3001
GENERATE_SOURCEMAP=false
# Set it to the URL from where APIs will be accessible, for local development it should be localhost:3000/api/app (use your local port number instead)
# REACT_APP_SERVERURL=http://localhost:8080/app
# A 12 character long random app identifier. The value of this should be same as APP_ID which is a variable used by backend API.
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
REACT_APP_APPID=opensign
# Backend ExpressJS config ****************************************************************************************************************************************************************************************
# A 12 character long random app identifier. The value of this should be same as REACT_APP_APPID which is a variable used by Frontend React App.
APP_ID=opensign
# Name of the app. It will be visible in the verification emails sent out.
appName=open_sign_server
# A 12 character long random secret key that allows access to all the data. It is used in Parse dashboard config to view all the data in the database.
@@ -111,3 +109,6 @@ CRUxFgQUDYlgGVxSxuOknhQc256x3++7BDwwMTAhMAkGBSsOAwIaBQAEFFjASdYl
# Provide Pass pharse of above PFX or p12 document
PASS_PHRASE=opensign
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
APP_ID=opensign
+31 -12
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:
@@ -14,34 +21,46 @@ jobs:
dockerfile: apps/OpenSign/Dockerhubfile
- image: opensign/opensignserver
dockerfile: apps/OpenSignServer/Dockerhubfile
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
# 👇 Enable emulation so we can build for arm64 on amd64 runners
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: linux/amd64,linux/arm64
# 👇 Enable Buildx for multi-arch builds
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Extract metadata (tags, labels) for Docker
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ matrix.image }}
-
name: Debug - List files opensign
- name: Debug - List files opensign
run: ls -R apps/OpenSign/ # Adjust the path as needed
-
name: Debug - List files opensignserver
- name: Debug - List files opensignserver
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
-
name: Build and push
- name: Build and push (multi-arch)
uses: docker/build-push-action@v4
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
# 👇 Build for both AMD64 & ARM64
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+2 -2
View File
@@ -1,7 +1,7 @@
{$HOST_URL} {
reverse_proxy client:3000
handle_path /app/* {
handle_path /api/* {
reverse_proxy server:8080
rewrite * /app{uri}
rewrite * {uri}
}
}
+20 -3
View File
@@ -60,7 +60,7 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
- **PDF Template Creation:** OpenSign™ allows you to create and store PDF document templates for repeated use thereby saving you a lot of time & collect e-signatures seamlessly.
- **OpenSign™ Drive:** It is a centralised secure vault for your digital documents that makes storing, signing, organizing, sharing & archieving your docs a breeze.
- **Audit Trails & completion certificate:** Being a security focused solution, OpenSign™ makes it a top priority to save detailed logs for tracking document activities along with time-stamps, IP addresses, email IDs & phone numbers. A completion certificate is generated as soon as document is completed which contains all the document related logs for added safety.
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/opensign-api-v-1) to start integrating it in your existing applications.
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/v1.1) to start integrating it in your existing applications.
- **Integrations:** The open source document signing experience becomes even more seamless because of integrations with various Cloud storage systems, CRMs & enterprise platforms. We also have a Zapier integration that allows you to integrate it with virtually any application.
<img alt="Login page" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/8016edb6-7d6e-4185-ab72-82d2c4a16032" height='200' >
<img alt="Dash_board" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/6e7b96f7-3154-4c25-bc1e-caf33925cc47" height='200'>
@@ -73,11 +73,27 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
---
### Installation
### Deploy
Note: The default MongoDB instance used in deployment is not persistant and will be cleared on every restart. To retain your data, configure and supply your own MongoDB connection URL.
#### DigitalOcean
[![Deploy on DigitalOcean](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/OpenSignLabs/Deploy-OpenSign-to-Digital-Ocean/tree/main&refcode=30db1c901ab0)
#### Docker
The simplest way to install OpenSign on your own server is using official docker images by running the following command -
**Command for linux/MacOS**
```
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/docker_beta/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/docker_beta/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/docker_beta/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
```
**Command for Windows (Powershell)**
```
$env:HOST_URL="https://opensign.yourdomain.com"; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml -OutFile docker-compose.yml; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile -OutFile Caddyfile; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev -OutFile .env.local_dev; Rename-Item -Path .env.local_dev -NewName .env.prod; docker compose up --force-recreate
```
**Command for Windows (CMD/Terminal)**
```
set HOST_URL=https://opensign.yourdomain.com && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && rename .env.local_dev .env.prod && docker compose up --force-recreate
```
Make sure that you have `Docker` and `git` installed before you run this command -
@@ -149,6 +165,7 @@ We would like to thank all our contributors and users for their support and feed
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehowe"><img src="https://avatars.githubusercontent.com/u/756984?v=4?s=100" width="100px;" alt="Eugene Howe"/><br /><sub><b>Eugene Howe</b></sub></a><br /><a href="#code-ehowe" title="Code">💻</a></td>
</tr>
</tbody>
</table>
-25
View File
@@ -1,25 +0,0 @@
# Use an official Node runtime as the base image
FROM node:18
# Set the working directory inside the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json first to leverage Docker cache
COPY ./package*.json ./
# Install application dependencies
RUN npm install
# Copy the current directory contents into the container
COPY ./ .
COPY ./.husky .
# Make port 3000 available to the world outside this container
EXPOSE 3000
# Define environment variables if needed
# ENV NODE_ENV production
# Run the application
ENTRYPOINT npm run start
+11 -3
View File
@@ -1,5 +1,5 @@
# Use an official Node runtime as the base image
FROM node:18
FROM node:22.14.0
# Set the working directory inside the container
WORKDIR /usr/src/app
@@ -13,16 +13,24 @@ RUN npm install
# Copy the current directory contents into the container
COPY apps/OpenSign/ .
COPY apps/OpenSign/.husky .
COPY apps/OpenSign/entrypoint.sh .
# make the entrypoint.sh file executable
RUN chmod +x entrypoint.sh
# Define environment variables if needed
ENV NODE_ENV=production
ENV REACT_APP_DEPLOYMENT=free_selfhost
ENV GENERATE_SOURCEMAP=false
# build
RUN npm run build
# Inject env.js loader into index.html
RUN sed -i '/<head>/a\<script src="/env.js"></script>' build/index.html
# Make port 3000 available to the world outside this container
EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]
# Run the application
CMD ["npm", "start"]
+213
View File
@@ -0,0 +1,213 @@
# Tailwind Dark Mode Usage Guide
## Overview
This guide shows how to use the new Tailwind utilities for better dark mode visibility in OpenSign.
## Button Styling
### VS Code-style Disabled Buttons
```jsx
// Option 1: Direct VS Code styling
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
Disabled Button
</button>
// Option 2: Themed disabled styling
<button className="op-btn btn-themed-disabled">
Themed Button
</button>
// Option 3: Conditional styling
<button
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
disabled={isDisabled}
>
Dynamic Button
</button>
```
## Icon Styling
### Theme-aware Icons
```jsx
// Better visibility in dark mode
<i className="fa-light fa-folder icon-improved"></i>
// Muted but still visible
<i className="fa-light fa-plus icon-muted"></i>
// Disabled state
<i className="fa-light fa-trash icon-disabled"></i>
```
### CSS Variable Approach
```jsx
// Using CSS variables
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
// Inline styles with CSS variables
<i
className="fa-light fa-plus"
style={{ color: 'var(--icon-color)' }}
/>
```
### Legacy JavaScript Function (Still Supported)
```jsx
// Existing approach - still works
<i
className="fa-light fa-plus"
style={{ color: getThemeIconColor() }}
/>
```
## Text Styling
### Improved Gray Text
```jsx
// These automatically improve in dark mode
<span className="text-gray-500">More visible in dark mode</span>
<span className="text-gray-400">Muted but readable</span>
<span className="text-gray-600">Clear text</span>
```
## Complete Examples
### Toolbar with Better Visibility
```jsx
const Toolbar = () => (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
```
### Form with Disabled States
```jsx
const Form = ({ isSubmitting }) => (
<form>
<input className="op-input" />
<button
className={`op-btn op-btn-primary ${isSubmitting ? 'op-btn-vscode-disabled' : ''}`}
disabled={isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
```
## React-Tour and Tooltip Dark Mode Support
### React-Tour Modals
The react-tour modals now automatically support dark mode with VS Code-inspired styling:
```jsx
// These components automatically get dark mode styling
<Tour
onRequestClose={closeTour}
steps={tourConfig}
isOpen={isOpen}
rounded={5}
/>
```
### ReactTooltip Components
All ReactTooltip instances now support dark mode:
```jsx
// Automatically styled for dark mode
<ReactTooltip id="my-tooltip" className="z-[999]">
<div className="max-w-[200px]">
<p>Tooltip content</p>
</div>
</ReactTooltip>
```
### HoverCard Balloon UI
The balloon tooltips in OpenSign Drive now properly support dark mode:
```jsx
// These automatically get dark styling in dark mode
<HoverCard>
<HoverCardContent>
Document information
</HoverCardContent>
</HoverCard>
```
## Dark Mode Features Added
### 1. **React-Tour Modal Styling**
- Background: `#1F2937` (VS Code modal background)
- Text: `#E5E7EB` (soft white for readability)
- Borders: `#374151` (subtle borders)
- Buttons: VS Code-style primary/secondary buttons
### 2. **ReactTooltip Styling**
- Background: `#1F2937` with proper contrast
- Border: `#374151` for definition
- Box shadow: Enhanced for dark backgrounds
- Text: `#E5E7EB` for optimal readability
### 3. **HoverCard Balloon UI**
- Background: `#1F2937` (matches VS Code)
- Text: `#E5E7EB` for readability
- Arrow: Automatically matches background color
- Enhanced shadows for dark backgrounds
### 4. **React-Datepicker Support**
- Calendar background: `#1F2937`
- Selected dates: VS Code blue (`#007ACC`)
- Hover states: Proper contrast ratios
- Navigation arrows: Themed appropriately
## CSS Classes Reference
| Class | Purpose | Dark Mode Color |
|-------|---------|----------------|
| `icon-improved` | Better icon visibility | `#CCCCCC` |
| `icon-muted` | Muted but visible icons | `#999999` |
| `icon-disabled` | Disabled icon state | `#858585` |
| `op-btn-vscode-disabled` | VS Code disabled button | Background: `#3C3C3C` |
| `btn-themed-disabled` | Themed disabled button | Uses CSS variables |
| `icon-themed` | Variable-based icon color | `var(--icon-color)` |
| `.reactour__helper` | `#1F2937` background |
| `.react-tooltip` | `#1F2937` background |
| `.HoverCardContent` | `#1F2937` background |
| `.react-datepicker` | `#1F2937` background |
## Migration Guide
### From JavaScript Function to Tailwind
```jsx
// Before
<i style={{ color: getThemeIconColor() }} className="fa-light fa-plus" />
// After
<i className="fa-light fa-plus icon-improved" />
```
### From Hardcoded Colors to Theme-aware
```jsx
// Before
<i className="fa-light fa-plus text-gray-500" />
// After (automatic improvement)
<i className="fa-light fa-plus text-gray-500" />
// OR explicitly
<i className="fa-light fa-plus icon-improved" />
```
## Migration Notes
All existing tooltip and tour components will automatically inherit the new dark mode styling when the theme is set to `opensigndark`. No code changes required for existing implementations.
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
ENV_FILE=./build/env.js
DOTENV_FILE=./.env.prod # ✅ use .env.prod
echo "Generating runtime env file at $ENV_FILE..."
echo "window.RUNTIME_ENV = {" > $ENV_FILE
# List of keys to include
RUNTIME_KEYS="REACT_APP_SERVERURL"
for key in $RUNTIME_KEYS; do
# First check docker env (-e), fallback to .env file
value=$(printenv "$key")
if [ -z "$value" ] && [ -f "$DOTENV_FILE" ]; then
# fallback: read from .env
value=$(grep "^$key=" "$DOTENV_FILE" | cut -d '=' -f2- | tr -d '\r\n' | sed 's/"/\\"/g')
else
value=$(echo "$value" | sed 's/"/\\"/g')
fi
echo " $key: \"$value\"," >> $ENV_FILE
done
echo "};" >> $ENV_FILE
exec "$@"
@@ -0,0 +1,90 @@
/**
* Tailwind Dark Mode Usage Examples for OpenSign
*
* This file demonstrates how to use the new Tailwind utilities
* for better dark mode visibility of buttons and icons.
*/
// Example 1: VS Code-style disabled buttons
const DisabledButtonExamples = () => {
return (
<div>
{/* Option A: Using the VS Code disabled style */}
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
VS Code Style Disabled Button
</button>
{/* Option B: Using themed disabled style */}
<button className="op-btn btn-themed-disabled">
Themed Disabled Button
</button>
{/* Option C: Conditional styling */}
<button
className={`op-btn op-btn-primary ${isDisabled ? "op-btn-vscode-disabled" : ""}`}
disabled={isDisabled}
>
Conditional Button
</button>
</div>
);
};
// Example 2: Icon visibility improvements
const IconExamples = () => {
return (
<div>
{/* Theme-aware icons with better visibility */}
<i className="fa-light fa-folder icon-improved"></i>
<i className="fa-light fa-plus icon-muted"></i>
<i className="fa-light fa-trash icon-disabled"></i>
{/* Using CSS variables */}
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
{/* Gray text that automatically improves in dark mode */}
<span className="text-gray-500">
This text is now more visible in dark mode
</span>
<span className="text-gray-400">Muted but still readable</span>
</div>
);
};
// Example 3: Using CSS variables in inline styles
const InlineStyleExamples = () => {
return (
<div>
{/* Using CSS variables directly */}
<i className="fa-light fa-plus" style={{ color: "var(--icon-color)" }} />
{/* Using the existing JavaScript function */}
<i className="fa-light fa-minus" style={{ color: getThemeIconColor() }} />
</div>
);
};
// Example 4: Toolbar with improved icons
const ToolbarExample = () => {
return (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
};
export {
DisabledButtonExamples,
IconExamples,
InlineStyleExamples,
ToolbarExample
};
@@ -0,0 +1,94 @@
/**
* Tailwind Dark Mode Usage Examples for OpenSign
*
* This file demonstrates how to use the new Tailwind utilities
* for better dark mode visibility of buttons and icons.
*/
// Example 1: VS Code-style disabled buttons
const DisabledButtonExamples = () => {
return (
<div>
{/* Option A: Using the VS Code disabled style */}
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
VS Code Style Disabled Button
</button>
{/* Option B: Using themed disabled style */}
<button className="op-btn btn-themed-disabled">
Themed Disabled Button
</button>
{/* Option C: Conditional styling */}
<button
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
disabled={isDisabled}
>
Conditional Button
</button>
</div>
);
};
// Example 2: Icon visibility improvements
const IconExamples = () => {
return (
<div>
{/* Theme-aware icons with better visibility */}
<i className="fa-light fa-folder icon-improved"></i>
<i className="fa-light fa-plus icon-muted"></i>
<i className="fa-light fa-trash icon-disabled"></i>
{/* Using CSS variables */}
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
{/* Gray text that automatically improves in dark mode */}
<span className="text-gray-500">This text is now more visible in dark mode</span>
<span className="text-gray-400">Muted but still readable</span>
</div>
);
};
// Example 3: Using CSS variables in inline styles
const InlineStyleExamples = () => {
return (
<div>
{/* Using CSS variables directly */}
<i
className="fa-light fa-plus"
style={{ color: 'var(--icon-color)' }}
/>
{/* Using the existing JavaScript function */}
<i
className="fa-light fa-minus"
style={{ color: getThemeIconColor() }}
/>
</div>
);
};
// Example 4: Toolbar with improved icons
const ToolbarExample = () => {
return (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
};
export {
DisabledButtonExamples,
IconExamples,
InlineStyleExamples,
ToolbarExample
};
@@ -1,20 +1,22 @@
<!DOCTYPE html>
<html lang="en" data-theme="opensigncss">
<head>
<meta charset="utf-8" />
<link rel="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"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
<title>OpenSign™</title>
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
<title>...Loading</title>
</head>
<body>
<div id="root" style="touch-action:pan-x pan-y;"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
-28358
View File
File diff suppressed because it is too large Load Diff
+74 -70
View File
@@ -1,74 +1,71 @@
{
"name": "open_sign",
"version": "0.1.0",
"version": "2.34.0",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@lottiefiles/dotlottie-react": "^0.12.3",
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.17.10",
"@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",
"@radix-ui/themes": "^3.2.1",
"@reactour/tour": "^3.8.0",
"@reduxjs/toolkit": "^2.11.2",
"axios": "^1.13.2",
"body-scroll-lock": "^4.0.0-beta.0",
"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": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"nth-check": "^2.1.1",
"parse": "^5.3.0",
"parse": "^8.0.0",
"pdf-lib": "^1.17.1",
"pkijs": "^3.3.3",
"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-cookie": "^7.2.2",
"react-datepicker": "^7.6.0",
"prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^19.2.3",
"react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0",
"react-datepicker": "^9.0.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dom": "^19.2.3",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-konva": "^18.2.10",
"react-pdf": "^9.2.1",
"react-quill-new": "^3.3.3",
"react-i18next": "^16.5.0",
"react-konva": "^19.2.1",
"react-pdf": "^10.2.0",
"react-quill-new": "^3.6.0",
"react-redux": "^9.2.0",
"react-rnd": "^10.4.14",
"react-router": "^7.1.5",
"react-scripts": "5.0.1",
"react-rnd": "^10.5.2",
"react-router": "^7.10.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.0",
"react-signature-canvas": "^1.0.7",
"react-syntax-highlighter": "^15.6.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8",
"react-tooltip": "^5.28.0",
"react-web-share": "^2.0.2",
"reactour": "^1.19.4",
"react-to-print": "^3.2.0",
"react-tooltip": "^5.30.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"regex-parser": "^2.3.0",
"serve": "^14.2.4",
"styled-components": "^5.3.0",
"web-vitals": "^4.2.4",
"ws": "^8.18.0",
"xlsx": "^0.18.5"
"regex-parser": "^2.3.1",
"serve": "^14.2.5",
"web-vitals": "^5.1.0",
"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,33 +93,40 @@
}
},
"devDependencies": {
"@babel/core": "^7.26.8",
"@babel/preset-env": "^7.26.8",
"@babel/preset-react": "^7.26.3",
"@babel/runtime-corejs2": "^7.26.7",
"autoprefixer": "^10.4.20",
"babel-loader": "^9.2.1",
"@babel/core": "^7.28.3",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.28.3",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.7",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^4.0.15",
"autoprefixer": "^10.4.23",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.1.2",
"concurrently": "^9.2.1",
"css-loader": "^7.1.2",
"daisyui": "^4.12.23",
"dotenv": "^16.4.7",
"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",
"mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.5.1",
"prettier": "^3.5.0",
"pretty-quick": "^4.0.0",
"tailwindcss": "^3.4.17",
"terser-webpack-plugin": "^5.3.11",
"webpack-cli": "^5.1.4"
"daisyui": "^4.12.24",
"eslint": "^9.39.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^27.3.0",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.19",
"tmp": "^0.2.5",
"vite": "^7.3.0",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^6.0.1",
"vitest": "^4.0.15"
},
"overrides": {
"nth-check": "$nth-check",
"ws": "$ws"
"tmp": "$tmp"
},
"engines": {
"node": "18 || 20 || 22"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+568 -73
View File
@@ -1,12 +1,14 @@
{
"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",
"sandbox-news": "Dies ist eine Sandbox-Umgebung. Bitte nicht für produktive Zwecke verwenden.",
"create-account": "Konto erstellen",
"login": "Anmelden",
"language": "Sprache",
"dark-mode": "Dunkelmodus",
"name": "Name",
"phone": "Telefon",
"phone-optional": "optional",
"phone-optional": "Optional",
"email": "E-Mail",
"company": "Unternehmen",
"job-title": "Berufsbezeichnung",
@@ -24,6 +26,8 @@
"Name": "Name",
"Date": "Datum"
},
"folder": "Ordner",
"pdf": "Pdf",
"context-menu": {
"Download": "Herunterladen",
"Rename": "Umbenennen",
@@ -39,8 +43,14 @@
"save": "Speichern",
"cancel": "Abbrechen",
"upgrade-now": "Jetzt upgraden",
"contact-now": "Jetzt kontaktieren",
"upgrade-to": "Upgrade zu",
"plan": "Plan",
"connect": "Verbinden",
"connect-to-g-drive": "Mit Google Drive verbinden",
"reconnect-to-g-drive": "Erneut mit Google Drive verbinden",
"gdrive-info-connect": "Wenn Google Drive verbunden ist, wird das abgeschlossene Dokument im Ordner {{appName}} auf Google Drive gespeichert.",
"subscription-renew-warning": "Ihr Abonnement läuft in {{remainingDays}} Tagen ab. Bitte verlängern Sie Ihr Abonnement.",
"subscribe-card-teamplan": "Entfesseln Sie die volle Kraft der Zusammenarbeit! Erstellen Sie unbegrenzt Organisationen, Teams und Hierarchien. Teilen Sie Vorlagen nahtlos zwischen Teams und weisen Sie benutzerdefinierte Benutzerrollen zu. Optimieren Sie Ihren Workflow noch heute!",
"subscribe-card-plan": "Entsperren Sie Premium-Funktionen ab nur {{premiumPrice}}/Monat. Genießen Sie eine verbesserte Leistung und zahlen Sie nur {{addonPrice}} pro zusätzlichem Credit nach den enthaltenen Premium-Credits.",
"user-name-limit-char": "Um einen Benutzernamen mit weniger als 8 Zeichen zu haben, abonnieren Sie bitte.",
@@ -51,17 +61,17 @@
"docs": "Dokumente",
"session-expired": "Ihre Sitzung ist abgelaufen.",
"public-profile-help": "Dies ist Ihre öffentliche URL. Kopieren oder teilen Sie sie mit dem Unterzeichner, und Sie können alle öffentlich eingestellten Vorlagen sehen.",
"tagline-help": "Dieser Slogan wird in Ihrem öffentlichen OpenSign-Profil angezeigt, z. B. https://opensign.me/alex",
"welcome": "Willkommen zurück!",
"Login-to-your-account": "Melden Sie sich bei Ihrem Konto an",
"password": "Passwort",
"forgot-password": "Passwort vergessen?",
"forgot-password": "Passwort vergessen",
"loading": "Wird geladen...",
"of": "von",
"sign-SSO": "Mit SSO anmelden",
"login-page": "Anmeldeseite",
"or": "ODER",
"signup-page": "Registrierungsseite",
"password-match-length": "Neues Passwort und Bestätigungspasswort sollten übereinstimmen",
"password-length": "Das Passwort sollte 8 Zeichen lang sein",
"password-case": "Das Passwort sollte einen Großbuchstaben, einen Kleinbuchstaben und eine Zahl enthalten",
"password-special-char": "Das Passwort sollte ein Sonderzeichen enthalten",
@@ -74,12 +84,23 @@
"Sign yourself": "Selbst unterschreiben",
"Request signatures": "Signaturen anfordern",
"Templates": "Vorlagen",
"Analytics": "Analytik",
"Branding": "Markenbildung",
"Mail": "Mail",
"Storage": "Speicher",
"Signing certificate": "Signierzertifikat",
"Teams": "Teams",
"General": "Allgemein",
"Teams-Children": {
"Organizations": "Organisationen",
"OrgAdmins": "OrgAdmins"
},
"Templates-Children": {
"Create template": "Vorlage erstellen",
"Manage templates": "Vorlagen verwalten",
"Embedding": "Einbetten"
},
"OpenSign™ Drive": "OpenSign™ drive",
"OpenSign™ Drive": "{{appName}} Drive",
"Documents": "Dokumente",
"Reports": "Berichte",
"Documents-Children": {
@@ -100,7 +121,8 @@
"Teams": "Teams",
"Team-update-successfully": "Team erfolgreich aktualisiert.",
"Users": "Benutzer"
}
},
"Global SMTP": "Globales SMTP"
},
"dashboard-card": {
"Need your Signature": "Benötigt Ihre Unterschrift",
@@ -132,6 +154,7 @@
"Quick send": "Schnell senden",
"Edit": "Bearbeiten",
"Share with team": "Mit Team teilen",
"Share with user": "Mit Kollegen teilen",
"Share": "Teilen",
"View": "Ansehen",
"option": "Option",
@@ -140,7 +163,15 @@
"Copy Public URL": "Öffentliche URL kopieren",
"extend-expiry-date": "Ablaufdatum verlängern",
"Duplicate Template": "Vorlage duplizieren",
"Duplicate": "Duplikat"
"Duplicate": "Duplikat",
"daily-mail-quota": "Tägliches E-Mail-Kontingent",
"Save as template": "Als Vorlage speichern",
"Fix & resend": "Korrigieren und erneut senden",
"Kiosk Mode": "Kiosk Modus",
"Reset password": "Passwort zurücksetzen",
"View formdata": "Formulardaten ansehen",
"Download all formdata": "Alle Formulardaten herunterladen",
"API credits": "API-Credits"
},
"report-heading": {
"Sr.No": "Nr.",
@@ -160,9 +191,23 @@
"Name": "Name",
"Status": "Status",
"created-date": "Erstellungsdatum",
"Type": "Typ",
"Type": "Type",
"Logs": "Protokolle",
"Expiry-date": "Ablaufdatum"
"Expiry-date": "Ablaufdatum",
"Company": "Unternehmen",
"JobTitle": "Berufsbezeichnung",
"Time to complete (Days)": "Zeit zur Fertigstellung (Tage)",
"Auto reminder": "Automatische Erinnerung",
"Remind once in every (Days)": "Erinnere einmal alle (Tage)",
"Enable OTP verification": "OTP-Verifizierung aktivieren",
"Enable Tour": "Tour aktivieren",
"Notify on signatures": "Bei Signaturen benachrichtigen",
"Allow modifications": "Änderungen zulassen",
"Redirect url": "Weiterleitungs-URL",
"Created Date": "Erstellungsdatum",
"Updated Date": "Aktualisierungsdatum",
"Expiry Date": "Ablaufdatum",
"Sent Date": "Sendedatum"
},
"report-help": {
"Draft Documents": "Dies sind Dokumente, die Sie begonnen, aber noch nicht zum Versenden fertiggestellt haben.",
@@ -174,16 +219,15 @@
"Contactbook": "Dies ist eine Liste von Kontakten/Unterzeichnern, die Sie hinzugefügt haben. Diese erscheinen als Vorschläge, wenn Sie neue Unterzeichner hinzufügen möchten.",
"Templates": "Dies ist eine Liste von Vorlagen, die Ihnen zur Verfügung stehen, um Dokumente zu erstellen. Sie können die Schaltfläche 'Verwenden' anklicken, um ein neues Dokument mit einer Vorlage zu erstellen, das Dokument zu ändern und Unterzeichner im nächsten Schritt hinzuzufügen."
},
"form-name": {
"Sign Yourself": "Selbst unterschreiben",
"Request Signatures": "Signaturen anfordern",
"New Template": "Neue Vorlage"
},
"Sign Yourself": "Selbst unterschreiben",
"Request Signatures": "Signaturen anfordern",
"New Template": "Neue Vorlage",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "Datei ausgewählt",
"template-title": "Vorlagentitel",
"document-title": "Dokumenttitel",
"title": "Titel",
"description": "Beschreibung",
"time-to-complete": "Bearbeitungszeit (Tage)",
"send-in-order": "In der Reihenfolge senden",
@@ -198,7 +242,7 @@
"remind-once": "Einmal erinnern alle (Tage)",
"next": "Weiter",
"select-folder": "Ordner auswählen",
"OpenSign-drive": "OpenSign™ Drive",
"OpenSign-drive": "{{appName}} Drive",
"drive-document-status": {
"Completed": "Abgeschlossen",
"Declined": "Abgelehnt",
@@ -206,7 +250,7 @@
"Expired": "Abgelaufen",
"In Progress": "In Bearbeitung"
},
"select-folder-help": "Wenn Sie keinen Ordner auswählen, wird Ihr unterzeichnetes Dokument im Hauptordner des OpenSign-Drives gespeichert.",
"select-folder-help": "Wenn Sie keinen Ordner auswählen, wird Ihr unterzeichnetes Dokument im Hauptordner des {{appName}} Drive gespeichert.",
"no-data": "Keine Daten gefunden",
"save-here": "Hier speichern",
"back": "Zurück",
@@ -226,17 +270,18 @@
"signature": "Unterschrift",
"upload-image": "Bild hochladen",
"clear": "Löschen",
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen",
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen/Stempel",
"initials": "Initialen",
"API": "API",
"api-token": "API-Token",
"regenerate-token": "Live-Token neu generieren",
"remove-background": "Hintergrund entfernen",
"generate-token": "Live-Token generieren",
"view-docs": "Dokumentation ansehen",
"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",
@@ -260,7 +305,7 @@
"deactivate": "deaktivieren",
"this-user": "diesen Benutzer",
"delete-user": "Benutzer löschen",
"delete": "löschen",
"delete": "Löschen",
"add-user": "Benutzer hinzufügen",
"password-generateed": "Das Passwort wird nur einmal generiert; stellen Sie sicher, dass Sie es kopieren.",
"Team status": "Teamstatus",
@@ -272,13 +317,14 @@
"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",
"embed-template": "Vorlage einbetten",
"public-url-copy": "Hier ist Ihre öffentliche URL: ",
"public-url-copy-mssg": "Kopieren oder teilen Sie diese URL, und Sie können alle öffentlich eingestellten Vorlagen sehen.",
"add-public-url-alert": "Sie können Ihr öffentliches OpenSign-Profil generieren",
"add-public-url-alert": "Sie können Ihr öffentliches {{appName}} Profil generieren",
"share-with-alert": "Sie können eine Vorlage nicht teilen, wenn Rollen bereits Kontakte zugewiesen haben. Entfernen Sie bitte alle Kontaktzuweisungen von den Rollen, bevor Sie die Vorlage teilen.",
"share-with": "Teilen mit",
"team-not-found": "Team nicht gefunden",
@@ -290,14 +336,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",
@@ -327,7 +373,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...",
@@ -341,11 +387,13 @@
"date": "Datum",
"text": "Text",
"text input": "Texteingabe",
"cells": "Zellen",
"checkbox": "Checkbox",
"dropdown": "Dropdown",
"radio button": "Radiobutton",
"image": "Bild",
"email": "E-Mail"
"email": "E-Mail",
"number": "Zahl"
},
"fields": "Felder",
"recipients": "Empfänger",
@@ -357,6 +405,7 @@
"certificate": "Zertifikat",
"decline": "Ablehnen",
"finish": "Fertigstellen",
"done": "Fertig",
"mail": "E-Mail",
"sign-now": "Jetzt unterzeichnen",
"successfully-signed": "Erfolgreich unterzeichnet!",
@@ -366,6 +415,10 @@
"Email-verified-alert-1": "E-Mail wurde verifiziert.",
"Email-verified-alert-2": "E-Mail wurde bereits verifiziert.",
"upload-stamp-image": "Stempelbild hochladen",
"draw-signature": "Unterschrift zeichnen",
"draw-initials": "Initialen zeichnen",
"enter-text": "Text eingeben",
"enter-widgettype": "{{widgetType}} eingeben",
"draw": "Zeichnen",
"type": "Schreiben",
"color-type": {
@@ -378,7 +431,7 @@
"upload": "Hochladen",
"initial-teb": "Initialen",
"signature-tab": "Unterschrift",
"initial-alert": "Meine Initiale nicht gefunden",
"initial-alert": "Meine Initiale wurde nicht gefunden",
"copy-title": "Widget kopieren zu",
"contact-delete-alert": "Sind Sie sicher, dass Sie diesen Kontakt löschen möchten?",
"reset-password-alert-1": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.",
@@ -393,12 +446,27 @@
"Next to current widget": "Neben dem aktuellen Widget"
},
"options": "Optionen",
"minimun-check": "Minimale Anzahl",
"minimum-check": "Minimale Anzahl",
"maximum-check": "Maximale Anzahl",
"cell-count": "Zellzahl",
"default-value": "Standardwert",
"formula": "Formel",
"formula-placeholder": "Erstellen Sie eine Formel mit Zahlen-Widgets",
"formula-no-widgets": "Für diesen Unterzeichner sind noch keine weiteren Zahlen-Widgets vorhanden.",
"supported-operators": "Unterstützte Operatoren:",
"formula-helper": "Klicken Sie auf einen Zahlen-Widget-Chip, um ihn einzufügen (z. B. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Widget-Namen müssen in doppelte geschweifte Klammern gesetzt werden (z. B. {{number-1}}).",
"formula-error-characters": "Außerhalb der Klammern sind nur Zahlen sowie +, -, *, /, (, ) erlaubt.",
"formula-error-braces": "Jede öffnende {{ muss eine passende }} haben.",
"formula-error-operators": "Verwenden Sie bei Operatoren oder Klammern mindestens zwei Werte.",
"formula-error-leading-operator": "Formeln dürfen nicht mit Operatoren beginnen.",
"decimal-places": "Dezimalstellen",
"number-of-decimal-places": "Anzahl der Dezimalstellen",
"decimal-places-helper": "Auf 0 setzen für ganze Zahlen",
"select": "Auswählen",
"read-only": "Nur lesen",
"read-only": "Ist schreibgeschützt",
"hide-labels": "Labels ausblenden",
"layout": "Layout",
"checkbox": "Checkbox",
"alert": "Warnung",
"zoom-in": "Vergrößern",
@@ -441,12 +509,13 @@
"add-recipients": "Empfänger hinzufügen",
"loading-mssg": "Das könnte etwas Zeit in Anspruch nehmen",
"send-mail": "E-Mail senden",
"signature-field-widget": "Für jeden Benutzer muss mindestens ein Signaturfeld hinzugefügt werden. Sie haben keine Signaturfelder für {{signersName}} hinzugefügt",
"signature-field-widget": "Jeder Benutzer muss mindestens ein Signatur-Widget haben. Sie haben kein Signatur-Widget für {{signersName}} hinzugefügt.",
"placeholder-alert-1": "Bitte stellen Sie sicher, dass für alle Empfänger mindestens ein Signatur-Widget hinzugefügt wurde.",
"placeholder-alert-2": "Bitte bestätigen Sie, dass Sie das Textfeld ausgefüllt haben.",
"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!",
@@ -462,6 +531,7 @@
"document-alert": "Dokument-Warnung",
"owner-subscription-expired": "Das Abonnement des Besitzers ist abgelaufen.",
"subscription-expired": "Abonnement abgelaufen",
"owner-doesnt-have-paid-plan": "Der Inhaber hat keinen kostenpflichtigen Plan.",
"alert-message": "Warnmeldung",
"document-decline": "Dokument ablehnen",
"decline-alert-1": "Sind Sie sicher, dass Sie dieses Dokument ablehnen möchten?",
@@ -493,7 +563,7 @@
"role-not-found": "Rolle nicht gefunden.",
"do-not-access": "Sie haben keinen Zugriff. Bitte kontaktieren Sie den Administrator.",
"add-admin": "Admin hinzufügen",
"opensign-setup": "OpenSign-Einrichtung",
"opensign-setup": "{{appName}} Einrichtung",
"join-discord": "Treten Sie unserem Discord-Server bei",
"admin-already-exist": "Admin existiert bereits.",
"password-update-alert-1": "Passwort erfolgreich aktualisiert.",
@@ -510,7 +580,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",
@@ -518,7 +588,7 @@
"provide-your-details": "Bitte geben Sie Ihre Daten an",
"provide-email": "Bitte E-Mail angeben.",
"additional-info": "Zusätzliche Informationen",
"signature-saved-alert": "Unterschrift erfolgreich gespeichert.",
"signature-saved-alert": "erfolgreich gespeichert.",
"saved-successfully": "Erfolgreich gespeichert.",
"fill-field": "Bitte dieses Feld ausfüllen",
"error-template": "Fehler: Vorlage nicht gefunden!",
@@ -550,7 +620,6 @@
"filed-required-correctly": "Bitte füllen Sie die erforderlichen Details korrekt aus.",
"admin-created": "Admin erstellt",
"invalid-masterkey": "Ungültiger Hauptschlüssel",
"opensign-Setup": "OpenSign-Einrichtung",
"master-key": "Hauptschlüssel",
"profile-update-alert": "Profil erfolgreich aktualisiert.",
"date": "Datum",
@@ -575,7 +644,8 @@
"tour-mssg": {
"home-layout-1": "Sie haben sich erfolgreich angemeldet! Werfen wir einen Blick darauf.",
"home-layout-2": "Um Dokumente zum Selbstunterzeichnen hochzuladen oder um Unterschriften anderer anzufordern, wählen Sie einfach die entsprechenden Schaltflächen.",
"home-layout-3": "Sie sind bereit, OpenSign zu verwenden! Wenn Sie Unterstützung benötigen, kontaktieren Sie uns gerne.",
"home-layout-3": "Sie sind bereit, {{appName}} zu verwenden!",
"home-layout-4": "Markiere uns auf",
"generate-token": "Jetzt upgraden, um ein Produktions-API-Token zu generieren.",
"opensign-drive-1": "Klicken Sie auf die Breadcrumb-Links, um einfach durch die Ordnerhierarchie zu navigieren und die Dokumente in jedem Ordner anzuzeigen.",
"opensign-drive-2": "Klicken Sie auf die Schaltfläche Hinzufügen, um einen neuen Ordner oder ein neues Dokument zu erstellen.",
@@ -585,27 +655,26 @@
"opensign-drive-6": "Klicken Sie mit der rechten Maustaste auf ein Dokument, um Optionen wie Herunterladen, Umbenennen, Verschieben und Löschen anzuzeigen. Klicken Sie auf das Dokument, um es zu öffnen.",
"opensign-drive-7": "Klicken Sie mit der rechten Maustaste auf einen Ordner, um Optionen anzuzeigen. Wählen Sie Umbenennen, um den Namen des Ordners zu ändern, oder klicken Sie auf den Ordner, um seinen Inhalt anzuzeigen.",
"pdf-request-file-1": "Liste der Unterzeichner, die das Dokument noch unterschreiben müssen.",
"pdf-request-file-2": "Klicken Sie auf die Platzhalter im Dokument, um zu unterschreiben. Sie sehen dann Optionen, Ihre Unterschrift zu zeichnen, einzugeben oder ein Bild hochzuladen.",
"pdf-request-file-3": "Klicken Sie auf die Schaltflächen Ablehnen oder Fertig stellen, um durch Ihr Dokument zu navigieren. Verwenden Sie das Auslassungsmenü für weitere Optionen, einschließlich der Schaltfläche Herunterladen.",
"pdf-request-file-2": "Klicken Sie auf eines der Felder im Dokument, um zu beginnen. Danach können Sie die erforderlichen Informationen eingeben.",
"pdf-request-file-3": "Sobald Sie alle erforderlichen Felder ausgefüllt haben, klicken Sie auf „Fertigstellen“. Danach können Sie das unterschriebene Dokument herunterladen. Je nach den Einstellungen des Absenders erhalten Sie möglicherweise eine Kopie des abgeschlossenen Dokuments zusammen mit dem Abschlusszertifikat, sobald alle Empfänger unterschrieben haben.",
"pdf-request-file-4": "Liste der Unterzeichner, die das Dokument bereits unterzeichnet haben.",
"pdf-request-file-5": "Sie können auf Automatisch alles unterschreiben klicken, um automatisch an allen für Sie vorgesehenen Stellen zu unterschreiben. Überprüfen Sie das Dokument gründlich, bevor Sie diese Schaltfläche verwenden.",
"pdf-request-file-6": "Bitte füllen Sie die Felder auf Seite {{pagenumbers}} aus, die alle zur einfachen Identifizierung in der gleichen Farbe hervorgehoben sind.",
"placeholder-sign-1": "Wählen Sie einen Empfänger aus dieser Liste aus, um einen Platzhalter hinzuzufügen, an dem er unterschreiben soll. Der Platzhalter wird in der gleichen Farbe wie der Empfängername angezeigt, sobald Sie ihn im Dokument ablegen.",
"placeholder-sign-1": "Wählen Sie einen Empfänger aus dieser Liste, um ein Widget hinzuzufügen. Das Widget wird in derselben Farbe wie der Empfängername angezeigt, sobald Sie es auf das Dokument ziehen.",
"placeholder-sign-2": "Das Klicken auf die Schaltfläche 'Empfänger hinzufügen' ermöglicht es Ihnen, weitere Unterzeichner hinzuzufügen.",
"placeholder-sign-3": "Klicken Sie auf diese Schaltfläche, um weitere Empfänger/Unterzeichner zum Dokument hinzuzufügen.",
"placeholder-sign-4": "Ziehen Sie ein Feld in das Dokument oder klicken Sie darauf, um es hinzuzufügen.",
"placeholder-sign-5": "Der PDF-Inhaltsbereich zeigt bereits die vorhandenen Platzhalter der Vorlage an. Diese Platzhalter entsprechen der Farbe des Empfängernamens, um sie leicht erkennbar zu machen.",
"placeholder-sign-4": "Ziehen oder klicken Sie auf ein Widget, um es dem Dokument hinzuzufügen.",
"placeholder-sign-5": "Der PDF-Inhaltsbereich zeigt bereits die vorhandenen Widgets der Vorlage an. Diese Widgets entsprechen der Farbe des Empfängernamens, um sie leicht erkennbar zu machen.",
"placeholder-sign-6": "Mit einem Klick auf 'Weiter' wird das Dokument gespeichert. Im nächsten Schritt können Sie die E-Mails, die an die Empfänger versendet werden sollen, anpassen oder die Signaturlinks kopieren und diese selbst mit den Empfängern teilen.",
"report-1": "Klicken Sie auf die Schaltfläche „Hinzufügen“, um eine neue Vorlage zu erstellen. Vorlagen sind wiederverwendbare Dokumente, mit denen schnell neue Dokumente mit derselben Struktur und unterschiedlichen Unterzeichnern erstellt werden können. Eine HR-Vorlage für die Einarbeitung könnte beispielsweise vordefinierte Rollen wie „Personalleiter“ und „Neuer Mitarbeiter“ enthalten. Bei jeder Verwendung der Vorlage können Sie die Rolle „Neuer Mitarbeiter“ verschiedenen neuen Mitarbeitern zuweisen, während die Rolle „Personalleiter“ unverändert bleibt. So wird ein nahtloser Einarbeitungsprozess für jeden neuen Mitarbeiter ermöglicht.",
"redirect": "Klicken Sie auf die Schaltfläche 'Verwenden', um ein neues Dokument aus einer bestehenden Vorlage zu erstellen.",
"bulksend": "Um schnell mehrere Dokumente mithilfe einer vorhandenen Vorlage zu versenden, indem Sie einfach die E-Mail-Adressen der Empfänger erstellen, klicken Sie auf die Schaltfläche Massenversand",
"option": "Dieses Menü zeigt weitere Optionen wie Bearbeiten und Löschen. Verwenden Sie die Schaltfläche 'Bearbeiten', um Unterzeichnerrollen hinzuzufügen, Felder zu ändern und Ihre Vorlage zu aktualisieren. Änderungen gelten für alle zukünftigen Dokumente, die aus dieser Vorlage erstellt werden, wirken sich jedoch nicht auf vorhandene Dokumente aus. Verwenden Sie die Schaltfläche 'Löschen', um die Vorlage zu entfernen.",
"signyour-self-1": "Wählen und ziehen Sie Ihre bevorzugten Widgets auf das PDF, um Ihr Dokument vor dem Unterzeichnen anzupassen. Wählen Sie die perfekten Stellen für jede Anpassung aus, um das Dokument an Ihre Bedürfnisse anzupassen.",
"bulksend": "Um schnell mehrere Dokumente mit einer vorhandenen Vorlage zu versenden, geben Sie einfach die E-Mail-Adressen der Empfänger ein und klicken Sie auf die Schaltfläche 'Massenversand'. Sie können bis zu 50 Empfänger erreichen.",
"option": "Dieses Menü zeigt weitere Optionen wie Bearbeiten, Löschen, Umbenennen, Duplizieren, Teilen usw. <1>Klicken Sie hier</1>, um mehr über alle verfügbaren Optionen zu erfahren. <3>Hinweis: Änderungen an einer vorhandenen Vorlage gelten für alle zukünftigen Dokumente, die aus dieser Vorlage erstellt werden, betreffen jedoch nicht bereits versandte Dokumente.</3>",
"signyour-self-1": "Wählen oder ziehen Sie Ihre bevorzugten Widgets auf das PDF, um Ihr Dokument vor der Unterzeichnung anzupassen. Platzieren Sie die Widgets an den perfekten Stellen, um das Dokument an Ihre Bedürfnisse anzupassen.",
"signyour-self-2": "Ziehen Sie ein Widget irgendwo in diesen Bereich. Sie können es später in der Größe ändern und verschieben.",
"template-placeholder-1": "Das Klicken auf die Schaltfläche 'Rolle hinzufügen' ermöglicht es Ihnen, verschiedene Unterzeichnerrollen hinzuzufügen. Sie können Benutzern diese Rollen in den folgenden Schritten zuweisen.",
"template-placeholder-2": "Sobald Rollen hinzugefügt wurden, wählen Sie eine Rolle aus der Liste aus, um einen Platzhalter hinzuzufügen, an dem der Unterzeichner unterschreiben soll. Der Platzhalter wird in der gleichen Farbe wie der Rollenname angezeigt, sobald Sie ihn im Dokument ablegen.",
"template-placeholder-3": "Ziehen Sie ein Feld in das Dokument oder klicken Sie darauf, um es hinzuzufügen.",
"template-placeholder-4": "Ziehen Sie den Platzhalter für eine Rolle an eine beliebige Stelle im Dokument. Denken Sie daran, dass er in der gleichen Farbe wie der Name des Empfängers angezeigt wird, um die Zuordnung zu erleichtern.",
"template-placeholder-5": "Das Klicken auf 'Weiter' speichert die aktuelle Vorlage. Nach dem Speichern werden Sie gefragt, ob Sie ein neues Dokument aus dieser Vorlage erstellen möchten.",
"template-placeholder-2": "Nachdem Sie Rollen hinzugefügt haben, wählen Sie eine aus der Liste, um ein Widget für diesen Empfänger zu platzieren. Sie können entweder auf das Widget klicken oder es auf das Dokument ziehen. Sobald es platziert ist, wird das Widget in derselben Farbe wie die ausgewählte Rolle angezeigt.",
"template-placeholder-3": "Das Klicken auf 'Weiter' speichert die aktuelle Vorlage. Nach dem Speichern werden Sie gefragt, ob Sie ein neues Dokument aus dieser Vorlage erstellen möchten.",
"webhook-1": "Jetzt upgraden, um einen Webhook einzurichten",
"Need your Signature": "Das Klicken auf diese Karte führt Sie zu einer Liste der Dokumente, die auf Ihre Überprüfung warten.",
"Out for signatures": "Das Klicken auf diese Karte führt Sie zu einer Liste der Dokumente, die auf eine Signatur warten.",
@@ -635,7 +704,7 @@
"select-date-format": "Wählen Sie ein Datumsformat",
"quantity-of-credits": "Anzahl der Premium-Credits",
"remaining-credits": "Verfügbare Premium-Credits:",
"remaining-credits-help": "Verwenden Sie Premium-Credits für API-Dokumentensignaturen, Massenversand oder die Einbettung der OpenSign-Integration auf Ihrer Website. Sie haben {{allowedcredits}} enthaltene Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig.",
"remaining-credits-help": "Verwenden Sie Premium-Credits für die API-Dokumentensignatur, den Massenversand oder die Einbettung der OpenSign-Integration auf Ihrer Website. Sie haben noch {{allowedcredits}} inkludierte Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig. Wenn Sie einen Teams- oder Enterprise-Plan nutzen, können diese Credits von allen Benutzern in Ihrem Unternehmen gemeinsam verwendet werden, sofern der Kontoadministrator für Ihr Konto kein Limit festgelegt hat.",
"additional-credits": "Bitte kaufen Sie Premium-Credits hinzu",
"quota-err-quicksend": "Kontingent erreicht, Sie haben nicht genügend Credits.",
"buy-credits": "Premium-Credits kaufen",
@@ -654,7 +723,7 @@
"public-template-mssg-1": "Um OpenSign in Ihr React- oder Next.js-Projekt zu integrieren, führen Sie einfach den folgenden Befehl aus:",
"public-template-mssg-2": "Stellen Sie sicher, dass npm oder yarn in Ihrem Projekt eingerichtet ist. Wenn Sie Yarn verwenden, können Sie npm install durch yarn add @opensign/react ersetzen.",
"public-template-mssg-3": "Benötigen Sie weitere Details oder Beispiele?",
"public-template-mssg-4": "Besuchen Sie die",
"public-template-mssg-4": "Besuchen Sie die ",
"public-template-mssg-5": " npm für die neuesten Updates, detaillierte Dokumentationen und Versionshistorie.",
"public-template-mssg-6": "Bevor Sie diesen Code-Schnipsel verwenden können, müssen Sie diese Vorlage öffentlich machen.",
"public-template-mssg-7": "Bevor Sie einen öffentlichen Link generieren können, müssen Sie diese Vorlage öffentlich machen.",
@@ -689,20 +758,20 @@
"download-pdf": "PDF herunterladen",
"pdf-certificate": "PDF + Zertifikat herunterladen",
"document-logs": "Dokumentenprotokolle",
"server-down": "Verbindung zum OpenSign-Server nicht möglich. Wenn Sie OpenSign selbst hosten, stellen Sie bitte sicher, dass alle Schritte in der Dokumentation korrekt befolgt wurden. Wenn Sie OpenSign lokal ausführen, verwenden Sie möglicherweise eine falsche Portnummer.",
"server-down": "Verbindung zum {{appName}} Server nicht möglich. Wenn Sie {{appName}} selbst hosten, stellen Sie bitte sicher, dass alle Schritte in der Dokumentation korrekt befolgt wurden. Wenn Sie {{appName}} lokal ausführen, verwenden Sie möglicherweise eine falsche Portnummer.",
"admin-exists": "Admin existiert bereits. Bitte melden Sie sich mit den Admin-Anmeldedaten an, um Benutzer zu verwalten.",
"enable-tour": "Tour aktivieren",
"istourenabled-help": {
"p1": "Dies aktiviert eine geführte Tour für Unterzeichner, die während des Unterzeichnungsprozesses Anweisungen gibt.",
"p2": "Dies deaktiviert die geführte Tour, wodurch ein schneller, ununterbrochener Unterzeichnungsprozess gewährleistet wird.",
"p3": "Tipp: Wenn Ihre Unterzeichner mit OpenSign vertraut sind und es häufig verwenden, können Sie die geführte Tour deaktivieren, um ein nahtloses Erlebnis zu gewährleisten. Wählen Sie die Option, die am besten zu Ihren Anforderungen passt."
"p3": "Tipp: Wenn Ihre Unterzeichner mit {{appName}} vertraut sind und es häufig verwenden, können Sie die geführte Tour deaktivieren, um ein nahtloses Erlebnis zu gewährleisten. Wählen Sie die Option, die am besten zu Ihren Anforderungen passt."
},
"form-title-1": "Dokumentenfluss-Einstellungen",
"form-title-2": "Sicherheitseinstellungen",
"public-tour-message": "Die Vorlage muss öffentlich sein, bevor Sie einen teilbaren Link generieren können.",
"add-user-template": "Sie müssen eine Rolle hinzufügen, bevor Sie Felder dafür hinzufügen können.",
"pdf-uncompatible": "Dieses PDF ist nicht kompatibel mit OpenSign. Bitte kontaktieren Sie <support@opensignlabs.com>",
"text-field-tour": "Felder vom Typ 'Text' müssen im Voraus ausgefüllt werden, bevor das Dokument versendet wird. Wenn Sie möchten, dass die Unterzeichner Eingaben machen, verwenden Sie das Feld 'Texteingabe'.",
"pdf-uncompatible": "Diese PDF-Datei ist nicht kompatibel, bitte kontaktieren Sie {{appName}}",
"text-field-tour": "Felder zum Vorabfüllen müssen vor dem Senden des Dokuments ausgefüllt werden. Wenn Sie Eingaben von den Unterzeichnern benötigen, verwenden Sie stattdessen die Felder für Unterzeichner.",
"attach-signer-tour": "Sie müssen einen Unterzeichner jeder Rolle zuweisen. Dies können Sie durch Klicken auf dieses Symbol tun. Sobald Sie einen Unterzeichner auswählen, wird er allen Feldern der zugehörigen Rolle zugewiesen, die in derselben Farbe erscheinen.",
"allowed-signature-types": "Erlaubte Signaturtypen",
"at-least-one-signature-type": "Mindestens ein Signaturtyp sollte aktiviert sein.",
@@ -728,12 +797,15 @@
"delete-page": "Seite löschen",
"merge-pdf": "PDFs zusammenführen",
"add-pages": "Seiten hinzufügen",
"reorder-pages": "Seiten neu anordnen",
"delete-alert": "Einzelne Seite kann nicht gelöscht werden.",
"delete-alert-2": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
"delete-note": "Hinweis: Sobald Sie diese Seite löschen, kann dies nicht rückgängig gemacht werden.",
"Rotation-alert": "Seite drehen",
"bulk-import": "Massenimport",
"contacts-file": "Kontaktdatei (xlsx, csv)",
"import-guideline": "Laden Sie eine CSV- oder Excel-Datei mit den Spalten Name, Email und optional Phone hoch. Es werden nur die ersten 100 Kontakte importiert.",
"download-sample": "Beispieldatei herunterladen",
"100-records-only": "Derzeit können Sie nur bis zu 100 Datensätze importieren.",
"csv-excel-support-only": "Laden Sie eine Datei in einem der folgenden Formate hoch: CSV, XLSX oder XLS.",
"contact-imported": "{{imported}} Kontakte wurden importiert. {{failed}} Kontakte konnten nicht importiert werden.",
@@ -741,55 +813,55 @@
"delete-folder-alert": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten?",
"delete-folder-alert-1": "Löschen nicht möglich: Der Ordner muss leer sein.",
"defaultSign-alert": "{{widgetsType}} Widgets sind nicht für die automatische Signatur verfügbar.",
"embed-heading": "OpenSign-Vorlagen einbetten",
"embed-heading": "{{appName}} Vorlagen einbetten",
"public-template-edit-title": "Öffentliche Vorlage bearbeiten",
"public-template-edit-mssg": "Eine öffentliche Vorlage können Sie nicht direkt bearbeiten. Um Änderungen vorzunehmen, wird die Vorlage auf privat gesetzt und Sie müssen sie anschließend wieder öffentlich machen. Möchten Sie fortfahren?",
"agree-p1": "Ich bestätige, dass ich die ",
"agree-p2": "Offenlegung elektronischer Aufzeichnungen und Signaturen",
"agree-p3": "gelesen und verstanden habe und der Verwendung elektronischer Aufzeichnungen und Signaturen zustimme.",
"agrre-button": "Zustimmen & Fortfahren",
"agrre-button": "Ich bestätige und stimme zu, fortzufahren",
"term-cond-title": "Allgemeine Geschäftsbedingungen",
"term-cond-h": "OFFENLEGUNG ELEKTRONISCHER AUFZEICHNUNGEN UND SIGNATUREN",
"term-cond-p1": "Diese Offenlegung elektronischer Aufzeichnungen und Signaturen ('Offenlegung') ist eine Vereinbarung zwischen dem Dokumentersteller ('Sender') und dem Unterzeichner ('Ihnen'), bereitgestellt über die OpenSign-Plattform ('Plattform'). Durch das Signieren von Dokumenten über OpenSign stimmen Sie den in dieser Offenlegung beschriebenen Bedingungen zu. Bitte lesen Sie sie sorgfältig durch, bevor Sie fortfahren.",
"term-cond-p1": "Diese Offenlegung elektronischer Aufzeichnungen und Signaturen ('Offenlegung') ist eine Vereinbarung zwischen dem Dokumentersteller ('Sender') und dem Unterzeichner ('Ihnen'), bereitgestellt über die {{appName}} Plattform ('Plattform'). Durch das Signieren von Dokumenten über {{appName}} stimmen Sie den in dieser Offenlegung beschriebenen Bedingungen zu. Bitte lesen Sie sie sorgfältig durch, bevor Sie fortfahren.",
"term-cond-h1": "1. Zweck und Geltungsbereich",
"term-cond-p2": "Diese Offenlegung regelt die Nutzung elektronischer Aufzeichnungen und Signaturen für Dokumente, die vom Sender bereitgestellt und von Ihnen über die OpenSign-Plattform unterzeichnet werden. Mit Ihrer Zustimmung erkennen Sie an, dass:",
"term-cond-p3": "Sie Dokumente elektronisch über OpenSign empfangen und unterzeichnen werden.",
"term-cond-p2": "Diese Offenlegung regelt die Nutzung elektronischer Aufzeichnungen und Signaturen für Dokumente, die vom Sender bereitgestellt und von Ihnen über die {{appName}} Plattform unterzeichnet werden. Mit Ihrer Zustimmung erkennen Sie an, dass:",
"term-cond-p3": "Sie Dokumente elektronisch über {{appName}} empfangen und unterzeichnen werden.",
"term-cond-p4": "Ihre elektronische Signatur rechtlich bindend und einer handschriftlichen Signatur gleichgestellt ist.",
"term-cond-h2": "2. Zustimmung zur Nutzung elektronischer Aufzeichnungen und Signaturen",
"term-cond-p5": "Durch Ihre Zustimmung zu dieser Offenlegung:",
"term-cond-p6": "Erklären Sie sich einverstanden, elektronisch mit dem Sender über OpenSign zu kommunizieren, und verstehen, dass diese Zustimmung gültig ist, bis sie widerrufen wird.",
"term-cond-p7": "Stimmen Sie zu, Dokumente über OpenSign elektronisch zu prüfen, zu unterzeichnen und zurückzusenden.",
"term-cond-p6": "Erklären Sie sich einverstanden, elektronisch mit dem Sender über {{appName}} zu kommunizieren, und verstehen, dass diese Zustimmung gültig ist, bis sie widerrufen wird.",
"term-cond-p7": "Stimmen Sie zu, Dokumente über {{appName}} elektronisch zu prüfen, zu unterzeichnen und zurückzusenden.",
"term-cond-p8": "Diese Zustimmung gilt nur für die spezifischen Dokumente, die vom Sender bereitgestellt werden.",
"term-cond-h3": "3. Recht auf Widerruf der Zustimmung",
"term-cond-p9": "Sie haben das Recht, Ihre Zustimmung zur Nutzung elektronischer Aufzeichnungen und Unterschriften zu widerrufen, indem Sie:",
"term-cond-p10": "Den Absender direkt über die in seiner Kommunikation angegebenen Kontaktdaten kontaktieren.",
"term-cond-p11": "Den Absender vor Abschluss des Unterschriftsprozesses benachrichtigen.",
"term-cond-h4": "4. Hardware- und Softwareanforderungen",
"term-cond-p12": "Um Dokumente mit OpenSign zu unterzeichnen, benötigen Sie:",
"term-cond-p12": "Um Dokumente mit {{appName}} zu unterzeichnen, benötigen Sie:",
"term-cond-p13": "Ein Gerät mit Internetzugang.",
"term-cond-p14": "Einen kompatiblen Webbrowser mit aktiviertem JavaScript.",
"term-cond-p15": "Ein E-Mail-Konto, um Benachrichtigungen und unterzeichnete Dokumente zu erhalten.",
"term-cond-p16": "Wenn sich diese Anforderungen ändern, werden Sie vom Absender oder OpenSign über die Aktualisierungen informiert.",
"term-cond-p16": "Wenn sich diese Anforderungen ändern, werden Sie vom Absender oder {{appName}} über die Aktualisierungen informiert.",
"term-cond-h5": "5. Aufbewahrung von Unterlagen und Anforderung von Papierkopien",
"term-cond-p17": "Sie sind dafür verantwortlich, sicherzustellen, dass Sie auf die elektronisch unterzeichneten Dokumente zugreifen und diese aufbewahren können. OpenSign stellt Tools zur Verfügung, mit denen Sie das unterzeichnete Dokument unmittelbar nach Abschluss des Unterzeichnungsprozesses herunterladen oder ausdrucken können.",
"term-cond-p17": "Sie sind dafür verantwortlich, sicherzustellen, dass Sie auf die elektronisch unterzeichneten Dokumente zugreifen und diese aufbewahren können. {{appName}} stellt Tools zur Verfügung, mit denen Sie das unterzeichnete Dokument unmittelbar nach Abschluss des Unterzeichnungsprozesses herunterladen oder ausdrucken können.",
"term-cond-p18": "Wenn Sie eine Papierkopie eines elektronisch unterzeichneten Dokuments bevorzugen, können Sie diese direkt beim Absender anfordern. Der Absender kann eine angemessene Gebühr für die Bereitstellung von Papierkopien erheben, sofern dies nicht durch geltendes Recht verboten ist.",
"term-cond-h6": "6. Anerkennung und Zustimmung",
"term-cond-p19": "Durch das Fortfahren bestätigen Sie:",
"term-cond-p20": "Dass Sie in der Lage sind, elektronische Dokumente wie in dieser Offenlegung beschrieben zu öffnen und zu überprüfen.",
"term-cond-p21": "Der Nutzung elektronischer Aufzeichnungen und Unterschriften für die angegebenen Transaktionen mit dem Absender zuzustimmen.",
"term-cond-p22": "Zu verstehen, dass OpenSign eine Plattform zur Unterstützung der Transaktion ist und keine Partei der Vereinbarung ist.",
"term-cond-p22": "Zu verstehen, dass {{appName}} eine Plattform zur Unterstützung der Transaktion ist und keine Partei der Vereinbarung ist.",
"term-cond-h7": "7. Rechtliche Wirkung",
"term-cond-p23": "Ihre durch OpenSign vermittelte elektronische Unterschrift:",
"term-cond-p23": "Ihre durch {{appName}} vermittelte elektronische Unterschrift:",
"term-cond-p24": "Entspricht den geltenden Gesetzen über elektronische Unterschriften, einschließlich, aber nicht beschränkt auf den E-SIGN Act in den Vereinigten Staaten, die EU-eIDAS-Verordnung und das indische Information Technology Act.",
"term-cond-p25": "Ist rechtlich bindend zwischen Ihnen und dem Absender für die unterzeichneten Dokumente.",
"term-cond-h8": "8. Plattformrolle und Haftungsbeschränkung",
"term-cond-p26": "OpenSign dient als Plattform zur Unterstützung elektronischer Transaktionen. Es ist nicht verantwortlich für den Inhalt, die Gültigkeit oder die Durchsetzbarkeit der vom Absender gesendeten Dokumente. Alle Streitigkeiten oder Probleme im Zusammenhang mit dem Dokument oder seiner Unterzeichnung müssen direkt zwischen Ihnen und dem Absender gelöst werden.",
"term-cond-p26": "{{appName}} dient als Plattform zur Unterstützung elektronischer Transaktionen. Es ist nicht verantwortlich für den Inhalt, die Gültigkeit oder die Durchsetzbarkeit der vom Absender gesendeten Dokumente. Alle Streitigkeiten oder Probleme im Zusammenhang mit dem Dokument oder seiner Unterzeichnung müssen direkt zwischen Ihnen und dem Absender gelöst werden.",
"term-cond-h9": "9. Beendigung der Vereinbarung",
"term-cond-p27": "Ihre Zustimmung gilt nur für die aktuelle Transaktion, es sei denn, der Absender gibt etwas anderes an. OpenSign behält sich das Recht vor, den Zugriff auf seine Plattform bei Verstößen gegen die Nutzungsbedingungen oder Richtlinien zu beenden.",
"term-cond-p28": "Durch Auswahl von 'Ich stimme zu' bestätigen Sie, dass Sie die Bedingungen dieser Offenlegung über elektronische Aufzeichnungen und Unterschriften verstanden haben und diesen zustimmen sowie der elektronischen Unterzeichnung der vom Absender über OpenSign bereitgestellten Dokumente zustimmen.",
"term-cond-p29": "Wenn Sie Fragen zu dieser Offenlegung haben, kontaktieren Sie den Absender direkt. Für technischen Support mit OpenSign besuchen Sie",
"term-cond-p30": "oder senden Sie eine E-Mail an",
"js-snippet-msg": "Um OpenSign-Vorlagen in Ihre HTML-Websites oder Landingpages zu integrieren, können Sie den folgenden Code verwenden:",
"term-cond-p27": "Ihre Zustimmung gilt nur für die aktuelle Transaktion, es sei denn, der Absender gibt etwas anderes an. {{appName}} behält sich das Recht vor, den Zugriff auf seine Plattform bei Verstößen gegen die Nutzungsbedingungen oder Richtlinien zu beenden.",
"term-cond-p28": "Durch Auswahl von 'Ich stimme zu' bestätigen Sie, dass Sie die Bedingungen dieser Offenlegung über elektronische Aufzeichnungen und Unterschriften verstanden haben und diesen zustimmen sowie der elektronischen Unterzeichnung der vom Absender über {{appName}} bereitgestellten Dokumente zustimmen.",
"term-cond-p29": "Wenn Sie Fragen zu dieser Offenlegung haben, kontaktieren Sie den Absender direkt. Für technischen Support mit {{appName}} besuchen Sie ",
"term-cond-p30": " oder senden Sie eine E-Mail an",
"js-snippet-msg": "Um {{appName}} Vorlagen in Ihre HTML-Websites oder Landingpages zu integrieren, können Sie den folgenden Code verwenden:",
"js-snippet-msg-1": "Die TemplateId erhalten Sie von der",
"js-snippet-msg-2": "Vorlagen verwalten",
"js-snippet-msg-3": "Seite.",
@@ -814,13 +886,13 @@
},
"signature-setting": "Signatur-Einstellungen",
"activation-key": "Aktivierungsschlüssel",
"paid-selfhost-help": "Bitte kontaktieren Sie OpenSign für weitere Informationen unter ",
"remaining-credits-selfhost-help": "Verwenden Sie Premium-Credits für API-Dokumentensignaturen, den Massenversand und die OpenSign-Integration auf Ihrer Website. Sie haben {{allowedcredits}} enthaltene Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig.",
"paid-selfhost-help": "Bitte kontaktieren Sie {{appName}} für weitere Informationen unter ",
"remaining-credits-selfhost-help": "Verwenden Sie Premium-Credits für API-Dokumentensignaturen, den Massenversand und die {{appName}} Integration auf Ihrer Website. Sie haben {{allowedcredits}} enthaltene Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig.",
"signature-type": "Ihre Signatur",
"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.",
@@ -833,5 +905,428 @@
"variables-use": "Zu verwendende Variablen",
"send-to-sign": "hat Sie gebeten zu unterschreiben",
"use-template": "Vorlage verwenden",
"contact-already-exists": "Der Kontakt existiert bereits."
}
"contact-already-exists": "Der Kontakt existiert bereits.",
"docx-error": "Wir haben derzeit Probleme mit der Verarbeitung von DOCX-Dateien. Bitte laden Sie die PDF-Datei hoch",
"docx-error-contact": "oder kontaktieren Sie uns unter support@opensignlabs.com",
"agreement-note": "Hinweis: Durch Ihre Zustimmung unterzeichnen Sie das Dokument nicht sofort. Sie können das Dokument nur elektronisch einsehen. Sie haben die Möglichkeit, es vollständig zu lesen und anschließend zu entscheiden, ob Sie es unterzeichnen möchten.",
"draft-template-info-p1": "Um Ihre Vorlage öffentlich zu machen, muss sie entweder eine einzelne Rolle enthalten oder, wenn sie mehrere Rollen umfasst, müssen alle zusätzlichen Rollen bereits den Unterzeichnern zugewiesen sein. Die nicht zugewiesene öffentliche Rolle muss leer bleiben und an erster Stelle stehen.",
"visit-below-link": "Besuchen Sie den untenstehenden Link, um mehr zu erfahren -",
"storage-help": "Durch die Aktivierung von BYOC können Sie Ihren eigenen S3-Speicher verbinden, sodass Ihre Dateien vollständig unter Ihrer Kontrolle bleiben und keine externen Kopien gespeichert werden. Wenn Ihnen Datenautonomie wichtig ist, erwägen Sie ein Upgrade auf Teams, um diese Funktion freizuschalten.",
"daily-quota-reached": "Sie haben Ihr tägliches Kontingent erreicht. Für Unterstützung kontaktieren Sie bitte quotas@opensignlabs.com.",
"enabled-signature-type": "Aktivierte Signaturtypen",
"enabled-signature-type-help": "Die Einstellung 'Aktivierte Signaturtypen' bestimmt, welche Signaturoptionen in Ihrer Organisation verfügbar sind. Wenn Sie beispielsweise die Option 'Zeichnen' deaktivieren, wird sie den Mitgliedern Ihrer Organisation im Signatur-Widget nicht angezeigt, während die anderen drei Optionen weiterhin zugänglich bleiben.",
"indexing-public-profile": "Erlaube die Indexierung des öffentlichen Profils durch Suchmaschinen",
"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.",
"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",
"please-select-rating": "Bitte wählen Sie eine Bewertung aus",
"feedback-optional": "Feedback (optional)",
"feedback-saved": "Feedback wurde gespeichert.",
"feedback-save-error": "Feedback konnte nicht gespeichert werden, bitte versuchen Sie es erneut.",
"date-format": "Datumsformat",
"document-deleted": "Das Dokument wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
"save-as-template-?": "Sind Sie sicher, dass Sie dieses Dokument als Vorlage speichern möchten?",
"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",
"finish-mssg": "Sind Sie sicher, dass Sie das Dokument abschließen möchten?",
"review": "Überprüfen",
"next-field": "Nächstes Feld",
"required-mssg": "{{leftRequiredWidget}} von {{totalWidget}} Feldern übrig",
"verify-document": "Dokument verifizieren",
"verify-document-signature": "Dokumentensignatur überprüfen",
"select-pdf-document": "PDF-Dokument auswählen",
"selected-file": "Ausgewählte Datei",
"verify-signature": "Signatur überprüfen",
"verification-status": "Überprüfungsstatus",
"verification-in-progress": "Überprüfung läuft...",
"verification-results-will-appear-here": "Überprüfungsergebnisse werden hier angezeigt",
"please-select-pdf": "Bitte wählen Sie eine gültige PDF-Datei aus",
"please-select-file-to-verify": "Bitte wählen Sie eine Datei zur Überprüfung aus",
"no-signature-found": "Keine Signatur im Dokument gefunden",
"error-verifying-pdf": "Fehler beim Überprüfen der PDF",
"signature-valid-basic": "Signatur ist gültig",
"signature-invalid-basic": "Signatur ist ungültig",
"all-signatures-verified-convincing": "Dokument überprüft: Alle Signaturen wurden erfolgreich validiert.",
"some-signatures-invalid-basic": "Einige Signaturen sind ungültig",
"no-signatures-processed": "Keine Signaturen verarbeitet",
"unnamed-signature-field": "Unbenanntes Signaturfeld",
"error-processing-signature": "Fehler beim Verarbeiten der Signatur",
"signer-info-not-available": "Signaturinformationen nicht verfügbar",
"cert-validity-not-checked": "Gültigkeit des Zertifikats nicht geprüft",
"valid": "Gültig",
"expired-or-not-yet-valid": "Abgelaufen oder noch nicht gültig",
"valid-from": "Gültig von",
"to": "bis",
"signer": "Unterzeichner",
"issuer": "Aussteller",
"not-available": "Nicht verfügbar",
"not-performed": "Nicht durchgeführt",
"missing-acrofield-dict": "Fehlendes Acrofield-Wörterbuch",
"signature-dictionary-not-found-or-invalid": "Signaturwörterbuch nicht gefunden oder ungültig",
"missing-or-invalid-byterange": "Fehlender oder ungültiger ByteRange",
"missing-or-invalid-contents": "Fehlender oder ungültiger Inhalt",
"missing-signature-contents": "Fehlender Signaturinhalt",
"invalid-signature-hex-format": "Ungültiges Signatur-Hex-Format",
"unsupported-signature-format-not-signeddata": "Nicht unterstütztes Signaturformat - nicht SignedData",
"signer-certificate-not-found": "Unterzeichnerzertifikat nicht gefunden",
"no-certificates-in-signature": "Keine Zertifikate in der Signatur",
"no-signer-info-in-pkcs7": "Keine Signaturinformationen in PKCS#7",
"could-not-parse-signer-info": "Signaturinformationen konnten nicht analysiert werden",
"not-calculated": "Nicht berechnet",
"not-found-in-signature": "Nicht in Signatur gefunden",
"readonly-error": "Das schreibgeschützte {{widgetName}}-Widget muss einen Standardwert haben oder kann optional gemacht werden.",
"choose-one": "Wählen Sie eine aus",
"search-templates": "Vorlagen durchsuchen…",
"search-documents": "Dokumente suchen…",
"search-contacts": "Kontakte durchsuchen…",
"edit-draft": "Entwurf bearbeiten",
"add-role-alert": "Bitte fügen Sie mindestens eine Rolle hinzu",
"invalid-email-found": "Ungültige E-Mail gefunden: {{email}}",
"duplicate-email-found": "Doppelte E-Mail gefunden: {{email}}",
"vertical": "Vertikal",
"horizontal": "Horizontal",
"billing": "Abrechnung",
"console": "Konsole",
"prefill-widget": "Vorausgefüllte Widgets",
"action-prohibited": "Diese Aktion ist für Ihre E-Mail-Domain nicht erlaubt. Bitte wenden Sie sich an Ihren Administrator, um Hilfe zu erhalten.",
"must-have-at-least-one-vacant-role": "Mindestens eine Rolle muss unzugewiesen sein, bevor Sie ein template auf 'public' setzen.",
"remove-duplicate": "Bitte doppelte Option entfernen",
"prefill-bulk-error": "Der Massenversand ist nicht erlaubt, wenn Prefill-Widgets hinzugefügt wurden. Bitte entfernen Sie die Prefill-Widgets, um fortzufahren.",
"session-expired-title": "Sitzung abgelaufen",
"access-denied": "Zugriff verweigert",
"upgrade": "Upgrade",
"do-not-access-app": "Sie haben keinen Zugriff auf diese Anwendung.",
"dont-have-access": "Sie haben keinen Zugriff.",
"valid-email-alert": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"otp-not-validate": "OTP ist ungültig.",
"domain-not-allowed": "Diese Domain ist nicht erlaubt",
"atleast-one-recipient-alert": "Bitte fügen Sie mindestens einen Empfänger hinzu!",
"incorrect-password-or-decryption-failed": "Falsches Passwort oder Entschlüsselung fehlgeschlagen.",
"incorrect-password-for-file": "Falsches Passwort für Datei: {{file}}",
"error-uploading-pdf": "Fehler beim Hochladen der PDF.",
"provide-password": "Bitte geben Sie das Passwort an.",
"only-pdf-allowed": "Nur PDF-Dateien sind erlaubt.",
"invalid-username-password-region": "Ungültiger Benutzername/Passwort oder Region.",
"pfx-extension-alert": "Bitte laden Sie eine Datei mit der Endung .pfx hoch.",
"email-already-exist": "E-Mail existiert bereits",
"branding": "Markenbildung",
"branding-help": "Branding ermöglicht White-Labeling für Ihre App",
"custom-sub-domain": "Benutzerdefinierte Subdomain",
"app-name": "App Name",
"provide-domain-name": "Geben Sie Ihren Domainnamen an",
"provide-app-name": "Geben Sie Ihren App-Namen an",
"logo": "Logo",
"upload-app-logo": "Laden Sie Ihr App-Logo hoch",
"prefill-unfilled-widget": "Die folgenden Pflichtfelder dürfen nicht leer sein: {{emptyWidget}}. Bitte füllen Sie diese aus, um fortzufahren.",
"Dashboard": "Armaturenbrett",
"Analytics": "Analytik",
"Templates": "Vorlagen",
"Need your sign": "Benötigt Ihre Unterschrift",
"In Progress": "In Bearbeitung",
"Completed": "Abgeschlossen",
"Drafts": "Entwürfe",
"Declined": "Abgelehnt",
"Expired": "Abgelaufen",
"Contactbook": "Kontaktbuch",
"My Signature": "Meine Unterschrift",
"API Token": "API-Token",
"Webhook": "Webhook",
"Preferences": "Einstellungen",
"Teams": "Teams",
"Users": "Benutzer",
"Drive": "Drive",
"Branding": "Markenbildung",
"Mail": "Mail",
"Storage": "Speicher",
"Signing certificate": "Signierzertifikat",
"General": "Allgemein",
"Organizations": "Organisationen",
"OrgAdmins": "OrgAdmins",
"Debug Pdf": "PDF debuggen",
"New Document": "Neues Dokument",
"subscription": "Abonnement",
"Draft document": "Dokumententwurf",
"Draft template": "Vorlagenentwurf",
"Public sign": "Öffentliche Signatur",
"Signup": "Registrieren",
"delete-contact": "Kontakt löschen",
"total-records-found": "Gesamtanzahl gefundener Einträge: {{count}}",
"Invalid-records-found": "Ungültige Einträge gefunden: {{records}}",
"previous": "Zurück",
"page-n-of-n": "Seite {{currentPage}} von {{totalPages}}",
"import": "Importieren",
"search": "Suchen",
"viewed-on": "Angesehen am: {{ViewedOn}}",
"signed-on": "Unterschrieben am: {{SignedOn}}",
"hide": "Ausblenden",
"show-more": "Mehr anzeigen",
"browse-or-drag-to-replace-existing-file": "Durchsuchen oder per Drag & Drop eine neue Datei ziehen, um die vorhandene zu ersetzen",
"optional-details": "Optionale Angaben",
"hide-optional-details": "optionale Angaben ausblenden",
"mail-adapter-subscription-alert": "Bitte upgraden Sie auf den Professional- oder Team-Plan, um den Mail-Adapter einzurichten.",
"connect-to-mail": "Mit Gmail verbinden",
"custom-smtp": "Benutzerdefiniertes SMTP",
"default-smtp": "{{appName}} Standard-SMTP",
"host": "Host",
"port": "Port",
"sender-email": "Absender-E-Mail",
"username": "Benutzername",
"use-default-mail-adapter": "Möchten Sie wirklich die Standard-Mailserver von {{appName}} verwenden, um Ihre Signaturanfragen zu versenden? Wir empfehlen, Ihre eigenen Gmail- oder SMTP-Server zu verwenden, um die Zustellbarkeit zu verbessern.",
"verification-code-sent-registered-email": "Ein Bestätigungscode wurde an Ihre registrierte E-Mail-Adresse <1>{{useremail}}</1> gesendet. Bitte geben Sie den Code unten ein, um Ihre Einstellungen zu bestätigen.",
"smpt-credentials": "SMTP-Zugangsdaten",
"delete-account": "Konto löschen",
"delete-account-que": "Sind Sie sicher, dass Sie Ihr Konto löschen möchten?",
"delete-account-que-user": "Sie sind dabei, diesen Benutzer und alle zugehörigen Daten dauerhaft zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
"user-deleted-successfully": "Benutzer und alle zugehörigen Daten wurden erfolgreich gelöscht.",
"account-deletion-request-sent-via-mail": "Wir haben Ihnen einen Bestätigungslink per E-Mail gesendet. Genehmigen Sie die Anfrage, um die Löschung Ihres Kontos abzuschließen.",
"type-exact-email-delete": "Geben Sie die genaue E-Mail ein, um das Löschen zu aktivieren",
"email-does-not-match": "E-Mail stimmt nicht überein.",
"please-type-to-confirm": "Bitte geben Sie <1>{{userEmail}}</1> ein, um zu bestätigen:",
"email-settings": "E-Mail-Einstellungen",
"email-settings-help": "Um eine bessere Zustellbarkeit Ihrer Signaturanfrage-E-Mails im Posteingang zu gewährleisten, können Sie Ihren eigenen E-Mail-Anbieter verbinden. Wählen Sie eine der folgenden Optionen:",
"connect-to-gmail": "Mit Gmail verbinden: ",
"connect-to-gmail-help": "Verwenden Sie Ihr Gmail-Konto, um Signaturanfrage-E-Mails direkt aus Ihrem eigenen Posteingang zu senden. Dies verbessert die Zustellraten und Zuverlässigkeit.",
"connect-to-smtp": "Benutzerdefiniertes SMTP: ",
"connect-to-smtp-help": "Verwenden Sie Ihren eigenen SMTP-Server, um E-Mails über Ihre Domain zu versenden. Diese Option gibt Ihnen die volle Kontrolle über Ihre E-Mail-Infrastruktur und verbessert die Zustellbarkeit sowie die Markenkonsistenz.",
"connect-to-default": "Wenn Sie möchten, können Sie auch die Standard-Mailserver von {{appName}} verwenden, wir empfehlen jedoch für optimale Ergebnisse die Nutzung Ihrer eigenen.",
"email-settings-redirect-message": "Diese Einstellung wurde von der Konsole zu den Haupteinstellungen → Präferenzen verschoben. Diese Seite wird in zukünftigen Versionen entfernt.",
"go-to-preferences-menu": "Zum Menü 'Einstellungen' gehen",
"document-download-filename-format": "Dateiname-Format für Dokument-Download",
"preview": "Vorschau: ",
"download-filename-format-help": "Wählen Sie, wie heruntergeladene PDFs benannt werden. Ihre Auswahl wird in Ihrem Profil gespeichert und in der gesamten App verwendet.",
"delete-action-prohibited": "Diese Aktion ist nicht erlaubt. Bitte wenden Sie sich an Ihren Administrator, um die Löschung des Kontos zu beantragen.",
"not-verified": "Nicht verifiziert",
"verified": "Verifiziert",
"expires": "Läuft ab",
"fix-resend-error": "Sie können dieses Dokument nicht korrigieren und erneut senden, da es vorab ausgefüllte Widgets enthält.",
"duplicate-template-error": "Sie können diese Vorlage nicht duplizieren, da sie vorab ausgefüllte Widgets enthält.",
"save-as-template-error": "Dieses Dokument kann nicht als Vorlage gespeichert werden, da es vorab ausgefüllte Widgets enthält.",
"redirecting-you-in": "Sie werden in {{redirectTimeLeft}} Sek. weitergeleitet...",
"pdf-tools-tour": "Klicken Sie auf diese Schaltflächen, um Seiten hinzuzufügen, zu löschen, neu anzuordnen, zu drehen und zu zoomen.",
"widgets": "Widgets",
"prefill-tour": "Verwenden Sie diese Option, um Informationen im Voraus einzugeben, bevor Sie das Dokument an Empfänger senden.",
"empty-prefill-error": "Die folgenden Pflichtfelder dürfen nicht leer bleiben:",
"please-fill-out": "Bitte füllen Sie diese aus, um fortzufahren.",
"custom-signing-certificate": "Benutzerdefiniertes Signaturzertifikat",
"signing-certificate-help": "Sie können Ihr eigenes Dokumentensignaturzertifikat hochladen, das zum Signieren aller Ihrer Dokumente sowie Abschlusszertifikate verwendet wird. Die Zertifikatsdatei sollte ein P12-Zertifikat im PFX-Format sein.",
"certificate-file-p12-in-PFX-format": "Zertifikatsdatei (p12-Zertifikat im PFX-Format)",
"password-of-pfx-file": "Passwort der PFX-Datei eingeben",
"update": "Aktualisieren",
"use-default-certificate": "Standardzertifikat von {{appName}} verwenden",
"upgrade-to-team-plan": "Auf Team-Plan upgraden",
"setup-file-storage": "Dateispeicher einrichten",
"save-and-activate": "Speichern & aktivieren",
"logging-out-to-apply-settings": "Sie werden abgemeldet, um neue Einstellungen anzuwenden",
"reconnect-to-default": "Erneut mit {{appName}} verbinden",
"active-file-adapter": "Aktiver Dateiadapter",
"file-adapter-unique-name": "Eindeutiger Name des Dateiadapters",
"unique-name-for-file-adapter": "Eindeutigen Namen für Dateiadapter eingeben",
"storage-provider": "Speicheranbieter",
"enter-bucket-name": "Bucket-Namen eingeben",
"enter-space-name": "Space-Namen eingeben",
"enter-region-of-bucket": "Region des Buckets eingeben",
"enter-region-of-space": "Region des Spaces eingeben",
"enter-access-key": "Access Key eingeben",
"enter-secret-access-key": "Secret Access Key eingeben",
"otp-email": "Wir haben einen Bestätigungscode gesendet",
"save-as-temp-warn": "Hinweis: Dieses Dokument enthält vorausgefüllte Widgets, die automatisch entfernt werden, da sie bereits im Basisdokument integriert sind.",
"edit-document": "Dokument bearbeiten",
"modify": "Ändern",
"merge-certificate-to-pdf": "Zertifikat mit PDF zusammenführen",
"merge-cc-to-pdf-help": {
"p1": "Dies stellt sicher, dass das Abschlusszertifikat im endgültigen PDF-Dokument enthalten ist. Bitte beachten Sie jedoch, dass das Zertifikat nach dem Zusammenführen nicht mehr vom Hauptdokument getrennt werden kann.",
"p2": "Wenn Sie sich entscheiden, nicht zusammenzuführen, wird das Abschlusszertifikat als separate PDF-Datei zusammen mit dem unterzeichneten Dokument bereitgestellt."
},
"read-only-date-error": "Das schreibgeschützte Datums-Widget muss ein Standarddatum haben.",
"set-date": "Datum festlegen",
"set-today": "Unterzeichnungsdatum",
"enter-name": "Name eingeben",
"enter-email": "E-Mail eingeben",
"subscribe-to-opensign-msg": "Abonnieren Sie {{appName}} und genießen Sie unbegrenzt kostenlose digitale Signaturen.",
"duplicate-subscribe-msg": "Mit Duplizieren können Sie eine exakte Kopie der ausgewählten Vorlage erstellen, die Sie wiederverwenden oder ändern können, ohne das Original zu beeinflussen.",
"save-as-template-msg": "Speichern Sie dieses Dokument als wiederverwendbare Vorlage, die Sie für zukünftige Dokumente erneut nutzen können.",
"public-credit-alert": "Das Signieren über diesen Link verbraucht Ihre kostenlosen E-Mail-Credits. Als kostenloser Nutzer erhalten Sie 15 E-Mail-Credits pro Monat, um zu verhindern, dass Spammer unsere Systeme missbrauchen. Um höhere Limits und ununterbrochenen Zugriff zu genießen, abonnieren Sie die <1>OpenSign™-Bezahlpläne.</1>",
"know-more-about": "Mehr erfahren über",
"free-unlimited-signatures": "Kostenlose unbegrenzte Signaturen",
"email-quota-updated": "Tägliches E-Mail-Kontingent erfolgreich aktualisiert.",
"password-has-been-reset": "Passwort wurde zurückgesetzt.",
"Unauthorized to reset your own password.": "Nicht berechtigt, Ihr eigenes Passwort zurückzusetzen.",
"User not found or not allowed.": "Benutzer nicht gefunden oder nicht erlaubt.",
"User not found.": "Benutzer nicht gefunden.",
"Admin user tenant not found.": "Admin-Benutzer-Tenant nicht gefunden.",
"User is not authenticated.": "Benutzer ist nicht authentifiziert.",
"Please provide required parameters.": "Bitte geben Sie die erforderlichen Parameter an.",
"reset-password": "Benutzerpasswort zurücksetzen",
"enter-strong-password": "Geben Sie ein sicheres Passwort ein oder generieren Sie eines automatisch.",
"autogenerate": "Automatisch generieren",
"extend": "Verlängern",
"console-general-help": "Um einzelnen Benutzern zu ermöglichen, ihre eigenen E-Mail-Vorlagen anzupassen, navigieren Sie zu",
"console-mail-help": "Um unternehmensweite E-Mail-Vorlagen zu aktualisieren, gehen Sie zu",
"admin-setting-disabled": "Diese Einstellung ist deaktiviert. Bitte wenden Sie sich für Unterstützung an den Administrator.",
"company-email-settings": "Unternehmens-E-Mail-Einstellungen",
"allow-email-template-individually": "E-Mail-Vorlagenanpassung für Benutzer individuell erlauben ",
"user-email-template-general-help": "Wenn diese Option deaktiviert ist, können Benutzer ihre E-Mail-Vorlagen nicht mehr anpassen.",
"disable-individual-user-smtp": "individuelle benutzer-smtp-einstellungen deaktivieren",
"enable-individual-user-smtp": "individuelle benutzer-smtp-einstellungen aktivieren",
"user-smtp-general-help": "Wenn deaktiviert, können Benutzer ihre eigenen SMTP-Einstellungen nicht konfigurieren. Alle E-Mails verwenden dann die globale SMTP-Konfiguration.",
"global-smtp-settings": "Globale SMTP-Einstellungen",
"global-smtp-settings-help-p1": "Diese Einstellungen gelten global für Ihr gesamtes {{appName}}-Konto. Wenn ein Benutzer keine eigenen E-Mail-Zustellungs­einstellungen konfiguriert hat, verwendet das System automatisch die hier definierten globalen SMTP-Einstellungen.",
"global-smtp-settings-help-p2": "Um eine bessere Zustellbarkeit aller Signaturanfragen sicherzustellen, können Sie Ihren bevorzugten E-Mail-Anbieter verbinden. Wählen Sie eine der folgenden Optionen:",
"global-connect-to-gmail-help": "Verwenden Sie ein Gmail-Konto, um Signaturanfragen direkt aus dem Posteingang Ihrer Organisation zu senden. Dies verbessert die Zustellrate, Reputation und Zuverlässigkeit.",
"global-connect-to-smtp-help": "Verwenden Sie Ihren eigenen SMTP-Server, um E-Mails über Ihre Domain zu senden. Dies bietet volle Kontrolle über Ihre E-Mail-Infrastruktur, verbessert die Zustellbarkeit und sorgt für konsistentes Branding.",
"global-smtp-settings-help-p3": "Wenn keine individuellen Benutzereinstellungen konfiguriert sind, werden automatisch diese globalen Einstellungen angewendet.",
"hide-text-with-asterisks": "Text mit Sternchen ausblenden",
"hide-text-with-asterisks-help": "Der Text im unterzeichneten Dokument wird ausgeblendet, bleibt jedoch für den Dokumentabsender (Eigentümer) über sein {{appName}}-Konto zugänglich.",
"document-id": "Dokument-ID",
"sent-date": "Sendedatum",
"status": "Status",
"signer-form-data-for": "Formulardaten für Unterzeichner: {{signerName}} <{{signerEmail}}>",
"widget-name": "Widget-Name",
"widget-responses": "Widget-Antworten",
"formdata-download-note": "Hinweis: Signaturen, Stempel, Initialen und Bilder werden nicht exportiert.",
"export-all-formdata-error": "Sie können die Formulardaten nicht exportieren, da die Vorlage doppelte Widget-Namen enthält. Bitte erstellen Sie eine neue Vorlage, um Formulardaten exportieren zu können.",
"export-formdata-error": "Sie können die Formulardaten nicht exportieren, da das Dokument doppelte Widget-Namen enthält. Bitte erstellen Sie ein neues Dokument ohne doppelte Widget-Namen, um Formulardaten exportieren zu können.",
"duplicate-widget-name-error": "Dieser Widget-Name ist bereits einem anderen Unterzeichner zugewiesen. Bitte wählen Sie einen anderen Namen.",
"duplicate-template-widget-error": "Sie können diese Vorlage nicht duplizieren, da sie doppelte Widget-Namen enthält.",
"credit-alert": "Kreditwarnung",
"document-form-data": "Dokumenten-Formulardaten",
"view-formdata-paid-title-error": "Die Funktion 'Formulardaten anzeigen' ermöglicht es Ihnen, die von Unterzeichnern ausgefüllten Widget-Daten auszudrucken und herunterzuladen.",
"view-formdata-paid-error": "Hinweis: Die Funktion 'Formulardaten anzeigen' ist nur für Dokumente verfügbar, die ab Version v2.32.0 erstellt wurden und keine doppelten Widget-Namen enthalten.",
"download-all-formdata-paid-title-error": "Die Funktion 'Alle Formulardaten herunterladen' ermöglicht es Ihnen, die von Unterzeichnern ausgefüllten Widget-Daten für alle aus der Vorlage erstellten Dokumente herunterzuladen.",
"download-all-formdata-paid-error": "Hinweis: Die Funktion 'Alle Formulardaten herunterladen' ist nur für Vorlagen verfügbar, die ab Version v2.32.0 erstellt wurden und keine doppelten Widget-Namen enthalten.",
"api-token-not-found": "API-Token nicht gefunden.",
"sub-head-api-credits": "Legen Sie die maximale Anzahl von API-Credits fest, die dieser Benutzer verbrauchen darf.",
"consumed-credits": "Verbrauchte Credits",
"allowed-api-credits": "Zulässige API-Credits",
"allowed-api-credits-help": "Die zulässigen Credits sollten größer oder gleich den verbrauchten Credits sein.",
"Enter-allowed-api-credits": "Zulässige API-Credits eingeben",
"login-to-sandbox": "Im Sandbox-Modus anmelden",
"your-credits-usage": "Ihr Kreditverbrauch",
"remaining": "verbleibend",
"used": "verwendet",
"your-credits-usage-help": "Dies ist die Anzahl der Credits, die mit Ihrem API-Schlüssel verbraucht wurden.",
"api-credits-updated": "API-Credits wurden erfolgreich zugewiesen.",
"min-date": "Mindestdatum",
"max-date": "Höchstdatum",
"invalid-min-date": "Ungültiges Mindestdatum.",
"min-less-max": "Das Mindestdatum muss kleiner oder gleich dem Höchstdatum sein.",
"invalid-max-date": "Ungültiges Höchstdatum.",
"max-less-min": "Das Höchstdatum muss größer oder gleich dem Mindestdatum sein.",
"date-range": "Datumsbereich",
"date-range-help": "Legen Sie ein Mindest- und/oder Höchstdatum fest, um die Auswahl einzuschränken. Das gewählte Datum muss innerhalb dieses Bereichs liegen.",
"option-disabled-by-owner": "Der Dokumentersteller hat diese Option deaktiviert",
"pen-colors": "Stiftfarben",
"pen-colors-help": "Wählen Sie aus, welche Stiftfarben für Signatur- und Initialen-Widgets verfügbar sind. Aktivieren Sie ein Kontrollkästchen, um eine Farbe anzuzeigen, und deaktivieren Sie es, um sie auszublenden.",
"security-key": "Sicherheitsschlüssel",
"remove-limit": "Limit entfernen",
"removed-api-credits": "Das API-Credit-Limit wurde entfernt.",
"enable-authentication": "Authentifizierung aktivieren",
"working": "In Bearbeitung",
"test-security-key": "Sicherheitsschlüssel testen",
"generate": "Generieren",
"regenerate": "Neu generieren"
}
File diff suppressed because it is too large Load Diff
+569 -74
View File
@@ -1,12 +1,14 @@
{
"header-news": "Nueva característica: los usuarios del plan Teams ahora pueden integrar sus propios depósitos de AWS S3 para el almacenamiento de archivos",
"header-news-btn": "Configurar ahora",
"sandbox-news": "Este es un entorno sandbox. Por favor, no lo utilice con fines de producción.",
"create-account": "Crear cuenta",
"login": "Iniciar sesión",
"language": "Idioma",
"dark-mode": "Modo oscuro",
"name": "Nombre",
"phone": "Teléfono",
"phone-optional": "opcional",
"phone-optional": "Opcional",
"email": "Correo",
"company": "Compañía",
"job-title": "Título del puesto",
@@ -24,6 +26,8 @@
"Name": "Nombre",
"Date": "Fecha"
},
"folder": "Carpeta",
"pdf": "Pdf",
"context-menu": {
"Download": "Descargar",
"Rename": "Renombrar",
@@ -39,8 +43,14 @@
"save": "Guardar",
"cancel": "Cancelar",
"upgrade-now": "Mejorar ahora",
"contact-now": "Contactar ahora",
"upgrade-to": "Mejorar a",
"plan": "Plan",
"connect": "Conectar",
"connect-to-g-drive": "Conectar con Google Drive",
"reconnect-to-g-drive": "Volver a conectar con Google Drive",
"gdrive-info-connect": "Cuando Google Drive está conectado, el documento completado se guardará en la carpeta {{appName}} de Google Drive.",
"subscription-renew-warning": "Su suscripción vencerá en {{remainingDays}} días. Por favor, renueve su suscripción.",
"subscribe-card-teamplan": "¡Libera todo el poder de la colaboración! Crea organizaciones, equipos y jerarquías ilimitadas. Comparte plantillas sin problemas entre equipos y asigna funciones de usuario personalizadas. ¡Mejora tu flujo de trabajo hoy mismo!",
"subscribe-card-plan": "Desbloquea funciones premium desde solo {{premiumPrice}}/mes. Disfruta de un rendimiento mejorado y solo {{addonPrice}} por crédito adicional después de tus créditos premium incluidos.",
"user-name-limit-char": "Para tener un nombre de usuario menor a 8 caracteres por favor suscríbete",
@@ -51,17 +61,17 @@
"docs": "Documentación",
"session-expired": "Tu sesión ha expirado.",
"public-profile-help": "Esta es tu URL pública. Cópiala o compártela con el firmante, y podrás ver todas tus plantillas públicas.",
"tagline-help": "Este eslogan aparecerá en tu perfil público de OpenSign, por ejem. https://opensign.me/alex",
"welcome": "¡Bienvenido de nuevo!",
"Login-to-your-account": "Ingresa a tu cuenta",
"password": "Contraseña",
"forgot-password": "¿Olvidaste la contraseña?",
"forgot-password": "¿Olvidaste la contraseña",
"loading": "Cargando...",
"of": "de",
"sign-SSO": "Ingresar con SSO",
"login-page": "Página de inicio de sesión",
"or": "O",
"signup-page": "Página de registro",
"password-match-length": "La nueva contraseña y la confirmación deben coincidir",
"password-length": "La contraseña debe tener 8 caracteres",
"password-case": "La contraseña debe incluir mayúsculas, minúsculas y números",
"password-special-char": " La contraseña debe incluir caracteres especiales",
@@ -74,12 +84,23 @@
"Sign yourself": "Firma tu mismo",
"Request signatures": "Solicitar firmas",
"Templates": "Plantillas",
"Analytics": "Analítica",
"Branding": "Marca",
"Mail": "Correo",
"Storage": "Almacenamiento",
"Signing certificate": "Certificado de firma",
"Teams": "Equipos",
"General": "General",
"Teams-Children": {
"Organizations": "Organizaciones",
"OrgAdmins": "OrgAdmins"
},
"Templates-Children": {
"Create template": "Crear plantilla",
"Manage templates": "Administrar plantillas",
"Embedding": "Incrustar"
},
"OpenSign™ Drive": "OpenSign™ Drive",
"OpenSign™ Drive": "{{appName}} Drive",
"Reports": "Reportes",
"Documents": "Documentos",
"Documents-Children": {
@@ -100,7 +121,8 @@
"Teams": "Equipos",
"Team-update-successfully": "Equipo actualizado correctamente.",
"Users": "Usuarios"
}
},
"Global SMTP": "SMTP Global"
},
"dashboard-card": {
"Need your Signature": "Necesitan tu firma",
@@ -132,6 +154,7 @@
"Quick send": "Envío rápido",
"Edit": "Editar",
"Share with team": "Compartir con el equipo",
"Share with user": "Compartir con un colega",
"Share": "Compartir",
"View": "Ver",
"option": "Opción",
@@ -140,7 +163,15 @@
"Copy Public URL": "Copiar URL pública",
"extend-expiry-date": "Date d'expiration",
"Duplicate Template": "Plantilla duplicada",
"Duplicate": "Duplicada"
"Duplicate": "Duplicada",
"daily-mail-quota": "Cuota diaria de correos electrónicos",
"Save as template": "Guardar como plantilla",
"Fix & resend": "Corregir y reenviar",
"Kiosk Mode": "Modo Kiosco",
"Reset password": "Restablecer contraseña",
"View formdata": "Ver datos del formulario",
"Download all formdata": "Descargar todos los datos del formulario",
"API credits": "Créditos de API"
},
"report-heading": {
"Sr.No": "Nº",
@@ -162,7 +193,21 @@
"created-date": "Fecha de creación",
"Type": "Tipo",
"Logs": "Registros",
"Expiry-date": "Date d'expiration"
"Expiry-date": "Date d'expiration",
"Company": "Compañía",
"JobTitle": "Título del puesto",
"Time to complete (Days)": "Tiempo para completar (días)",
"Auto reminder": "Recordatorio automático",
"Remind once in every (Days)": "Recordar una vez cada (días)",
"Enable OTP verification": "Habilitar verificación OTP",
"Enable Tour": "Habilitar recorrido",
"Notify on signatures": "Notificar en las firmas",
"Allow modifications": "Permitir modificaciones",
"Redirect url": "URL de redirección",
"Created Date": "Fecha de creación",
"Updated Date": "Fecha de actualización",
"Expiry Date": "Fecha de vencimiento",
"Sent Date": "Fecha de envío"
},
"report-help": {
"Draft Documents": "Estos son documentos que has iniciado pero no has finalizado para su envío.",
@@ -174,16 +219,15 @@
"Contactbook": "Esta es una lista de contactos/firmantes añadidos por ti. Aparecerán como sugerencias cuando intentes añadir firmantes a un nuevo documento.",
"Templates": "Esta es una lista de plantillas que están a tu disposición para crear documentos. Puedes hacer clic en el botón «usar» para crear un nuevo documento utilizando una plantilla, modifica el documento y añade firmantes en el siguiente paso."
},
"form-name": {
"Sign Yourself": "Firmar",
"Request Signatures": "Solicitar firmas",
"New Template": "Nueva plantilla"
},
"Sign Yourself": "Firmar",
"Request Signatures": "Solicitar firmas",
"New Template": "Nueva plantilla",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "archivo seleccionado",
"template-title": "Título de la plantilla",
"document-title": "Título del documento",
"title": "Título",
"description": "Descripción",
"time-to-complete": "Plazo de finalización (días)",
"send-in-order": "Enviar en orden",
@@ -198,7 +242,7 @@
"remind-once": " Recordar una vez cada (Días)",
"next": "Siguiente",
"select-folder": "Seleccionar carpeta",
"OpenSign-drive": "OpenSign™ Drive",
"OpenSign-drive": "{{appName}} Drive",
"drive-document-status": {
"Completed": "Completado",
"Declined": "Rechazado",
@@ -206,7 +250,7 @@
"Expired": "Expirado",
"In Progress": "En progreso"
},
"select-folder-help": "Si no seleccionas una carpeta, tu documento firmado se guardará en la carpeta principal de OpenSign Drive.",
"select-folder-help": "Si no seleccionas una carpeta, tu documento firmado se guardará en la carpeta principal de {{appName}} Drive.",
"no-data": "No se encontraron datos",
"save-here": "Guardar aquí",
"back": "Atrás",
@@ -223,21 +267,21 @@
"submit": "Enviar",
"reset": "Volver a empezar",
"my-signature": "Mi firma",
"initial-alert": "Mi inicial no encontrada",
"signature": "Firma",
"upload-image": " Subir imagen",
"clear": "Limpiar",
"upload-signature/Image": "Por favor, sube la firma/imagen",
"upload-signature/Image": "Por favor, sube la firma/imagen/estampilla",
"initials": "Iniciales",
"API": "API",
"api-token": "Token API",
"regenerate-token": "Regenerar token activo",
"remove-background": "Eliminar fondo",
"generate-token": "Generar token activo",
"view-docs": "Ver documentación",
"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",
@@ -261,7 +305,7 @@
"deactivate": "desactivar",
"this-user": " este usuario",
"delete-user": "Eliminar usuario",
"delete": "eliminar",
"delete": "Eliminar",
"add-user": "Agregar usuario",
"password-generateed": "La contraseña solo será generada una vez; asegúrate de copiarla.",
"Team status": "Estado del equipo",
@@ -273,13 +317,14 @@
"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",
"embed-template": "Insertar plantilla",
"public-url-copy": "Aquí está tu URL pública: ",
"public-url-copy-mssg": "Cópiala o compártela con el firmante y podrás ver todas tus plantillas públicas.",
"add-public-url-alert": "Puedes generar tu perfil público OpenSign",
"add-public-url-alert": "Puedes generar tu perfil público {{appName}}",
"share-with-alert": "No puedes compartir una plantilla si alguno de los roles ya tiene contactos asignados. Elimina todas las asignaciones de contactos de los roles antes de compartir la plantilla.",
"share-with": "Compartir con",
"team-not-found": "Equipo no encontrado",
@@ -291,14 +336,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",
@@ -328,7 +373,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...",
@@ -342,11 +387,13 @@
"date": "fecha",
"text": "texto",
"text input": "entrada de texto",
"cells": "células",
"checkbox": "casilla",
"dropdown": "desplegable",
"radio button": "botón de radio",
"image": "imagen",
"email": "correo"
"email": "correo",
"number": "número"
},
"fields": "Campos",
"recipients": "Destinatarios",
@@ -358,6 +405,7 @@
"certificate": "Certificado",
"decline": "Rechazar",
"finish": "Finalizar",
"done": "Hecho",
"mail": "Correo",
"sign-now": "Firmar ahora",
"successfully-signed": "¡Firmado exitosamente!",
@@ -367,6 +415,10 @@
"Email-verified-alert-1": "El correo está verificado.",
"Email-verified-alert-2": "El correo ya ha sido verificado.",
"upload-stamp-image": "Subir imagen del sello",
"draw-signature": "Dibujar firma",
"draw-initials": "Dibujar iniciales",
"enter-text": "Introducir texto",
"enter-widgettype": "Introduzca {{widgetType}}",
"draw": "Dibujar",
"type": "Escribir",
"color-type": {
@@ -379,7 +431,7 @@
"upload": "Subir",
"initial-teb": "Iniciales",
"signature-tab": "Firma",
"your-signature": "Tu firma",
"initial-alert": "Mi inicial no encontrada",
"copy-title": "Copiar widget a",
"contact-delete-alert": "¿En definitiva quieres eliminar este contacto?",
"reset-password-alert-1": "El enlace para restablecer la contraseña ha sido enviado a tu correo electrónico",
@@ -387,6 +439,7 @@
"reset-password-alert-3": "Restablece tu contraseña",
"faild-animation": "Error al cargar la animación",
"apply": "Aplicar",
"select-columns": "Seleccionar columnas",
"copy-type": {
"All pages": "Todas las páginas",
"All pages but last": "Todas las páginas menos la última",
@@ -394,12 +447,27 @@
"Next to current widget": "Junto al widget actual"
},
"options": "Opciones",
"minimun-check": "Chequeo mínimo",
"minimum-check": "Chequeo mínimo",
"maximum-check": "Chequeo máximo",
"cell-count": "recuento de células",
"default-value": "Valor por defecto",
"formula": "Fórmula",
"formula-placeholder": "Crea una fórmula utilizando widgets numéricos",
"formula-no-widgets": "Todavía no hay otros widgets numéricos asignados a este firmante.",
"supported-operators": "Operadores compatibles:",
"formula-helper": "Haz clic en una ficha de widget numérico para insertarla (p. ej. {{number-1}} + {{number-2}}).",
"formula-error-letter": "Envuelve los nombres de los widgets entre dobles llaves (p. ej. {{number-1}}).",
"formula-error-characters": "Fuera de las llaves solo se permiten números y +, -, *, /, (, ).",
"formula-error-braces": "Cada {{ de apertura debe tener una }} correspondiente.",
"formula-error-operators": "Cuando uses operadores o paréntesis, agrega al menos dos valores.",
"formula-error-leading-operator": "Las fórmulas no pueden comenzar con operadores.",
"decimal-places": "Decimales",
"number-of-decimal-places": "Número de decimales",
"decimal-places-helper": "Establecer en 0 para números enteros",
"select": "Seleccionar",
"read-only": "Es de solo lectura",
"hide-labels": "Esconder etiquetas",
"layout": "Diseño",
"checkbox": "Casilla",
"alert": "Alerta",
"zoom-in": "Acercar",
@@ -442,11 +510,13 @@
"add-recipients": "Agregar destinatarios",
"loading-mssg": "Esto podría llevar algún tiempo",
"send-mail": "Enviar correo",
"signature-field-widget": "Cada usuario debe tener al menos un widget de firma. No ha añadido un widget de firma para {{signersName}}.",
"placeholder-alert-1": "Por favor, asegúrate de que hay al menos un widget de firma para cada destinatario.",
"placeholder-alert-2": "Por favor, confirma que hayas rellenado el campo de texto.",
"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 siguientes recibirán un correo electrónico una vez que usted 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!",
@@ -461,6 +531,7 @@
"mail-not-delivered": "correo no entregado",
"document-alert": "Alerta de documento",
"owner-subscription-expired": "La suscripción del propietario ha expirado.",
"owner-doesnt-have-paid-plan": "El propietario no tiene un plan de pago.",
"subscription-expired": "Suscripción expirada",
"alert-message": "Mensaje de alerta",
"document-decline": "Rechazar documento",
@@ -493,7 +564,7 @@
"role-not-found": "Rol no encontrado.",
"do-not-access": "No tienes acceso, por favor, contacta al administrador.",
"add-admin": "Agregar administrador",
"opensign-setup": "Instalación de OpenSign",
"opensign-setup": "Instalación de {{appName}}",
"join-discord": "Únete a nuestro servidor de Discord",
"admin-already-exist": "El administrador ya existe.",
"password-update-alert-1": "Contraseña actualizada exitosamente.",
@@ -510,7 +581,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",
@@ -518,7 +589,7 @@
"provide-your-details": "Proporciona tus detalles",
"provide-email": "Por favor, proporciona un correo.",
"additional-info": "Información adicional",
"signature-saved-alert": "Firma guardada exitosamente.",
"signature-saved-alert": "guardada exitosamente.",
"saved-successfully": "Guardado con éxito.",
"fill-field": "Por favor, rellena este campo",
"error-template": "Error: ¡Plantilla no encontrada!",
@@ -550,7 +621,6 @@
"filed-required-correctly": "Por favor, rellena correctamente los detalles requeridos.",
"admin-created": "Administrador creado",
"invalid-masterkey": "Llave maestra inválida",
"opensign-Setup": "Instalación de OpenSign",
"master-key": " Llave maestra",
"profile-update-alert": "Perfil actualizado exitosamente.",
"date": "Fecha",
@@ -575,7 +645,8 @@
"tour-mssg": {
"home-layout-1": "¡Has iniciado sesión exitosamente! Echemos un vistazo.",
"home-layout-2": "Para subir documentos para firmar tú mismo o para solicitar la firma de otros, simplemente selecciona los botones correspondientes.",
"home-layout-3": "¡Ya puedes empezar a utilizar OpenSign! Si necesitas soporte siéntete libre de contactarnos.",
"home-layout-3": "¡Ya puedes empezar a utilizar {{appName}}!",
"home-layout-4": "Danos una estrella en",
"generate-token": "Mejora ahora para generar un token API de producción.",
"opensign-drive-1": "Haz clic en los enlaces para navegar fácilmente por la estructura jerárquica de carpetas y ver los documentos de cada carpeta.",
"opensign-drive-2": "Haz clic en el botón «Agregar» para crear una nueva carpeta o documento.",
@@ -585,34 +656,32 @@
"opensign-drive-6": "Haz clic con el botón derecho en un documento para ver opciones como «Descargar», «Renombrar», «Mover» y «Eliminar». Haz clic en el documento para abrirlo.",
"opensign-drive-7": "Haz clic con el botón derecho en cualquier carpeta para ver las opciones. Elige «Renombrar» para cambiar el nombre de la carpeta o haz clic en ella para navegar por su contenido.",
"pdf-request-file-1": "Lista de firmantes que todavía tienen que firmar este documento.",
"pdf-request-file-2": "Haz clic en cualquiera de los marcadores de posición que aparecen en el documento para firmar. A continuación, verás opciones para dibujar tu firma, escribirla o subir una imagen .",
"pdf-request-file-3": "Haga clic en los botones Rechazar o Finalizar para navegar por el documento. Utilice el menú de puntos suspensivos para ver opciones adicionales, incluido el botón Descargar.",
"pdf-request-file-2": "Haga clic en cualquiera de los campos que aparecen en el documento para comenzar. Luego podrá completar la información requerida.",
"pdf-request-file-3": "Una vez que haya completado todos los campos obligatorios, haga clic en 'Finalizar'. Luego podrá descargar el documento firmado. Según la configuración del remitente, es posible que reciba una copia del documento completado junto con el certificado de finalización una vez que todos los destinatarios hayan firmado.",
"pdf-request-file-4": "Lista de firmantes que ya han firmado el documento.",
"pdf-request-file-5": "Puedes hacer clic en «Firmar todo automáticamente» para firmar de manera automática en todos los lugares que deban ser firmados por ti. Asegúrate de revisar correctamente el documento antes de hacer clic en ese botón.",
"pdf-request-file-6": "Por favor, complete los campos en la página {{pagenumbers}}, todos resaltados con el mismo color para facilitar la identificación.",
"placeholder-sign-1": "Selecciona un destinatario de esta lista para agregar un marcador de posición dónde este destinatario tendrá que firmar. El marcador de posición aparecerá con el mismo color que el nombre del destinatario una vez lo sueltes en el documento.",
"placeholder-sign-1": "Seleccione un destinatario de esta lista para añadir un widget. El widget aparecerá con el mismo color que el nombre del destinatario una vez que lo coloque en el documento.",
"placeholder-sign-2": "Hacer clic en el botón «Agregar destinatarios» te permitirá añadir más firmantes.",
"placeholder-sign-3": "Haz clic en este botón para agregar más destinatarios/firmantes al documento.",
"placeholder-sign-4": "Arrastra o haz clic en un capo para agregarlo al documento.",
"placeholder-sign-5": "El área de contenido del PDF ya muestra los marcadores de posición existentes en la plantilla. Para tu comodidad, estos marcadores de posición coincidirán con el color del nombre del destinatario, haciéndolos fácilmente identificables.",
"placeholder-sign-4": "Arrastre o haga clic en un widget para añadirlo al documento.",
"placeholder-sign-5": "El área de contenido del PDF ya muestra los widgets existentes de la plantilla. Para su comodidad, estos widgets coincidirán con el color del nombre del destinatario, lo que los hace fácilmente identificables.",
"placeholder-sign-6": "Al hacer clic en 'Siguiente' se guardará el documento. En el siguiente paso, puede personalizar los correos electrónicos que se enviarán a los destinatarios o copiar los enlaces de firma y compartirlos usted mismo con los destinatarios.",
"report-1": "Haz clic en el botón «Agregar» para crear una nueva plantilla. Las plantillas son documentos reusables diseñados para generar rápidamente nuevos documentos con la misma estructura pero diferentes firmantes. Por ejemplo, una plantilla de RR.HH. para el proceso de incorporación podría tener roles predefinidos como «Director de RR.HH.» y «Nuevo empleado». Cada vez que utilices la plantilla, puedes asignar el rol de «Nuevo empleado» a los distintos miembros del personal que se incorporen, mientras que el rol de « Director de RR.HH.» permanece constante, lo que facilita un proceso de incorporación fluido para cada persona contratada.",
"redirect": "Haz clic en el botón «Usar» para crear un nuevo documento desde una plantilla existente.",
"bulksend": "Para enviar rápidamente múltiples documentos utilizando una plantilla existente creando simplemente las direcciones de correo electrónico de los destinatarios, haz clic en el botón 'Envío Masivo'.",
"option": "Este menú revela más opciones como «Editar» y «Eliminar». Usa el botón «Editar» para agregar roles, modificar campos y actualizar tu plantilla. Los cambios aplicarán para todos los documentos futuros que sean creados usando esta plantilla pero no afectarán a los documentos ya existentes. Con el botón «Eliminar» puedes eliminar la plantilla.",
"signyour-self-1": "Para personalizar el documento antes de que sea firmado, selecciona y arrastra al documento los widgets que prefieras. Escoge el espacio perfecto para cada modificación para adaptar el documento a tus necesidades.",
"bulksend": "Para enviar rápidamente varios documentos utilizando una plantilla existente, simplemente introduzca las direcciones de correo electrónico de los destinatarios y haga clic en el botón 'Envío masivo'. Puede enviar hasta 50 destinatarios.",
"option": "Este menú muestra más opciones como Editar, Eliminar, Renombrar, Duplicar, Compartir, etc. <1>Haga clic aquí</1> para leer más sobre todas las opciones disponibles. <3>Nota: Los cambios en una plantilla existente se aplicarán a todos los futuros documentos creados a partir de esa plantilla, pero no afectarán a los documentos que ya se hayan enviado.</3>",
"signyour-self-1": "Seleccione o arrastre sus widgets preferidos al PDF para personalizar su documento antes de firmar. Elija los lugares perfectos para cada widget y adapte el documento a sus necesidades.",
"signyour-self-2": "Arrastra y suelta en cualquier lugar de esta área. Después podrás cambiar el tamaño y moverlo.",
"template-placeholder-1": "Hacer clic en el botón «Agregar rol» te permitirá agregar varios roles de firmantes. Puedes asociar usuarios a cada rol en los siguientes pasos.",
"template-placeholder-2": "Una vez que los roles hayan sido agregados, selecciona un rol de la lista para agregar un marcador de posición donde éste tendrá que firmar. El marcador de posición aparecerá con el mismo color que el nombre del rol una vez lo sueltes en el documento.",
"template-placeholder-3": "Arrastra o haz clic en un capo para agregarlo al documento.",
"template-placeholder-4": "Arrastra el marcador de posición para un rol a cualquier lugar del documento. Recuerda, aparecerá con el mismo color que el nombre del rol para poder referenciarlo fácilmente.",
"template-placeholder-5": "Hacer clic en «Siguiente» guardará la plantilla actual. Luego de guardar, se te consultará si deseas crear un nuevo documento usando esta plantilla.",
"template-placeholder-2": "Después de añadir roles, elija uno de la lista para colocar un widget para ese destinatario. Puede hacer clic en el widget o arrastrarlo al documento. Una vez colocado, el widget se mostrará en el mismo color que el rol seleccionado.",
"template-placeholder-3": "Hacer clic en «Siguiente» guardará la plantilla actual. Luego de guardar, se te consultará si deseas crear un nuevo documento usando esta plantilla.",
"webhook-1": "Mejora ahora para configurar un webhook",
"Need your Signature": "Hacer clic en esta tarjeta te llevará a la lista de documentos que esperan por tu revisión.",
"Out for signatures": "Hacer clic en esta tarjeta te llevará a la lista de documentos que esperan tu firma.",
"Recent signature requests": "Esta es una lista de documentos que esperan por tu firma.",
"Recently sent for signatures": "Esta es una lista de documentos que has enviado a otras partes para que sea firmado.",
"Drafts": "Estos son documentos que has empezado pero que todavía no finalizas para su envío.",
"Drafts": "Estos son documentos que ha comenzado pero que aún no ha finalizado para enviar.",
"public-template": "Este video muestra cómo puedes configurar tu perfil público personalizado, como «https://opensign.me/tu-nombre-de-usuario». También aprenderás cómo personalizar tu eslogan y cómo hacer tus plantillas disponibles para su firmado público.",
"allowModify-widgets": "Puede arrastrar y soltar cualquiera de estos campos en el documento, además de los campos ya designados para usted por el creador del documento"
},
@@ -636,7 +705,7 @@
"select-date-format": "Selecciona un formato de fecha",
"quantity-of-credits": "Cantidad de créditos premium",
"remaining-credits": "Créditos premium disponibles:",
"remaining-credits-help": "Usa créditos premium para la API para la firma de documentos, envío en masa, o para incorporar la integración de OpenSign en tu página web. Te quedan {{allowedcredits}} créditos incluidos y {{addoncredits}} créditos comprados adicionalmente.",
"remaining-credits-help": "Utilice créditos premium para la firma de documentos mediante API, el envío masivo o para integrar OpenSign en su sitio web. Le quedan {{allowedcredits}} créditos incluidos y {{addoncredits}} créditos adicionales comprados. Si está en un plan Teams o Enterprise, estos créditos pueden compartirse entre todos los usuarios de su empresa, a menos que el administrador de la cuenta haya establecido un límite para su cuenta.",
"additional-credits": "Por favor, compra créditos premium",
"quota-err-quicksend": "Cuota alcanzada, no tienes créditos suficientes.",
"buy-credits": "Comprar créditos premium",
@@ -655,7 +724,7 @@
"public-template-mssg-1": "Para integrar OpenSign a tu proyecto React o Next.js, simplemente ejecuta los siguientes comandos:",
"public-template-mssg-2": "Asegúrate de tener «npm» o «yarn» configurado en tu proyecto. Si estás usando «yarn», puedes reemplazar «npm install» con «yarn add @opensign/react».",
"public-template-mssg-3": "¿Necesitas más detalles o ejemplos?",
"public-template-mssg-4": "Visita la",
"public-template-mssg-4": "Visita la ",
"public-template-mssg-5": " «npm» para las últimas actualizaciones, documentación detallada e historial de versiones.",
"public-template-mssg-6": "Antes de que puedas usar este fragmento de código, debes convertir esta plantilla en pública.",
"public-template-mssg-7": "Antes de poder generar un enlace público, debes hacer que esta plantilla sea pública.",
@@ -690,20 +759,20 @@
"download-pdf": "Descargar PDF",
"pdf-certificate": "Descargar PDF + Certificado",
"document-logs": "Registros de documentos",
"server-down": "No se pudo conectar al servidor de OpenSign. Si estás alojando OpenSign por tu cuenta, por favor, asegúrate de que todos los pasos de la documentación han sido seguidos correctamente. Si estás ejecutando OpenSign localmente, puedes estar accediendo a OpenSign por un número de puerto incorrecto.",
"server-down": "No se pudo conectar al servidor de {{appName}}. Si estás alojando {{appName}} por tu cuenta, por favor, asegúrate de que todos los pasos de la documentación han sido seguidos correctamente. Si estás ejecutando {{appName}} localmente, puedes estar accediendo a {{appName}} por un número de puerto incorrecto.",
"admin-exists": "El administrador ya existe. Por favor, inicia sesión en la aplicación usando las credenciales de administrador para poder administrar los usuarios.",
"public-tour-message": "Por favor, convierte la plantilla en pública para copiar la URL pública",
"add-user-template": "Es necesario agregar un rol antes de poder agregarle campos.",
"pdf-uncompatible": "Este pdf no es compatible con opensign, comuníquese con <support@opensignlabs.com>",
"pdf-uncompatible": "Este PDF no es compatible, por favor contacta con {{appName}}",
"enable-tour": "Habilitar recorrido",
"istourenabled-help": {
"p1": "Esto permitirá realizar una visita guiada para los firmantes, brindando instrucciones durante el proceso de firma.",
"p2": "Esto desactivará la visita guiada, lo que garantizará una experiencia de firma más rápida e ininterrumpida.",
"p3": "Consejo: si sus firmantes están familiarizados con OpenSign y lo utilizan con frecuencia, es posible que desee desactivar la visita guiada para disfrutar de una experiencia más fluida. Elija la opción que mejor se adapte a sus necesidades de firma de documentos"
"p3": "Consejo: si sus firmantes están familiarizados con {{appName}} y lo utilizan con frecuencia, es posible que desee desactivar la visita guiada para disfrutar de una experiencia más fluida. Elija la opción que mejor se adapte a sus necesidades de firma de documentos"
},
"form-title-1": "Configuración del flujo de documentos",
"form-title-2": "Configuración de seguridad",
"text-field-tour": "Los campos de tipo 'Texto' deben completarse con anticipación antes de enviar el documento. Si necesita que los firmantes proporcionen información, utilice el campo 'Entrada de texto'",
"text-field-tour": "Los campos de 'Rellenado previo' deben completarse antes de enviar el documento. Si necesita que los firmantes proporcionen información, utilice los campos de firmantes.",
"attach-signer-tour": "Debe adjuntar un firmante a cada función. Puede hacerlo haciendo clic en este icono. Una vez que seleccione un Firmante, se adjuntará a todos los campos asociados con ese rol que aparecen en el mismo color.",
"allowed-signature-types": "Tipos de firma permitidos",
"at-least-one-signature-type": "Se debe habilitar al menos un tipo de firma.",
@@ -729,12 +798,15 @@
"delete-page": "eliminar página",
"merge-pdf": "fusionar pdf",
"add-pages": "Agregar páginas",
"reorder-pages": "Reordenar páginas",
"delete-alert": "No se puede eliminar una sola página",
"delete-alert-2": "¿Está seguro de que desea eliminar esta página?",
"delete-note": "Nota: una vez que elimines esta página, no podrás deshacerla",
"Rotation-alert": "Girar página",
"bulk-import": "Importación masiva",
"contacts-file": "Archivo de contactos (xlsx, csv)",
"import-guideline": "Sube un archivo CSV o Excel con las columnas Name, Email y opcionalmente Phone. Solo se importarán los primeros 100 contactos.",
"download-sample": "Descargar archivo de ejemplo",
"100-records-only": "Currently you can only import up to 100 records.",
"csv-excel-support-only": "Cargue un archivo en uno de los siguientes formatos: CSV, XLSX o XLS.",
"contact-imported": "Se importaron {{imported}} contactos. {{failed}} contactos no pudieron importarse.",
@@ -742,58 +814,58 @@
"delete-folder-alert": "¿Está seguro de que desea eliminar este Carpeta?",
"delete-folder-alert-1": "No se puede eliminar: la carpeta debe estar vacía",
"defaultSign-alert": "Los widgets {{widgetsType}} no están disponibles para la firma automática",
"embed-heading": "Incrustar plantillas de OpenSign",
"embed-heading": "Incrustar plantillas de {{appName}}",
"public-template-edit-title": "Editar plantilla pública",
"public-template-edit-mssg": "No puede editar una plantilla pública directamente. Para realizar cambios, la plantilla se configurará como privada y luego tendrás que volver a hacerla pública. ¿Quieres continuar?",
"agree-p1": "Confirmo que he leído y comprendido el ",
"agree-p2": "Registro Electrónico y Divulgación de Firma",
"agree-p3": "y consentimiento para utilizar registros y firmas electrónicas.",
"agrre-button": "Aceptar y continuar",
"agrre-button": "Confirmo y acepto continuar",
"term-cond-title": "Términos y condiciones",
"term-cond-h": "DIVULGACIÓN DE REGISTRO ELECTRÓNICO Y FIRMA",
"term-cond-p1": "Esta Divulgación de Firma y Registro Electrónico ('Divulgación') es un acuerdo entre el Creador del Documento ('Remitente') y el Firmante ('Usted'), facilitado a través de la plataforma OpenSign ('Plataforma'). Al firmar documentos a través de OpenSign, usted acepta los términos descritos en esta Divulgación. Léala detenidamente antes de continuar.",
"term-cond-p1": "Esta Divulgación de Firma y Registro Electrónico ('Divulgación') es un acuerdo entre el Creador del Documento ('Remitente') y el Firmante ('Usted'), facilitado a través de la plataforma {{appName}} ('Plataforma'). Al firmar documentos a través de {{appName}}, usted acepta los términos descritos en esta Divulgación. Léala detenidamente antes de continuar.",
"term-cond-h1": "1. Objet et portée",
"term-cond-p2": "Esta Divulgación rige el uso de registros y firmas electrónicas para los documentos enviados por el Remitente y firmados por Usted a través de la plataforma OpenSign. Al aceptar, Usted reconoce que:",
"term-cond-p3": "Recibirá y firmará documentos electrónicamente a través de OpenSign.",
"term-cond-p2": "Esta Divulgación rige el uso de registros y firmas electrónicas para los documentos enviados por el Remitente y firmados por Usted a través de la plataforma {{appName}}. Al aceptar, Usted reconoce que:",
"term-cond-p3": "Recibirá y firmará documentos electrónicamente a través de {{appName}}.",
"term-cond-p4": "Su firma electrónica es jurídicamente vinculante y equivalente a una firma manuscrita.",
"term-cond-h2": "2. Consentimiento para el uso de registros y firmas electrónicas",
"js-snippet-msg-2": "Administrar plantillas",
"js-snippet-msg-3": "página.",
"term-cond-p5": "Al aceptar esta Divulgación:",
"term-cond-p6": "Usted acepta realizar transacciones electrónicas con el Remitente utilizando OpenSign y comprende que este consentimiento es válido hasta que se retire.",
"term-cond-p7": "Usted acepta revisar, firmar y devolver documentos electrónicamente utilizando OpenSign.",
"term-cond-p6": "Usted acepta realizar transacciones electrónicas con el Remitente utilizando {{appName}} y comprende que este consentimiento es válido hasta que se retire.",
"term-cond-p7": "Usted acepta revisar, firmar y devolver documentos electrónicamente utilizando {{appName}}.",
"term-cond-p8": "Este consentimiento se aplica únicamente a los documentos específicos proporcionados por el Remitente.",
"term-cond-h3": "3. Derecho a retirar el consentimiento",
"term-cond-p9": "Tiene derecho a retirar su consentimiento para el uso de registros y firmas electrónicas mediante:",
"term-cond-p10": "Contactar directamente con el Remitente utilizando la información de contacto proporcionada en su comunicación.",
"term-cond-p11": "Notificar al remitente antes de completar el proceso de firma.",
"term-cond-h4": "4. Requisitos de hardware y software",
"term-cond-p12": "Para firmar documentos usando OpenSign, debe tener:",
"term-cond-p12": "Para firmar documentos usando {{appName}}, debe tener:",
"term-cond-p13": "Un dispositivo con acceso a Internet.",
"term-cond-p14": "Un navegador web compatible con JavaScript habilitado.",
"term-cond-p15": "Una cuenta de correo electrónico para recibir notificaciones y documentos firmados.",
"term-cond-p16": "Si estos requisitos cambian, el Remitente u OpenSign le notificarán las actualizaciones.",
"term-cond-p16": "Si estos requisitos cambian, el Remitente u {{appName}} le notificarán las actualizaciones.",
"term-cond-h5": "5. Conservación de registros y solicitud de copias en papel",
"term-cond-p17": "Usted es responsable de garantizar que pueda acceder y conservar los documentos firmados electrónicamente. OpenSign proporciona herramientas para descargar o imprimir el documento firmado inmediatamente después de que se complete el proceso de firma.",
"term-cond-p17": "Usted es responsable de garantizar que pueda acceder y conservar los documentos firmados electrónicamente. {{appName}} proporciona herramientas para descargar o imprimir el documento firmado inmediatamente después de que se complete el proceso de firma.",
"term-cond-p18": "Si prefiere una copia en papel de cualquier documento firmado electrónicamente, puede solicitarla directamente al Remitente. El Remitente puede cobrar una tarifa razonable por proporcionar copias en papel, a menos que lo prohíba la ley aplicable.",
"term-cond-h6": "6. Reconocimiento y Acuerdo",
"term-cond-p19": "Al continuar, usted:",
"term-cond-p20": "Confirme que puede acceder y revisar documentos electrónicos como se describe en esta Divulgación.",
"term-cond-p21": "Acepta utilizar registros y firmas electrónicas para las transacciones especificadas con el Remitente.",
"term-cond-p22": "Comprenda que OpenSign es una plataforma que facilita la transacción y no es parte del acuerdo.",
"term-cond-p22": "Comprenda que {{appName}} es una plataforma que facilita la transacción y no es parte del acuerdo.",
"term-cond-h7": "7. Efecto jurídico",
"term-cond-p23": "Tu firma electrónica facilitada a través de OpenSign:",
"term-cond-p23": "Tu firma electrónica facilitada a través de {{appName}}:",
"term-cond-p24": "Cumple con las leyes de firma electrónica aplicables, incluidas, entre otras, la Ley E-SIGN de los Estados Unidos, el Reglamento eIDAS de la UE y la Ley de tecnología de la información de la India",
"term-cond-p25": "Es legalmente vinculante entre Usted y el Remitente para los documentos firmados",
"term-cond-h8": "8. Platform Role and Limitation of Liability",
"term-cond-p26": "OpenSign sirve como plataforma para facilitar transacciones electrónicas. No es responsable del contenido, validez o aplicabilidad de los documentos enviados por el Remitente. Cualquier disputa o problema relacionado con el documento o su firma debe resolverse directamente entre Usted y el Remitente.",
"term-cond-p26": "{{appName}} sirve como plataforma para facilitar transacciones electrónicas. No es responsable del contenido, validez o aplicabilidad de los documentos enviados por el Remitente. Cualquier disputa o problema relacionado con el documento o su firma debe resolverse directamente entre Usted y el Remitente.",
"term-cond-h9": "9. Terminación del Acuerdo",
"term-cond-p27": "Su consentimiento se aplica únicamente a la transacción actual a menos que el Remitente especifique lo contrario. OpenSign se reserva el derecho de cancelar el acceso a su plataforma por violaciones de sus Términos de Servicio o políticas.",
"term-cond-p28": "Al seleccionar 'Acepto', usted confirma que comprende y acepta los términos de este Registro electrónico y Divulgación de firma y acepta firmar electrónicamente los documentos proporcionados por el Remitente a través de OpenSign.",
"term-cond-p29": "Si tiene preguntas sobre esta Divulgación, comuníquese directamente con el Remitente. Para obtener soporte técnico con OpenSign, visite",
"term-cond-p30": "o correo electrónico",
"js-snippet-msg": "Para integrar plantillas OpenSign en sus sitios web HTML o páginas de destino, puede utilizar el siguiente código:",
"term-cond-p27": "Su consentimiento se aplica únicamente a la transacción actual a menos que el Remitente especifique lo contrario. {{appName}} se reserva el derecho de cancelar el acceso a su plataforma por violaciones de sus Términos de Servicio o políticas.",
"term-cond-p28": "Al seleccionar 'Acepto', usted confirma que comprende y acepta los términos de este Registro electrónico y Divulgación de firma y acepta firmar electrónicamente los documentos proporcionados por el Remitente a través de {{appName}}.",
"term-cond-p29": "Si tiene preguntas sobre esta Divulgación, comuníquese directamente con el Remitente. Para obtener soporte técnico con {{appName}}, visite ",
"term-cond-p30": " o correo electrónico",
"js-snippet-msg": "Para integrar plantillas {{appName}} en sus sitios web HTML o páginas de destino, puede utilizar el siguiente código:",
"js-snippet-msg-1": "Puede obtener el TemplateId en la página Administrar plantillas",
"js-snippet-msg-2": "Administrar plantillas",
"js-snippet-msg-3": "página.",
"agrrement-alert": "Para continuar, debe consentir la divulgación de registros y firmas electrónicas.",
"webhook-already-exists": "¡La URL ya existe! Pruebe con uno diferente.",
"webhook-must-be-secure": "La URL del webhook debe ser segura y utilizar https://",
@@ -815,13 +887,13 @@
"template-form-description": "Utilice este formulario para crear plantillas reutilizables para sus documentos.",
"change-signer": "Cambiar firmante",
"activation-key": "Clave de activación",
"paid-selfhost-help": "Por favor, póngase en contacto con OpenSign para más información en ",
"remaining-credits-selfhost-help": "Use créditos premium para firmar documentos mediante la API, enviar en masa e integrar OpenSign en su sitio web. Tiene {{allowedcredits}} créditos incluidos y {{addoncredits}} créditos adicionales comprados restantes.",
"paid-selfhost-help": "Por favor, póngase en contacto con {{appName}} para más información en ",
"remaining-credits-selfhost-help": "Use créditos premium para firmar documentos mediante la API, enviar en masa e integrar {{appName}} en su sitio web. Tiene {{allowedcredits}} créditos incluidos y {{addoncredits}} créditos adicionales comprados restantes.",
"signature-type": "Tu firma",
"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.",
@@ -834,5 +906,428 @@
"variables-use": "Variables para usar",
"send-to-sign": "le ha solicitado que firme",
"use-template": "Usar plantilla",
"contact-already-exists": "El contacto ya existe."
}
"contact-already-exists": "El contacto ya existe.",
"docx-error": "Actualmente estamos experimentando problemas con el procesamiento de archivos DOCX. Por favor, sube el archivo PDF",
"docx-error-contact": "o contáctanos en support@opensignlabs.com",
"agreement-note": "Nota: Aceptar esto no significa que esté firmando el documento de inmediato. Esto solo le permite revisar el documento electrónicamente. Tendrá la oportunidad de leerlo en su totalidad y decidir si desea firmarlo después.",
"draft-template-info-p1": "Para hacer que tu plantilla sea pública, debe contener un único rol o, si incluye múltiples roles, todos los roles adicionales deben estar ya asignados a firmantes. El rol público no asignado debe permanecer vacío y debe estar en la primera posición.",
"visit-below-link": "Visita el siguiente enlace para saber más -",
"storage-help": "Habilitar BYOC te permite conectar tu propio almacenamiento S3 para que tus archivos permanezcan completamente bajo tu control, sin copias externas retenidas. Si la autonomía de los datos es importante para ti, considera actualizar a Teams para desbloquear esta función.",
"daily-quota-reached": "Ha alcanzado su cuota diaria. Para obtener ayuda, comuníquese con quotas@opensignlabs.com.",
"enabled-signature-type": "Tipos de firma habilitados",
"enabled-signature-type-help": "La configuración de 'Tipos de firma habilitados' determina qué opciones de firma están disponibles en su organización. Por ejemplo, si desactiva la opción 'Dibujar', los miembros de su organización no la verán en el widget de firma, mientras que las otras tres opciones seguirán siendo accesibles.",
"indexing-public-profile": "Permitir la indexación del perfil público por los motores de búsqueda",
"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.",
"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",
"please-select-rating": "Por favor seleccione una calificación",
"feedback-optional": "Comentarios (opcional)",
"feedback-saved": "Los comentarios han sido guardados.",
"feedback-save-error": "No se pudieron guardar los comentarios, inténtelo de nuevo.",
"date-format": "Formato de fecha",
"document-deleted": "El documento ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
"save-as-template-?": "¿Está seguro de que desea guardar este documento como plantilla?",
"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",
"finish-mssg": "¿Está seguro de que desea finalizar el documento?",
"review": "Revisar",
"next-field": "Siguiente campo",
"required-mssg": "{{leftRequiredWidget}} de {{totalWidget}} campos restantes",
"verify-document": "Verificar documento",
"verify-document-signature": "Verificar firma del documento",
"select-pdf-document": "Seleccionar documento PDF",
"selected-file": "Archivo seleccionado",
"verify-signature": "Verificar firma",
"verification-status": "Estado de verificación",
"verification-in-progress": "Verificación en curso...",
"verification-results-will-appear-here": "Los resultados de la verificación aparecerán aquí",
"please-select-pdf": "Por favor, seleccione un archivo PDF válido",
"please-select-file-to-verify": "Por favor, seleccione un archivo para verificar",
"no-signature-found": "No se encontró ninguna firma en el documento",
"error-verifying-pdf": "Error al verificar el PDF",
"signature-valid-basic": "La firma es válida",
"signature-invalid-basic": "La firma no es válida",
"all-signatures-verified-convincing": "Documento verificado: Todas las firmas han sido validadas exitosamente.",
"some-signatures-invalid-basic": "Algunas firmas no son válidas",
"no-signatures-processed": "No se procesaron firmas",
"unnamed-signature-field": "Campo de firma sin nombre",
"error-processing-signature": "Error al procesar la firma",
"signer-info-not-available": "Información del firmante no disponible",
"cert-validity-not-checked": "Validez del certificado no verificada",
"valid": "Válido",
"expired-or-not-yet-valid": "Caducado o aún no válido",
"valid-from": "Válido desde",
"to": "hasta",
"signer": "Firmante",
"issuer": "Emisor",
"not-available": "No disponible",
"not-performed": "No realizado",
"missing-acrofield-dict": "Falta el diccionario Acrofield",
"signature-dictionary-not-found-or-invalid": "Diccionario de firmas no encontrado o inválido",
"missing-or-invalid-byterange": "ByteRange faltante o inválido",
"missing-or-invalid-contents": "Contenido faltante o inválido",
"missing-signature-contents": "Falta el contenido de la firma",
"invalid-signature-hex-format": "Formato hexadecimal de firma inválido",
"unsupported-signature-format-not-signeddata": "Formato de firma no compatible - no SignedData",
"signer-certificate-not-found": "Certificado del firmante no encontrado",
"no-certificates-in-signature": "No hay certificados en la firma",
"no-signer-info-in-pkcs7": "No hay información del firmante en PKCS#7",
"could-not-parse-signer-info": "No se pudo analizar la información del firmante",
"not-calculated": "No calculado",
"not-found-in-signature": "No encontrado en la firma",
"readonly-error": "El widget de solo lectura {{widgetName}} debe tener un valor predeterminado o puede hacerlo opcional.",
"choose-one": "Elige uno",
"search-templates": "Buscar plantillas…",
"search-documents": "Buscar documentos…",
"search-contacts": "Buscar contactos…",
"invalid-email-found": "Correo electrónico no válido encontrado: {{email}}",
"duplicate-email-found": "Correo electrónico duplicado encontrado: {{email}}",
"vertical": "Vertical",
"add-role-alert": "Por favor, agregue al menos un rol",
"edit-draft": "Editar borrador",
"horizontal": "Horizontal",
"billing": "Facturación",
"console": "Consola",
"prefill-widget": "Widgets de Relleno Previo",
"action-prohibited": "Esta acción no está permitida para su dominio de correo electrónico. Por favor, contacte con su administrador para obtener ayuda.",
"must-have-at-least-one-vacant-role": "Debe dejar al menos un rol sin asignar antes de establecer un template como 'public'.",
"remove-duplicate": "Por favor, elimina la opción duplicada",
"prefill-bulk-error": "El envío masivo no está permitido cuando se han agregado widgets de pre-rellenado. Por favor, elimine los widgets de pre-rellenado para continuar.",
"session-expired-title": "Sesión expirada",
"access-denied": "Acceso denegado",
"upgrade": "Actualizar",
"do-not-access-app": "No tienes acceso a esta aplicación.",
"dont-have-access": "No tienes acceso.",
"valid-email-alert": "Por favor ingresa una dirección de correo electrónico válida.",
"otp-not-validate": "OTP no es válido.",
"domain-not-allowed": "Este dominio no está permitido",
"atleast-one-recipient-alert": "¡Por favor agrega al menos un destinatario!",
"incorrect-password-or-decryption-failed": "Contraseña incorrecta o fallo de descifrado.",
"incorrect-password-for-file": "Contraseña incorrecta para el archivo: {{file}}",
"error-uploading-pdf": "Error al subir PDF.",
"provide-password": "Por favor proporciona la contraseña.",
"only-pdf-allowed": "Solo se permiten archivos PDF.",
"invalid-username-password-region": "Usuario/contraseña o región inválidos.",
"pfx-extension-alert": "Por favor sube un archivo con extensión .pfx.",
"email-already-exist": "El correo electrónico ya existe",
"branding": "Marca",
"branding-help": "El branding permite el white labelling de su aplicación",
"custom-sub-domain": "Subdominio personalizado",
"app-name": "Nombre de la aplicación",
"provide-domain-name": "Proporcione su nombre de dominio",
"provide-app-name": "Proporcione el nombre de su aplicación",
"logo": "Logo",
"upload-app-logo": "Suba el logotipo de su aplicación",
"prefill-unfilled-widget": "Los siguientes campos obligatorios no pueden quedar vacíos: {{emptyWidget}}. Por favor, complételos para continuar.",
"Dashboard": "Panel",
"Analytics": "Analítica",
"Templates": "Plantillas",
"Need your sign": "Necesitan tu firma",
"In Progress": "En progreso",
"Completed": "Completado",
"Drafts": "Borradores",
"Declined": "Rechazados",
"Expired": "Expirados",
"Contactbook": "Agenda de contactos",
"My Signature": "Mi firma",
"API Token": "Token API",
"Webhook": "Webhook",
"Preferences": "Preferencias",
"Teams": "Equipos",
"Users": "Usuarios",
"Drive": "Drive",
"Branding": "Marca",
"Mail": "Correo",
"Storage": "Almacenamiento",
"Signing certificate": "Certificado de firma",
"General": "General",
"Organizations": "Organizaciones",
"OrgAdmins": "OrgAdmins",
"Debug Pdf": "Depurar PDF",
"New Document": "Nuevo documento",
"subscription": "Suscripción",
"Draft document": "Borrador de documento",
"Draft template": "Borrador de plantilla",
"Public sign": "Firma pública",
"Signup": "Registrarse",
"delete-contact": "Eliminar contacto",
"total-records-found": "Total de registros encontrados: {{count}}",
"Invalid-records-found": "Registros inválidos encontrados: {{records}}",
"previous": "Anterior",
"page-n-of-n": "Página {{currentPage}} de {{totalPages}}",
"import": "Importar",
"search": "Buscar",
"viewed-on": "Visto el: {{ViewedOn}}",
"signed-on": "Firmado el: {{SignedOn}}",
"hide": "Ocultar",
"show-more": "Mostrar más",
"browse-or-drag-to-replace-existing-file": "Busque o arrastre y suelte un nuevo archivo para reemplazar el existente",
"optional-details": "Detalles opcionales",
"hide-optional-details": "ocultar detalles opcionales",
"mail-adapter-subscription-alert": "Bitte upgraden Sie auf den Professional- oder Team-Plan, um ein benutzerdefiniertes SMTP einzurichten.",
"connect-to-mail": "Conectar con Gmail",
"custom-smtp": "SMTP personalizado",
"default-smtp": "SMTP predeterminado de {{appName}}",
"host": "Host",
"port": "Puerto",
"sender-email": "Correo del remitente",
"username": "Nombre de usuario",
"use-default-mail-adapter": "¿Está seguro de que desea usar los servidores de correo predeterminados de {{appName}} para enviar solicitudes de firma? Recomendamos usar su propio servidor de Gmail o SMTP para una mejor entregabilidad.",
"verification-code-sent-registered-email": "Se ha enviado un código de verificación a su correo registrado <1>{{useremail}}</1>. Ingrese el código a continuación para confirmar su configuración.",
"smpt-credentials": "Credenciales SMTP",
"delete-account": "Eliminar cuenta",
"delete-account-que": "¿Está seguro de que desea eliminar su cuenta?",
"delete-account-que-user": "Está a punto de eliminar permanentemente este usuario y todos los datos asociados. Esta acción no se puede deshacer.",
"user-deleted-successfully": "Usuario y todos los datos asociados eliminados correctamente.",
"account-deletion-request-sent-via-mail": "Le hemos enviado por correo electrónico un enlace de confirmación. Apruebe la solicitud para completar la eliminación de su cuenta.",
"type-exact-email-delete": "Escriba el correo electrónico exacto para habilitar la eliminación",
"email-does-not-match": "El correo electrónico no coincide.",
"please-type-to-confirm": "Por favor, escriba <1>{{userEmail}}</1> para confirmar:",
"email-settings": "Configuración de correo electrónico",
"email-settings-help": "Para garantizar una mejor entregabilidad en la bandeja de entrada de sus correos electrónicos de solicitud de firma, puede conectar su propio proveedor de correo electrónico. Elija una de las siguientes opciones:",
"connect-to-gmail": "Conectar con Gmail: ",
"connect-to-gmail-help": "Utilice su cuenta de Gmail para enviar correos electrónicos de solicitud de firma directamente desde su propia bandeja de entrada, mejorando la tasa de entrega y la fiabilidad.",
"connect-to-smtp": "SMTP personalizado: ",
"connect-to-smtp-help": "Utilice su propio servidor SMTP para enviar correos electrónicos a través de su dominio. Esta opción le brinda control total sobre su infraestructura de correo electrónico, mejorando la entregabilidad y la coherencia de la marca.",
"connect-to-default": "Si lo prefiere, también puede usar los servidores de correo predeterminados de {{appName}}, aunque recomendamos usar los suyos para obtener mejores resultados.",
"email-settings-redirect-message": "Esta configuración se ha movido de la consola a Configuración del menú principal → Preferencias. Esta página se eliminará en próximas versiones.",
"go-to-preferences-menu": "Ir al menú de preferencias",
"document-download-filename-format": "Formato de nombre de archivo para la descarga del documento",
"preview": "Vista previa: ",
"download-filename-format-help": "Elija cómo se nombran los archivos PDF descargados. Su selección se guarda en su perfil y se usa en toda la aplicación.",
"delete-action-prohibited": "Esta acción no está permitida. Por favor, póngase en contacto con su administrador para solicitar la eliminación de la cuenta.",
"not-verified": "No verificado",
"verified": "Verificado",
"expires": "Expira",
"fix-resend-error": "No puede corregir y reenviar este documento porque contiene widgets predefinidos.",
"duplicate-template-error": "No puede duplicar esta plantilla porque contiene widgets predefinidos.",
"save-as-template-error": "Este documento no se puede guardar como plantilla porque contiene widgets predefinidos.",
"redirecting-you-in": "Redirigiéndolo en {{redirectTimeLeft}} seg...",
"pdf-tools-tour": "Haga clic en estos botones para agregar, eliminar, reorganizar, rotar y acercar/alejar páginas.",
"widgets": "Widgets",
"prefill-tour": "Use esta opción para ingresar información por adelantado antes de enviar el documento a los destinatarios.",
"empty-prefill-error": "Los siguientes campos obligatorios no pueden quedar vacíos:",
"please-fill-out": "Por favor, complételos para continuar.",
"custom-signing-certificate": "Certificado de firma personalizado",
"signing-certificate-help": "Puede cargar su propio certificado de firma de documentos, que se utilizará para firmar todos sus documentos así como los certificados de finalización. El archivo del certificado debe ser un certificado P12 en formato PFX.",
"certificate-file-p12-in-PFX-format": "Archivo de certificado (certificado p12 en formato PFX)",
"password-of-pfx-file": "Ingrese la contraseña del archivo pfx",
"update": "Actualizar",
"use-default-certificate": "Usar el certificado predeterminado de {{appName}}",
"upgrade-to-team-plan": "Actualizar al plan de equipo",
"setup-file-storage": "Configurar almacenamiento de archivos",
"save-and-activate": "Guardar y activar",
"logging-out-to-apply-settings": "Se está cerrando la sesión para aplicar la nueva configuración",
"reconnect-to-default": "Volver a conectar con {{appName}}",
"active-file-adapter": "Adaptador de archivos activo",
"file-adapter-unique-name": "Nombre único del adaptador de archivos",
"unique-name-for-file-adapter": "Ingrese un nombre único para el adaptador de archivos",
"storage-provider": "Proveedor de almacenamiento",
"enter-bucket-name": "Ingrese el nombre del bucket",
"enter-space-name": "Ingrese el nombre del espacio",
"enter-region-of-bucket": "Ingrese la región del bucket",
"enter-region-of-space": "Ingrese la región del espacio",
"enter-access-key": "Ingrese la clave de acceso",
"enter-secret-access-key": "Ingrese la clave secreta de acceso",
"otp-email": "Hemos enviado un código de verificación",
"save-as-temp-warn": "Nota: Este documento incluye widgets prellenados, que se eliminarán automáticamente ya que ya están incorporados en el documento base.",
"edit-document": "Editar documento",
"modify": "Modificar",
"merge-certificate-to-pdf": "Combinar certificado con PDF",
"merge-cc-to-pdf-help": {
"p1": "Esto garantizará que el certificado de finalización esté incluido en el documento PDF final. Sin embargo, tenga en cuenta que una vez combinado, el certificado no se podrá separar del documento principal.",
"p2": "Si elige no combinar, el certificado de finalización se proporcionará como un archivo PDF separado junto con el documento firmado."
},
"read-only-date-error": "El widget de fecha de solo lectura debe tener una fecha predeterminada.",
"set-date": "Establecer fecha",
"set-today": "Fecha de firma",
"enter-name": "Ingrese nombre",
"enter-email": "Ingrese correo electrónico",
"subscribe-to-opensign-msg": "Suscríbase a {{appName}} y disfrute de firmas digitales gratuitas ilimitadas.",
"duplicate-subscribe-msg": "Duplicar le permite crear una copia exacta de la plantilla seleccionada, lo que le permite reutilizarla o modificarla sin afectar al original.",
"save-as-template-msg": "Guarde este documento como una plantilla reutilizable que podrá usar nuevamente para futuros documentos.",
"public-credit-alert": "Firmar a través de este enlace consumirá sus créditos de correo electrónico gratuitos. Como usuario gratuito, se le asignan 15 créditos de correo electrónico por mes para evitar que los spammers abusen de nuestros sistemas. Para disfrutar de límites más altos y acceso ininterrumpido, suscríbase a los planes de pago de <1>OpenSign™.</1>",
"know-more-about": "Saber más sobre",
"free-unlimited-signatures": "¿Firmas ilimitadas gratis",
"email-quota-updated": "La cuota diaria de correos se actualizó correctamente.",
"password-has-been-reset": "La contraseña ha sido restablecida.",
"Unauthorized to reset your own password.": "No está autorizado para restablecer su propia contraseña.",
"User not found or not allowed.": "Usuario no encontrado o no autorizado.",
"User not found.": "Usuario no encontrado.",
"Admin user tenant not found.": "Inquilino de usuario administrador no encontrado.",
"User is not authenticated.": "El usuario no está autenticado.",
"Please provide required parameters.": "Por favor, proporcione los parámetros requeridos.",
"reset-password": "Restablecer contraseña de usuario",
"enter-strong-password": "Ingrese una contraseña segura o genere una automáticamente.",
"autogenerate": "Generar automáticamente",
"extend": "Extender",
"console-general-help": "Para permitir que los usuarios individuales personalicen sus propias plantillas de correo electrónico, vaya a",
"console-mail-help": "Para actualizar las plantillas de correo electrónico de toda la empresa, vaya a",
"admin-setting-disabled": "Esta configuración está deshabilitada. Comuníquese con el administrador para obtener ayuda.",
"company-email-settings": "Configuración de correo electrónico de la empresa",
"allow-email-template-individually": "Permitir la personalización individual de plantillas de correo electrónico para los usuarios ",
"user-email-template-general-help": "Si se desactiva, los usuarios no podrán personalizar sus plantillas de correo electrónico.",
"disable-individual-user-smtp": "desactivar la configuración smtp individual del usuario",
"enable-individual-user-smtp": "activar la configuración smtp individual del usuario",
"user-smtp-general-help": "Cuando está desactivado, los usuarios no podrán configurar su propio SMTP. Todos los correos utilizarán la configuración SMTP global.",
"global-smtp-settings": "Configuración SMTP Global",
"global-smtp-settings-help-p1": "Estas configuraciones se aplican globalmente en toda su cuenta de {{appName}}. Si un usuario no ha configurado sus propios ajustes de entrega de correo, el sistema utilizará automáticamente la configuración SMTP global definida aquí.",
"global-smtp-settings-help-p2": "Para garantizar una mejor entregabilidad de todos los correos de solicitud de firma, puede conectar su proveedor de correo preferido. Elija una de las siguientes opciones:",
"global-connect-to-gmail-help": "Use una cuenta de Gmail para enviar correos de solicitud de firma directamente desde la bandeja de entrada de su organización. Esto mejora la entregabilidad, la reputación y la fiabilidad.",
"global-connect-to-smtp-help": "Use su propio servidor SMTP para enviar correos a través de su dominio. Esto proporciona control total sobre su infraestructura de correo, mejora la entregabilidad y mantiene la coherencia de marca.",
"global-smtp-settings-help-p3": "Si no se configuran ajustes a nivel de usuario, se aplicarán automáticamente estos ajustes globales.",
"hide-text-with-asterisks": "Ocultar texto con asteriscos",
"hide-text-with-asterisks-help": "El texto en el documento firmado se ocultará, pero seguirá siendo accesible para el remitente (propietario) desde su cuenta de {{appName}}.",
"document-id": "ID del documento",
"sent-date": "Fecha de envío",
"status": "Estado",
"signer-form-data-for": "Datos del formulario para el firmante: {{signerName}} <{{signerEmail}}>",
"widget-name": "Nombre del widget",
"widget-responses": "Respuestas del widget",
"formdata-download-note": "Nota: Las firmas, sellos, iniciales e imágenes no se exportarán.",
"export-all-formdata-error": "No puede exportar los datos del formulario porque la plantilla contiene nombres de widgets duplicados. Cree una nueva plantilla para poder exportar los datos.",
"export-formdata-error": "No puede exportar los datos del formulario porque el documento contiene nombres de widgets duplicados. Cree un documento nuevo sin nombres duplicados para poder exportar los datos.",
"duplicate-widget-name-error": "Este nombre de widget ya está asignado a otro firmante. Por favor elija un nombre diferente.",
"duplicate-template-widget-error": "No puede duplicar esta plantilla porque contiene nombres de widgets duplicados.",
"credit-alert": "Alerta de crédito",
"document-form-data": "Datos del formulario del documento",
"view-formdata-paid-title-error": "La función 'Ver datos del formulario' le permite imprimir y descargar los datos de los widgets completados por los firmantes durante la firma.",
"view-formdata-paid-error": "Nota: La función 'Ver datos del formulario' solo está disponible para documentos creados en o después de la versión v2.32.0 que no contengan nombres de widgets duplicados.",
"download-all-formdata-paid-title-error": "La función 'Descargar todos los datos del formulario' le permite descargar los datos de los widgets completados por los firmantes para todos los documentos creados a partir de la plantilla.",
"download-all-formdata-paid-error": "Nota: La función 'Descargar todos los datos del formulario' solo está disponible para plantillas creadas en o después de la versión v2.32.0 que no contengan nombres de widgets duplicados.",
"api-token-not-found": "Token de API no encontrado.",
"sub-head-api-credits": "Defina la cantidad máxima de créditos API que este usuario puede consumir.",
"consumed-credits": "Créditos consumidos",
"allowed-api-credits": "Créditos API permitidos",
"allowed-api-credits-help": "Los créditos permitidos deben ser mayores o iguales a los créditos consumidos.",
"Enter-allowed-api-credits": "Ingrese los créditos API permitidos",
"login-to-sandbox": "Iniciar sesión en sandbox",
"your-credits-usage": "Uso de sus créditos",
"remaining": "restantes",
"used": "usados",
"your-credits-usage-help": "Esta es la cantidad de créditos consumidos con su clave API.",
"api-credits-updated": "Los créditos de API se han asignado correctamente.",
"min-date": "Fecha mínima",
"max-date": "Fecha máxima",
"invalid-min-date": "Fecha mínima no válida.",
"min-less-max": "La fecha mínima debe ser menor o igual a la fecha máxima.",
"invalid-max-date": "Fecha máxima no válida.",
"max-less-min": "La fecha máxima debe ser mayor o igual a la fecha mínima.",
"date-range": "Rango de fechas",
"date-range-help": "Establezca una fecha mínima y/o máxima para restringir la selección. La fecha elegida debe estar dentro del rango.",
"option-disabled-by-owner": "El creador del documento ha deshabilitado esta opción",
"pen-colors": "Colores del bolígrafo",
"pen-colors-help": "Elija qué colores de bolígrafo están disponibles para los widgets de Firma e Iniciales. Marque una casilla para mostrar un color y desmárquela para ocultarlo.",
"security-key": "Clave de seguridad",
"remove-limit": "Eliminar límite",
"removed-api-credits": "El límite de créditos API ha sido eliminado.",
"enable-authentication": "Habilitar autenticación",
"working": "En funcionamiento",
"test-security-key": "Probar clave de seguridad",
"generate": "Generar",
"regenerate": "Regenerar"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

+4
View File
@@ -0,0 +1,4 @@
Name,Email,Phone,Company,JobTitle
John Doe,john@example.com,1234567890,abc firm,dev
Jane Smith,jane@example.com,9876543210,xyz firm,manager
Foo Bar,foo@example.com,5555555555,xyz firm,director
1 Name Email Phone Company JobTitle
2 John Doe john@example.com 1234567890 abc firm dev
3 Jane Smith jane@example.com 9876543210 xyz firm manager
4 Foo Bar foo@example.com 5555555555 xyz firm director
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
-175
View File
@@ -1,175 +0,0 @@
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";
import HomeLayout from "./layout/HomeLayout";
import PageNotFound from "./pages/PageNotFound";
import ValidateRoute from "./primitives/ValidateRoute";
import Validate from "./primitives/Validate";
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
import SignYourSelf from "./pages/SignyourselfPdf";
import DraftDocument from "./components/pdf/DraftDocument";
import PlaceHolderSign from "./pages/PlaceHolderSign";
import PdfRequestFiles from "./pages/PdfRequestFiles";
import LazyPage from "./primitives/LazyPage";
import Loader from "./primitives/Loader";
import UserList from "./pages/UserList";
import { serverUrl_fn } from "./constant/appinfo";
import DocSuccessPage from "./pages/DocSuccessPage";
const DebugPdf = lazy(() => import("./pages/DebugPdf"));
const ForgetPassword = lazy(() => import("./pages/ForgetPassword"));
const GuestLogin = lazy(() => import("./pages/GuestLogin"));
const ChangePassword = lazy(() => import("./pages/ChangePassword"));
const UserProfile = lazy(() => import("./pages/UserProfile"));
const Opensigndrive = lazy(() => import("./pages/Opensigndrive"));
const ManageSign = lazy(() => import("./pages/Managesign"));
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
const Preferences = lazy(() => import("./pages/Preferences"));
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
const AppLoader = () => {
return (
<div className="flex justify-center items-center h-[100vh]">
<Loader />
</div>
);
};
function App() {
const [isloading, setIsLoading] = useState(true);
useEffect(() => {
handleCredentials();
}, []);
const handleCredentials = () => {
const appId = process.env.REACT_APP_APPID
? process.env.REACT_APP_APPID
: "opensign";
const baseurl = serverUrl_fn();
try {
localStorage.setItem("baseUrl", `${baseurl}/`);
localStorage.setItem("parseAppId", appId);
setIsLoading(false);
} catch (error) {
console.log("err ", error);
}
};
return (
<div className="bg-base-200">
{isloading ? (
<AppLoader />
) : (
<BrowserRouter>
<Routes>
<Route element={<ValidateRoute />}>
<Route exact path="/" element={<Login />} />
<Route
path="/addadmin"
element={<LazyPage Page={AddAdmin} />}
/>
<Route
path="/upgrade-2.1"
element={<LazyPage Page={UpdateExistUserAdmin} />}
/>
</Route>
<Route element={<Validate />}>
<Route
path="/load/template/:templateId"
element={<TemplatePlaceholder />}
/>
<Route
exact
path="/load/placeholdersign/:docId"
element={<PlaceHolderSign />}
/>
<Route
exact
path="/load/recipientSignPdf/:docId/:contactBookId"
element={<PdfRequestFiles />}
/>
</Route>
<Route
path="/loadmf/signmicroapp/login/:id/:userMail/:contactBookId/:serverUrl"
element={<LazyPage Page={GuestLogin} />}
/>
<Route
path="/login/:id/:userMail/:contactBookId/:serverUrl"
element={<LazyPage Page={GuestLogin} />}
/>
<Route
path="/login/:base64url"
element={<LazyPage Page={GuestLogin} />}
/>
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
<Route
path="/forgetpassword"
element={<LazyPage Page={ForgetPassword} />}
/>
<Route element={<HomeLayout />}>
<Route
path="/changepassword"
element={<LazyPage Page={ChangePassword} />}
/>
<Route path="/form/:id" element={<Form />} />
<Route path="/report/:id" element={<Report />} />
<Route path="/dashboard/:id" element={<Dashboard />} />
<Route
path="/profile"
element={<LazyPage Page={UserProfile} />}
/>
<Route
path="/opensigndrive"
element={<LazyPage Page={Opensigndrive} />}
/>
<Route
path="/managesign"
element={<LazyPage Page={ManageSign} />}
/>
<Route
path="/template/:templateId"
element={<TemplatePlaceholder />}
/>
{/* signyouself route with no rowlevel data using docId from url */}
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
{/* draft document route to handle and navigate route page accordiing to document status */}
<Route path="/draftDocument" element={<DraftDocument />} />
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
<Route
path="/placeHolderSign/:docId"
element={<PlaceHolderSign />}
/>
{/* for user signature (need your sign route) with row level data */}
<Route path="/pdfRequestFiles" element={<PdfRequestFiles />} />
{/* for user signature (need your sign route) with no row level data */}
<Route
path="/pdfRequestFiles/:docId"
element={<PdfRequestFiles />}
/>
{/* recipient signature route with no rowlevel data using docId from url */}
<Route
path="/recipientSignPdf/:docId/:contactBookId"
element={<PdfRequestFiles />}
/>
<Route
path="/recipientSignPdf/:docId"
element={<PdfRequestFiles />}
/>
<Route path="/users" element={<UserList />} />
<Route
path="/preferences"
element={<LazyPage Page={Preferences} />}
/>
</Route>
<Route path="/success" element={<DocSuccessPage />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
)}
</div>
);
}
export default App;
+148
View File
@@ -0,0 +1,148 @@
import { useState, useEffect } from "react";
import { lazyWithRetry, hideUpgradeProgress } from "./utils";
import { Routes, Route, BrowserRouter } from "react-router";
import { pdfjs } from "react-pdf";
import Form from "./pages/Form";
import Report from "./pages/Report";
import Dashboard from "./pages/Dashboard";
import HomeLayout from "./layout/HomeLayout";
import PageNotFound from "./pages/PageNotFound";
import ValidateRoute from "./primitives/ValidateRoute";
import Validate from "./primitives/Validate";
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
import SignYourSelf from "./pages/SignyourselfPdf";
import DraftDocument from "./components/pdf/DraftDocument";
import PlaceHolderSign from "./pages/PlaceHolderSign";
import PdfRequestFiles from "./pages/PdfRequestFiles";
import Lazy from "./primitives/LazyPage";
import Loader from "./primitives/Loader";
import UserList from "./pages/UserList";
import { serverUrl_fn } from "./constant/appinfo";
import DocSuccessPage from "./pages/DocSuccessPage";
import DragProvider from "./components/DragProivder";
import Title from "./components/Title";
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
const ForgetPassword = lazyWithRetry(() => import("./pages/ForgetPassword"));
const GuestLogin = lazyWithRetry(() => import("./pages/GuestLogin"));
const ChangePassword = lazyWithRetry(() => import("./pages/ChangePassword"));
const UserProfile = lazyWithRetry(() => import("./pages/UserProfile"));
const Opensigndrive = lazyWithRetry(() => import("./pages/Opensigndrive"));
const ManageSign = lazyWithRetry(() => import("./pages/Managesign"));
const AddAdmin = lazyWithRetry(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazyWithRetry(
() => import("./pages/UpdateExistUserAdmin")
);
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
const Login = lazyWithRetry(() => import("./pages/Login"));
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
const AppLoader = () => {
return (
<div className="flex justify-center items-center h-[100vh]">
<Loader />
</div>
);
};
function App() {
const [isloading, setIsLoading] = useState(true);
useEffect(() => {
// initialize creds
const id = process.env.REACT_APP_APPID ?? "opensign";
localStorage.setItem("parseAppId", id);
localStorage.setItem("baseUrl", `${serverUrl_fn()}/`);
hideUpgradeProgress();
localStorage.removeItem("showUpgradeProgress");
setIsLoading(false);
}, []);
return (
<div className="bg-base-200">
{isloading ? (
<AppLoader />
) : (
<BrowserRouter>
<Title />
<Routes>
<Route element={<ValidateRoute />}>
<Route exact path="/" element={<Lazy Page={Login} />} />
<Route path="/addadmin" element={<Lazy Page={AddAdmin} />} />
<Route
path="/upgrade-2.1"
element={<Lazy Page={UpdateExistUserAdmin} />}
/>
</Route>
<Route element={<Validate />}>
<Route
exact
path="/load/recipientSignPdf/:docId/:contactBookId"
element={<DragProvider Page={PdfRequestFiles} />}
/>
</Route>
<Route
path="/login/:base64url"
element={<Lazy Page={GuestLogin} />}
/>
<Route path="/debugpdf" element={<Lazy Page={DebugPdf} />} />
<Route
path="/forgetpassword"
element={<Lazy Page={ForgetPassword} />}
/>
<Route element={<HomeLayout />}>
<Route path="/users" element={<UserList />} />
<Route
path="/changepassword"
element={<Lazy Page={ChangePassword} />}
/>
<Route path="/form/:id" element={<Form />} />
<Route path="/report/:id" element={<Report />} />
<Route path="/dashboard/:id" element={<Dashboard />} />
<Route path="/profile" element={<Lazy Page={UserProfile} />} />
<Route path="/drive" element={<Lazy Page={Opensigndrive} />} />
<Route path="/managesign" element={<Lazy Page={ManageSign} />} />
<Route
path="/template/:templateId"
element={<DragProvider Page={TemplatePlaceholder} />}
/>
{/* signyouself route with no rowlevel data using docId from url */}
<Route
path="/signaturePdf/:docId"
element={<DragProvider Page={SignYourSelf} />}
/>
{/* draft document route to handle and navigate route page according to document status */}
<Route
path="/draftDocument"
element={<DragProvider Page={DraftDocument} />}
/>
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
<Route
path="/placeHolderSign/:docId"
element={<DragProvider Page={PlaceHolderSign} />}
/>
{/* recipient signature route with no rowlevel data using docId from url */}
<Route
path="/recipientSignPdf/:docId/:contactBookId"
element={<DragProvider Page={PdfRequestFiles} />}
/>
<Route
path="/recipientSignPdf/:docId"
element={<DragProvider Page={PdfRequestFiles} />}
/>
<Route
path="/verify-document"
element={<Lazy Page={VerifyDocument} />}
/>
<Route
path="/preferences"
element={<Lazy Page={Preferences} />}
/>
</Route>
<Route path="/success" element={<DocSuccessPage />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
)}
</div>
);
}
export default App;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

-300
View File
@@ -1,300 +0,0 @@
import React, { useState, useEffect } from "react";
import Parse from "parse";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import { emailRegex } from "../constant/const";
const AddSigner = (props) => {
const { t } = useTranslation();
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [addYourself, setAddYourself] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const [isUserExist, setIsUserExist] = useState(false);
useEffect(() => {
checkUserExist();
}, []);
// Load user details from localStorage when the component mounts
useEffect(() => {
const savedUserDetails = JSON.parse(
localStorage.getItem("UserInformation")
);
if (savedUserDetails && addYourself) {
setName(savedUserDetails.name);
setPhone(savedUserDetails?.phone || "");
setEmail(savedUserDetails.email);
}
}, [addYourself]);
const checkUserExist = async () => {
const user = Parse.User.current();
try {
const query = new Parse.Query("contracts_Contactbook");
query.equalTo("CreatedBy", user);
query.notEqualTo("IsDeleted", true);
query.equalTo("Email", user.getEmail());
const res = await query.first();
// console.log(res);
if (!res) {
setIsUserExist(true);
}
} catch (err) {
console.log("err", err);
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!emailRegex.test(email)) {
alert("Please enter a valid email address.");
} else {
setIsLoader(true);
if (localStorage.getItem("TenantId")) {
try {
const user = Parse.User.current();
const query = new Parse.Query("contracts_Contactbook");
query.equalTo("CreatedBy", user);
query.notEqualTo("IsDeleted", true);
query.equalTo("Email", email);
const res = await query.first();
// console.log(res);
if (!res) {
const contactQuery = new Parse.Object("contracts_Contactbook");
contactQuery.set("Name", name);
if (phone) {
contactQuery.set("Phone", phone);
}
contactQuery.set("Email", email);
contactQuery.set("UserRole", "contracts_Guest");
contactQuery.set("TenantId", {
__type: "Pointer",
className: "partners_Tenant",
objectId: localStorage.getItem("TenantId")
});
try {
const _users = Parse.Object.extend("User");
const _user = new _users();
_user.set("name", name);
_user.set("username", email);
_user.set("email", email);
_user.set("password", email);
if (phone) {
_user.set("phone", phone);
}
const user = await _user.save();
if (user) {
const currentUser = Parse.User.current();
contactQuery.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
contactQuery.set("UserId", user);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
contactQuery.setACL(acl);
const res = await contactQuery.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.details) {
props.details({
value: parseData[props.valueKey],
label: parseData[props.displayKey],
email: parseData?.Email
});
}
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
props.handleUserData(parseData);
}
setIsLoader(false);
// Reset the form fields
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
}
} catch (err) {
console.log("err ", err);
if (err.code === 202) {
const params = { email: email };
const userRes = await Parse.Cloud.run("getUserId", params);
const currentUser = Parse.User.current();
contactQuery.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
contactQuery.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);
contactQuery.setACL(acl);
const res = await contactQuery.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.details) {
props.details({
value: parseData[props.valueKey],
label: parseData[props.displayKey],
email: parseData?.Email
});
}
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
props.handleUserData(parseData);
}
setIsLoader(false);
// Reset the form fields
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
}
}
} else {
alert(t("add-signer-alert"));
setIsLoader(false);
}
} catch (err) {
console.log("err in fetch contact", err);
setIsLoader(false);
alert(t("something-went-wrong-mssg"));
}
} else {
setIsLoader(false);
alert(t("something-went-wrong-mssg"));
}
}
};
// Define a function to handle the "add yourself" checkbox
const handleAddYourselfChange = () => {
if (addYourself) {
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
} else {
setAddYourself(true);
}
};
const handleReset = () => {
setAddYourself(false);
};
return (
<div className="h-full p-[20px]">
{isLoader && (
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
<div className="w-full mx-auto p-2 text-base-content">
<p className="mb-3 text-xs text-gray-500">{t("add-signer-note")}</p>
{isUserExist && (
<div className="mb-3 flex items-center">
<input
type="checkbox"
id="addYourself"
checked={addYourself}
onChange={handleAddYourselfChange}
className="op-checkbox op-checkbox-sm"
/>
<label htmlFor="addYourself" className="ml-2 mb-0">
{t("add-yourself")}
</label>
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="name" className="block text-xs font-semibold">
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={addYourself}
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 htmlFor="email" className="block text-xs font-semibold">
{t("email")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) =>
setEmail(e.target.value?.toLowerCase()?.replace(/\s/g, ""))
}
required
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-3">
<label htmlFor="phone" className="block text-xs font-semibold">
{t("phone")}
</label>
<input
type="text"
id="phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("phone-optional")}
/>
</div>
<div className="mt-4 flex gap-x-2 justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
<button
type="button"
onClick={() => handleReset()}
className="op-btn op-btn-secondary"
>
{t("reset")}
</button>
</div>
</form>
</div>
</div>
);
};
export default AddSigner;
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import Parse from "parse";
import Title from "./Title";
import Loader from "../primitives/Loader";
import {
copytoData,
@@ -9,7 +8,9 @@ import {
import {
emailRegex,
} from "../constant/const";
import { useTranslation } from "react-i18next";
import {
useTranslation
} from "react-i18next";
function generatePassword(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -70,173 +71,62 @@ const AddUser = (props) => {
e.preventDefault();
e.stopPropagation();
if (!emailRegex.test(formdata.email)) {
alert("Please enter a valid email address.");
alert(t("valid-email-alert"));
} else {
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
setIsFormLoader(true);
const res = await checkUserExist();
if (res) {
props.setIsAlert({ type: "danger", msg: t("user-already-exist") });
props.showAlert("danger", t("user-already-exist"));
setIsFormLoader(false);
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1000);
} else {
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")
});
}
if (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 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));
if (props.closePopup) {
props.closePopup();
}
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
);
}
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.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: ""
});
}
console.log("err", err);
setIsFormLoader(false);
props.showAlert("danger", t("something-went-wrong-mssg"));
}
} catch (err) {
console.log("err", err);
setIsFormLoader(false);
props.setIsAlert({
type: "danger",
msg: t("something-went-wrong-mssg")
});
} finally {
setTimeout(
() => props.setIsAlert({ type: "success", msg: "" }),
1500
);
} else {
props.showAlert("danger", t("something-went-wrong-mssg"));
}
}
}
@@ -259,12 +149,10 @@ const AddUser = (props) => {
const copytoclipboard = (text) => {
copytoData(text);
props.setIsAlert({ type: "success", msg: t("copied") });
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1500); // Reset copied state after 1.5 seconds
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 />
@@ -275,7 +163,7 @@ const AddUser = (props) => {
<div className="mb-3">
<label
htmlFor="name"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
@@ -291,12 +179,13 @@ const AddUser = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/>
</div>
<div className="mb-3">
<label
htmlFor="email"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("email")}
<span className="text-[red] text-[13px]"> *</span>
@@ -312,10 +201,11 @@ const AddUser = (props) => {
}
onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
/>
</div>
<div className="mb-3">
<label className="block text-xs text-gray-700 font-semibold">
<label className="block text-xs 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]">
@@ -332,10 +222,9 @@ const AddUser = (props) => {
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("phone")}
{/* <span className="text-[red] text-[13px]"> *</span> */}
</label>
<input
type="text"
@@ -349,7 +238,7 @@ const AddUser = (props) => {
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("Role")}
<span className="text-[red] text-[13px]"> *</span>
@@ -15,16 +15,27 @@ const BulkSendUi = (props) => {
const [isSignatureExist, setIsSignatureExist] = useState();
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const [signers, setSigners] = useState([]);
const [emails, setEmails] = useState([]);
const [isPrefillExist, setIsPrefillExist] = useState(false);
useEffect(() => {
signatureExist();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//function to check atleast one signature field exist
//function to check at least one signature field exist
const signatureExist = async () => {
const isPrefill =
props?.Placeholders?.some((x) => x?.Role === "prefill") || false;
if (isPrefill) {
setIsPrefillExist(isPrefill);
}
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
const checkIsSignatureExistt = getPlaceholder?.every((placeholderObj) =>
const removePrefill = getPlaceholder?.filter(
(x) => x?.Role !== "prefill"
);
const checkIsSignatureExistt = removePrefill?.every((placeholderObj) =>
placeholderObj?.placeHolder?.some((holder) =>
holder?.pos?.some((posItem) => posItem?.type === "signature")
)
@@ -47,7 +58,14 @@ const BulkSendUi = (props) => {
(() => {
if (props?.Placeholders?.length > 0) {
let users = [];
let emails = [];
props?.Placeholders?.forEach((element) => {
const signerEmail = element?.email || element?.signerPtr?.Email;
// only add when there's a non-empty signerEmail
if (signerEmail) {
emails = [...emails, signerEmail];
}
if (!element.signerObjId) {
users = [
...users,
@@ -60,7 +78,10 @@ const BulkSendUi = (props) => {
];
}
});
setEmails(emails);
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
const signer = props.item?.Signers?.filter((x) => x?.objectId);
setSigners(signer);
}
})();
// eslint-disable-next-line
@@ -82,10 +103,16 @@ const BulkSendUi = (props) => {
function validateEmails(data) {
for (const item of data) {
let email = "";
for (const field of item.fields) {
if (!emailRegex.test(field.email)) {
alert(`Invalid email found: ${field.email}`);
alert(t("invalid-email-found", { email: field.email }));
return false;
} else if (email === field.email || emails?.includes(field.email)) {
alert(t("duplicate-email-found", { email: field.email }));
return false;
} else {
email = field.email;
}
}
}
@@ -143,15 +170,14 @@ const BulkSendUi = (props) => {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
Signers: props.item.Signers
? [...props.item.Signers, ...existSigner]
: [...existSigner]
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
});
} else {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
SignatureType: props.signatureType
SignatureType: props.signatureType,
Signers: signers
});
}
});
@@ -164,9 +190,7 @@ const BulkSendUi = (props) => {
const batchQuery = async (Documents) => {
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
const functionsUrl = `${localStorage.getItem(
"baseUrl"
)}functions/batchdocuments`;
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
@@ -175,13 +199,14 @@ const BulkSendUi = (props) => {
const params = { Documents: JSON.stringify(Documents) };
try {
const res = await axios.post(functionsUrl, params, { headers: headers });
// console.log("res ", res);
if (res.data && res.data.result) {
props.handleClose("success", Documents?.length);
}
} catch (err) {
console.log("Err ", err);
props.handleClose("error", 0);
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("Error sending documents:", message);
props.handleClose("error", 0, message);
} finally {
setIsSubmit(false);
}
@@ -199,7 +224,11 @@ const BulkSendUi = (props) => {
<Loader />
</div>
)}
{!isDisableBulkSend ? (
{isPrefillExist ? (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("prefill-bulk-error")}
</div>
) : !isDisableBulkSend ? (
<>
{props.Placeholders?.length > 0 ? (
isSignatureExist ? (
@@ -218,7 +247,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"
@@ -0,0 +1,62 @@
import { useState, useEffect } from "react";
import ModalUi from "../primitives/ModalUi";
import { useTranslation } from "react-i18next";
const ColumnSelector = ({
isOpen,
allColumns = [],
visibleColumns = [],
columnLabels = {},
onApply,
onClose
}) => {
const { t } = useTranslation();
const [selected, setSelected] = useState(visibleColumns);
const [names, setNames] = useState(columnLabels);
useEffect(() => {
setSelected(visibleColumns);
setNames(columnLabels);
}, [visibleColumns, columnLabels]);
const handleChange = (col) => {
setSelected((prev) =>
prev.includes(col) ? prev.filter((c) => c !== col) : [...prev, col]
);
};
const handleApply = () => {
onApply && onApply(selected, names);
onClose && onClose();
};
return (
<ModalUi isOpen={isOpen} title={t("select-columns")} handleClose={onClose}>
<div className="p-[20px] flex flex-col gap-2">
{allColumns.map((col, i) => (
<div key={col} className="flex justify-between items-center gap-2">
<span className="flex justify-center items-center h-full">
<input
id={col + "_" + i}
type="checkbox"
checked={selected.includes(col)}
onChange={() => handleChange(col)}
className="mb-0 cursor-pointer"
/>
<span className="whitespace-nowrap ml-1">
{t(`report-heading.${col}`, { defaultValue: col })}
</span>
</span>
</div>
))}
<div className="flex justify-start mt-2">
<button onClick={handleApply} className="op-btn op-btn-primary">
{t("apply")}
</button>
</div>
</div>
</ModalUi>
);
};
export default ColumnSelector;
@@ -0,0 +1,27 @@
import { HTML5Backend } from "react-dnd-html5-backend";
import {
DndProvider,
MouseTransition
} from "react-dnd-multi-backend";
import LazyPage from "../primitives/LazyPage";
import { GuidelinesProvider } from "../context/GuidelinesContext";
const HTML5toTouch = {
backends: [
{
id: "html5",
backend: HTML5Backend,
transition: MouseTransition
}
]
};
export default function DragProvider({ Page, lazy = false }) {
return (
<DndProvider options={HTML5toTouch}>
<GuidelinesProvider>
{lazy ? <LazyPage Page={Page} /> : <Page />}
</GuidelinesProvider>
</DndProvider>
);
}
@@ -4,6 +4,7 @@ import axios from "axios";
import { openInNewTab } from "../constant/Utils";
import { useTranslation } from "react-i18next";
const Footer = () => {
const appName = "OpenSign™";
const { t } = useTranslation();
const [showButton, setShowButton] = useState(false);
const [version, setVersion] = useState("");
@@ -39,7 +40,6 @@ const Footer = () => {
};
}, []);
const appName = "OpenSign™";
const openUrl = () => {
openInNewTab(
"https://github.com/OpenSignLabs/OpenSign/releases/tag/" + version
-187
View File
@@ -1,187 +0,0 @@
import React, { useState, useEffect } from "react";
import dp from "../assets/images/dp.png";
import FullScreenButton from "./FullScreenButton";
import { useNavigate } from "react-router";
import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize";
import {
getAppLogo,
openInNewTab,
saveLanguageInLocal
} from "../constant/Utils";
import { useTranslation } from "react-i18next";
const Header = ({ showSidebar, setIsMenu }) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { width } = useWindowSize();
const username = localStorage.getItem("username") || "";
const image = localStorage.getItem("profileImg") || dp;
const [isOpen, setIsOpen] = useState(false);
const [applogo, setAppLogo] = useState(
localStorage.getItem("appLogo") || " "
);
const toggleDropdown = () => {
setIsOpen(!isOpen);
if (width <= 768) {
setIsMenu(false);
}
};
useEffect(() => {
initializeHead();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function initializeHead() {
const applogo = await getAppLogo();
if (applogo?.logo) {
setAppLogo(applogo?.logo);
} else {
setAppLogo(localStorage.getItem("appLogo") || "");
}
}
const closeDropdown = async () => {
setIsOpen(false);
try {
await Parse.User.logOut();
} catch (err) {
console.log("Err while logging out", err);
}
let appdata = localStorage.getItem("userSettings");
let applogo = localStorage.getItem("appLogo");
let defaultmenuid = localStorage.getItem("defaultmenuid");
let PageLanding = localStorage.getItem("PageLanding");
let baseUrl = localStorage.getItem("baseUrl");
let appid = localStorage.getItem("parseAppId");
localStorage.clear();
saveLanguageInLocal(i18n);
localStorage.setItem("appLogo", applogo);
localStorage.setItem("defaultmenuid", defaultmenuid);
localStorage.setItem("PageLanding", PageLanding);
localStorage.setItem("userSettings", appdata);
localStorage.setItem("baseUrl", baseUrl);
localStorage.setItem("parseAppId", appid);
navigate("/");
};
//handle to close profile drop down menu onclick screen
useEffect(() => {
const closeMenuOnOutsideClick = (e) => {
if (isOpen && !e.target.closest("#profile-menu")) {
setIsOpen(false);
}
};
document.addEventListener("click", closeMenuOnOutsideClick);
return () => {
// Cleanup the event listener when the component unmounts
document.removeEventListener("click", closeMenuOnOutsideClick);
};
}, [isOpen]);
return (
<div>
<div className="op-navbar bg-base-100 shadow">
<div className="flex-none">
<button
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
onClick={showSidebar}
>
<i className="fa-light fa-bars text-xl text-base-content"></i>
</button>
</div>
<div className="flex-1 ml-2">
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
<img
className="object-contain h-full w-auto"
src={applogo}
alt="img"
/>
</div>
</div>
<div id="profile-menu" className="flex-none gap-2">
<div>
<FullScreenButton />
</div>
{width >= 768 && (
<div
onClick={toggleDropdown}
className="cursor-pointer w-[35px] h-[35px] rounded-full ring-[1px] ring-offset-2 ring-gray-400 overflow-hidden"
>
<img
className="w-[35px] h-[35px] object-contain"
src={image}
alt="img"
/>
</div>
)}
{width >= 768 && (
<div
onClick={toggleDropdown}
className="cursor-pointer text-base-content text-sm"
>
{username && username}
</div>
)}
<div className="op-dropdown op-dropdown-end" id="profile-menu">
<div
tabIndex={0}
role="button"
className="op-btn op-btn-ghost op-btn-xs w-[10px] h-[20px] hover:bg-transparent"
>
<i
tabIndex={0}
role="button"
onClick={toggleDropdown}
className="fa-light fa-angle-down text-base-content"
></i>
</div>
<ul
tabIndex={0}
className={`mt-3 z-[1] p-2 shadow op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-52 ${
isOpen ? "" : "hidden"
}`}
>
<li onClick={() => openInNewTab("https://docs.opensignlabs.com")}>
<span>
<i className="fa-light fa-book"></i> {t("docs")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/profile");
}}
>
<span>
<i className="fa-light fa-user"></i> {t("profile")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/changepassword");
}}
>
<span>
<i className="fa-light fa-lock"></i> {t("change-password")}
</span>
</li>
<li onClick={closeDropdown}>
<span>
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
{t("log-out")}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default Header;
+273
View File
@@ -0,0 +1,273 @@
import { useState, useEffect } from "react";
import dp from "../assets/images/dp.png";
import FullScreenButton from "./FullScreenButton";
import ThemeToggle from "./ThemeToggle";
import { useNavigate } from "react-router";
import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize";
import {
getAppLogo,
openInNewTab,
saveLanguageInLocal
} from "../constant/Utils";
import { useTranslation } from "react-i18next";
import { appInfo } from "../constant/appinfo";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../redux/reducers/sidebarReducer";
import { sessionStatus } from "../redux/reducers/userReducer";
const Header = ({ isConsole, setIsLoggingOut }) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { width } = useWindowSize();
const dispatch = useDispatch();
const username = localStorage.getItem("username") || "";
const image = localStorage.getItem("profileImg") || dp;
const [isOpen, setIsOpen] = useState(false);
const [applogo, setAppLogo] = useState("");
const [isDarkTheme, setIsDarkTheme] = useState();
const toggleDropdown = () => {
setIsOpen(!isOpen);
closeSidebar();
};
const closeSidebar = () => {
if (width && width <= 768) {
dispatch(toggleSidebar(false));
}
};
useEffect(() => {
initializeHead();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
closeSidebar();
}, [width]);
const showSidebar = () => {
dispatch(toggleSidebar());
};
async function initializeHead() {
const applogo = await getAppLogo();
if (applogo?.logo) {
setAppLogo(applogo?.logo);
} else {
const logo = localStorage.getItem("appLogo") || appInfo.applogo;
setAppLogo(logo);
}
}
const handleLogout = async () => {
setIsOpen(false);
setIsLoggingOut(true);
try {
await Parse.User.logOut();
} catch (err) {
console.log("Err while logging out", err);
} finally {
dispatch(sessionStatus(true));
}
let appdata = localStorage.getItem("userSettings");
let applogo = localStorage.getItem("appLogo");
let defaultmenuid = localStorage.getItem("defaultmenuid");
let PageLanding = localStorage.getItem("PageLanding");
let baseUrl = localStorage.getItem("baseUrl");
let appid = localStorage.getItem("parseAppId");
let favicon = localStorage.getItem("favicon");
localStorage.clear();
saveLanguageInLocal(i18n);
localStorage.setItem("appLogo", applogo);
localStorage.setItem("defaultmenuid", defaultmenuid);
localStorage.setItem("PageLanding", PageLanding);
localStorage.setItem("userSettings", appdata);
localStorage.setItem("baseUrl", baseUrl);
localStorage.setItem("parseAppId", appid);
localStorage.setItem("favicon", favicon);
setIsLoggingOut(false);
navigate("/");
};
//handle to close profile drop down menu onclick screen
useEffect(() => {
const closeMenuOnOutsideClick = (e) => {
if (isOpen && !e.target.closest("#profile-menu")) {
setIsOpen(false);
}
};
document.addEventListener("click", closeMenuOnOutsideClick);
return () => {
// Cleanup the event listener when the component unmounts
document.removeEventListener("click", closeMenuOnOutsideClick);
};
}, [isOpen]);
useEffect(() => {
const updateThemeStatus = () => {
const isDarkTheme =
document.documentElement.getAttribute("data-theme") === "opensigndark";
setIsDarkTheme(isDarkTheme);
};
updateThemeStatus();
const observer = new MutationObserver(() => {
updateThemeStatus();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"]
});
return () => observer.disconnect();
}, []);
return (
<>
<div className="op-navbar bg-base-100 shadow touch-none">
<div className="flex-none">
<button
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
onClick={showSidebar}
>
<i className="fa-light fa-bars text-xl text-base-content"></i>
</button>
</div>
<div className="flex-1 ml-2">
<div
onClick={() => navigate("/dashboard/35KBoSgoAK")}
className="h-[25px] md:h-[40px] w-auto overflow-hidden cursor-pointer"
>
{applogo && (
<img
className="object-contain h-full w-auto"
src={
isDarkTheme
? "/static/js/assets/images/logo-dark.png"
: applogo
}
alt="logo"
/>
)}
</div>
</div>
<div id="profile-menu" className="flex-none gap-2">
<div>
<FullScreenButton />
</div>
{width >= 768 && (
<div
onClick={toggleDropdown}
className="cursor-pointer w-[35px] h-[35px] rounded-full ring-[1px] ring-offset-2 ring-gray-400 overflow-hidden"
>
<img
className="w-[35px] h-[35px] object-contain"
src={image}
alt="img"
/>
</div>
)}
{width >= 768 && (
<div
onClick={toggleDropdown}
role="button"
tabIndex="0"
className="cursor-pointer text-base-content text-sm"
>
{username && username}
</div>
)}
<div
className="op-dropdown op-dropdown-open op-dropdown-end"
id="profile-menu"
>
<div
tabIndex={0}
role="button"
onClick={toggleDropdown}
className="op-btn op-btn-ghost op-btn-xs w-[10px] h-[20px] hover:bg-transparent"
>
<i className="fa-light fa-angle-down text-base-content"></i>
</div>
<ul
tabIndex={0}
className={`mt-4 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
isOpen ? "" : "hidden"
}`}
>
{!isConsole && (
<>
<li
onClick={() =>
openInNewTab("https://docs.opensignlabs.com")
}
>
<span>
<i className="fa-light fa-book"></i> {t("docs")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/profile");
}}
>
<span>
<i className="fa-light fa-user"></i> {t("profile")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/changepassword");
}}
>
<span>
<i className="fa-light fa-lock"></i>{" "}
{t("change-password")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/verify-document");
}}
>
<span>
<i className="fa-light fa-check-square"></i>{" "}
{t("verify-document")}
</span>
</li>
<li>
<span>
<i className="fa-light fa-moon"></i>
{t("dark-mode")}
<span className="text-[10px] font-semibold bg-base-300 text-base-content px-1 rounded-md">
BETA
</span>
<ThemeToggle />
</span>
</li>
</>
)}
<li onClick={handleLogout}>
<span>
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
{t("log-out")}
</span>
</li>
</ul>
</div>
</div>
</div>
</>
);
};
export default Header;
@@ -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}
@@ -26,7 +26,7 @@ function RotateAlert(props) {
props.setShowRotateAlert({ status: false, degree: 0 })
}
type="button"
className="op-btn op-btn-ghost shadow-md"
className="op-btn op-btn-ghost text-base-content shadow-md"
>
{t("no")}
</button>
@@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
const ThemeToggle = () => {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const storedTheme = localStorage.getItem("theme");
if (storedTheme === "dark") {
setIsDark(true);
document.documentElement.setAttribute("data-theme", "opensigndark");
} else {
document.documentElement.setAttribute("data-theme", "opensigncss");
}
}, []);
const handleChange = () => {
const newTheme = !isDark;
setIsDark(newTheme);
if (newTheme) {
document.documentElement.setAttribute("data-theme", "opensigndark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.setAttribute("data-theme", "opensigncss");
localStorage.setItem("theme", "light");
}
};
return (
<>
<input
id="dark-mode-toggle"
type="checkbox"
className="op-toggle checked:[--tglbg:#3368ff] transition-all checked:bg-white"
checked={isDark}
onChange={handleChange}
/>
</>
);
};
export default ThemeToggle;
-19
View File
@@ -1,19 +0,0 @@
import React from "react";
import { Helmet } from "react-helmet";
function Title({ title, drive }) {
return (
<Helmet>
<title>{drive ? title : `${title} - OpenSign™`}</title>
<meta name="description" content={`${title} - OpenSign™`} />
<link
rel="icon"
type="image/png"
href={localStorage.getItem("fev_Icon")}
sizes="40x40"
/>
</Helmet>
);
}
export default Title;
+81
View File
@@ -0,0 +1,81 @@
import { useLocation, matchPath } from "react-router";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { useManifestUrl } from "../hook/useManifestUrl";
const TITLE_MAP = {
"/": "login",
// Homelayout
"/dashboard/35KBoSgoAK": "Dashboard",
"/form/sHAnZphf69": "Sign Yourself",
"/form/8mZzFxbG1z": "Request Signatures",
"/form/template": "New Template",
"/report/6TeaPr321t": "Templates",
"/report/4Hhwbp482K": "Need your sign",
"/report/1MwEuxLEkF": "In Progress",
"/report/kQUoW4hUXz": "Completed",
"/report/ByHuevtCFY": "Drafts",
"/report/UPr2Fm5WY3": "Declined",
"/report/zNqBHXHsYH": "Expired",
"/report/contacts": "Contactbook",
"/drive": "Drive",
"/managesign": "My Signature",
"/preferences": "Preferences",
"/users": "Users",
"/profile": "profile",
"/changepassword": "change-password",
"/verify-document": "verify-document",
"/signaturePdf/:docId": "Sign Yourself",
"/placeHolderSign/:docId": "Request Signatures",
"/template/:templateId": "New Template",
"/recipientSignPdf/:docId": "Request Signatures",
"/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
"/load/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
// alone
"/debugpdf": "Debug Pdf",
"/forgetpassword": "forgot-password",
"/success": "success",
"/addadmin": "add-admin",
"/upgrade-2.1": "add-admin",
"/draftDocument": "New Document",
"/login/:base64url": "Request Signatures",
};
function resolveTitle(pathname, override) {
if (override) return override;
for (let [pattern, label] of Object.entries(TITLE_MAP)) {
if (matchPath({ path: pattern, end: true }, pathname)) {
return label;
}
}
return "";
}
export default function Title() {
const { pathname, state } = useLocation();
const { t } = useTranslation();
const appName =
"OpenSign™";
const logo = useMemo(() => localStorage.getItem("favicon"), []);
const prefix = useMemo(
() => resolveTitle(pathname, state?.title),
[pathname, state?.title]
);
const title = useMemo(
() => (prefix ? `${t(prefix)} - ${appName}` : appName),
[t, prefix, appName]
);
const manifestUrl = useManifestUrl(appName, logo);
return (
<>
<title>{title}</title>
<meta name="description" content={title} />
{logo && <link rel="icon" type="image/png" href={logo} />}
<link rel="manifest" href={manifestUrl} />
</>
);
}
@@ -29,7 +29,7 @@ const DashboardButton = (props) => {
: "cursor-default"
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
>
<div className="flex flex-row items-center">
<div className="flex flex-row items-center text-base-content">
<div className="flex flex-row items-center">
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
<i
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
></i>
</span>
</div>
<div className="text-lg ml-3 text-base-content">
<div className="text-lg ml-3">
{t(`sidebar.${props.Label}`)}
{props.Label === "Sign yourself" && (
<div className="text-gray-500 text-xs mt-1">
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import Parse from "parse";
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
@@ -148,7 +148,7 @@ const DashboardCard = (props) => {
);
let arr = [];
for (const obj of listData) {
const isSigner = obj.Signers.some(
const isSigner = obj.Signers?.some(
(item) => item.UserId.objectId === currentUser.id
);
if (isSigner) {
@@ -168,25 +168,21 @@ const DashboardCard = (props) => {
}
}
setresponse(arr.length);
setLoading(false);
});
} else {
await axios.get(url, { headers: headers }).then((res) => {
if (res.data.results.length > 0) {
setLoading(false);
if (props.Data.key !== "count") {
setresponse(res.data.results[0][props.Data.key]);
} else {
setresponse(res.data[props.Data.key]);
}
if (res?.data?.[props.Data.key]) {
setresponse(parseInt(res.data[props.Data.key]));
} else if (res?.data?.results?.length > 0) {
setresponse(res.data.results.length);
} else {
setresponse(0);
setLoading(false);
}
});
}
} catch (e) {
console.error("Problem", e);
} finally {
setLoading(false);
}
}
@@ -346,7 +342,6 @@ const DashboardCard = (props) => {
id={props.Label}
iconColor={"white"}
message={t(`tour-mssg.${props.Label}`)}
// {props?.Data?.tourMessage}
/>
</div>
</div>
@@ -1,13 +1,12 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import Parse from "parse";
import ReportTable from "../../primitives/GetReportDisplay";
import DocumentsReport from "../../reports/document/DocumentsReport";
import reportJson from "../../json/ReportJson";
import axios from "axios";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
function DashboardReport(props) {
const { t } = useTranslation();
// console.log("props ", props)
const [List, setList] = useState([]);
const [isLoader, setIsLoader] = useState(true);
const [reportName, setReportName] = useState("");
@@ -17,10 +16,16 @@ function DashboardReport(props) {
const [isMoreDocs, setIsMoreDocs] = useState(true);
const abortController = new AbortController();
const docPerPage = 5;
const [searchTerm, setSearchTerm] = useState("");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [isSearchResult, setIsSearchResult] = useState(false);
const debounceTimer = useRef(null);
useEffect(() => {
setReportName("");
getReportData(props.Record.reportId);
setSearchTerm("");
setMobileSearchOpen(false);
getReportData(props.Record.reportId, 0, 20, "");
// Function returned from useEffect is called on unmount
return () => {
@@ -36,12 +41,69 @@ function DashboardReport(props) {
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(props.Record.reportId, List.length, 20);
getReportData(props.Record.reportId, List.length, 20, searchTerm);
}
// eslint-disable-next-line
}, [isNextRecord]);
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
const handleSearchChange = async (e) => {
const term = e.target.value.toLowerCase();
setSearchTerm(term);
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(async () => {
try {
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
};
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(
url,
{
reportId: props.Record.reportId,
searchTerm: term,
skip: 0,
limit: docPerPage
},
{ headers }
);
const data = res.data?.result || [];
if (!data.error) {
setList(data);
setIsMoreDocs(data.length >= docPerPage);
setIsNextRecord(false);
setIsSearchResult(true);
}
} catch (err) {
console.error("Search error:", err);
}
}, 300);
setIsSearchResult(false);
};
const handleSearchPaste = (e) => {
setTimeout(() => {
handleSearchChange({ target: { value: e.target.value } });
}, 0);
};
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const getReportData = async (
id,
skipUserRecord = 0,
limit = 20,
term = searchTerm
) => {
setIsLoader(true);
const json = reportJson(id);
if (json) {
@@ -59,6 +121,9 @@ function DashboardReport(props) {
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
if (term) {
params.searchTerm = term;
}
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(url, params, {
headers: headers,
@@ -68,7 +133,7 @@ function DashboardReport(props) {
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
let arr = [];
for (const obj of listData) {
const isSigner = obj.Signers.some(
const isSigner = obj.Signers?.some(
(item) => item.UserId.objectId === currentUser
);
if (isSigner) {
@@ -132,7 +197,7 @@ function DashboardReport(props) {
) : (
<>
{reportName ? (
<ReportTable
<DocumentsReport
ReportName={reportName}
List={List}
setList={setList}
@@ -141,11 +206,17 @@ function DashboardReport(props) {
setIsNextRecord={setIsNextRecord}
isMoreDocs={isMoreDocs}
docPerPage={docPerPage}
mobileSearchOpen={mobileSearchOpen}
setMobileSearchOpen={setMobileSearchOpen}
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
handleSearchPaste={handleSearchPaste}
isSearchResult={isSearchResult}
/>
) : (
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
<div className="text-center">
<p className="text-xl text-black">{t("report-not-found")}</p>
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded-box">
<div className="text-center text-xl text-base-content">
{t("report-not-found")}
</div>
</div>
)}
@@ -1,8 +1,9 @@
import React, { Suspense, lazy } from "react";
import { useState, useEffect, Suspense } from "react";
import { lazyWithRetry } from "../../utils";
import { useTranslation } from "react-i18next";
const DashboardButton = lazy(() => import("./DashboardButton"));
const DashboardCard = lazy(() => import("./DashboardCard"));
const DashboardReport = lazy(() => import("./DashboardReport"));
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
const DashboardCard = lazyWithRetry(() => import("./DashboardCard"));
const DashboardReport = lazyWithRetry(() => import("./DashboardReport"));
const buttonList = [
{
label: "Sign yourself",
@@ -19,6 +20,7 @@ const buttonList = [
];
const GetDashboard = (props) => {
const { t } = useTranslation();
const Button = ({ label, redirectId, redirectType, icon }) => (
<DashboardButton
Icon={icon}
@@ -58,7 +60,9 @@ const GetDashboard = (props) => {
<div data-tut={col.widget.data.tourSection}>
<Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} />
<DashboardReport
Record={col.widget}
/>
</div>
</Suspense>
</div>
@@ -92,7 +96,9 @@ const GetDashboard = (props) => {
return (
<Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} />
<DashboardReport
Record={col.widget}
/>
</div>
</Suspense>
);
@@ -1,10 +1,10 @@
import React, { useState, useEffect, useRef } from "react";
import "../../styles/opensigndrive.css";
import axios from "axios";
import * as ContextMenu from "@radix-ui/react-context-menu";
import { ContextMenu } from "radix-ui";
import { useNavigate } from "react-router";
import Table from "react-bootstrap/Table";
import * as HoverCard from "@radix-ui/react-hover-card";
import { HoverCard } from "radix-ui";
import ModalUi from "../../primitives/ModalUi";
import FolderModal from "../shared/fields/FolderModal";
import { useTranslation } from "react-i18next";
@@ -56,9 +56,7 @@ function DriveBody(props) {
setRename("");
const trimmedValue = renameValue.trim();
if (trimmedValue.length > 0) {
const updateName = {
Name: renameValue
};
const updateName = { Name: renameValue };
const docId = data.objectId;
const docData = props.pdfData;
const updatedData = docData.map((item) => {
@@ -71,11 +69,9 @@ function DriveBody(props) {
});
props.setPdfData(updatedData);
props.sortingData(null, null, updatedData);
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${docId}`,
try {
await axios.put(
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${docId}`,
updateName,
{
headers: {
@@ -84,18 +80,14 @@ function DriveBody(props) {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
)
.then(() => {
// const res = result.data;
// console.log("res", res);
})
.catch((err) => {
console.log("Err ", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
);
} catch (err) {
console.error("Error in rename doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
}
}
};
@@ -166,9 +158,7 @@ function DriveBody(props) {
const handleDeleteDocument = async (docData) => {
setIsDeleteDoc({});
const docId = docData.objectId;
const data = {
IsArchive: true
};
const data = { IsArchive: true };
await axios
.put(
@@ -190,7 +180,7 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("Err ", err);
console.error("Err in delete doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -224,16 +214,12 @@ function DriveBody(props) {
}
};
} else {
updateData = {
Folder: { __op: "Delete" }
};
updateData = { Folder: { __op: "Delete" } };
}
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${updateDocId}`,
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${updateDocId}`,
updateData,
{
headers: {
@@ -245,7 +231,6 @@ function DriveBody(props) {
)
.then((Listdata) => {
// console.log("Listdata ", Listdata);
const res = Listdata.data;
if (res) {
const updatedData = props.pdfData.filter(
@@ -255,7 +240,7 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("err", err);
console.error("err in move folder", err);
});
setIsOpenMoveModal(false);
@@ -303,7 +288,7 @@ function DriveBody(props) {
props.setPdfData(updatedData);
}
} catch (err) {
console.log("Err ", err);
console.error("Err in delete folder", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -347,12 +332,14 @@ function DriveBody(props) {
}
const signersName = () => {
const getSignersName = signerExist.map((data) => data.Name);
const signerName = getSignersName.join(", ");
const getSignersName =
signerExist?.length > 0 && signerExist?.map((data) => data?.Name || "");
const signerName =
getSignersName?.length > 0 ? getSignersName?.join(", ") : "";
return (
<span className="text-[12px] font-medium w-[90%] break-words">
{signerName}
{signerName && signerName}
</span>
);
};
@@ -364,14 +351,14 @@ function DriveBody(props) {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[26px] h-[26px] fill-current op-text-secondary"
className="w-[26px] h-[26px] fill-current"
>
<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>
<span className="text-[12px] font-medium">{data.Name}</span>
</td>
<td>_</td>
<td>Folder</td>
<td>{t("folder")}</td>
<td>_</td>
<td>_</td>
</tr>
@@ -388,7 +375,7 @@ function DriveBody(props) {
<span className="text-[12px] font-medium">{data.Name}</span>
</td>
<td>{createddate}</td>
<td>Pdf</td>
<td>{t("pdf")}</td>
<td>{t(`drive-document-status.${status}`)}</td>
<td>
<i
@@ -403,7 +390,7 @@ function DriveBody(props) {
</tr>
)
) : listType === "list" && data.Type === "Folder" ? (
<div key={ind} className="relative w-[100px] h-[100px] mx-2 my-3">
<div className="relative w-[100px] h-[100px] mx-2 my-3">
<ContextMenu.Root>
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
{/* folder */}
@@ -419,7 +406,7 @@ function DriveBody(props) {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[100px] h-[100px] fill-current op-text-secondary"
className="w-[100px] h-[100px] fill-current"
>
<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>
@@ -445,7 +432,6 @@ function DriveBody(props) {
)}
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content
className="ContextMenuContent"
@@ -457,14 +443,14 @@ function DriveBody(props) {
className="ContextMenuItem"
>
<i className="fa-light fa-font mr-[8px]"></i>
<span>Rename</span>
<span>{t(`context-menu.Rename`)}</span>
</ContextMenu.Item>
<ContextMenu.Item
onClick={() => handleMenuItemClick("Delete", data, data.Type)}
className="ContextMenuItem"
>
<i className="fa-light fa-trash mr-[8px]"></i>
<span>Delete</span>
<span>{t(`context-menu.Delete`)}</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
@@ -549,7 +535,6 @@ function DriveBody(props) {
)
)}
</div>
<ContextMenu.Portal>
<ContextMenu.Content
className="ContextMenuContent"
@@ -602,7 +587,6 @@ function DriveBody(props) {
{signersName()}
</>
)}
<HoverCard.Arrow className="HoverCardArrow" />
</HoverCard.Content>
</HoverCard.Portal>
@@ -626,21 +610,21 @@ function DriveBody(props) {
</tr>
</thead>
<tbody>
{props.pdfData.map((data, ind) => {
return (
<React.Fragment key={ind}>
{handleFolderData(data, ind, "table")}
</React.Fragment>
);
})}
{props?.pdfData?.map((data, ind) => (
<React.Fragment key={ind}>
{handleFolderData(data, ind, "table")}
</React.Fragment>
))}
</tbody>
</Table>
</div>
) : (
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
{props.pdfData.map((data, ind) => {
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>;
})}
{props?.pdfData?.map((data, ind) => (
<React.Fragment key={ind}>
{handleFolderData(data, ind, "list")}
</React.Fragment>
))}
</div>
)}
@@ -10,15 +10,15 @@ const AddRoleModal = (props) => {
isOpen={props.isModalRole}
handleClose={props.handleCloseRoleModal}
>
<div className="h-full py-[10px] px-[20px]">
<div className="text-base-content h-full py-[10px] px-[20px]">
<form className="flex flex-col" onSubmit={props.handleAddRole}>
<input
value={props.roleName}
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"
/>
@@ -33,7 +33,7 @@ const AddRoleModal = (props) => {
<button
onClick={props.handleCloseRoleModal}
type="button"
className="op-btn op-btn-ghost ml-2"
className="op-btn op-btn-ghost text-base-content ml-2"
>
{t("close")}
</button>
@@ -4,10 +4,11 @@ import ModalUi from "../../primitives/ModalUi";
function AgreementContent(props) {
const { t } = useTranslation();
const appName =
"OpenSign™";
const h2Style = "text-base-content font-medium text-lg";
const ulStyle = "list-disc px-4 py-3";
const handleOnclick = () => {
props.setIsAgreeTour(false);
props.setIsAgree(true);
props.setIsShowAgreeTerms(false);
props.showFirstWidget();
@@ -21,78 +22,94 @@ function AgreementContent(props) {
>
<div className="h-[100%] p-[20px]">
<h2 className={h2Style}>{t("term-cond-h")}</h2>
<span className="mt-2">{t("term-cond-p1")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">
{t("term-cond-p1", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h1")}</h2>
<span className="mt-2">{t("term-cond-p2")}</span>
<span className="mt-2">
{t("term-cond-p2", { appName: appName })}
</span>
<ul className={ulStyle}>
<li>{t("term-cond-p3")}</li>
<li>{t("term-cond-p3", { appName: appName })}</li>
<li>{t("term-cond-p4")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h2")}</h2>
<span className="mt-2">{t("term-cond-p5")}</span>
<ul className={ulStyle}>
<li>{t("term-cond-p6")}</li>
<li>{t("term-cond-p7")}</li>
<li>{t("term-cond-p6", { appName: appName })}</li>
<li>{t("term-cond-p7", { appName: appName })}</li>
<li>{t("term-cond-p8")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h3")}</h2>
<span className="mt-2">{t("term-cond-p9")}</span>
<ul className={ulStyle}>
<li>{t("term-cond-p10")}</li>
<li>{t("term-cond-p11")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h4")}</h2>
<span className="mt-2">{t("term-cond-p12")}</span>
<span className="mt-2">
{t("term-cond-p12", { appName: appName })}
</span>
<ul className={ulStyle}>
<li>{t("term-cond-p13")}</li>
<li>{t("term-cond-p14")}</li>
<li>{t("term-cond-p15")}</li>
</ul>
<span>{t("term-cond-p16")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span>{t("term-cond-p16", { appName: appName })}</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h5")}</h2>
<p>{t("term-cond-p17")}</p>
<p>{t("term-cond-p17", { appName: appName })}</p>
<p className="mt-2">{t("term-cond-p18")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h6")}</h2>
<span className="mt-2">{t("term-cond-p19")}</span>
<ul className={ulStyle}>
<li>{t("term-cond-p20")}</li>
<li>{t("term-cond-p21")}</li>
<li>{t("term-cond-p22")}</li>
<li>{t("term-cond-p22", { appName: appName })}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h7")}</h2>
<span className="mt-2">{t("term-cond-p23")}</span>
<span className="mt-2">
{t("term-cond-p23", { appName: appName })}
</span>
<ul className={ulStyle}>
<li>{t("term-cond-p24")}</li>
<li>{t("term-cond-p25")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h8")}</h2>
<span className="mt-2">{t("term-cond-p26")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">
{t("term-cond-p26", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h9")}</h2>
<span className="mt-2">{t("term-cond-p27")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2 font-medium">{t("term-cond-p28")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">{t("term-cond-p29")}</span>
<a
href="www.opensignlabs.com"
target="_blank"
className="text-blue-700 cursor-pointer"
>
{" "}
www.opensignlabs.com{" "}
</a>
<span>{t("term-cond-p30")}</span>
<span className="font-medium"> support@opensignlabs.com </span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">
{t("term-cond-p27", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<span className="mt-2 font-medium">
{t("term-cond-p28", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<span className="mt-2">
{t("term-cond-p29", { appName: appName })}
</span>
<a
href="www.opensignlabs.com"
target="_blank"
className="text-blue-700 cursor-pointer"
>
www.opensignlabs.com
</a>
<span>{t("term-cond-p30")}</span>
<span className="font-medium"> support@opensignlabs.com </span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<div className="mt-6 flex justify-start gap-2">
<button
onClick={() => handleOnclick()}
@@ -101,7 +118,7 @@ function AgreementContent(props) {
{t("agrre-button")}
</button>
<button
className="op-btn op-btn-ghost"
className="op-btn op-btn-ghost text-base-content"
onClick={() => props.setIsShowAgreeTerms(false)}
>
{t("close")}
@@ -4,7 +4,6 @@ import AgreementContent from "./AgreementContent";
function AgreementSign(props) {
const { t } = useTranslation();
const [isChecked, setIsChecked] = useState(false);
const [isShowAgreeTerms, setIsShowAgreeTerms] = useState(false);
return (
@@ -12,26 +11,12 @@ function AgreementSign(props) {
<div className="op-modal op-modal-open absolute z-[448]">
<div className="w-[95%] md:w-[60%] lg:w-[40%] op-modal-box overflow-y-auto hide-scrollbar text-sm p-4">
<div className="flex flex-row items-center">
<input
data-tut="IsAgree"
className="mr-3 op-checkbox op-checkbox-m"
type="checkbox"
value={isChecked}
onChange={(e) => {
setIsChecked(e.target.checked);
if (e.target.checked) {
props.setIsAgreeTour(false);
}
props.showFirstWidget();
}}
/>
<div className="text-[11px] md:text-base">
<div className="text-[11px] md:text-base text-base-content">
<span>{t("agree-p1")}</span>
<span
className="font-bold text-blue-600 cursor-pointer"
onClick={() => {
setIsShowAgreeTerms(true);
props.setIsAgreeTour(false);
}}
>
{t("agree-p2")}
@@ -39,26 +24,24 @@ function AgreementSign(props) {
<span> {t("agree-p3")}</span>
</div>
</div>
<div className="flex ml-[35px] mt-3">
<div className="flex mt-3">
<button
onClick={() => {
if (isChecked) {
props.setIsAgreeTour(false);
props.setIsAgree(true);
} else {
props.setIsAgreeTour(true);
}
props.setIsAgree(true);
props.showFirstWidget();
}}
className="op-btn op-btn-primary op-btn-sm w-full md:w-auto"
>
{t("agrre-button")}
</button>
</div>
<div className="mt-2 text-base-content">
<span className="text-[11px]">{t("agreement-note")}</span>
</div>
</div>
</div>
{isShowAgreeTerms && (
<AgreementContent
setIsAgreeTour={props.setIsAgreeTour}
setIsAgree={props.setIsAgree}
setIsShowAgreeTerms={setIsShowAgreeTerms}
showFirstWidget={props.showFirstWidget}
@@ -12,12 +12,9 @@ function BorderResize(props) {
return (
<div
style={{
width: getHeight() || "14px",
height: getHeight() || "14px"
}}
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
props.top ? `-bottom-[12px]` : "-bottom-[2px] "
props.top ? `-bottom-[12px]` : "-bottom-[2px]"
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
></div>
);
@@ -0,0 +1,110 @@
import { useState, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi";
import { fontsizeArr, fontColorArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
export default function CellsSettingModal({
isOpen,
handleClose,
defaultData,
handleSave,
}) {
const { t } = useTranslation();
const [name, setName] = useState("");
const [cellCount, setCellCount] = useState(5);
const [fontSize, setFontSize] = useState(12);
const [fontColor, setFontColor] = useState("black");
useEffect(() => {
if (defaultData) {
setName(defaultData.options?.name || "Cells");
setCellCount(defaultData.options?.cellCount || 5);
setFontSize(defaultData.options?.fontSize || 12);
setFontColor(defaultData.options?.fontColor || "black");
}
}, [defaultData]);
const onSubmit = (e) => {
e.preventDefault();
handleSave &&
handleSave({
name,
cellCount: parseInt(cellCount, 10),
fontSize,
fontColor,
});
};
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
<form
onSubmit={onSubmit}
className="p-[20px] text-base-content flex flex-col gap-3"
>
<div>
<label htmlFor="name" className="text-[13px]">
{t("name")} <span className="text-[red]">*</span>
</label>
<input
required
name="name"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="cellCount" className="text-[13px]">
{t("cell-count")}
</label>
<input
type="number"
min="1"
name="cellCount"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={cellCount}
onChange={(e) => setCellCount(e.target.value)}
/>
</div>
<div className="flex flex-col md:flex-row md:items-center gap-3">
<div className="flex items-center gap-2">
<span className="whitespace-nowrap">{t("font-size")}: </span>
<select
className="ml-[7px] w-[60%] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontSize}
onChange={(e) => setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex items-center">
<span>{t("color")}: </span>
<select
className="ml-[33px] md:ml-4 w-[65%] md:w-full op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontColor}
onChange={(e) => setFontColor(e.target.value)}
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
className="w-5 h-[19px] ml-1"
style={{ background: fontColor }}
/>
</div>
</div>
<div className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div>
<button type="submit" className="op-btn op-btn-primary">
{t("save")}
</button>
</form>
</ModalUi>
);
}
@@ -0,0 +1,143 @@
import { useState, useEffect, useRef } from "react";
const Cell = ({
isEnabled,
count,
h,
value,
editable,
onChange,
onKeyDown,
onBlur,
inputRef,
index,
fontSize,
fontColor,
hint
}) => (
<div
className={`${isEnabled ? "bg-white border-gray-400" : "select-none-cls pointer-events-none border-gray-500"} flex items-center justify-center border-[1px]`}
style={{ flex: `0 0 ${100 / count}%`, height: h }}
>
<input
disabled={!isEnabled}
maxLength={1}
value={value}
readOnly={!editable}
ref={inputRef}
onChange={editable ? (e) => onChange && onChange(e, index) : undefined}
onKeyDown={editable ? (e) => onKeyDown && onKeyDown(e, index) : undefined}
// trigger validation when leaving a cell
onBlur={editable ? (e) => onBlur && onBlur(e, index) : undefined}
className={`${isEnabled ? "placeholder-gray-300" : "placeholder-gray-500"} w-full text-center focus:outline-none bg-transparent`}
placeholder={hint}
style={{ fontFamily: "Arial, sans-serif", fontSize, color: fontColor }}
/>
</div>
);
export default function CellsWidget({
isEnabled,
count = 8,
height = 40,
value = "",
editable = false,
onChange,
onKeyDown,
onBlur,
onCellCountChange,
inputRefs,
resizable = false,
fontSize = "12px",
fontColor = "black",
hint = ""
}) {
const [cellCount, setCellCount] = useState(count);
// keep internal state in sync with prop updates
useEffect(() => setCellCount(count), [count]);
const startX = useRef(0);
const startCount = useRef(cellCount);
const capture = (downEv, onMove, onUp = () => {}) => {
const id = downEv.pointerId;
const move = (ev) => id === ev.pointerId && onMove(ev);
const up = (ev) => {
if (id !== ev.pointerId) return;
onUp(ev);
downEv.target.releasePointerCapture(id);
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
downEv.target.setPointerCapture(id);
};
const onTopHandlePointerDown = (ev) => {
// Prevent triggering the widget drag logic
ev.stopPropagation();
ev.preventDefault();
startX.current = ev.clientX;
startCount.current = cellCount;
capture(ev, (moveEv) => {
const dx = moveEv.clientX - startX.current;
const delta = Math.floor(-dx / 5);
let newCount = Math.max(1, startCount.current + delta);
if (newCount !== cellCount) {
setCellCount(newCount);
onCellCountChange?.(newCount);
}
});
};
const cells = Array.from({ length: cellCount }).map((_, i) => value[i] || "");
const hints = Array.from({ length: cellCount }).map((_, i) => hint[i] || "");
return (
<div
className="relative flex w-full h-full overflow-visible"
style={{ height }}
>
{resizable && (
<div
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -bottom-4 rotate-180 cursor-ew-resize touch-none"
onPointerDown={onTopHandlePointerDown}
>
<svg
className="w-4 h-4 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="m9.69 18.933.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 0 0 .281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 1 0 3 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 0 0 2.273 1.765 11.842 11.842 0 0 0 .976.544l.062.029.018.008.006.003ZM10 11.25a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5Z"
clipRule="evenodd"
/>
</svg>
</div>
)}
{cells.map((val, i) => (
<Cell
key={i}
isEnabled={isEnabled}
count={cellCount}
h={height}
value={val}
editable={editable}
onChange={onChange}
onKeyDown={onKeyDown}
onBlur={onBlur}
inputRef={inputRefs ? (el) => (inputRefs.current[i] = el) : undefined}
index={i}
fontSize={fontSize}
fontColor={fontColor}
hint={hints[i]}
/>
))}
</div>
);
}
@@ -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;
@@ -0,0 +1,160 @@
import { useRef, useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import { EmailBody } from "./EmailBody";
import {
contractDocument,
defaultMailBody,
defaultMailSubject,
sendEmailToSigners
} from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import Loader from "../../primitives/Loader";
import { useNavigate } from "react-router";
function CustomizeMail(props) {
const { t } = useTranslation();
const editorRef = useRef();
const navigate = useNavigate();
const copyUrlRef = useRef(null);
const [isCustomize, setIsCustomize] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const handleCloseSendmailModal = () => {
if (props?.handleClose) {
props?.handleClose();
return;
}
props?.setIsMailModal(false);
navigate("/report/1MwEuxLEkF");
};
const handleOnchangeRequest = () => {
if (editorRef.current) {
const html = editorRef.current.editor.root.innerHTML;
props?.setCustomizeMail((prev) => ({
...prev,
body: html
}));
}
};
const handleEmailSendToSigners = async () => {
setIsLoader(true);
const documentData = await contractDocument(props?.documentId);
if (documentData && documentData?.length > 0) {
props?.setDocumentDetails(documentData[0]);
if (
documentData?.[0]?.SendinOrder &&
documentData?.[0]?.SendinOrder === true
) {
const ownerEmail = documentData[0].ExtUserPtr.Email;
const ownerDetails = documentData[0].Signers.find(
(x) => x.Email === ownerEmail
);
props?.setCurrUserId(ownerDetails?.objectId);
}
//function is used to send email to signers for sign the document
const mailRes = await sendEmailToSigners(
documentData,
props?.signerList,
props?.customizeMail,
props?.defaultMail,
isCustomize,
);
props?.setIsMailModal(false);
props?.setIsSend(true);
setIsLoader(false);
if (mailRes?.status === "success") {
props?.setMailStatus("success");
} else if (mailRes?.status === "quota-reached") {
props?.setMailStatus("quotareached");
} else if (mailRes?.status === "daily-quota-reached") {
props?.setMailStatus("dailyquotareached");
} else {
props?.setMailStatus("failed");
}
// setMailStatus(mail_status);
} else {
alert("something-went-wrong-mssg");
}
};
return (
<>
{isLoader ? (
<div className="absolute w-full h-full flex justify-center items-center bg-black/30 rounded-box z-30">
<Loader />
</div>
) : (
<ModalUi
isOpen={props?.isMailModal}
title={t("send-mail")}
handleClose={() => handleCloseSendmailModal()}
>
<div className="max-h-96 overflow-y-scroll scroll-hide p-[20px] text-base-content">
{!isCustomize && <span>{t("placeholder-alert-3")}</span>}
{
isCustomize && (
<>
<EmailBody
editorRef={editorRef}
requestBody={props?.customizeMail.body}
requestSubject={props?.customizeMail.subject}
handleOnchangeRequest={handleOnchangeRequest}
setCustomizeMail={props?.setCustomizeMail}
/>
<div
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
onClick={() => {
props?.setCustomizeMail({
subject: defaultMailSubject,
body: defaultMailBody
});
}}
>
<span>{t("reset-to-default")}</span>
</div>
</>
)
}
<div className="flex flex-row items-center gap-2 md:gap-6 mt-2">
<div className="flex flex-row gap-2">
<button
onClick={() => handleEmailSendToSigners()}
className="op-btn op-btn-primary font-[500] text-sm shadow"
>
{t("send")}
</button>
{isCustomize && (
<button
onClick={() => setIsCustomize(false)}
className="op-btn op-btn-ghost font-[500] text-sm"
>
{t("close")}
</button>
)}
</div>
{
!isCustomize && (
<span
className="op-link op-link-accent text-sm"
onClick={() => setIsCustomize(!isCustomize)}
>
{t("cutomize-email")}
</span>
)
}
</div>
<div className="flex justify-center items-center mt-3">
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
<span className="ml-[5px] mr-[5px]">{t("or")}</span>
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
</div>
<div className="my-3">{props?.handleShareList()}</div>
<p id="copyUrl" ref={copyUrlRef} className="hidden"></p>
</div>
</ModalUi>
)}
</>
);
}
export default CustomizeMail;
@@ -1,42 +1,41 @@
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
function DefaultSignature(props) {
const { t } = useTranslation();
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
const myInitial = useSelector((state) => state.widget.myInitial);
const tabName = ["my-signature", "my-initials"];
const [activeTab, setActiveTab] = useState(0);
const confirmToaddDefaultSign = (type) => {
if (!props.isAgree) {
props.setIsAgreeTour(true);
} else {
if (props?.xyPosition.length > 0) {
//check signature or initial widgets exist or not for auto signing
const getCurrentSignerXY = props?.xyPosition.filter(
(data) => data.Id === props.uniqueId
);
const checkIsSignInitialExist = getCurrentSignerXY?.every(
(placeholderObj) =>
placeholderObj?.placeHolder?.some((placeholder) =>
placeholder?.pos?.some((posItem) => posItem?.type === type)
)
);
if (checkIsSignInitialExist) {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("default-sign-alert", { widgetsType: type }),
type: type
});
} else {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("defaultSign-alert", { widgetsType: type })
});
}
if (props?.xyPosition.length > 0) {
//check signature or initial widgets exist or not for auto signing
const getCurrentSignerXY = props?.xyPosition.filter(
(data) => data.Id === props.uniqueId
);
const checkIsSignInitialExist = getCurrentSignerXY?.every(
(placeholderObj) =>
placeholderObj?.placeHolder?.some((placeholder) =>
placeholder?.pos?.some((posItem) => posItem?.type === type)
)
);
if (checkIsSignInitialExist) {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("default-sign-alert", { widgetsType: type }),
type: type
});
} else {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("please-select-position!")
alertMessage: t("defaultSign-alert", { widgetsType: type })
});
}
} else {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("please-select-position!")
});
}
};
@@ -69,15 +68,15 @@ function DefaultSignature(props) {
<img
alt="signature"
className="w-full h-full object-contain"
src={props?.defaultSignImg}
src={defaultSignImg}
/>
) : (
activeTab === 1 &&
(props?.myInitial ? (
(myInitial ? (
<img
alt="signature"
className="w-full h-full object-contain"
src={props?.myInitial}
src={myInitial}
/>
) : (
<div className="flex justify-center items-center h-full">
@@ -97,7 +96,7 @@ function DefaultSignature(props) {
disabled={
activeTab === 0 && !props?.isDefault
? true
: activeTab === 1 && !props.myInitial
: activeTab === 1 && !myInitial
? true
: false
}
@@ -106,9 +105,7 @@ function DefaultSignature(props) {
</button>
{!props.isDefault && (
<div className="absolute bg-black/70 text-white w-full h-full flex items-center justify-center text-[11px] cursor-default">
<span className="-rotate-45">
Document creator has disabled this option
</span>
<span className="-rotate-45">{t("option-disabled-by-owner")}</span>
</div>
)}
</div>
@@ -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,5 +1,5 @@
import React from "react";
import { getWidgetType, widgets } from "../../constant/Utils";
import { widgets } from "../../constant/Utils";
import getWidgetType from "./getWidgetType";
function DragElement(item) {
const getWidgets = widgets;
@@ -0,0 +1,76 @@
import { useDragLayer } from "react-dnd";
import { defaultWidthHeight } from "../../constant/Utils";
import { useEffect, useRef } from "react";
function DragGuideLinesLayer(props) {
const { showGuidelines } = props;
const prevDragState = useRef({ isDragging: false, x: 0, y: 0 });
const { isDraggingWidgets, offset, itemType } = useDragLayer((monitor) => ({
isDraggingWidgets: monitor.isDragging(),
offset: monitor.getClientOffset(),
itemType: monitor.getItem()
}));
useEffect(() => {
if (!offset) return;
//getting container by id
const container = document.getElementById("container");
if (!container) return;
if (itemType?.text) {
// Calculate the position of the dragged element relative to the container
// Get the container's bounding rectangle (position and size in the viewport)
const containerRect = container.getBoundingClientRect();
// Compute the X and Y coordinates of the dragged item inside the container
// by subtracting the container's top-left offset from the current drag offset
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
// Check if theres any change in the dragging state or the dragged elements position
// Compare the current drag state with the previously stored one
const hasStateChanged =
prevDragState.current.isDragging !== isDraggingWidgets || // Drag started or stopped
prevDragState.current.x !== x || // X position changed
prevDragState.current.y !== y; // Y position changed
if (hasStateChanged) {
// Update the previous drag state with the latest values
prevDragState.current = { isDragging: isDraggingWidgets, x, y };
if (isDraggingWidgets) {
// Adjust the current drag coordinates relative to the initial widgets button position
const getXPosition = props?.signBtnPosition?.[0]
? x - props.signBtnPosition[0].xPos
: x;
const getYPosition = props?.signBtnPosition?.[0]
? y - props.signBtnPosition[0].yPos
: y;
// Get the default width and height of the dragged checkbox/radio widget type
const el = document.getElementById("checkbox&radio-preview");
let rect;
if (el) {
rect = el.getBoundingClientRect();
}
const widget = {
type: itemType?.text,
width: defaultWidthHeight(itemType?.text).width,
height: defaultWidthHeight(itemType?.text).height
};
const getWidth =
rect?.width || props?.posWidth(widget, props?.isSignYourself);
const getHeight =
rect?.height || props?.posHeight(widget, props?.isSignYourself);
// Display the alignment guidelines based on the current drag position and widget size
showGuidelines(true, getXPosition, getYPosition, getWidth, getHeight);
} else {
// Hide guidelines when dragging stops
showGuidelines(false);
}
}
}
}, [isDraggingWidgets, offset, itemType, showGuidelines]);
}
export default DragGuideLinesLayer;
@@ -7,34 +7,37 @@ import { fontColorArr, fontsizeArr } from "../../constant/Utils";
function DropdownWidgetOption(props) {
const { t } = useTranslation();
const [dropdownOptionList, setDropdownOptionList] = useState([
"option-1",
"option-2"
"Option-1",
"Option-2"
]);
const [minCount, setMinCount] = useState(0);
const [maxCount, setMaxCount] = useState(0);
const [dropdownName, setDropdownName] = useState(props.type);
const [dropdownName, setDropdownName] = useState();
const [isReadOnly, setIsReadOnly] = useState(false);
const [isHideLabel, setIsHideLabel] = useState(false);
const [status, setStatus] = useState("required");
const [defaultValue, setDefaultValue] = useState("");
const statusArr = ["required", "optional"];
const [defaultCheckbox, setDefaultCheckbox] = useState([]);
const [layout, setLayout] = useState("vertical");
const statusArr = ["required", "optional"];
const layoutArr = ["vertical", "horizontal"];
const isPrefillExist = props?.roleName === "prefill";
const resetState = () => {
setDropdownOptionList(["option-1", "option-2"]);
setDropdownName(props.type);
setDropdownOptionList(["Option-1", "Option-2"]);
setDropdownName(props.currWidgetsDetails?.options?.name || props.type);
setIsReadOnly(false);
setIsHideLabel(false);
setMinCount(0);
setMaxCount(0);
setDefaultCheckbox([]);
setDefaultValue("");
setLayout("vertical");
};
useEffect(() => {
if (
props.currWidgetsDetails?.options?.name &&
props.currWidgetsDetails?.options?.values
props.currWidgetsDetails?.options?.values?.length > 0
) {
setDropdownName(props.currWidgetsDetails?.options?.name);
setDropdownOptionList(props.currWidgetsDetails?.options?.values);
@@ -49,6 +52,7 @@ function DropdownWidgetOption(props) {
setStatus(props.currWidgetsDetails?.options?.status || "required");
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
setLayout(props.currWidgetsDetails?.options?.layout || "vertical");
} else {
setStatus("required");
resetState();
@@ -84,7 +88,7 @@ function DropdownWidgetOption(props) {
const deleteOption = true;
const addOption = false;
const getUpdatedOptions = dropdownOptionList.filter(
(data, index) => index !== ind
(_, index) => index !== ind
);
setDropdownOptionList(getUpdatedOptions);
props.handleSaveWidgetsOptions(
@@ -99,11 +103,43 @@ function DropdownWidgetOption(props) {
};
const handleSaveOption = () => {
if (["checkbox", radioButtonWidget, "dropdown"].includes(props.type)) {
const allUnique =
new Set(dropdownOptionList).size === dropdownOptionList.length;
if (!allUnique) {
alert("Please remove duplicate option");
return;
}
}
const defaultData =
defaultCheckbox && defaultCheckbox.length > 0
? defaultCheckbox
: defaultValue;
const isDropdownOrRadio =
props?.type === "dropdown" || props?.type === radioButtonWidget;
const readOnlyWithoutValue =
isReadOnly && !defaultValue && status !== "optional";
const isCheckbox = props?.type === "checkbox";
const WidgetLayout = ["checkbox", radioButtonWidget].includes(props.type)
? layout
: null;
// If its a dropdown and its read-only without a value (nor marked optional), stop here.
if (isDropdownOrRadio && readOnlyWithoutValue) {
alert(t("readonly-error", { widgetName: props?.type }));
return;
} else if (
isCheckbox &&
isReadOnly &&
minCount > 0 &&
defaultCheckbox?.length === 0
) {
alert(t("readonly-error", { widgetName: props?.type }));
return;
}
// Otherwise (either not a dropdown, or a valid dropdown), do the save + reset exactly once.
props.handleSaveWidgetsOptions(
dropdownName,
dropdownOptionList,
@@ -114,18 +150,10 @@ function DropdownWidgetOption(props) {
null,
status,
defaultData,
isHideLabel
isHideLabel,
WidgetLayout
);
// props.setShowDropdown(false);
setDropdownOptionList(["option-1", "option-2"]);
setDropdownName(props.type);
// props.setCurrWidgetsDetails({});
setIsReadOnly(false);
setIsHideLabel(false);
setMinCount(0);
setMaxCount(0);
setDefaultCheckbox([]);
setDefaultValue("");
resetState();
};
@@ -137,7 +165,6 @@ function DropdownWidgetOption(props) {
return false;
}
};
return (
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
<div className="h-full p-[15px] text-base-content">
@@ -148,25 +175,25 @@ function DropdownWidgetOption(props) {
}}
>
<div>
<label className="text-[13px] font-semibold">
<label htmlFor="title" className="text-[13px] font-semibold">
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
id="title"
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
defaultValue={dropdownName}
value={dropdownName}
onChange={(e) => setDropdownName(e.target.value)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
required
/>
<label className="text-[13px] font-semibold mt-[5px]">
{t("options")}
</label>
<div className="flex flex-col">
{dropdownOptionList.map((option, index) => (
{dropdownOptionList?.map((option, index) => (
<div
key={index}
className="flex flex-row mb-[5px] items-center"
@@ -210,58 +237,59 @@ function DropdownWidgetOption(props) {
></i>
</div>
))}
<i
onClick={handleAddInput}
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
></i>
<div>
<i
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
aria-label="Add option"
onClick={handleAddInput}
></i>
</div>
</div>
{["dropdown", radioButtonWidget].includes(props.type) && (
<>
<label className="text-[13px] font-semibold mt-[5px]">
{t("default-value")}
</label>
<select
onChange={(e) => setDefaultValue(e.target.value)}
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
name="defaultvalue"
value={defaultValue}
placeholder="select default value"
>
<option value="" disabled hidden className="text-[13px]">
{t("select")}...
</option>
{dropdownOptionList.map((data, ind) => {
return (
<option className="text-[13px]" key={ind} value={data}>
{data}
</option>
);
})}
</select>
</>
)}
{props.type !== "checkbox" && props.type !== radioButtonWidget && (
<>
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="status"
onChange={() => setStatus(data.toLowerCase())}
checked={status.toLowerCase() === data.toLowerCase()}
/>
<div className="text-[13px] font-500">{data}</div>
</div>
);
})}
</div>
</>
{["dropdown", radioButtonWidget].includes(props.type) &&
!isPrefillExist && (
<>
<label className="text-[13px] font-semibold mt-[5px]">
{t("default-value")}
</label>
<select
value={defaultValue}
onChange={(e) => setDefaultValue(e.target.value)}
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
name="defaultvalue"
>
<option value="" disabled hidden className="text-[13px]">
{t("select")}...
</option>
{dropdownOptionList.map((data, ind) => {
return (
<option className="text-[13px]" key={ind} value={data}>
{data}
</option>
);
})}
</select>
</>
)}
{props.type !== "checkbox" && !isPrefillExist && (
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
{statusArr.map((data, ind) => (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="status"
onChange={() => setStatus(data.toLowerCase())}
checked={status.toLowerCase() === data.toLowerCase()}
/>
<div className="text-[13px] font-500 capitalize">
{data}
</div>
</div>
))}
</div>
)}
<div className="flex items-center mt-3 mb-3">
<span>{t("font-size")} :</span>
@@ -282,8 +310,8 @@ function DropdownWidgetOption(props) {
);
})}
</select>
<div className="flex flex-row gap-1 items-center ml-4 ">
<span>{t("color")} : </span>
<div className="flex flex-row gap-1 items-center ml-4">
<span className="capitalize">{t("color")} : </span>
<select
value={
props.fontColor ||
@@ -316,7 +344,7 @@ function DropdownWidgetOption(props) {
props.type
) && (
<div className="flex flex-row gap-5 my-2 items-center text-center">
{props.isShowAdvanceFeature && (
{props.isShowAdvanceFeature && !isPrefillExist && (
<div className="flex items-center">
<input
id="isreadonly"
@@ -325,7 +353,10 @@ function DropdownWidgetOption(props) {
className="op-checkbox op-checkbox-sm"
onChange={(e) => setIsReadOnly(e.target.checked)}
/>
<label className="ml-1 mb-0" htmlFor="isreadonly">
<label
className="ml-2 mb-0 capitalize"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
@@ -340,15 +371,45 @@ function DropdownWidgetOption(props) {
onChange={(e) => setIsHideLabel(e.target.checked)}
/>
<label className="ml-1 mb-0" htmlFor="ishidelabel">
<label
className="ml-2 mb-0 capitalize"
htmlFor="ishidelabel"
>
{t("hide-labels")}
</label>
</div>
)}
</div>
)}
{["checkbox", radioButtonWidget].includes(props.type) && (
<>
<div className="text-[13px] font-semibold mt-[5px] capitalize">
{t("layout")}
</div>
<div
className={`${props.type === "checkbox" ? "mb-[10px]" : ""} flex flex-row gap-[10px] mt-[0.5rem]`}
>
{layoutArr.map((data, ind) => (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="layout"
checked={layout.toLowerCase() === data.toLowerCase()}
onChange={() => setLayout(data.toLowerCase())}
/>
<label className="text-[13px] font-500 mb-0">
{t(data)}
</label>
</div>
))}
</div>
</>
)}
</div>
<div
className={`${
props.type === "checkbox" && props.isShowAdvanceFeature
@@ -367,7 +428,7 @@ function DropdownWidgetOption(props) {
{props.currWidgetsDetails?.options?.values?.length > 0 && (
<button
type="submit"
className="op-btn op-btn-ghost ml-1"
className="op-btn op-btn-ghost text-base-content ml-1"
onClick={() => {
props.handleClose && props.handleClose();
resetState();
@@ -1,324 +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 { 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 = {};
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")}</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,595 @@
import {
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,
isAddYourSelfCheckbox,
}) => {
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 pensList = ["blue", "red", "black"];
const [selectedColors, setSelectedColors] = useState(
template?.PenColors || pensList
);
const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [uploadPdf, setUploadPdf] = useState({
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(t("only-pdf-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);
const userId = template?.ExtUserPtr?.UserId?.objectId;
SaveFileSize(buffer.length, pdfUrl, tenantId, userId);
}
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,
PenColors: selectedColors,
...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();
};
const handleColorsChange = (color) => {
setSelectedColors((prev) => {
// If user tries to uncheck the last remaining color → block it
if (prev.length === 1 && prev.includes(color)) {
return prev;
}
// Normal toggle behavior
return prev.includes(color)
? prev.filter((c) => c !== color) // remove
: [...prev, color]; // add
});
};
return (
<ModalUi
isOpen
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"
>
{t("browse-or-drag-to-replace-existing-file")}
</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 mb-4">
<label htmlFor="penColors">
{t("pen-colors")}
<a data-tooltip-id="pen-colors-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<Tooltip id="pen-colors-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("pen-colors")}</p>
<div>{t("pen-colors-help")}</div>
</div>
</Tooltip>
</label>
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{pensList.map((color) => (
<div
key={color}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
name="penColors"
checked={selectedColors.includes(color)}
onChange={() => handleColorsChange(color)}
/>
<div className="hover:underline underline-offset-2 cursor-default capitalize">
{color}
</div>
</div>
))}
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
helpText={t("bcc-help")}
isCaptureAllData
isAddYourSelfCheckbox={isAddYourSelfCheckbox}
/>
</div>
<div className="text-xs mt-2">
<label className="block">{t("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,5 +1,10 @@
import React from "react";
import { Quill } from "react-quill-new";
// Use a third-party plugin so editors can toggle raw HTML view.
import htmlEditButton from "quill-html-edit-button";
// Register the module which adds a "<>" button for HTML editing.
Quill.register("modules/htmlEditButton", htmlEditButton);
// Custom Undo button icon component for Quill editor. You can import it directly
// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
@@ -56,7 +61,9 @@ export const module1 = {
container: "#toolbar1",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Modules object for setting up the Quill editor
@@ -65,7 +72,9 @@ export const module2 = {
container: "#toolbar2",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Formats objects for setting up the Quill editor
@@ -18,7 +18,13 @@ export function EmailBody(props) {
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
value={props.requestSubject}
onChange={(e) => props.setRequestSubject(e.target.value)}
onChange={(e) => {
props?.setRequestSubject(e.target.value);
props?.setCustomizeMail((prev) => ({
...prev,
subject: e.target.value
}));
}}
placeholder='${senderName} has requested you to sign "${documentName}"'
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
@@ -1,20 +1,21 @@
import React, { useState } from "react";
import axios from "axios";
import { handleToPrint } from "../../constant/Utils";
import { themeColor, 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,
pdfUrl,
setIsEmail,
setSuccessEmail,
pdfDetails,
sender,
setIsAlert,
extUserId,
setIsDownloadModal
}) {
const { t } = useTranslation();
@@ -24,81 +25,13 @@ 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 () => {
const pdfName = pdfDetails[0]?.Name;
setIsLoading(true);
let sendMail;
const docId = pdfDetails?.[0]?.objectId || "";
let presignedUrl = pdfUrl;
try {
const axiosRes = await axios.post(
`${localStorage.getItem("baseUrl")}/functions/getsignedurl`,
{
url: pdfUrl,
docId: docId,
},
{
headers: {
"content-type": "Application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
);
presignedUrl = axiosRes.data.result;
} catch (err) {
console.log("err in getsignedurl", err);
}
for (let i = 0; i < emailList.length; i++) {
try {
const imgPng =
"https://qikinnovation.ams3.digitaloceanspaces.com/logo.png";
let url = `${localStorage.getItem("baseUrl")}functions/sendmailv3`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessionToken: localStorage.getItem("accesstoken")
};
const openSignUrl = "https://www.opensignlabs.com/contact-us";
const themeBGcolor = themeColor;
let params = {
extUserId: extUserId,
pdfName: pdfName,
url: presignedUrl,
recipient: emailList[i],
subject: `${sender.name} has signed the doc - ${pdfName}`,
replyto:
pdfDetails?.[0]?.ExtUserPtr?.Email ||
"",
from:
sender.email,
html:
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body> <div style='background-color:#f5f5f5;padding:20px'> <div style='box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;background-color:white;'> <div><img src=" +
imgPng +
" height='50' style='padding:20px,width:170px,height:40px'/> </div><div style='padding:2px;font-family:system-ui; background-color:" +
themeBGcolor +
";'> <p style='font-size:20px;font-weight:400;color:white;padding-left:20px',> Document Copy</p></div><div><p style='padding:20px;font-family:system-ui;font-size:14px'>A copy of the document <strong>" +
pdfName +
" </strong>is attached to this email. Kindly download the document from the attachment.</p></div> </div><div><p>This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender " +
sender.email +
" directly. If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href= " +
openSignUrl +
" target=_blank>here</a> </p></div></div></body></html>"
};
sendMail = await axios.post(url, params, { headers: headers });
} catch (error) {
console.log("error", error);
setIsLoading(false);
setIsEmail(false);
setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
}
}
if (sendMail?.data?.result?.status === "success") {
const params = { docId: pdfDetails?.[0]?.objectId, recipients: emailList };
const sendmail = await Parse.Cloud.run("forwarddoc", params);
if (sendmail?.status === "success") {
setSuccessEmail(true);
setIsEmail(false);
setTimeout(() => {
@@ -107,7 +40,8 @@ function EmailComponent({
setEmailList([]);
}, 1500);
setIsLoading(false);
} else {
}
else {
setIsLoading(false);
setIsEmail(false);
setIsAlert({
@@ -177,7 +111,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">
@@ -186,14 +120,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);
@@ -209,12 +143,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]">
@@ -234,7 +168,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")}
@@ -251,7 +185,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] text-base-content rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
onChange={handleEmailValue}
onKeyDown={handleEnterPress}
placeholder={t("enter-email-plaholder")}
@@ -269,34 +203,22 @@ function EmailComponent({
{t("email-error-1")}
</p>
)}
<button
className={`${
emailValue ? "cursor-pointer" : "cursor-default"
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
onClick={() => emailValue && handleEnterPress("add")}
>
<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 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 text-base-content ml-2"
onClick={() => handleClose()}
>
{t("close")}
</button>
</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>
</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;
@@ -0,0 +1,30 @@
const Guidelines = ({ x1, x2, y1, y2 }) => {
return (
<>
{/* Horizontal guidelines */}
{/* top guide */}{" "}
<div
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: y1 }}
/>
{/* bottom guide */}
<div
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: y2 }}
/>
{/* Vertical guidelines */}
{/* left guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: x1 }}
/>
{/* right guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: x2 }}
/>
</>
);
};
export default Guidelines;
@@ -0,0 +1,94 @@
import React, { useEffect, useState, useRef } from "react";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
export default function PageReorderModal({
isOpen,
handleClose,
totalPages = 0,
onSave
}) {
const { t } = useTranslation();
const [order, setOrder] = useState([]);
// Keeps track of the page order relative to the original PDF
const orderRef = useRef([]);
// Captures the order when the modal opens
const initialOrderRef = useRef([]);
// Initialize orderRef when total pages change (e.g. after upload)
useEffect(() => {
if (orderRef.current.length !== totalPages) {
orderRef.current = Array.from({ length: totalPages }, (_, i) => i + 1);
}
}, [totalPages]);
// When modal opens, display the last saved order and store it as initial
useEffect(() => {
if (isOpen) {
setOrder(orderRef.current);
initialOrderRef.current = [...orderRef.current];
}
}, [isOpen]);
const move = (index, dir) => {
const swapIndex = index + dir;
if (swapIndex < 0 || swapIndex >= order.length) return;
const newOrder = [...order];
[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]];
setOrder(newOrder);
};
const handleSave = () => {
const saveOrder = order.map((num) =>
initialOrderRef.current.indexOf(num) + 1
);
// Persist the new display order for next time
orderRef.current = [...order];
onSave && onSave(saveOrder);
};
const isUnchanged =
order.length === initialOrderRef.current.length &&
order.every((n, i) => n === initialOrderRef.current[i]);
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("reorder-pages")}>
<div className="p-[20px] flex flex-col gap-2 text-base-content">
{order.map((num, i) => (
<div key={num} className="flex items-center justify-between">
<span>
{t("page")} {num}
</span>
<div className="flex gap-1">
<button
className="op-btn op-btn-xs op-btn-ghost text-base-content"
disabled={i === 0}
onClick={() => move(i, -1)}
>
<i className="fa-light fa-arrow-up"></i>
</button>
<button
className="op-btn op-btn-xs op-btn-ghost text-base-content"
disabled={i === order.length - 1}
onClick={() => move(i, 1)}
>
<i className="fa-light fa-arrow-down"></i>
</button>
</div>
</div>
))}
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button onClick={handleSave} type="button" className="op-btn op-btn-primary" disabled={isUnchanged}>
{t("save")}
</button>
<button
onClick={handleClose}
type="button"
className="op-btn op-btn-ghost text-base-content ml-1"
>
{t("close")}
</button>
</div>
</ModalUi>
);
}
@@ -2,33 +2,41 @@ import React, { useRef, useState } from "react";
import PrevNext from "./PrevNext";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleDownloadCertificate,
handleDownloadPdf,
handleRemoveWidgets,
handleToPrint
handleToPrint,
reorderPdfPages
} from "../../constant/Utils";
import "../../styles/signature.css";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { DropdownMenu } from "radix-ui";
import ModalUi from "../../primitives/ModalUi";
import Loader from "../../primitives/Loader";
import PageReorderModal from "./PageReorderModal";
import { useTranslation } from "react-i18next";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
function Header(props) {
const { t } = useTranslation();
const filterPrefill =
props?.signerPos &&
props?.signerPos?.filter((data) => data.Role !== "prefill");
const isMobile = window.innerWidth < 767;
const [isDownloading, setIsDownloading] = useState("");
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const mergePdfInputRef = useRef(null);
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,18 +58,69 @@ 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) {
alert("Please upload a valid PDF file.");
alert(t("please-select-pdf"));
return;
}
if (!file.type.includes("pdf")) {
alert("Only PDF files are allowed.");
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
}
} else {
alert(t("provide-password"));
}
} else {
console.log("Err ", err);
alert(t("error-uploading-pdf"));
}
}
} else {
alert(t("error-uploading-pdf"));
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -78,6 +137,13 @@ function Header(props) {
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
@@ -88,12 +154,43 @@ function Header(props) {
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
const handleDownloadDoc = async () => {
await handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
);
};
const handleDownloadBtn = async () => {
if (
props?.isCompleted
) {
props?.setIsDownloadModal(true);
} else {
await handleDownloadDoc();
}
};
return (
<div className="flex py-[5px]">
{isMobile && props?.isShowHeader ? (
<div
id="navbar"
className="stickyHead"
className="stickyHead touch-none"
style={{
width: window.innerWidth + "px"
}}
@@ -127,17 +224,7 @@ function Header(props) {
>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => {
if (props?.isCompleted) {
props?.setIsDownloadModal(true);
} else {
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
);
}
}}
onClick={() => handleDownloadBtn()}
>
<div className="flex flex-row">
<i
@@ -147,25 +234,27 @@ function Header(props) {
{t("download")}
</div>
</DropdownMenu.Item>
{props?.isCompleted && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
>
<div className="border-none bg-[#fff]">
<i
className="fa-light fa-award mr-[3px]"
aria-hidden="true"
></i>
{t("certificate")}
</div>
</DropdownMenu.Item>
)}
{
props?.isCompleted && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
>
<div className="border-none bg-[#fff]">
<i
className="fa-light fa-award mr-[3px]"
aria-hidden="true"
></i>
{t("certificate")}
</div>
</DropdownMenu.Item>
)
}
{props?.isSignYourself && (
<DropdownMenu.Item
className="DropdownMenuItem"
@@ -216,7 +305,7 @@ function Header(props) {
<div
onClick={() => {
if (!props?.isMailSend) {
props?.alertSendEmail();
props?.handleSaveDoc();
}
}}
className={`${
@@ -258,15 +347,23 @@ 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={() =>
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
)
}
onClick={() => handleDownloadDoc()}
>
<div className="flex flex-row">
<i
@@ -302,6 +399,17 @@ function Header(props) {
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsReorderModal(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("reorder-pages")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
@@ -375,42 +483,21 @@ function Header(props) {
/>
{props?.isPlaceholder ? (
<>
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none">
{!props?.isMailSend &&
props?.signersdata.length > 0 &&
props?.signersdata.length !== filterPrefill.length && (
<div>
{filterPrefill.length === 0 ? (
<span className="text-[13px] text-[#f5405e]">
{t("add")}{" "}
{props?.signersdata.length - filterPrefill.length}{" "}
{t("recipients")} {t("widgets-name.signature")}
</span>
) : (
<span className="text-[13px] text-[#f5405e]">
{t("add")}{" "}
{props?.signersdata.length - filterPrefill.length}{" "}
{t("more")}
{t("recipients")} {t("widgets-name.signature")}
</span>
)}
</div>
)}
</div>
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none"></div>
<div className="flex">
{props?.setIsEditTemplate && (
<button
onClick={() => props?.setIsEditTemplate(true)}
className="outline-none border-none text-center mr-[3px]"
>
<i className="fa-light fa-gear fa-lg"></i>
<i className="fa-light fa-gear fa-lg text-base-content"></i>
</button>
)}
{enabledBackBtn && (
<button
onClick={() => window.history.go(-2)}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
>
{t("back")}
</button>
@@ -419,7 +506,7 @@ function Header(props) {
disabled={props?.isMailSend && true}
data-tut="headerArea"
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
onClick={() => props?.alertSendEmail()}
onClick={() => props?.handleSaveDoc()}
>
{props?.completeBtnTitle
? props?.completeBtnTitle
@@ -445,38 +532,32 @@ function Header(props) {
></i>
<span className="hidden lg:block">{t("print")}</span>
</button>
{props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
>
<i
className="fa-light fa-award py-[3px]"
aria-hidden="true"
></i>
<span className="hidden lg:block">{t("certificate")}</span>
</button>
)}
{
props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
>
<i
className="fa-light fa-award py-[3px]"
aria-hidden="true"
></i>
<span className="hidden lg:block">
{t("certificate")}
</span>
</button>
)
}
<button
type="button"
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
onClick={() => {
if (props?.isCompleted) {
props?.setIsDownloadModal(true);
} else {
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
);
}
}}
onClick={() => handleDownloadBtn()}
>
<i
className="fa-light fa-download py-[3px]"
@@ -487,29 +568,13 @@ 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 && (
<button
onClick={() =>
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
)
}
onClick={() => handleDownloadDoc()}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
>
<span className="hidden lg:block">{t("download")}</span>
</button>
@@ -525,14 +590,8 @@ function Header(props) {
{!props?.templateId && (
<button
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
onClick={() =>
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
)
}
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
onClick={() => handleDownloadDoc()}
>
<i className="fa-light fa-arrow-down font-semibold lg:hidden"></i>
<span className="hidden lg:block">{t("download")}</span>
@@ -551,23 +610,25 @@ function Header(props) {
)
) : props?.isCompleted ? (
<div className="flex flex-row">
{props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
>
<i className="fa-light fa-award" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">
{t("certificate")}
</span>
</button>
)}
{
props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
>
<i className="fa-light fa-award" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">
{t("certificate")}
</span>
</button>
)
}
<button
onClick={(e) =>
handleToPrint(e, setIsDownloading, props?.pdfDetails)
@@ -581,7 +642,8 @@ function Header(props) {
<button
type="button"
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
onClick={() => props?.setIsDownloadModal(true)}
// onClick={() => props?.setIsDownloadModal(true)}
onClick={() => handleDownloadBtn()}
>
<i className="fa-light fa-download" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">{t("download")}</span>
@@ -610,7 +672,7 @@ function Header(props) {
<button
onClick={() => window.history.go(-2)}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
>
{t("back")}
</button>
@@ -655,8 +717,8 @@ function Header(props) {
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p className="font-medium">{t("delete-alert-2")}</p>
<p className="pt-3">{t("delete-note")}</p>
<p className="font-medium text-base-content">{t("delete-alert-2")}</p>
<p className="pt-3 text-base-content">{t("delete-note")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
@@ -668,12 +730,18 @@ function Header(props) {
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost"
className="op-btn op-btn-ghost text-base-content"
>
{t("no")}
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</div>
);
}
@@ -0,0 +1,298 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleRemoveWidgets,
reorderPdfPages
} from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
import PageReorderModal from "./PageReorderModal";
function PdfTools(props) {
const { t } = useTranslation();
const mergePdfInputRef = useRef(null);
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const handleDetelePage = async () => {
props.setIsUploadPdf && props.setIsUploadPdf(true);
try {
const pdfupdatedData = await deletePdfPage(
props.pdfArrayBuffer,
props.pageNumber
);
if (pdfupdatedData?.totalPages === 1) {
alert(t("delete-alert"));
} else {
props.setPdfBase64Url(pdfupdatedData.base64);
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
setIsDeletePage(false);
handleRemoveWidgets(
props.setSignerPos,
props.signerPos,
props.pageNumber
);
props.setAllPages(pdfupdatedData.remainingPages || 1);
if (props.allPages === props.pageNumber) {
props.setPageNumber(props.pageNumber - 1);
} else if (props.allPages > 2) {
props.setPageNumber(props.pageNumber);
}
}
} catch (e) {
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) => {
props.setIsTour && props.setIsTour(false);
const file = e.target.files[0];
if (!file) {
alert(t("please-select-pdf"));
return;
}
if (!file.type.includes("pdf")) {
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
try {
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
}
} else {
alert(t("provide-password"));
}
} else {
console.log("Err ", err);
alert(t("error-uploading-pdf"));
}
}
} else {
alert(t("error-uploading-pdf"));
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
const basePdfDoc = await PDFDocument.load(props.pdfArrayBuffer);
// Copy pages from the uploaded PDF to the base PDF
const uploadedPdfPages = await basePdfDoc.copyPages(
uploadedPdfDoc,
uploadedPdfDoc.getPageIndices()
);
uploadedPdfPages.forEach((page) => basePdfDoc.addPage(page));
// Save the updated PDF
const pdfBase64 = await basePdfDoc.saveAsBase64({
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
mergePdfInputRef.current.value = "";
} catch (error) {
mergePdfInputRef.current.value = "";
console.error("Error merging PDF:", error);
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
const handleDeletePage = () => {
setIsDeletePage(true);
props.setIsTour && props.setIsTour(false);
};
const handleReorderPages = () => {
setIsReorderModal(true);
props.setIsTour && props.setIsTour(false);
};
const handleZoomIn = () => {
props.clickOnZoomIn();
props.setIsTour && props.setIsTour(false);
};
const handleZoomOut = () => {
props.clickOnZoomOut();
props.setIsTour && props.setIsTour(false);
};
const handleRotate = () => {
props.handleRotationFun(90);
props.setIsTour && props.setIsTour(false);
};
const handleAntiRotate = () => {
props.handleRotationFun(-90);
props.setIsTour && props.setIsTour(false);
};
return (
<>
<span
data-tut="pdftools"
className="hidden h-max md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]"
>
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => mergePdfInputRef.current.click()}
title={t("add-pages")}
>
<input
type="file"
className="hidden"
accept="application/pdf"
ref={mergePdfInputRef}
onChange={handleFileUpload}
/>
<i className="fa-light fa-plus text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleDeletePage}
title={t("delete-page")}
>
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleReorderPages}
title={t("reorder-pages")}
>
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleZoomIn}
title={t("zoom-in")}
>
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[25px]"></i>
</span>
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleRotate}
title={t("rotate-right")}
>
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
title={t("rotate-left")}
onClick={handleAntiRotate}
>
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleZoomOut}
title={t("zoom-out")}
>
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px]"></i>
</span>
</span>
<ModalUi
isOpen={isDeletePage}
title={t("delete-page")}
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p className="font-medium text-base-content">{t("delete-alert-2")}</p>
<p className="pt-3 text-base-content">{t("delete-note")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
type="button"
className="op-btn op-btn-primary"
>
{t("yes")}
</button>
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost text-base-content ml-1"
>
{t("no")}
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</>
);
}
export default PdfTools;
-177
View File
@@ -1,177 +0,0 @@
import React, { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
base64ToArrayBuffer,
deletePdfPage,
handleRemoveWidgets
} from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
function PdfZoom(props) {
const { t } = useTranslation();
const mergePdfInputRef = useRef(null);
const [isDeletePage, setIsDeletePage] = useState(false);
const handleDetelePage = async () => {
props.setIsUploadPdf && props.setIsUploadPdf(true);
try {
const pdfupdatedData = await deletePdfPage(
props.pdfArrayBuffer,
props.pageNumber
);
if (pdfupdatedData?.totalPages === 1) {
alert(t("delete-alert"));
} else {
props.setPdfBase64Url(pdfupdatedData.base64);
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
setIsDeletePage(false);
handleRemoveWidgets(
props.setSignerPos,
props.signerPos,
props.pageNumber
);
props.setAllPages(pdfupdatedData.remainingPages || 1);
if (props.allPages === props.pageNumber) {
props.setPageNumber(props.pageNumber - 1);
} else if (props.allPages > 2) {
props.setPageNumber(props.pageNumber);
}
}
} catch (e) {
console.log("error in delete pdf page", e);
}
};
const handleFileUpload = async (e) => {
const file = e.target.files[0];
if (!file) {
alert("Please upload a valid PDF file.");
return;
}
if (!file.type.includes("pdf")) {
alert("Only PDF files are allowed.");
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
const basePdfDoc = await PDFDocument.load(props.pdfArrayBuffer);
// Copy pages from the uploaded PDF to the base PDF
const uploadedPdfPages = await basePdfDoc.copyPages(
uploadedPdfDoc,
uploadedPdfDoc.getPageIndices()
);
uploadedPdfPages.forEach((page) => basePdfDoc.addPage(page));
// Save the updated PDF
const pdfBase64 = await basePdfDoc.saveAsBase64({
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
mergePdfInputRef.current.value = "";
} catch (error) {
mergePdfInputRef.current.value = "";
console.error("Error merging PDF:", error);
}
};
return (
<>
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]">
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => mergePdfInputRef.current.click()}
title={t("add-pages")}
>
<input
type="file"
className="hidden"
accept="application/pdf"
ref={mergePdfInputRef}
onChange={handleFileUpload}
/>
<i className="fa-light fa-plus text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => setIsDeletePage(true)}
title={t("delete-page")}
>
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.clickOnZoomIn()}
title={t("zoom-in")}
>
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[25px]"></i>
</span>
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.handleRotationFun(90)}
title={t("rotate-right")}
>
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
title={t("rotate-left")}
onClick={() => props.handleRotationFun(-90)}
>
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px]"
onClick={() => props.clickOnZoomOut()}
style={{
cursor: props.zoomPercent > 0 ? "pointer" : "default"
}}
title={t("zoom-out")}
>
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px]"></i>
</span>
</span>
<ModalUi
isOpen={isDeletePage}
title={t("delete-page")}
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p className="font-medium">{t("delete-alert-2")}</p>
<p className="pt-3">{t("delete-note")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
type="button"
className="op-btn op-btn-primary"
>
{t("yes")}
</button>
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost ml-1"
>
{t("no")}
</button>
</div>
</ModalUi>
</>
);
}
export default PdfZoom;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,60 +0,0 @@
import React from "react";
import { themeColor } from "../../constant/const";
import {
defaultWidthHeight,
isMobile,
radioButtonWidget,
resizeBorderExtraWidth,
textWidget
} from "../../constant/Utils";
function PlaceholderBorder(props) {
const getResizeBorderExtraWidth = resizeBorderExtraWidth();
const defaultWidth = defaultWidthHeight(props.pos.type).width;
const defaultHeight = defaultWidthHeight(props.pos.type).height;
const width = () => {
const getWidth =
props.placeholderBorder.w || props.pos.Width || defaultWidth;
return (
getWidth * props.scale * props.containerScale + getResizeBorderExtraWidth
);
};
const height = () => {
const getHeight =
props.placeholderBorder.h || props.pos.Height || defaultHeight;
return (
getHeight * props.scale * props.containerScale + getResizeBorderExtraWidth
);
};
const handleMinWidth = () => {
if (props.pos.type === "checkbox" || props.pos.type === radioButtonWidget) {
return props.getCheckboxRenderWidth.width + getResizeBorderExtraWidth;
} else {
return width();
}
};
const handleMinHeight = () => {
if (props.pos.type === "checkbox" || props.pos.type === radioButtonWidget) {
return props.getCheckboxRenderWidth.height + getResizeBorderExtraWidth;
} else {
return height();
}
};
return (
<div
onMouseEnter={() => !isMobile && props?.setDraggingEnabled(false)}
onTouchEnd={() =>
props.pos.type === textWidget && props?.setDraggingEnabled(false)
}
className="absolute inline-block w-[14px] h-[14px] border-[0.2px] overflow-hidden border-dashed"
style={{
borderColor: themeColor,
minWidth: handleMinWidth() || 0,
minHeight: handleMinHeight() || 0
}}
></div>
);
}
export default PlaceholderBorder;
@@ -1,14 +1,13 @@
import React, { useState } from "react";
import { useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import {
handleCopyNextToWidget,
randomId,
textWidget
} from "../../constant/Utils";
import { handleCopyNextToWidget, randomId } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { setPrefillImg } from "../../redux/reducers/widgetSlice";
import { useDispatch } from "react-redux";
function PlaceholderCopy(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const copyType = [
{ id: 1, type: "All pages" },
{ id: 2, type: "All pages but last" },
@@ -125,14 +124,19 @@ function PlaceholderCopy(props) {
);
for (let i = 0; i < props.allPages; i++) {
const newId = randomId();
const newPlaceholder = { ...currentPlaceholder, key: newId };
const nameId = randomId(2);
const widgetName = `${currentPlaceholder?.options?.name}${nameId}`;
const newPlaceholder = {
...currentPlaceholder,
key: newId,
options: { ...currentPlaceholder?.options, name: widgetName }
};
//get exist placeholder position for particular page
const existPlaceholder = filterSignerPosition[0].placeHolder.filter(
(data) => data.pageNumber === newPageNumber
);
const existPlaceholderPosition =
existPlaceholder[0] && existPlaceholder[0].pos;
//function for get copy to requested location of placeholder position
const getPlaceholderObj = getCopyPlaceholderPosition(
type,
@@ -144,6 +148,18 @@ function PlaceholderCopy(props) {
newPlaceholderPosition.push(getPlaceholderObj);
}
newPageNumber++;
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentPlaceholder?.options?.response &&
currentPlaceholder?.type === "image"
) {
dispatch(
setPrefillImg({
id: newId,
base64: currentPlaceholder?.options?.response
})
);
}
}
let updatedSignerPlaceholder;
if (props?.signerObjId) {
@@ -213,6 +229,7 @@ function PlaceholderCopy(props) {
//function for getting selected type placeholder copy
const handleApplyCopy = () => {
const newId = randomId();
if (selectCopyType === 4) {
const signerPosition = props.xyPosition;
let currentXYposition;
@@ -230,13 +247,25 @@ function PlaceholderCopy(props) {
);
//function to create new widget next to just widget
handleCopyNextToWidget(
newId,
currentXYposition,
props.widgetType,
props.xyPosition,
props.pageNumber,
props.setXyPosition,
props?.Id
);
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentXYposition?.options?.response &&
currentXYposition?.type === "image"
) {
dispatch(
setPrefillImg({
id: newId,
base64: currentXYposition?.options?.response
})
);
}
} else {
const getIndex = props?.xyPosition.findIndex(
(data) => data.pageNumber === props.pageNumber
@@ -248,8 +277,8 @@ function PlaceholderCopy(props) {
);
//function to create new widget next to just widget
handleCopyNextToWidget(
newId,
currentXYposition,
props.widgetType,
props.xyPosition,
getIndex,
props.setXyPosition
@@ -260,11 +289,6 @@ function PlaceholderCopy(props) {
}
};
const handleUniqueId = () => {
const signerId = props.signerObjId ? props.signerObjId : props.Id;
if (signerId && props.widgetType === textWidget && props.setTempSignerId) {
props.setUniqueId(props?.tempSignerId);
props.setTempSignerId("");
}
props.setIsPageCopy(false);
setSelectCopyType(1);
};
@@ -306,7 +330,7 @@ function PlaceholderCopy(props) {
</button>
<button
type="button"
className="op-btn op-btn-ghost ml-2"
className="op-btn op-btn-ghost text-base-content ml-2"
onClick={() => handleUniqueId()}
>
{t("cancel")}
@@ -1,864 +0,0 @@
import React, { useEffect, useState, forwardRef, useRef } from "react";
import {
getMonth,
getYear,
onChangeHeightOfTextArea,
onChangeInput,
radioButtonWidget,
range,
textInputWidget,
textWidget
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "../../styles/signature.css";
import RegexParser from "regex-parser";
import { emailRegex } from "../../constant/const";
import { useTranslation } from "react-i18next";
const textWidgetCls =
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] rounded-[2px] border-[1px] border-[#007bff] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap bg-white";
const selectWidgetCls =
"w-full h-full absolute left-0 top-0 border-[1px] border-[#007bff] rounded-[2px] focus:outline-none text-base-content";
const widgetCls =
"select-none-cls overflow-hidden w-full h-full text-black flex flex-col justify-center items-center";
function PlaceholderType(props) {
const { t } = useTranslation();
const type = props?.pos?.type;
const widgetTypeTraslation = t(`widgets-name.${props?.pos?.type}`);
const [selectOption, setSelectOption] = useState("");
const [validatePlaceholder, setValidatePlaceholder] = useState("");
const inputRef = useRef(null);
const [textValue, setTextValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const years = range(1950, getYear(new Date()) + 16, 1);
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
const textWidgetStyle = {
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif",
overflow: "hidden",
textAlign: "start",
width: "100%",
display: "flex",
height: "100%"
};
const validateExpression = (regexValidation) => {
if (textValue && regexValidation) {
let regexObject = regexValidation;
if (props.pos?.options?.validation?.type === "regex") {
regexObject = RegexParser(regexValidation);
}
// new RegExp(regexValidation);
let isValidate = regexObject.test(textValue);
if (!isValidate) {
props?.setValidateAlert(true);
inputRef.current.focus();
}
}
};
const handleInputBlur = () => {
const validateType = props.pos?.options?.validation?.type;
let regexValidation;
if (validateType && validateType !== "text") {
switch (validateType) {
case "email":
regexValidation = emailRegex;
validateExpression(regexValidation);
break;
case "number":
regexValidation = /^[0-9\s]*$/;
validateExpression(regexValidation);
break;
default:
regexValidation = props.pos?.options?.validation?.pattern || "";
validateExpression(regexValidation);
}
}
};
const handleTextValid = (e) => {
const textInput = e.target.value;
setTextValue(textInput);
};
function checkRegularExpress(validateType) {
switch (validateType) {
case "email":
setValidatePlaceholder("demo@gmail.com");
break;
case "number":
setValidatePlaceholder("12345");
break;
case "text":
setValidatePlaceholder("please enter text");
break;
default:
setValidatePlaceholder("please enter value");
}
}
useEffect(() => {
if (type && type === "checkbox" && props.isNeedSign) {
const isDefaultValue = props.pos.options?.defaultValue;
if (isDefaultValue) {
setSelectedCheckbox(isDefaultValue);
}
} else if (props.pos?.options?.hint) {
setValidatePlaceholder(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type);
}
setTextValue(
props.pos?.options?.response
? props.pos?.options?.response
: props.pos?.options?.defaultValue
? props.pos?.options?.defaultValue
: ""
);
setSelectOption(
props.pos?.options?.response
? props.pos?.options?.response
: props.pos?.options?.defaultValue
? props.pos?.options?.defaultValue
: ""
);
// 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);
}
} 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={{
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
className={`${selectWidgetCls} overflow-hidden`}
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
useEffect(() => {
if (
["name", "email", "job title", "company"].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
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type]);
//function for show checked checkbox
const selectCheckbox = (ind) => {
const res = props.pos.options?.response;
const defaultCheck = props.pos.options?.defaultValue;
if (res && res?.length > 0) {
const isSelectIndex = res.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
// }
} else if (defaultCheck) {
const isSelectIndex = defaultCheck.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
} else {
return false;
}
};
const handleRadioCheck = (data) => {
const defaultData = props.pos.options?.defaultValue;
if (textValue === data) {
return true;
} else if (defaultData === data) {
return true;
} else {
return false;
}
};
//function for set checked and unchecked value of checkbox
const handleCheckboxValue = (isChecked, ind) => {
let updateSelectedCheckbox = [],
checkedList;
let isDefaultValue, isDefaultEmpty;
if (type === "checkbox") {
updateSelectedCheckbox = selectedCheckbox ? selectedCheckbox : [];
if (isChecked) {
updateSelectedCheckbox.push(ind);
setSelectedCheckbox(updateSelectedCheckbox);
} else {
checkedList = selectedCheckbox.filter((data) => data !== ind);
setSelectedCheckbox(checkedList);
}
if (props.isNeedSign) {
isDefaultValue = props.pos.options?.defaultValue;
}
if (isDefaultValue && isDefaultValue.length > 0) {
isDefaultEmpty = true;
}
onChangeInput(
checkedList ? checkedList : updateSelectedCheckbox,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
null,
isDefaultEmpty
);
}
};
//function to handle select radio widget and set value seletced by user
const handleCheckRadio = (isChecked, data) => {
let isDefaultValue,
isDefaultEmpty,
isRadio = true;
if (props.isNeedSign) {
isDefaultValue = props.pos.options?.defaultValue;
}
if (isDefaultValue) {
isDefaultEmpty = true;
}
if (isChecked) {
setTextValue(data);
} else {
setTextValue("");
}
onChangeInput(
data,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
null,
isDefaultEmpty,
isRadio
);
};
//function to set onchange date
const handleOnDateChange = (date) => {
props.setStartDate(date);
};
//handle height on enter press in text area
const handleEnterPress = (e) => {
const height = 18;
if (e.key === "Enter") {
//function to save height of text area
onChangeHeightOfTextArea(
height,
props.pos.type,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id
);
}
};
switch (type) {
case "signature":
return props.pos.SignUrl ? (
<img
alt="signature"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls "
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "stamp":
return props.pos.SignUrl ? (
<img
alt="stamp"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls"
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "checkbox":
return (
<div style={{ zIndex: props.isSignYourself && "99" }}>
{props.pos.options?.values?.map((data, ind) => {
return (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`checkbox-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize }}
className={`${
ind === 0 ? "mt-0" : "mt-[5px]"
} flex justify-center op-checkbox rounded-[1px] `}
onBlur={handleInputBlur}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
type="checkbox"
checked={selectCheckbox(ind)}
onChange={(e) => {
if (e.target.checked) {
if (!props.isPlaceholder) {
const maxRequired =
props.pos.options?.validation?.maxRequiredCount;
const maxCountInt =
maxRequired && parseInt(maxRequired);
if (maxCountInt > 0) {
if (
selectedCheckbox &&
selectedCheckbox?.length <= maxCountInt - 1
) {
handleCheckboxValue(e.target.checked, ind);
}
} else {
handleCheckboxValue(e.target.checked, ind);
}
}
} else {
handleCheckboxValue(e.target.checked, ind);
}
}}
/>
{!props.pos.options?.isHideLabel && (
<label
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 text-center"
>
{data}
</label>
)}
</div>
);
})}
</div>
);
case textInputWidget:
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={validatePlaceholder || t("widgets-name.text")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
setTextValue(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={`${
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
? " bg-black/68 select-none "
: "" + textWidgetCls
}`}
style={{ fontSize: fontSize, color: fontColor }}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{textValue || widgetTypeTraslation}</span>
</div>
);
case "dropdown":
return props.data?.signerObjId === props.signerObjId ? (
<select
style={{ fontSize: fontSize, color: fontColor }}
className={`${
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
? " disabled:bg-inherit select-none "
: "" + `${selectWidgetCls} text-[12px] bg-inherit`
}`}
id="myDropdown"
value={selectOption}
onChange={(e) => {
setSelectOption(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
>
{/* Default/Title option */}
<option
style={{ fontSize: fontSize, color: fontColor }}
value=""
disabled
hidden
>
{props?.pos?.options?.name}
</option>
{props.pos?.options?.values?.map((data, ind) => {
return (
<option
style={{ fontSize: fontSize, color: fontColor }}
key={ind}
value={data}
>
{data}
</option>
);
})}
</select>
) : (
<div style={textWidgetStyle} className="select-none-cls">
{props.pos?.options?.name
? props.pos.options.name
: widgetTypeTraslation}
</div>
);
case "initials":
return props.pos.SignUrl ? (
<img
alt="initials"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls"
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "name":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.name")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
const isDefault = false;
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
isDefault
);
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
/>
) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}>
<span>{widgetTypeTraslation}</span>
</div>
);
case "company":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.company")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
</div>
);
case "job title":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.job title")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
</div>
);
case "date":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2 ">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
disabled={
props.isPlaceholder ||
(props.isNeedSign && props.data?.signerObjId !== props.signerObjId)
}
onBlur={handleInputBlur}
closeOnScroll={true}
className={`${selectWidgetCls} outline-[#007bff]`}
selected={
props?.startDate
? props?.startDate
: props.pos.options?.response &&
new Date(props.pos.options.response)
}
onChange={(date) => handleOnDateChange(date)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
props.selectDate
? props.selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
}
/>
) : (
<div
style={textWidgetStyle}
className="select-none-cls overflow-hidden"
>
<span>
{props.selectDate
? props.selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"}
</span>
</div>
);
case "image":
return props.pos.SignUrl ? (
<img
alt="image"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls"
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "email":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.email")}
rows={1}
onKeyDown={(e) => {
// Prevent new line on Enter key press
if (e.key === "Enter") {
e.preventDefault();
}
}}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
cols="1"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
</div>
);
case radioButtonWidget:
return (
<div>
{props.pos.options?.values.map((data, ind) => {
return (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`radio-${props.pos.key + ind}`}
style={{
width: fontSize,
height: fontSize,
marginTop: ind > 0 ? "10px" : "0px"
}}
className={`flex justify-center op-radio`}
type="radio"
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
checked={handleRadioCheck(data)}
onChange={(e) => {
if (!props.isPlaceholder) {
handleCheckRadio(e.target.checked, data);
}
}}
/>
{!props.pos.options?.isHideLabel && (
<label
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0"
>
{data}
</label>
)}
</div>
);
})}
</div>
);
case textWidget:
return (
<textarea
placeholder={t("widgets-name.text")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
setTextValue(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{
fontFamily: "Arial, sans-serif",
fontSize: fontSize,
color: fontColor
}}
cols="50"
/>
);
default:
return props.pos.SignUrl ? (
<div className="pointer-events-none">
<img
alt="image"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full "
/>
</div>
) : (
<div className={widgetCls}>
{props.pos.isStamp ? <div>stamp</div> : <div>signature</div>}
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
null,
props.pos
)}
</div>
);
}
}
export default PlaceholderType;
@@ -0,0 +1,614 @@
import {
useEffect,
useState,
forwardRef,
useRef
} from "react";
import {
onChangeInput,
getMonth,
getYear,
radioButtonWidget,
textInputWidget,
cellsWidget,
textWidget,
months,
years,
selectCheckbox,
isBase64
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "../../styles/signature.css";
import { useTranslation } from "react-i18next";
import CellsWidget from "./CellsWidget";
import { useSelector } from "react-redux";
import Loader from "../../primitives/Loader";
const textWidgetCls =
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap";
const widgetCls =
"select-none-cls overflow-hidden w-full h-full text-black flex flex-col justify-center items-center";
function PlaceholderType(props) {
const selectWidgetCls = `w-full h-full absolute left-0 top-0 focus:outline-none text-base-content`;
const { t } = useTranslation();
const textRef = useRef();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const prefillImgLoad = useSelector((state) => state.widget.prefillImgLoad);
const type = props?.pos?.type;
const iswidgetEnable =
props.isSignYourself ||
((props.isSelfSign || props.isNeedSign) &&
props.data?.signerObjId === props.signerObjId);
const isReadOnly =
props?.data?.Role !== "prefill" &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId);
// prefer the latest response value over any default value
const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [imgUrl, setImgUrl] = useState("");
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const textWidgetStyle = {
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif",
overflow: "hidden",
textAlign: "start",
width: "100%",
display: "flex",
height: "100%"
};
useEffect(() => {
if (type !== "date") {
if (type && type === "checkbox") {
setSelectedCheckbox(
props?.pos?.options?.response ||
props?.pos?.options?.defaultValue ||
[]
);
}
else {
// keep displayed value in sync with the stored response
setwidgetValue(widgetData);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos, widgetData, type]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
className={`${isReadOnly ? `select-none opacity-25` : ``} ${selectWidgetCls} overflow-hidden`}
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleRadioCheck = (data) => {
const defaultData = props.pos.options?.defaultValue;
if (widgetValue === data) {
return true;
} else if (defaultData === data) {
return true;
} else {
return false;
}
};
//function is used to get prefill image's signedUrl after expired
useEffect(() => {
const loadImage = async () => {
const isBase64Url = isBase64(props?.pos?.SignUrl);
if (
props.pos.SignUrl &&
props.pos.type === "image" &&
props?.data?.Role === "prefill" &&
!isBase64Url
) {
const getPrefillImg = prefillImg?.find((x) => x.id === props.pos.key);
if (getPrefillImg) {
setImgUrl(getPrefillImg?.base64);
}
} else {
setImgUrl(props.pos.SignUrl);
}
};
loadImage();
}, [props.pos.SignUrl]);
const formatWidgetName = () => {
const widgetName = props?.pos?.options?.name;
const name = widgetName ? widgetName?.split(`-`) : ["-", "-", "-"];
const lastWord = name.length > 1 ? `-${name[name.length - 1]}` : "";
const title =
props?.pos?.type === name[0] ? `${name[0]}${lastWord}` : widgetName;
return props?.pos?.options?.hint || title;
};
//useEffect is used to increase auto height of text/textInput widget when user using multiline option and enter value in next line
useEffect(() => {
if (!textRef.current) return;
if (
(type === "text" || type === textInputWidget) &&
textRef.current &&
widgetValue
) {
const el = textRef.current;
// Count actual number of lines
const lines = widgetValue?.split("\n")?.length || props.pos?.Height;
// Get line height from computed style
const lineHeight = parseInt(window.getComputedStyle(el).lineHeight);
const textWidgetHeight = lines * lineHeight;
// Set widget box height (your logic)
onChangeInput(
null,
props.pos,
props?.xyPosition,
props.index,
props?.setXyPosition,
props.data && props.data.Id,
null,
null,
null,
null,
null,
textWidgetHeight
);
}
}, [widgetValue]);
switch (type) {
case "signature":
return props.pos.SignUrl ? (
<img
alt="signature"
draggable="false"
src={props.pos.SignUrl}
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "stamp":
return props.pos.SignUrl ? (
<img
alt="stamp"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls object-contain"
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "checkbox":
const checkBoxLayout = props.pos.options?.layout || "vertical";
const isMultipleCheckbox =
props.pos.options?.values?.length > 0 ? true : false;
const checkBoxWrapperClass = `flex items-start whitespace-pre-wrap ${
checkBoxLayout === "horizontal"
? `flex-row flex-wrap lg:py-[1.6px] ${isMultipleCheckbox ? "gap-x-2" : ""}`
: `flex-col ${isMultipleCheckbox ? "gap-y-[3px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
<div
className={checkBoxWrapperClass}
style={{ zIndex: props.isSignYourself && "99" }}
>
{props.pos.options?.values?.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className={`mb-0 flex items-center gap-1`}
>
<input
id={`checkbox-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize }}
className="op-checkbox rounded-[1px]"
disabled={props.isNeedSign && isReadOnly}
type="checkbox"
readOnly
checked={!!selectCheckbox(ind, selectedCheckbox)}
/>
{!props.pos.options?.isHideLabel && (
<span className="leading-none">{data}</span>
)}
</label>
</div>
))}
</div>
);
case textInputWidget:
return props.isSignYourself || iswidgetEnable ? (
<textarea
placeholder={formatWidgetName()}
ref={textRef}
rows={50}
value={
widgetValue
}
className={`w-full resize-none overflow-hidden text-base-content item-center outline-none ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
name="text"
readOnly
disabled={props.isNeedSign && isReadOnly}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case cellsWidget: {
const count = props.pos.options?.cellCount || 5;
const cells = (widgetValue || "").split("");
const height = "100%";
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const handleCellResize = (newCount) => {
if (props.setCellCount) props.setCellCount(props.pos.key, newCount);
};
const isEditable =
props.isPlaceholder || props.isSignYourself || props.isSelfSign;
return (
<CellsWidget
isEnabled={iswidgetEnable}
count={count}
height={height}
value={
cells.join("")
}
editable={isEditable}
resizable={props?.isAllowModify}
fontSize={fontSize}
fontColor={fontColor}
hint={formatWidgetName()}
onCellCountChange={handleCellResize}
/>
);
}
case "dropdown":
return (
<div
style={textWidgetStyle}
className="select-none-cls flex justify-between items-center"
>
{widgetData || t("choose-one")}
<i className="fa-light fa-circle-chevron-down mr-1 "></i>
</div>
);
case "initials":
return props.pos.SignUrl ? (
<img
alt="initials"
draggable="false"
src={props.pos.SignUrl}
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium text-center"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "name":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}>
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "company":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "job title":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "date":
return iswidgetEnable || props?.data?.Role === "prefill" ? (
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2 ">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
disabled={true}
closeOnScroll={true}
className={`${selectWidgetCls} outline-[#007bff]`}
selected={props?.startDate}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
props?.selectDate?.format ||
props.pos?.options?.validation?.format ||
"MM/dd/yyyy"
}
/>
) : (
<div
style={textWidgetStyle}
className="select-none-cls overflow-hidden"
>
<span>
{props?.selectDate?.format ||
props.pos?.options?.validation?.format ||
"MM/dd/yyyy"}
</span>
</div>
);
case "image":
return prefillImgLoad[props.pos?.key] ? (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
<Loader />
</div>
) : imgUrl ? (
<img
alt="image"
draggable="false"
src={imgUrl}
className="w-full h-full select-none-cls object-contain"
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium text-center"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "email":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="1"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case radioButtonWidget:
const radioLayout = props.pos.options?.layout || "vertical";
const isOnlyOneBtn = props.pos.options?.values?.length > 0 ? true : false;
const radioWrapperClass = `flex items-start whitespace-pre-wrap ${
radioLayout === "horizontal"
? `flex-row flex-wrap lg:py-[1.6px] ${isOnlyOneBtn ? "gap-x-[10px]" : ""}`
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
<div className={radioWrapperClass}>
{props.pos.options?.values.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 flex items-center gap-1"
>
<input
readOnly
id={`radio-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize, lineHeight: 2 }}
className={`op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative ${
handleRadioCheck(data) ? "checked-radio" : ""
}`}
type="radio"
disabled={props.isNeedSign && isReadOnly}
checked={handleRadioCheck(data)}
/>
{!props.pos.options?.isHideLabel && (
<span className="leading-none">{data}</span>
)}
</label>
</div>
))}
</div>
);
case textWidget:
return (
<textarea
name="text"
ref={textRef}
readOnly
placeholder={t("widgets-name.text")}
rows={50}
cols={50}
defaultValue={
widgetValue
}
style={{
fontFamily: "Arial, sans-serif",
fontSize: fontSize,
color: fontColor,
background: "white"
}}
className="w-full resize-none overflow-hidden text-base-content item-center outline-none"
/>
);
default:
return props.pos.SignUrl ? (
<div className="pointer-events-none">
<img
alt="image"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full "
/>
</div>
) : (
<div className={widgetCls}>
{props.pos.isStamp ? <div>stamp</div> : <div>signature</div>}
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium"
>
{formatWidgetName()}
</div>
)}
</div>
);
}
}
export default PlaceholderType;
@@ -0,0 +1,753 @@
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import {
getMonth,
getYear,
radioButtonWidget,
compressedFileSize,
textWidget,
months,
changeDateToMomentFormat,
convertBase64ToFile,
generatePdfName,
isBase64
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { range } from "pdf-lib";
import { useTranslation } from "react-i18next";
import moment from "moment";
import AsyncSelect from "react-select/async";
import axios from "axios";
import AddContact from "../../primitives/AddContact";
import Loader from "../../primitives/Loader";
import { useDispatch, useSelector } from "react-redux";
import {
resetWidgetState,
setPrefillImg
} from "../../redux/reducers/widgetSlice";
import * as utils from "../../utils";
const widgetTitle = "font-medium";
const ShowTextWidget = ({ position, handleWidgetDetails }) => {
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState(position.options.response || "");
return (
<input
ref={inputRef}
rows={1}
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
handleWidgetDetails(position, e.target.value);
}}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
);
};
const ImageComponent = (props) => {
const { t } = useTranslation();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const imageRefs = useRef({});
let imgUrl = "";
const isBase64Url = isBase64(props?.position?.SignUrl);
if (isBase64Url) {
imgUrl = props?.position?.SignUrl;
} else {
const getPrefillImg = prefillImg?.find(
(x) => x.id === props?.position?.key
);
imgUrl = getPrefillImg?.base64;
}
return (
<>
<span className={widgetTitle}>{props?.position.options?.name}</span>
{imgUrl ? (
<>
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
<img
alt="print img"
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamicallys
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
onLoad={() => props?.handleImageLoaded?.(props?.position.key)}
onError={() => props?.handleImageLoaded?.(props?.position.key)}
/>
</div>
<span
onClick={() => props?.handleClearImage(props?.position)}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</>
) : (
<div
className="cursor-pointer op-card border-[1px] op-border-hover flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center"
onClick={() => imageRefs.current[props?.id]?.click()}
>
<input
type="file"
onChange={(e) => props?.onImageChange?.(e, props?.position)}
className="filetype"
accept="image/png,image/jpeg"
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamically
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
)}
</>
);
};
function PrefillWidgetModal(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
// Track already loaded image keys so they don't increment multiple times
const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates
const [image, setImage] = useState(null);
const [currentWidget, setCurrentWidget] = useState("");
const [userList, setUserList] = useState([]);
const [totalImages, setTotalImages] = useState(0);
const [loadedImages, setLoadedImages] = useState(0);
const [loading, setLoading] = useState(false);
const years = range(1950, getYear(new Date()) + 16, 1);
// useMemo to memoize the calculation of unique widgets
const uniqueWidget = useMemo(() => {
//functions to used remove duplicate name values across all pages
if (!props.prefillData) return [];
//This will help us track which name values have already been encountered across all pages.
const uniqueNames = new Set();
//Filter and flatten placeholder widgets while keeping unique names
const filteredArray = props.prefillData?.placeHolder?.map((item) => ({
...item,
pos: item.pos.filter((curr) => {
if (uniqueNames.has(curr?.options?.name)) return false; //Duplicate name found, remove it
uniqueNames.add(curr?.options?.name); //First time seen, add to set
return true;
})
}));
//latten the filtered array and exclude read-only widgets
const flatArray = filteredArray?.flatMap((page) =>
page.pos
.filter((widget) => !widget.options?.isReadOnly)
.map((widget) => ({
widget,
pageNumber: page.pageNumber
}))
);
return flatArray || [];
}, [props.prefillData]);
useEffect(() => {
dispatch(resetWidgetState([]));
}, []);
// Reset loader state when modal closes
useEffect(() => {
if (!props?.isPrefillModal) {
initializedRef.current = false;
setTotalImages(0);
setLoadedImages(0);
setLoading(false);
}
}, [props?.isPrefillModal]);
useEffect(() => {
//function is used to save all image base64 in redux state to display prefill images
const savePrefillImg = async () => {
const prefillImg = await utils?.savePrefillImg(props.xyPosition);
if (Array.isArray(prefillImg)) {
prefillImg.forEach((img) => dispatch(setPrefillImg(img)));
}
setLoading(false);
};
savePrefillImg();
}, [props.xyPosition]);
useEffect(() => {
if (totalImages > 0 && loadedImages === totalImages) {
setLoading(false);
}
}, [loadedImages, totalImages]);
useEffect(() => {
if (image?.src) {
handleWidgetDetails(currentWidget);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [image]);
// Run only once per modal open
useEffect(() => {
if (props?.isPrefillModal && !initializedRef.current) {
const getImgWidgets = uniqueWidget?.filter(
(w) => w.widget?.type === "image" && w.widget?.options?.response
);
if (getImgWidgets?.length > 0) {
const imgCount = getImgWidgets.length;
setTotalImages(imgCount);
setLoadedImages(0);
setLoading(true);
initializedRef.current = true; // mark as initialized
}
}
}, [props?.isPrefillModal, uniqueWidget]);
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
const getDatePickerDate = (selectedDate, format = "dd-MM-yyyy") => {
let date;
if (format && format === "dd-MM-yyyy") {
const [day, month, year] = selectedDate?.split("-");
date = new Date(`${year}-${month}-${day}`);
} else if (format && format === "dd.MM.yyyy") {
const [day, month, year] = selectedDate?.split(".");
date = new Date(`${year}.${month}.${day}`);
} else if (format && format === "dd/MM/yyyy") {
const [day, month, year] = selectedDate?.split("/");
date = new Date(`${year}/${month}/${day}`);
} else {
date = new Date(selectedDate);
}
return date;
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{ fontFamily: "Arial, sans-serif" }}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full"
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleDate = (position) => {
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
const getDate = getDatePickerDate(
position?.options.response || new Date(),
position?.options?.validation?.format
);
return getDate;
};
//function to set date with required date format onchange date
const handleOnDateChange = (date, position) => {
const format = position?.options?.validation?.format || "MM/dd/yyyy";
let updateDate = date;
let newDate;
const isSpecialDateFormat =
format && ["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
if (isSpecialDateFormat) {
newDate = moment(updateDate).format(changeDateToMomentFormat(format));
} else {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
newDate = new Date(updateDate);
newDate = moment(newDate.getTime()).format(
changeDateToMomentFormat(format)
);
}
handleWidgetDetails(position, newDate);
};
const handleSavePrefillImg = async (widgetDetails) => {
setLoading(true);
try {
const imageName = generatePdfName(16);
const imageUrl = await convertBase64ToFile(
imageName,
image.src,
image.imgType
);
if (imageUrl) {
return imageUrl;
}
} catch (e) {
console.log("error in handleSavePrefillImg function ", e);
}
};
//function is used to handle prefill widgets details and check if there are any duplicate widget name field exist then update all duplicate value
const handleWidgetDetails = async (widgetDetails, response) => {
const widgetName = widgetDetails?.options?.name;
const getPrefill = props.xyPosition.find((x) => x?.Role === "prefill");
const getPlaceholder = getPrefill?.placeHolder;
let imgUrl;
if (widgetDetails?.type === "image") {
imgUrl = await handleSavePrefillImg(widgetDetails);
}
const updatedData = getPlaceholder.map((page) => ({
...page,
pos: page.pos.map((item) => {
if (item.options.name === widgetName) {
if (widgetDetails?.type === "image") {
return {
...item,
SignUrl: imgUrl,
ImageType: image.imgType,
options: { ...item.options, response: imgUrl }
};
} else {
return {
...item,
options: { ...item.options, response: response }
};
}
} else {
return item;
}
})
}));
const newUpdateSigner = props.xyPosition.map((obj) => {
if (obj.Role === "prefill") {
return { ...obj, placeHolder: updatedData };
}
return obj;
});
props.setXyPosition(newUpdateSigner);
};
//function for set checked and unchecked value of checkbox
const handleCheckboxValue = (isChecked, ind, position) => {
let updateSelectedCheckbox = [];
updateSelectedCheckbox =
position.options?.defaultValue || position.options?.response || [];
if (isChecked) {
updateSelectedCheckbox.push(ind);
} else {
updateSelectedCheckbox = updateSelectedCheckbox.filter(
(data) => data !== ind
);
}
handleWidgetDetails(position, updateSelectedCheckbox);
};
//function for image upload or update
const onImageChange = (event, position) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
compressedFileSize(file, setImage);
setCurrentWidget(position);
}
};
const handleRadioCheck = (data, position) => {
const res = position?.options?.response;
const defaultCheck = position?.options?.defaultValue;
if (res === data || defaultCheck === data) {
return true;
} else {
return false;
}
};
//function for show checked checkbox
const selectCheckbox = (ind, position) => {
const res = position?.options?.response;
const defaultCheck = position?.options?.defaultValue;
if (res && res?.length > 0) {
const isSelectIndex = res.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
} else if (defaultCheck) {
const isSelectIndex = defaultCheck.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
} else {
return false;
}
};
const handleClearImage = (position) => {
let prefillPlaceholder = props?.xyPosition.filter(
(data) => data?.Role === "prefill"
);
const updatedArray = prefillPlaceholder[0]?.placeHolder?.map((page) => ({
...page,
pos: page.pos.map((item) => {
if (item.options.name === position.options.name) {
return {
...item,
SignUrl: "",
options: {
...position.options,
response: ""
}
};
}
return item;
})
}));
const newUpdateSigner = props.xyPosition.map((obj) => {
if (obj.Role === "prefill") {
return { ...obj, placeHolder: updatedArray };
}
return obj;
});
props.setXyPosition(newUpdateSigner);
};
const handleImageLoaded = (key) => {
// Prevent counting the same image multiple times.
// If this image (key) has not already been marked as loaded...
if (!loadedSet.current.has(key)) {
// Mark this image as loaded by adding its key to the Set
loadedSet.current.add(key);
// Increment the loadedImages state by 1
// (tracks how many images have finished loading)
setLoadedImages((prev) => prev + 1);
}
setLoading(false);
};
const handleWidgetType = (position, id) => {
switch (position?.type) {
case "checkbox":
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<div className="flex flex-col gap-y-1">
{position.options?.values?.map((data, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`checkbox-${position.key + ind}`}
className="mt-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
checked={selectCheckbox(ind, position)}
onChange={(e) =>
handleCheckboxValue(e.target.checked, ind, position)
}
/>
<label
htmlFor={`checkbox-${position.key + ind}`}
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
</>
);
case textWidget:
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<ShowTextWidget
position={position}
handleWidgetDetails={handleWidgetDetails}
/>
</>
);
case "dropdown":
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
id="myDropdown"
value={
position?.options?.response || position?.options?.defaultValue
}
onChange={(e) => handleWidgetDetails(position, e.target.value)}
>
{/* Default/Title option */}
<option value="" disabled hidden>
{t("choose-one")}
</option>
{position?.options?.values?.map((data, ind) => (
<option key={ind} value={data}>
{data}
</option>
))}
</select>
</>
);
case "date":
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<DatePicker
portalId="datepicker-portal-root"
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={handleDate(position)}
onChange={(date) => handleOnDateChange(date, position)}
customInput={<ExampleCustomInput />}
dateFormat={position?.options?.validation?.format || "MM/dd/yyyy"}
/>
</>
);
case "image":
return (
<ImageComponent
position={position}
onImageChange={onImageChange}
handleImageLoaded={handleImageLoaded}
handleClearImage={handleClearImage}
id={id}
/>
);
case radioButtonWidget:
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<div className="flex flex-col gap-y-1">
{position.options?.values.map((data, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`radio-${position.key + ind}`}
className="mt-[2px] op-radio op-radio-xs"
type="radio"
checked={handleRadioCheck(data, position)}
onChange={() => handleWidgetDetails(position, data)}
/>
<label
htmlFor={`radio-${position.key + ind}`}
className="text-xs mb-0 ml-[2px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
</>
);
default:
return position?.SignUrl ? (
<div className="pointer-events-none">
<img
alt="image"
draggable="false"
src={position?.SignUrl}
className="w-full h-full"
/>
</div>
) : (
<div className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full">
No widget
</div>
);
}
};
const handleEmbedPrefill = async (item) => {
await props.handleCreateDocument();
};
//`loadOptions` function to use show all list of signer in dropdown
const loadOptions = async (inputValue) => {
try {
const baseURL = localStorage.getItem("baseUrl");
const url = `${baseURL}functions/getsigners`;
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
};
const search = inputValue;
const axiosRes = await axios.post(url, { search }, { headers });
const contactRes = axiosRes?.data?.result || [];
if (contactRes) {
const res = JSON.parse(JSON.stringify(contactRes));
const result = res;
setUserList(result);
return await result.map((item) => ({
label: `${item.Name}<${item.Email}>`,
value: item.objectId
}));
}
} catch (error) {
console.log("err", error);
}
};
//`handleInputChange` function to get signers list from dropdown
const handleInputChange = (item, id) => {
const signerExist = props.forms.some((x) => x.label === item.label);
if (signerExist) {
alert(t("already-exist-signer"));
} else {
let newForm = [...props.forms];
let signerId = newForm[id].value;
newForm[id].label = item?.label;
// newForm[id].value = item?.value;
props.setForms(newForm);
const getSigner = userList.find((x) => x.objectId === item.value);
props.handleAddUser(getSigner, signerId);
}
};
//show modal to create new contact
const handleCreateNew = (e, id) => {
e.preventDefault();
props.setIsNewContact({ status: true, id: id });
};
const closePopup = () => {
props.setIsNewContact({ status: false, id: "" });
};
return (
<>
<ModalUi
title={uniqueWidget?.length > 0 ? t("prefill-widget") : "Recipients"}
isOpen={true}
handleClose={props.handleClosePrefillModal}
>
<div className="relative">
{(props?.isSubmit || loading) && (
<div className="absolute inset-0 flex items-center justify-center bg-white/70 z-[9999]">
<Loader />
</div>
)}
{uniqueWidget?.length > 0 && (
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 m-3 md:m-6 text-base-content flex flex-col relative">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
{uniqueWidget.map((x, id) => (
<div key={id} className="flex flex-col gap-2 w-full">
{handleWidgetType(x.widget, id)}
</div>
))}
</div>
</div>
)}
<div>
{props.forms.length > 0 && (
<div className="overflow-y-auto m-3">
{uniqueWidget?.length > 0 && (
<h1 className="font-medium text-[15px] mb-2">
{t("recipients")}
</h1>
)}
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 md:mx-3 text-base-content flex flex-col relative">
{props.forms?.map((field, id) => {
return (
<div className="flex flex-col" key={field?.value}>
<label>{field?.role}</label>
<div className="flex justify-between items-center gap-1">
<div className="flex-1">
<AsyncSelect
cacheOptions
defaultOptions
value={field}
loadingMessage={() => t("loading")}
noOptionsMessage={() => t("contact-not-found")}
loadOptions={loadOptions}
onChange={(item) => handleInputChange(item, id)}
unstyled
onFocus={() => loadOptions()}
classNames={{
control: () =>
"op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full h-full text-[11px]",
valueContainer: () =>
"flex flex-row gap-x-[2px] gap-y-[2px] md:gap-y-0 w-full my-[2px]",
multiValue: () =>
"op-badge op-badge-primary h-full text-[11px]",
multiValueLabel: () => "mb-[2px]",
menu: () =>
"mt-1 shadow-md rounded-lg bg-base-200 text-base-content absolute z-9999",
menuList: () => "shadow-md rounded-lg",
option: () =>
"bg-base-200 text-base-content rounded-lg m-1 hover:bg-base-300 p-2",
noOptionsMessage: () =>
"p-2 bg-base-200 rounded-lg m-1 p-2"
}}
menuPortalTarget={document.getElementById(
"selectSignerModal"
)}
/>
</div>
<button
onClick={(e) => handleCreateNew(e, field.value)}
className="op-btn op-btn-accent op-btn-outline op-btn-sm"
>
<i className="fa-light fa-plus"></i>
</button>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
<div className="flex gap-2 mx-4 mb-3">
<button
disabled={props?.isSubmit}
className="op-btn op-btn-primary op-btn-sm w-[80px]"
onClick={() => handleEmbedPrefill(props?.item)}
>
<span>{t("next")}</span>
</button>
<button
className="op-btn op-btn-ghost op-btn-sm"
onClick={() => props.navigatePageToDoc()}
>
<span>{t("edit-draft")}</span>
</button>
</div>
</div>
</ModalUi>
<ModalUi
title={t("add-contact")}
isOpen={props.isNewContact.status}
handleClose={closePopup}
>
<AddContact
isDisableTitle
isAddYourSelfCheckbox
details={props.handleAddUser}
closePopup={closePopup}
newContactId={props.isNewContact.id}
/>
</ModalUi>
</>
);
}
export default PrefillWidgetModal;
@@ -20,7 +20,7 @@ function PrevNext({ pageNumber, allPages, changePage }) {
onClick={previousPage}
>
<span className="block">
<i className="fa-light fa-backward" aria-hidden="true"></i>
<i className="fa-light fa-chevron-up" aria-hidden="true"></i>
</span>
</button>
<span className="text-xs text-base-content font-medium mx-2 2xl:text-[20px]">
@@ -32,7 +32,7 @@ function PrevNext({ pageNumber, allPages, changePage }) {
onClick={nextPage}
>
<span className="block">
<i className="fa-light fa-forward" aria-hidden="true"></i>
<i className="fa-light fa-chevron-down" aria-hidden="true"></i>
</span>
</button>
</div>
@@ -1,4 +1,4 @@
import React, { useRef, useState } from "react";
import { useRef, useState } from "react";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import {
color,
@@ -12,6 +12,7 @@ const cursor =
const RecipientList = (props) => {
const [animationParent] = useAutoAnimate();
const [isHover, setIsHover] = useState();
const [isPrefill, setIsPrefill] = useState(false);
const [isEdit, setIsEdit] = useState(false);
//function for onhover signer name change background color
const inputRef = useRef(null);
@@ -31,7 +32,7 @@ const RecipientList = (props) => {
e.preventDefault();
};
//handle draggable element drop and also used in mobile view on up and down key to chnage sequence of recipient's list
//handle draggable element drop and also used in mobile view on up and down key to change sequence of recipient's list
const handleChangeSequence = (e, ind, isUp, isDown, obj) => {
e.preventDefault();
let draggedItemId;
@@ -80,8 +81,42 @@ const RecipientList = (props) => {
});
props?.setSignerPos(changeOrderSignerList);
};
const handleSelectRecipient = (e, index, obj, prefill) => {
e.preventDefault();
props?.setIsSelectId(index);
props?.setUniqueId(obj.Id);
props?.setRoleName(obj.Role);
props?.setBlockColor(obj?.blockColor);
props?.handleModal && props?.handleModal();
setIsPrefill(prefill ?? false);
props.setIsTour && props?.setIsTour(false);
};
const isSelected = (ind) => {
const isUserSelected =
(!isMobile && isHover === ind) ||
(!isPrefill && props.isSelectListId === ind);
return isUserSelected;
};
return (
<>
{props?.prefillSigner?.length > 0 &&
props?.prefillSigner?.map((obj, ind) => (
<div
key={ind}
data-tut="prefillTour"
className={`${
props.uniqueId === obj.Id
? "op-bg-primary text-white"
: "transparent text-base-content"
} cursor-pointer px-2 py-1 m-1 mb-2 border-[1px] gap-1 rounded-xl flex justify-center items-center op-border-primary text-[12px] font-bold whitespace-nowrap text-ellipsis`}
onClick={(e) => handleSelectRecipient(e, ind, obj, true)}
>
<i
className={`${props.uniqueId === obj.Id ? "bg-white op-text-primary" : "op-bg-primary text-white"} w-[20px] h-[20px] flex justify-center items-center text-[10px] fa-light fa-signature rounded-full`}
></i>
<span>{obj.Name}</span>
</div>
))}
{props.signersdata.length > 0 &&
props.signersdata.map((obj, ind) => {
return (
@@ -118,20 +153,11 @@ const RecipientList = (props) => {
: color[ind % color.length]
: "transparent"
}}
onClick={(e) => {
e.preventDefault();
props.setIsSelectId(ind);
props.setUniqueId(obj.Id);
props.setRoleName(obj.Role);
props.setBlockColor(obj?.blockColor);
if (props.handleModal) {
props.handleModal();
}
}}
onClick={(e) => handleSelectRecipient(e, ind, obj)}
>
<div className="flex flex-row items-center w-full">
<div className="flex flex-row items-center w-full overflow-hidden pr-2">
<div
className="flex w-[30px] h-[30px] rounded-full items-center justify-center mr-2"
className="flex flex-shrink-0 w-[30px] h-[30px] rounded-full items-center justify-center mr-2"
style={{
background: obj?.blockColor
? darkenColor(obj?.blockColor, 0.4)
@@ -142,38 +168,22 @@ const RecipientList = (props) => {
{isWidgetExist(obj.Id) ? (
<i className="fa-light fa-check"></i>
) : (
<>
{obj.Name
? getFirstLetter(obj.Name)
: getFirstLetter(obj.Role)}
</>
<>{getFirstLetter(obj?.Name ?? obj?.Role)}</>
)}
</span>
</div>
<div
className={`${
obj.Name ? "flex-col" : "flex-row"
} flex items-center`}
className={`${obj.Name ? "flex-col" : "flex-row"} ${
isSelected(ind) ? "text-[#424242]" : "text-base-content"
} flex overflow-hidden flex-grow-0`}
>
{obj.Name ? (
<span
className={`${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} text-[12px] font-bold w-[100px] whitespace-nowrap overflow-hidden text-ellipsis`}
>
<span className="text-[12px] font-bold truncate whitespace-nowrap">
{obj.Name}
</span>
) : (
<span
className={`${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} text-[12px] font-bold w-[100px] whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer`}
className="text-[12px] font-bold truncate whitespace-nowrap cursor-pointer"
onClick={() => {
setIsEdit({ [obj.Id]: true });
props.setRoleName(obj.Role);
@@ -182,7 +192,7 @@ const RecipientList = (props) => {
{isEdit?.[obj.Id] && props.handleRoleChange ? (
<input
ref={inputRef}
className="bg-transparent p-[3px]"
className="bg-transparent p-[3px] w-full"
value={obj.Role}
onChange={(e) => props.handleRoleChange(e, obj.Id)}
onBlur={() => {
@@ -204,14 +214,7 @@ const RecipientList = (props) => {
</span>
)}
{obj.Name && (
<span
className={` ${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} text-[10px] font-medium w-[100px] whitespace-nowrap overflow-hidden text-ellipsis`}
>
<span className="text-[10px] font-medium truncate whitespace-nowrap">
{obj?.Role || obj?.Email}
</span>
)}
@@ -247,18 +250,13 @@ const RecipientList = (props) => {
</div>
</div>
)}
{props.handleDeleteUser && (
{props.handleDeleteUser && obj?.Role !== "prefill" && (
<div
onClick={(e) => {
e.stopPropagation();
props.handleDeleteUser(obj.Id);
}}
className={`${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} cursor-pointer`}
className={`${isSelected(ind) ? "text-[#424242]" : "text-base-content"} cursor-pointer`}
>
<i className="fa-light fa-trash-can 2xl:text-[22px]"></i>
</div>
@@ -3,7 +3,13 @@ import { useTranslation } from "react-i18next";
import { Document, Page } from "react-pdf";
import { useSelector } from "react-redux";
import { PDFDocument } from "pdf-lib";
import { base64ToArrayBuffer } from "../../constant/Utils";
import {
base64ToArrayBuffer,
decryptPdf,
flattenPdf,
getFileAsArrayBuffer
} from "../../constant/Utils";
import { maxFileSize } from "../../constant/const";
function RenderAllPdfPage(props) {
const { t } = useTranslation();
@@ -11,7 +17,7 @@ function RenderAllPdfPage(props) {
const mergePdfInputRef = useRef(null);
const [signPageNumber, setSignPageNumber] = useState([]);
const [bookmarkColor, setBookmarkColor] = useState("");
const isHeader = useSelector((state) => state.showHeader);
const isSidebar = useSelector((state) => state.sidebar.isOpen);
const [pageWidth, setPageWidth] = useState("");
//set all number of pages after load pdf
@@ -45,7 +51,7 @@ function RenderAllPdfPage(props) {
const timer = setTimeout(updateSize, 100); // match the transition duration
return () => clearTimeout(timer);
}, [isHeader, pageContainer, props?.containerWH]);
}, [isSidebar, pageContainer, props?.containerWH]);
//'function `addSignatureBookmark` is used to display the page where the user's signature is located.
const addSignatureBookmark = (index) => {
const ispageNumber = signPageNumber.includes(index + 1);
@@ -62,19 +68,69 @@ 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];
if (!file) {
alert("Please upload a valid PDF file.");
alert(t("please-select-pdf"));
return;
}
if (!file.type.includes("pdf")) {
alert("Only PDF files are allowed.");
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
}
} else {
alert(t("provide-password"));
}
} else {
console.log("Err ", err);
alert(t("error-uploading-pdf"));
}
}
} else {
alert(t("error-uploading-pdf"));
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -91,6 +147,13 @@ function RenderAllPdfPage(props) {
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
@@ -110,6 +173,7 @@ function RenderAllPdfPage(props) {
autoSignScroll hide-scrollbar max-h-[100vh] `}
>
<Document
error=""
loading={t("loading-doc")}
onLoadSuccess={onDocumentLoad}
file={pdfDataBase64}
@@ -155,8 +219,8 @@ function RenderAllPdfPage(props) {
onChange={handleFileUpload}
/>
<i className="fa-light fa-plus text-gray-500"></i>
<span className="text-xs lg:text-sm text-base-content ">
Add pages
<span className="text-xs lg:text-sm text-base-content">
{t("add-pages")}
</span>
</button>
)}
@@ -1,729 +0,0 @@
import React, { useState } from "react";
import RSC from "react-scrollbars-custom";
import { Document, Page } from "react-pdf";
import {
defaultWidthHeight,
getContainerScale,
handleImageResize,
handleSignYourselfImageResize,
isMobile
} from "../../constant/Utils";
import Placeholder from "./Placeholder";
import Alert from "../../primitives/Alert";
import { useTranslation } from "react-i18next";
function RenderPdf(props) {
const { t } = useTranslation();
const [scaledHeight, setScaledHeight] = useState();
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner");
// handle signature block width and height according to screen
const posWidth = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const defaultWidth = defaultWidthHeight(pos.type).width;
const posWidth = pos.Width ? pos.Width : defaultWidth;
if (signYourself) {
return posWidth * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * containerScale;
}
} else {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * pos.scale * containerScale;
}
}
} else {
return posWidth * props.scale * containerScale;
}
}
};
const posHeight = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const posHeight = pos.Height || defaultWidthHeight(pos.type).height;
if (signYourself) {
return posHeight * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * containerScale;
}
} else {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * pos.scale * containerScale;
}
}
} else {
return posHeight * props.scale * containerScale;
}
}
};
//function for render placeholder block over pdf document
const checkSignedSigners = (data) => {
let checkSign = [];
//condition to handle quick send flow and using normal request sign flow
checkSign = props.signedSigners
? props.signedSigners?.filter(
(sign) =>
sign?.Id === data?.Id || sign?.objectId === data?.signerObjId
)
: [];
const handleAllUserName = (Id, Role, type, pos) => {
return (
<React.Fragment>
<div className="text-black text-[8px] font-bold">
{
props.pdfDetails[0].Signers?.find(
(signer) => signer.objectId === data.signerObjId
)?.Name
}
</div>
{type && (
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "11px" }}
className="font-bold"
>
{type}
</div>
)}
{Role && (
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "8px" }}
className="text-black font-medium"
>
{`(${Role})`}
</div>
)}
</React.Fragment>
);
};
return (
checkSign.length === 0 &&
data?.placeHolder?.map((placeData, key) => {
return (
<React.Fragment key={key}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
pos && (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setSignKey={props.setSignKey}
setIsSignPad={props.setIsSignPad}
setIsStamp={props.setIsStamp}
handleSignYourselfImageResize={handleImageResize}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
isShowBorder={props.isSelfSign}
isAlllowModify={props.isAlllowModify}
signerObjId={props.signerObjectId}
isShowDropdown={true}
isNeedSign={props.pdfRequest}
isSelfSign={true}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
handleUserName={handleAllUserName}
isDragging={props.isDragging}
pdfDetails={props.pdfDetails}
setIsInitial={props.setIsInitial}
setValidateAlert={props.setValidateAlert}
unSignedWidgetId={props.unSignedWidgetId}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
setCurrWidgetsDetails={props.setCurrWidgetsDetails}
uniqueId={props.uniqueId}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
ispublicTemplate={props.ispublicTemplate}
handleUserDetails={props.handleUserDetails}
isResize={props.isResize}
setIsAgreeTour={props.setIsAgreeTour}
isAgree={props.isAgree}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
setWidgetType={props.setWidgetType}
setUniqueId={props.setUniqueId}
setIsSelectId={props.setIsSelectId}
handleDeleteSign={props.handleDeleteSign}
setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal}
setIsCheckbox={props.setIsCheckbox}
isFreeResize={false}
isOpenSignPad={true}
assignedWidgetId={props.assignedWidgetId}
isApplyAll={true}
setFontSize={props.setFontSize}
fontSize={props.fontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
setRequestSignTour={props.setRequestSignTour}
/>
</React.Fragment>
)
);
})}
</React.Fragment>
);
})
);
};
const calculateFontsize = (pos) => {
const width = posWidth(pos);
const height = posHeight(pos);
if (height === width || height < width) {
return `${height / 5}px`;
} else if (width < height) {
return `${width / 10}px`;
}
};
//function for render placeholder block over pdf document
const handleUserName = (Id, Role, type, pos) => {
if (Id) {
const checkSign = props.signersdata.find((sign) => sign.Id === Id);
if (checkSign?.Name) {
return (
<>
<div
style={{
fontSize: pos ? calculateFontsize(pos) : "8px"
}}
className="text-black font-medium"
>
{checkSign?.Name}
</div>
{type && (
<div
style={{
fontSize: pos ? calculateFontsize(pos) : "11px"
}}
className=" font-bold"
>
{type}
</div>
)}
{Role && (
<div
style={{
fontSize: pos ? calculateFontsize(pos) : "8px"
}}
className="text-black text-[8px] font-medium"
>
{`(${Role})`}
</div>
)}
</>
);
} else {
return (
<>
{type && (
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "11px" }}
className=" font-bold"
>
{type}
</div>
)}
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "8px" }}
className="text-black font-medium"
>
{Role}
</div>
</>
);
}
} else {
return (
<>
{type && <div className="text-[11px] font-bold">{type}</div>}
<div className="text-black text-[8px] font-medium">{Role}</div>
</>
);
}
};
const pdfDataBase64 = `data:application/pdf;base64,${props.pdfBase64Url}`;
//calculate render height of pdf in mobile view
const handlePageLoadSuccess = (page) => {
const containerWidth = props.divRef.current.offsetWidth; // Get container width
const viewport = page.getViewport({ scale: 1 });
const scale = containerWidth / viewport.width; // Scale to fit container width
const scaleHeight = viewport.height * scale;
setScaledHeight(scaleHeight);
};
return (
<>
{props.successEmail && (
<Alert type={"success"}>{t("success-email-alert")}</Alert>
)}
{isMobile ? (
<RSC
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
//49 is height of header
height: isGuestSigner ? window.innerHeight - 49 : scaledHeight,
zIndex: 0
}}
noScrollY={props.scale === 1 ? true : false}
noScrollX={props.scale === 1 ? true : false}
>
<div
data-tut="reactourForth"
className={`${
isGuestSigner ? "30px" : ""
} border-[0.1px] border-[#ebe8e8] overflow-x-auto`}
style={{
width:
props.containerWH?.width &&
props.containerWH?.width * props.scale
}}
ref={props.drop}
id="container"
>
{props.containerWH?.width &&
props.pdfOriginalWH.length > 0 &&
(props.pdfRequest || props.isSelfSign
? props.signerPos?.map((data, key) => {
return (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
);
})
: props.placeholder // placeholder mobile
? props.signerPos?.map((data, ind) => {
return (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => {
return (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={
props.handleDeleteSign
}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={
props.setShowDropdown
}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={
props.handleLinkUser
}
handleUserName={handleUserName}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setWidgetType={props.setWidgetType}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
setSelectWidgetId={
props.setSelectWidgetId
}
selectWidgetId={
props.selectWidgetId
}
handleNameModal={
props.handleNameModal
}
setTempSignerId={
props.setTempSignerId
}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})
: !props.pdfDetails?.[0]?.IsCompleted &&
props.xyPosition?.map((data, ind) => {
return (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map((pos, id) => {
return (
pos && (
<Placeholder
key={id}
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={props.handleDeleteSign}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
containerWH={props.containerWH}
setIsSignPad={props.setIsSignPad}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsInitial={props.setIsInitial}
setWidgetType={props.setWidgetType}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
handleUserName={handleUserName}
setIsCheckbox={props.setIsCheckbox}
setValidateAlert={props.setValidateAlert}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={false}
isOpenSignPad={true}
/>
)
);
})}
</React.Fragment>
);
}))}
<Document
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
// ref={pdfRef}'
onClick={() => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId("");
}
}}
file={pdfDataBase64}
>
<Page
onLoadSuccess={handlePageLoadSuccess}
scale={props.scale || 1}
key={props.index}
pageNumber={props.pageNumber}
width={props.containerWH.width}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
</div>
</RSC>
) : (
<RSC
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
height: window.innerHeight + "px",
zIndex: 0
}}
noScrollY={false}
noScrollX={props.scale === 1 ? true : false}
>
<div
style={{
width:
props.containerWH?.width &&
props.containerWH?.width * props.scale
}}
ref={props.drop}
id="container"
>
{props.pdfLoad &&
props.containerWH?.width &&
props.pdfOriginalWH.length > 0 &&
(props.pdfRequest || props.isSelfSign //pdf request sign flow
? props.signerPos?.map((data, key) => {
return (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
);
})
: props.placeholder //placeholder and template flow
? props.signerPos.map((data, ind) => {
return (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => {
return (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={
props.handleDeleteSign
}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={
props.setShowDropdown
}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={
props.handleLinkUser
}
handleUserName={handleUserName}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setWidgetType={props.setWidgetType}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
setSelectWidgetId={
props.setSelectWidgetId
}
selectWidgetId={
props.selectWidgetId
}
handleNameModal={
props.handleNameModal
}
setTempSignerId={
props.setTempSignerId
}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})
: !props.pdfDetails?.[0]?.IsCompleted &&
props.xyPosition?.map((data, ind) => {
// signyourself flow
return (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={props.handleDeleteSign}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={(event, dragElement) =>
props.handleStop(
event,
dragElement,
pos.type
)
}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
setIsSignPad={props.setIsSignPad}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsInitial={props.setIsInitial}
setWidgetType={props.setWidgetType}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
handleUserName={handleUserName}
setIsCheckbox={props.setIsCheckbox}
setValidateAlert={props.setValidateAlert}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={false}
isOpenSignPad={true}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
}))}
{/* this component for render pdf document is in middle of the component */}
<Document
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
onClick={() => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId("");
}
}}
file={pdfDataBase64}
>
<Page
key={props.index}
width={props.containerWH.width}
scale={props.scale || 1}
className={"-z-[1]"} // when user zoom-in in tablet widgets move backward that's why pass -z-[1]
pageNumber={props.pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
</div>
</RSC>
)}
</>
);
}
export default RenderPdf;
@@ -0,0 +1,566 @@
import React, {
useState,
useRef,
useCallback,
useLayoutEffect,
useEffect
} from "react";
import RSC from "react-scrollbars-custom";
import { Document, Page } from "react-pdf";
import {
defaultWidthHeight,
getContainerScale,
handleImageResize,
handleSignYourselfImageResize,
isMobile
} from "../../constant/Utils";
import Placeholder from "./Placeholder";
import Alert from "../../primitives/Alert";
import { useTranslation } from "react-i18next";
import usePdfPinchZoom from "../../hook/usePdfPinchZoom";
import { useDispatch, useSelector } from "react-redux";
import Guidelines from "./Guidelines";
import { useGuidelinesContext } from "../../context/GuidelinesContext";
import { toggleSidebar } from "../../redux/reducers/sidebarReducer";
import DragGuidelinesLayer from "./DragGridLinesLayer";
import { useDrop } from "react-dnd";
import WidgetsDragPreview from "./WidgetsDragPreview";
function RenderPdf(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => props?.addPositionOfSignature(item, monitor)
});
const [scaledHeight, setScaledHeight] = useState();
const { guideline, showGuidelines } = useGuidelinesContext();
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner");
const scrollTriggerId = useSelector((state) => state.widget.scrollTriggerId);
const isOpen = useSelector((state) => state.sidebar.isOpen);
const scrollRef = useRef(null);
const pdfContainerRef = useRef(null);
useEffect(() => {
dispatch(toggleSidebar(false));
return () => {
dispatch(toggleSidebar(true));
};
}, []);
// enable pinch to zoom only on actual pdf wrapper
usePdfPinchZoom(
pdfContainerRef,
props.scale,
props.setScale,
props.setZoomPercent
);
// handle signature block width and height according to screen
const posWidth = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const defaultWidth = defaultWidthHeight(pos.type).width;
const posWidth = pos.Width ? pos.Width : defaultWidth;
if (signYourself) {
return posWidth * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * containerScale;
}
} else {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * pos.scale * containerScale;
}
}
} else {
return posWidth * props.scale * containerScale;
}
}
};
const posHeight = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const posHeight = pos.Height || defaultWidthHeight(pos.type).height;
if (signYourself) {
return posHeight * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * containerScale;
}
} else {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * pos.scale * containerScale;
}
}
} else {
return posHeight * props.scale * containerScale;
}
}
};
// `smoothScrollTo` is used to provide smooth scrolling while focus on widget
const smoothScrollTo = (targetY, duration = 500) => {
const sb = scrollRef.current;
if (!sb) return;
const start = sb.scrollTop;
const change = targetY - start;
const startTime = performance.now();
const animate = (now) => {
const elapsed = now - startTime;
const t = Math.min(1, elapsed / duration);
// easeInOutQuad
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
sb.scrollTo(sb.scrollLeft, start + change * ease);
if (t < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
};
// `scrollToTarget` is used to focus on widget and scroll to top
const scrollToTarget = useCallback(() => {
// Get the scrollbar container ref
const sb = scrollRef.current;
// If there's no content element or no widget details, bail out
if (!sb?.contentElement || !props.currWidgetsDetails) return;
// The absolute Y position (relative to the document) where we want to scroll
const yPosition = props?.currWidgetsDetails?.yPosition || 0;
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const targetTop = yPosition * containerScale * props.scale;
// The Y offset of the scrollable content container itself
const { offsetTop } = sb.contentElement;
// Account for the header height + a little extra padding
// Different header height if user is a guest signer
const headerOffset = isGuestSigner ? 10 : 79;
// Compute the scroll position inside the container
const positionTop = targetTop - offsetTop - headerOffset;
const pageNumber = props.pageNumber > 0 ? props.pageNumber - 1 : 0;
const ogH = props.pdfOriginalWH[pageNumber]?.height;
// If the modal for this widget is open, we may need to expand the PDF container
if (props.isShowModal[scrollTriggerId]) {
// Original PDF height for this page
if (pdfContainerRef?.current) {
// Increase container height to include the area up to the target
pdfContainerRef.current.style.height = `${ogH + (targetTop - offsetTop)}px`;
}
} else {
// Otherwise, reset any inline height override
if (pdfContainerRef?.current) {
pdfContainerRef.current.style.height = "";
}
}
// Actually perform the scroll: keep the same horizontal scroll, scroll vertically
if (targetTop > ogH * 0.75 && !isGuestSigner && isOpen) {
smoothScrollTo(positionTop - 300);
} else if (targetTop > ogH * 0.75 && !isGuestSigner && !isOpen) {
smoothScrollTo(positionTop - 100);
} else {
smoothScrollTo(positionTop);
}
// Highlight only the target widget; reset others to default border
document.querySelectorAll(".signYourselfBlock").forEach((w) => {
w.style.border =
w.id === String(scrollTriggerId)
? "1.5px solid red" // active widget in red
: "1.5px solid #007bff"; // others in blue
});
}, [
scrollTriggerId,
props.currWidgetsDetails?.yPosition,
props.isShowModal,
props.pdfOriginalWH,
props.pageNumber
]);
useLayoutEffect(() => {
// Whenever scrollTriggerId changes, fire off the scroll in the next rAF
// to ensure the DOM has painted/layout is stable before scrolling
if (scrollTriggerId) {
scrollToTarget();
}
}, [scrollTriggerId, scrollToTarget]);
// function for render placeholder block over pdf document (all signing flow)
const checkSignedSigners = (data) => {
let checkSign = [];
//condition to handle quick send flow and using normal request sign flow
checkSign = props.signedSigners
? props.signedSigners?.filter(
(sign) =>
sign?.Id === data?.Id || sign?.objectId === data?.signerObjId
)
: [];
return (
checkSign.length === 0 &&
data?.placeHolder?.map((placeData, key) => (
<React.Fragment key={key}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map(
(pos, ind) =>
pos && (
<React.Fragment key={ind}>
<Placeholder
pos={pos}
handleSignYourselfImageResize={handleImageResize}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
isShowBorder={props.isSelfSign}
isAlllowModify={props.isAlllowModify}
signerObjId={props.signerObjectId}
isShowDropdown={true}
isNeedSign={props.pdfRequest}
isSelfSign={true}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
pdfDetails={props.pdfDetails}
unSignedWidgetId={props.unSignedWidgetId}
setCurrWidgetsDetails={props.setCurrWidgetsDetails}
uniqueId={props.uniqueId}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
ispublicTemplate={props.ispublicTemplate}
handleUserDetails={props.handleUserDetails}
isResize={props.isResize}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
setUniqueId={props.setUniqueId}
setIsSelectId={props.setIsSelectId}
handleDeleteWidget={props.handleDeleteWidget}
setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal}
handleCellSettingModal={props.handleCellSettingModal}
setIsCheckbox={props.setIsCheckbox}
isFreeResize={props.isSelfSign ? true : false}
isOpenSignPad={true}
assignedWidgetId={props.assignedWidgetId}
setCellCount={props.setCellCount}
setFontSize={props.setFontSize}
fontSize={props.fontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
setIsReqSignTourDisabled={props.setIsReqSignTourDisabled}
calculateFontsize={calculateFontsize}
currWidgetsDetails={props?.currWidgetsDetails}
handleNameModal={props?.handleNameModal}
/>
</React.Fragment>
)
)}
</React.Fragment>
))
);
};
const calculateFontsize = (pos) => {
const width = posWidth(pos);
const height = posHeight(pos);
if (height === width || height < width) {
return `${height / 5}px`;
} else if (width < height) {
return `${width / 10}px`;
}
};
const pdfDataBase64 = `data:application/pdf;base64,${props.pdfBase64Url}`;
// calculate render height of pdf in mobile view
const handlePageLoadSuccess = (page) => {
if (isMobile) {
const containerWidth = props.divRef.current.offsetWidth; // Get container width
const viewport = page.getViewport({ scale: 1 });
const scale = containerWidth / viewport.width; // Scale to fit container width
const scaleHeight = viewport.height * scale;
setScaledHeight(scaleHeight);
}
};
return (
<>
{props.successEmail && (
<Alert type={"success"}>{t("success-email-alert")}</Alert>
)}
<RSC
ref={scrollRef}
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
height: isMobile
? isGuestSigner
? window.innerHeight - 49 // 49 is height of header
: scaledHeight
: `${window.innerHeight}px`,
zIndex: 0
}}
noScrollY={isMobile ? props.scale === 1 : false}
noScrollX={props.scale === 1}
>
<div
data-tut={isMobile ? "reactourForth" : undefined}
className={
isMobile
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto relative`
: "relative"
}
style={{
width:
props.containerWH?.width && props.containerWH?.width * props.scale
}}
ref={(node) => {
pdfContainerRef.current = node;
drop && drop(node);
}}
id="container"
>
{props.pdfLoad !== false &&
props.containerWH?.width &&
props.pdfOriginalWH.length > 0 && (
<>
{props.pdfRequest || props.isSelfSign
? // request sign, guest sign,
props.signerPos?.map((data, key) => (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
))
: props.placeholder // placeholdersign document, draft document, create template, draft template
? props.signerPos?.map((data, ind) => (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
handleDeleteWidget={
props.handleDeleteWidget
}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={props.setShowDropdown}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={props.handleLinkUser}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleNameModal={props.handleNameModal}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
setCellCount={props.setCellCount}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
calculateFontsize={calculateFontsize}
currWidgetsDetails={
props?.currWidgetsDetails
}
setRoleName={props?.setRoleName}
pdfDetails={props.pdfDetails}
setIsReqSignTourDisabled={
props.setIsReqSignTourDisabled
}
/>
</React.Fragment>
))}
</React.Fragment>
))}
</React.Fragment>
))
: !props.pdfDetails?.[0]?.IsCompleted && // signyourself flow
props.xyPosition?.map((data, ind) => (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map(
(pos, id) =>
pos && (
<Placeholder
key={id}
pos={pos}
setIsPageCopy={props.setIsPageCopy}
handleDeleteWidget={
props.handleDeleteWidget
}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
containerWH={props.containerWH}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={true}
isOpenSignPad={true}
calculateFontsize={calculateFontsize}
currWidgetsDetails={
props?.currWidgetsDetails
}
setIsReqSignTourDisabled={
props.setIsReqSignTourDisabled
}
unSignedWidgetId={props.unSignedWidgetId}
handleNameModal={props.handleNameModal}
/>
)
)}
</React.Fragment>
))}
</>
)}
<Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={(e) => {
console.log("PDF load error", e);
props.setPdfLoad(false);
}}
loading={t("loading-doc")}
onLoadSuccess={(pdf) => {
props.setPdfLoad(true);
props.pageDetails(pdf);
}}
onClick={() =>
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({})
}
file={pdfDataBase64}
>
<Page
key={props.index}
onLoadSuccess={handlePageLoadSuccess}
width={props.containerWH.width}
scale={props.scale || 1}
className={isMobile ? "select-none touch-callout-none" : "-z-[1]"}
pageNumber={props.pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
{guideline.show && (
<Guidelines
x1={guideline.x1}
x2={guideline.x2}
y1={guideline.y1}
y2={guideline.y2}
/>
)}
<DragGuidelinesLayer
signBtnPosition={props.signBtnPosition}
showGuidelines={showGuidelines}
posWidth={posWidth}
posHeight={posHeight}
isSignYourself={props.signerPos?.length > 0 ? true : false}
/>
<WidgetsDragPreview
posWidth={posWidth}
posHeight={posHeight}
isSignYourself={props.signerPos?.length > 0 ? true : false}
uniqueId={props?.uniqueId}
data={props?.signerPos?.find((x) => x.Id === props?.uniqueId)}
isNeedSign={props?.pdfRequest}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
containerWH={props.containerWH}
scale={props?.scale}
/>
</div>
</RSC>
</>
);
}
export default RenderPdf;
@@ -9,7 +9,8 @@ function SelectLanguage(props) {
{ value: "es", text: "Española" }, //spanish
{ value: "fr", text: "Français" }, //french
{ value: "it", text: "Italiano" }, //italian
{ value: "de", text: "Deutsch" } //german
{ value: "de", text: "Deutsch" }, //german
{ value: "hi", text: "हिन्दी" } //hindi
];
const defaultLanguage = i18next.language || "en";
const [lang, setLang] = useState(defaultLanguage);
@@ -23,14 +24,14 @@ function SelectLanguage(props) {
<div
className={`${
!props.isProfile && " mt-[9px] pb-2 md:pb-0 "
} flex justify-center items-center `}
} flex justify-center items-center text-base-content`}
>
<select
value={lang}
onChange={handleChangeLang}
className={`${
!props.isProfile ? " md:w-[15%] w-[50%]" : "w-[180px]"
} op-select op-select-bordered bg-white op-select-sm `}
} op-select op-select-bordered op-select-sm `}
>
<option disabled>select</option>
{languages.map((item) => {
-814
View File
@@ -1,814 +0,0 @@
import React, { useRef, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import SignatureCanvas from "react-signature-canvas";
import Parse from "parse";
import {
generateTitleFromFilename,
getBase64FromUrl,
getSecureUrl
} from "../../constant/Utils";
import sanitizeFileName from "../../primitives/sanitizeFileName";
import { SaveFileSize } from "../../constant/saveFileSize";
import Loader from "../../primitives/Loader";
function SignPad(props) {
const { t } = useTranslation();
const [penColor, setPenColor] = useState("blue");
const allColor = ["blue", "red", "black"];
const canvasRef = useRef(null);
const [isDefaultSign, setIsDefaultSign] = useState(false);
const [isTab, setIsTab] = useState("");
const [isSignImg, setIsSignImg] = useState("");
const [textWidth, setTextWidth] = useState(0);
const [textHeight, setTextHeight] = useState(0);
const [signatureType, setSignatureType] = useState("");
const [isSignTypes, setIsSignTypes] = useState(true);
const [typedSignature, setTypedSignature] = useState("");
const fontOptions = [
{ value: "Fasthand" },
{ value: "Dancing Script" },
{ value: "Cedarville Cursive" },
{ value: "Delicious Handrawn" }
// Add more font options as needed
];
const [fontSelect, setFontSelect] = useState(fontOptions[0].value);
const [isSavedSign, setIsSavedSign] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const accesstoken = localStorage.getItem("accesstoken") || "";
const senderUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
);
const jsonSender = senderUser && JSON.parse(senderUser);
const currentUserName = jsonSender && jsonSender?.name;
useEffect(() => {
handleTab();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.signatureTypes]);
function handleTab() {
const signtypes = props?.signatureTypes || [];
const defaultIndex = signtypes?.findIndex(
(x) =>
x.name === "default" &&
x.enabled === true &&
props.defaultSign &&
(props?.currWidgetsDetails?.type || props.widgetType) !== "image" &&
(props?.currWidgetsDetails?.type || props.widgetType) !== "stamp"
);
const getIndex =
defaultIndex !== -1 // Check if the default index exists
? defaultIndex // If found, use it
: signtypes?.findIndex((x) => x.enabled === true);
if (getIndex !== -1) {
setIsSignTypes(true);
const tab = props?.signatureTypes[getIndex].name;
if (tab === "draw") {
setIsTab("draw");
setSignatureType("draw");
} else if (tab === "upload") {
props?.setIsImageSelect(true);
setIsTab("uploadImage");
} else if (tab === "typed") {
setIsTab("type");
} else if (tab === "default") {
setIsDefaultSign(true);
setIsTab("mysignature");
} else {
setIsTab(true);
}
} else {
setIsSignTypes(false);
}
}
function isTabEnabled(tabName) {
const isEnabled = props?.signatureTypes.find(
(x) => x.name === tabName
)?.enabled;
return isEnabled;
}
//function for clear signature image
const handleClear = () => {
if (isTab === "draw") {
if (canvasRef.current) {
canvasRef.current.clear();
} else if (props?.isStamp) {
props?.setImage("");
}
setIsSignImg("");
} else if (isTab === "uploadImage") {
props?.setImage("");
}
};
//function for set signature url
const handleSignatureChange = (data) => {
props?.setSignature(data);
setIsSignImg(data);
};
function base64StringtoFile(base64String, filename) {
let arr = base64String.split(","),
// type of uploaded image
mime = arr[0].match(/:(.*?);/)[1],
// decode base64
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const ext = mime.split("/").pop();
const name = `${filename}.${ext}`;
return new File([u8arr], name, { type: mime });
}
const uploadFile = async (file) => {
try {
const parseFile = new Parse.File(file.name, file);
const response = await parseFile.save();
if (response?.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(file.size, fileRes.url, tenantId);
return fileRes?.url;
} else {
alert(`${t("something-went-wrong-mssg")}`);
return false;
}
} else {
alert(`${t("something-went-wrong-mssg")}`);
return false;
}
} catch (err) {
console.log("sign upload err", err);
alert(`${err.message}`);
return false;
}
};
// `handlesavesign` is used to save signaute, initials, stamp as a default
const handleSaveSign = async () => {
if (isSignImg || props?.image?.src) {
setIsLoader(true);
try {
const User = Parse?.User?.current();
const sanitizename = generateTitleFromFilename(User?.get("name"));
const replaceSpace = sanitizeFileName(sanitizename);
let file;
if (isSignImg) {
file = base64StringtoFile(isSignImg, `${replaceSpace}__sign`);
} else {
file = base64StringtoFile(props?.image?.src, `${replaceSpace}__sign`);
}
const imageUrl = await uploadFile(file);
const userId = {
__type: "Pointer",
className: "_User",
objectId: User?.id
};
if (imageUrl) {
// below code is used to save or update default signaute, initials, stamp
try {
const signCls = new Parse.Object("contracts_Signature");
if (props?.saveSignCheckbox?.signId) {
signCls.id = props.saveSignCheckbox.signId;
}
if (
props.currWidgetsDetails?.type === "initials" ||
props?.widgetType === "initials"
) {
signCls.set("Initials", imageUrl);
} else if (
props.currWidgetsDetails?.type === "signature" ||
props?.widgetType === "signature"
) {
signCls.set("ImageURL", imageUrl);
}
signCls.set("UserId", userId);
const signRes = await signCls.save();
if (signRes) {
props.saveSignCheckbox.signId;
props.setSaveSignCheckbox((prev) => ({
...prev,
signId: signRes?.id
}));
const _signRes = JSON.parse(JSON.stringify(signRes));
if (
props.currWidgetsDetails?.type === "signature" ||
props?.widgetType === "signature"
) {
const defaultSign = await getBase64FromUrl(
_signRes?.ImageURL,
true
);
props.setDefaultSignImg(defaultSign);
} else if (
props.currWidgetsDetails?.type === "initials" ||
props?.widgetType === "initials"
) {
const defaultInitials = await getBase64FromUrl(
_signRes?.Initials,
true
);
props.setMyInitial(defaultInitials);
}
alert(t("saved-successfully"));
}
return signRes;
} catch (err) {
console.log(err);
alert(`${err.message}`);
} finally {
setIsLoader(false);
}
}
} catch (err) {
console.log("Err while saving signature", err);
}
}
};
const handleSaveBtn = async () => {
if (accesstoken && isSavedSign) {
await handleSaveSign();
resetToDefault();
} else {
resetToDefault();
}
};
const resetToDefault = () => {
props?.setCurrWidgetsDetails({});
if (!props?.image) {
if (isTab === "mysignature") {
setIsSignImg("");
if (props?.isInitial) {
props?.onSaveSign(signatureType, "initials");
} else {
props?.onSaveSign(null, "default");
}
} else {
if (isTab === "type") {
setIsSignImg("");
props?.onSaveSign(
null,
false,
!props?.isInitial && textWidth > 150 ? 150 : textWidth,
!props?.isInitial && textHeight > 35 ? 35 : textHeight,
typedSignature
);
} else {
setIsSignImg("");
canvasRef.current.clear();
props?.onSaveSign(signatureType);
}
}
setPenColor("blue");
} else {
setIsSignImg("");
props?.onSaveImage(signatureType);
}
props?.setIsSignPad(false);
props?.setIsInitial && props?.setIsInitial(false);
props?.setIsImageSelect(false);
setIsDefaultSign(false);
props?.setImage();
handleTab();
props?.setIsStamp(false);
};
//save button component
const SaveBtn = () => {
return (
<div>
{(isTab === "draw" || isTab === "uploadImage") && (
<button
type="button"
className="op-btn op-btn-ghost mr-1 mt-[2px]"
onClick={() => handleClear()}
>
{t("clear")}
</button>
)}
<button
onClick={() => handleSaveBtn()}
type="button"
className={`${
isSignImg ||
props?.image ||
isDefaultSign ||
textWidth ||
props.isAutoSign
? ""
: "pointer-events-none"
} op-btn op-btn-primary shadow-lg`}
disabled={
(isTab === "draw" && isSignImg) ||
(isTab === "image" && props?.image) ||
(isTab === "mysignature" && isDefaultSign) ||
(isTab === "type" && typedSignature) ||
props.isAutoSign
? false
: props?.image
? false
: true
}
>
{t("save")}
</button>
</div>
);
};
const autoSignAll = () => {
return (
<label className="cursor-pointer flex items-center mb-[6px] text-center text-[11px] md:text-base">
<input
className="mr-2 md:mr-3 op-checkbox op-checkbox-xs md:op-checkbox-sm"
type="checkbox"
value={props.isAutoSign}
onChange={(e) => {
props.setIsAutoSign(e.target.checked);
}}
/>
{t("auto-sign-mssg")}
</label>
);
};
//useEffect for set already draw or save signature url/text url of signature text type and draw type for initial type and signature type widgets
useEffect(() => {
if (props?.currWidgetsDetails && canvasRef.current && props.isSignPad) {
const isWidgetType = props?.currWidgetsDetails?.type;
const signatureType = props?.currWidgetsDetails?.signatureType;
const url = props?.currWidgetsDetails?.SignUrl;
//checking widget type and draw type signature url
if (props?.isInitial) {
if (isWidgetType === "initials" && signatureType === "draw" && url) {
canvasRef.current.fromDataURL(url);
}
} else if (
isWidgetType === "signature" &&
signatureType === "draw" &&
url
) {
canvasRef.current.fromDataURL(url);
}
const trimmedName = currentUserName && currentUserName?.trim();
const firstCharacter = trimmedName?.charAt(0);
const userName = props?.isInitial ? firstCharacter : currentUserName;
const signatureValue = props?.currWidgetsDetails?.typeSignature;
setTypedSignature(signatureValue || userName || "");
setFontSelect("Fasthand");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.isSignPad]);
useEffect(() => {
const loadFont = async () => {
try {
await document.fonts.load(`20px ${fontSelect}`);
const selectFontSTyle = fontOptions.find(
(font) => font.value === fontSelect
);
setFontSelect(selectFontSTyle?.value || fontOptions[0].value);
} catch (error) {
console.error("Error loading font:", error);
}
};
loadFont();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fontSelect]);
useEffect(() => {
// Load the default signature after the component mounts
if (canvasRef.current) {
canvasRef.current.fromDataURL(isSignImg);
}
if (isTab === "type") {
const trimmedName = typedSignature
? typedSignature?.trim()
: currentUserName?.trim();
const firstCharacter = trimmedName?.charAt(0);
const userName = props?.isInitial ? firstCharacter : typedSignature;
const signatureValue = props?.currWidgetsDetails?.typeSignature;
setTypedSignature(signatureValue || userName || "");
convertToImg(fontSelect, userName);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTab]);
//function for convert input text value in image
const convertToImg = async (fontStyle, text, color) => {
//get text content to convert in image
const textContent = text;
const fontfamily = fontStyle
? fontStyle
: fontSelect
? fontSelect
: "Fasthand";
const fontSizeValue = "40px";
//creating span for getting text content width
const span = document.createElement("span");
span.textContent = textContent;
span.style.font = `${fontSizeValue} ${fontfamily}`; // here put your text size and font family
span.style.color = color ? color : penColor;
span.style.display = "hidden";
document.body.appendChild(span); // Replace 'container' with the ID of the container element
//create canvas to render text in canvas and convert in image
const canvasElement = document.createElement("canvas");
// Draw the text content on the canvas
const ctx = canvasElement.getContext("2d");
const pixelRatio = window.devicePixelRatio || 1;
const addExtraWidth = props?.isInitial ? 10 : 50;
const width = span.offsetWidth + addExtraWidth;
const height = span.offsetHeight;
setTextWidth(width);
setTextHeight(height);
const font = span.style["font"];
// Set the canvas dimensions to match the span
canvasElement.width = width * pixelRatio;
canvasElement.height = height * pixelRatio;
// You can customize text styles if needed
ctx.font = font;
ctx.fillStyle = color ? color : penColor; // Set the text color
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.scale(pixelRatio, pixelRatio);
// Draw the content of the span onto the canvas
ctx.fillText(span.textContent, width / 2, height / 2); // Adjust the x,y-coordinate as needed
//remove span tag
document.body.removeChild(span);
// Convert the canvas to image data
const dataUrl = canvasElement.toDataURL("image/png");
props?.setSignature(dataUrl);
};
const PenColorComponent = (props) => {
return (
<div className="flex flex-row items-center m-[5px] gap-2">
{allColor.map((data, key) => {
return (
<i
key={key}
onClick={() => {
props?.convertToImg &&
props?.convertToImg(fontSelect, typedSignature, data);
setPenColor(allColor[key]);
}}
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
);
};
// `handleCancelBtn` function trigger when user click on cross button
const handleCancelBtn = () => {
setPenColor("blue");
props?.setIsSignPad(false);
props?.setIsInitial && props?.setIsInitial(false);
props?.setIsImageSelect(false);
setIsDefaultSign(false);
props?.setImage();
handleTab();
props?.setIsStamp(false);
};
const savesigncheckbox = (
<label className="cursor-pointer flex items-center mb-0 text-center text-[11px] md:text-base">
<input
className="mr-2 md:mr-3 op-checkbox op-checkbox-xs md:op-checkbox-sm"
type="checkbox"
checked={isSavedSign}
onChange={(e) => setIsSavedSign(e.target.checked)}
/>
Save {props?.currWidgetsDetails?.type || props?.widgetType}
</label>
);
return (
<div>
{props?.isSignPad && (
<div className="op-modal op-modal-open">
<div className="op-modal-box px-[13px] pt-2 pb-0">
{isLoader && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
<Loader />
</div>
)}
{isSignTypes ? (
<>
<div className="flex justify-between text-base-content items-center">
<div className="text-[1.2rem]">
<div className="flex flex-row justify-between mt-[3px]">
<div className="flex flex-row justify-between gap-[5px] md:gap-[8px] text-[11px] md:text-base">
{props?.isStamp ? (
<span className="text-base-content font-bold text-lg">
{props?.widgetType === "image" ||
props?.currWidgetsDetails?.type === "image"
? t("upload-image")
: t("upload-stamp-image")}
</span>
) : (
<>
{!props?.isInitial &&
props?.defaultSign &&
isTabEnabled("default") ? (
<div>
<span
onClick={() => {
setIsDefaultSign(true);
props?.setIsImageSelect(true);
setIsTab("mysignature");
setSignatureType("");
props?.setImage();
}}
className={`${
isTab === "mysignature"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("my-signature")}
</span>
</div>
) : (
props?.isInitial &&
props?.myInitial &&
isTabEnabled("default") && (
<div>
<span
onClick={() => {
setIsDefaultSign(true);
props?.setIsImageSelect(true);
setIsTab("mysignature");
setSignatureType("");
props?.setImage();
}}
className={`${
isTab === "mysignature"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("my-initials")}
</span>
</div>
)
)}
{isTabEnabled("draw") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
props?.setIsImageSelect(false);
setIsTab("draw");
props?.setImage();
if (isSignImg) {
props?.setSignature(isSignImg);
}
}}
className={`${
isTab === "draw"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("draw")}
</span>
</div>
)}
{isTabEnabled("upload") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
props?.setIsImageSelect(true);
setIsTab("uploadImage");
setSignatureType("");
}}
className={`${
isTab === "uploadImage"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("upload-image")}
</span>
</div>
)}
{isTabEnabled("typed") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
props?.setIsImageSelect(false);
setIsTab("type");
setSignatureType("");
props?.setImage();
}}
className={`${
isTab === "type"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("type")}
</span>
</div>
)}
</>
)}
</div>
</div>
</div>
<div
className="text-[1.5rem] cursor-pointer"
onClick={handleCancelBtn}
>
&times;
</div>
</div>
<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?.isImageSelect || props?.isStamp ? (
!props?.image ? (
<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`}
onClick={() => props?.imageRef.current.click()}
>
<input
type="file"
onChange={props?.onImageChange}
className="filetype"
accept="image/png,image/jpeg"
ref={props?.imageRef}
hidden
/>
<i className="fa-light fa-cloud-upload-alt uploadImgLogo"></i>
<div className="text-[10px]">{t("upload")}</div>
</div>
</div>
) : (
<>
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] mb-[6px] overflow-hidden`}
>
<img
alt="print img"
ref={props?.imageRef}
src={props?.image.src}
draggable="false"
className="object-contain h-full w-full"
/>
</div>
</div>
{props.setIsAutoSign && autoSignAll()}
<div className="flex justify-end">
<SaveBtn />
</div>
</>
)
) : isTab === "type" ? (
<div>
<div className="flex justify-between items-center">
<span className="mr-[5px] text-[12px]">
{props?.isInitial
? t("initial-teb")
: t("signature-tab")}
:
</span>
<input
maxLength={props?.isInitial ? 3 : 30}
style={{ fontFamily: fontSelect, color: penColor }}
type="text"
className="ml-1 op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-[20px]"
placeholder={
props?.isInitial
? t("initial-type")
: t("signature-type")
}
value={typedSignature}
onChange={(e) => {
setTypedSignature(e.target.value);
convertToImg(fontSelect, e.target.value);
}}
/>
</div>
<div className="border-[1px] border-[#d6d3d3] mt-[10px] ml-[5px]">
{fontOptions.map((font, ind) => {
return (
<div
key={ind}
style={{
cursor: "pointer",
fontFamily: font.value,
backgroundColor:
fontSelect === font.value &&
"rgb(206 225 247)"
}}
onClick={() => {
setFontSelect(font.value);
convertToImg(font.value, typedSignature);
}}
>
<div
className="py-[5px] px-[10px] text-[20px]"
style={{ color: penColor }}
>
{typedSignature
? typedSignature
: "Your signature"}
</div>
</div>
);
})}
</div>
<div className="flex flex-col justify-between mt-[10px]">
{props.setIsAutoSign && autoSignAll()}
<div className="flex flex-row justify-between mt-[10px]">
<PenColorComponent convertToImg={convertToImg} />
<SaveBtn />
</div>
</div>
</div>
) : (
<>
<div className="flex justify-center">
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
className: `${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} border-[1.3px] border-[#007bff]`
}}
onEnd={() =>
handleSignatureChange(
canvasRef.current?.toDataURL()
)
}
dotSize={1}
/>
</div>
<div className="flex flex-col justify-between mt-[10px]">
{props.setIsAutoSign && autoSignAll()}
{accesstoken &&
props?.saveSignCheckbox?.isVisible &&
savesigncheckbox}
<div className="flex flex-row justify-between mt-[10px]">
<PenColorComponent />
<SaveBtn />
</div>
</div>
</>
)}
</div>
</>
) : (
<div>
<div className="relative flex flex-row items-center justify-between">
<div className="text-base-content font-bold text-lg">
Signature
</div>
<div
className="text-[1.5rem] cursor-pointer"
onClick={handleCancelBtn}
>
&times;
</div>
</div>
<div className="mx-3 mb-6 mt-3">
<p>{t("at-least-one-signature-type")}</p>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}
export default SignPad;
@@ -27,12 +27,12 @@ function SignerListComponent(props) {
return (
<div
className="rounded-xl mx-1 flex flex-row items-center py-[10px] mt-1"
className="rounded-xl mx-1 flex flex-row flex-grow-0 items-center py-[10px] mt-1"
style={{ background: checkSignerBackColor(props.obj) }}
>
<div
style={{ background: checkUserNameColor(props.obj) }}
className="flex w-[30px] h-[30px] rounded-full justify-center items-center mx-1"
className="flex flex-shrink-0 w-[30px] h-[30px] rounded-full justify-center items-center mx-1"
>
<span className="text-[12px] text-center font-bold text-black uppercase">
{getFirstLetter(
@@ -40,11 +40,11 @@ function SignerListComponent(props) {
)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[12px] font-bold text-[#424242] w-[100px] whitespace-nowrap overflow-hidden text-ellipsis">
<div className="flex flex-grow-0 flex-col overflow-hidden pr-2">
<span className="text-[12px] font-bold truncate whitespace-nowrap">
{props.obj?.Name || props?.obj?.Role}
</span>
<span className="text-[10px] font-medium text-[#424242] w-[100px] whitespace-nowrap overflow-hidden text-ellipsis">
<span className="text-[10px] font-medium truncate whitespace-nowrap">
{props.obj?.Email || props.obj?.email}
</span>
</div>
@@ -1,16 +1,23 @@
import React from "react";
import RecipientList from "./RecipientList";
import { Tooltip } from "react-tooltip";
// import { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next";
function SignerListPlace(props) {
const { t } = useTranslation();
const handleAddRecipient = () => {
props?.setIsAddSigner(true);
props.setIsTour && props.setIsTour(false);
};
return (
<div>
<div className="mx-2 pr-2 pt-2 pb-1 text-[15px] text-base-content font-semibold border-b-[1px] border-base-300">
<span className="relative">
{props.title ? props.title : "Recipients"}
<span className="absolute text-xs z-[30] mt-1 ml-0.5">
<sup onClick={() => props.setIsTour && props.setIsTour(true)}>
<i className="ml-1 cursor-pointer fa-light fa-question rounded-full border-[1px] border-base-content text-[11px] py-[1px] px-[3px]"></i>
</sup>
{/* <span className="absolute text-xs z-[30] mt-1 ml-0.5">
{props?.title === "Roles" && (
<>
<a data-tooltip-id="my-tooltip">
@@ -30,7 +37,7 @@ function SignerListPlace(props) {
</Tooltip>
</>
)}
</span>
</span> */}
</span>
</div>
<div className="overflow-auto hide-scrollbar max-h-[180px]">
@@ -53,7 +60,7 @@ function SignerListPlace(props) {
data-tut="addRecipient"
className="op-btn op-btn-accent op-btn-outline w-full mt-[14px]"
disabled={props?.isMailSend ? true : false}
onClick={() => props.setIsAddSigner(true)}
onClick={handleAddRecipient}
>
<i className="fa-light fa-plus"></i> {t("add-recipients")}
</div>
@@ -1,81 +0,0 @@
import React from "react";
import ModalUi from "../../primitives/ModalUi";
import { fontColorArr, fontsizeArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
function TextFontSetting(props) {
const { t } = useTranslation();
return (
<ModalUi
headerColor={"#dc3545"}
isOpen={props.isTextSetting}
reduceWidth={"max-w-[350px]"}
title={t("text-field")}
handleClose={() => props.setIsTextSetting(false)}
>
<div className="h-full p-[20px]">
<div className="flex items-center">
<span>{t("font-size")} :</span>
<select
className="ml-[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={
props.fontSize ||
props.currWidgetsDetails?.options?.fontSize ||
12
}
onChange={(e) => {
props.setFontSize(parseInt(e.target.value));
}}
>
{fontsizeArr.map((size, ind) => {
return (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
);
})}
</select>
<div className="flex flex-row gap-1 items-center ml-4">
<span>{t("color")}: </span>
<select
value={
props.fontColor ||
props.currWidgetsDetails?.options?.fontColor ||
"black"
}
onChange={(e) => props.setFontColor(e.target.value)}
className="ml-[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
>
{fontColorArr.map((color, ind) => {
return (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
);
})}
</select>
<span
style={{
background:
props.fontColor ||
props.currWidgetsDetails?.options?.fontColor
}}
className="w-5 h-[19px] ml-1"
></span>
</div>
</div>
<div className="h-[1px] bg-[#9f9f9f] w-full mt-[15px] mb-2"></div>
<button
onClick={() => props.handleSaveFontSize()}
type="button"
className="op-btn op-btn-primary mt-2"
>
{t("save")}
</button>
</div>
</ModalUi>
);
}
export default TextFontSetting;
@@ -0,0 +1,80 @@
import ModalUi from "../../primitives/ModalUi";
import { fontColorArr, fontsizeArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
function TextFontSetting(props) {
const { t } = useTranslation();
return (
<ModalUi
headerColor={"#dc3545"}
isOpen={props.isTextSetting}
reduceWidth={"max-w-[350px]"}
title={t("text-field")}
handleClose={() => props.setIsTextSetting(false)}
>
<div className="h-full p-[20px] text-base-content">
<div className="flex flex-col md:flex-row md:items-center gap-3">
{/* Font Size Selector */}
<div className="flex items-center gap-2">
<span className="whitespace-nowrap">{t("font-size")}:</span>
<select
className="ml-[7px] w-[60%] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={
props.fontSize ||
props.currWidgetsDetails?.options?.fontSize ||
12
}
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option key={ind} className="text-[13px]" value={size}>
{size}
</option>
))}
</select>
</div>
{/* Font Color Selector */}
<div className="flex items-center">
<span className="whitespace-nowrap">{t("color")}:</span>
<select
className="ml-[33px] md:ml-4 w-[65%] md:w-[full] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={
props.fontColor ||
props.currWidgetsDetails?.options?.fontColor ||
"black"
}
onChange={(e) => props.setFontColor(e.target.value)}
>
{fontColorArr.map((color, ind) => (
<option key={ind} value={color}>
{t(`color-type.${color}`)}
</option>
))}
</select>
{/* Color Preview Box */}
<span
className="w-5 h-5 ml-2 rounded border border-gray-300"
style={{
backgroundColor:
props.fontColor ||
props.currWidgetsDetails?.options?.fontColor ||
"black"
}}
></span>
</div>
</div>
<div className="h-[1px] bg-[#9f9f9f] w-full mt-[15px] mb-2"></div>
<button
onClick={() => props.handleSaveFontSize()}
type="button"
className="op-btn op-btn-primary mt-2"
>
{t("save")}
</button>
</div>
</ModalUi>
);
}
export default TextFontSetting;

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