Compare commits
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
[](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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -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-Zustellungseinstellungen 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -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
|
||||
|
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 :
|
||||
<Text style={styles.textStyle2}>{getSignerName[0].Name}</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Email :
|
||||
<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 :
|
||||
<Text style={styles.textStyle2}>{pdfData[0].objectId}</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Document Name :
|
||||
<Text style={styles.textStyle2}>{pdfData[0].Name}</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Organization :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].ExtUserPtr.Company}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Completed on : {changeCompletedDate()}
|
||||
</Text>
|
||||
{multiSigner && multiSigner.length > 0 && (
|
||||
<Text style={styles.textStyle}>
|
||||
Signers :
|
||||
<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 :
|
||||
<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 : <Text style={styles.textStyle2}>1</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Name :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].ExtUserPtr.Name}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Email :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].ExtUserPtr.Email}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Accessed from :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].AuditTrail &&
|
||||
pdfData[0].AuditTrail[0].ipAddress}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text style={styles.textStyle}>
|
||||
Signed on : {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 there’s any change in the dragging state or the dragged element’s position
|
||||
// Compare the current drag state with the previously stored one
|
||||
const hasStateChanged =
|
||||
prevDragState.current.isDragging !== isDraggingWidgets || // Drag started or stopped
|
||||
prevDragState.current.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 it’s a dropdown and it’s 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;
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
@@ -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}
|
||||
>
|
||||
×
|
||||
</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}
|
||||
>
|
||||
×
|
||||
</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;
|
||||