Compare commits
@@ -241,6 +241,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"translation"
|
"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
|
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)
|
# 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
|
# 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
|
REACT_APP_APPID=opensign
|
||||||
|
|
||||||
|
|
||||||
# Backend ExpressJS config ****************************************************************************************************************************************************************************************
|
# 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.
|
# Name of the app. It will be visible in the verification emails sent out.
|
||||||
appName=open_sign_server
|
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.
|
# 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
|
# Provide Pass pharse of above PFX or p12 document
|
||||||
PASS_PHRASE=opensign
|
PASS_PHRASE=opensign
|
||||||
|
|
||||||
|
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
|
||||||
|
APP_ID=opensign
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
name: ci
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
- 'staging'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@@ -14,34 +21,46 @@ jobs:
|
|||||||
dockerfile: apps/OpenSign/Dockerhubfile
|
dockerfile: apps/OpenSign/Dockerhubfile
|
||||||
- image: opensign/opensignserver
|
- image: opensign/opensignserver
|
||||||
dockerfile: apps/OpenSignServer/Dockerhubfile
|
dockerfile: apps/OpenSignServer/Dockerhubfile
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
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
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
|
||||||
name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: ${{ matrix.image }}
|
images: ${{ matrix.image }}
|
||||||
-
|
|
||||||
name: Debug - List files opensign
|
- name: Debug - List files opensign
|
||||||
run: ls -R apps/OpenSign/ # Adjust the path as needed
|
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
|
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
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{ matrix.dockerfile }}
|
file: ${{ matrix.dockerfile }}
|
||||||
push: true
|
push: true
|
||||||
|
# 👇 Build for both AMD64 & ARM64
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{$HOST_URL} {
|
{$HOST_URL} {
|
||||||
reverse_proxy client:3000
|
reverse_proxy client:3000
|
||||||
handle_path /app/* {
|
handle_path /api/* {
|
||||||
reverse_proxy server:8080
|
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.
|
- **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.
|
- **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.
|
- **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.
|
- **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="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'>
|
<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 -
|
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 -
|
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/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/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/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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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
|
# Use an official Node runtime as the base image
|
||||||
FROM node:18
|
FROM node:22.14.0
|
||||||
|
|
||||||
# Set the working directory inside the container
|
# Set the working directory inside the container
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -13,16 +13,24 @@ RUN npm install
|
|||||||
# Copy the current directory contents into the container
|
# Copy the current directory contents into the container
|
||||||
COPY apps/OpenSign/ .
|
COPY apps/OpenSign/ .
|
||||||
COPY apps/OpenSign/.husky .
|
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
|
# Define environment variables if needed
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV REACT_APP_DEPLOYMENT=free_selfhost
|
ENV GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
# build
|
# build
|
||||||
RUN npm run 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
|
# Make port 3000 available to the world outside this container
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["npm", "start"]
|
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>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="opensigncss">
|
<html lang="en" data-theme="opensigncss">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<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="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
|
<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"
|
<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" />
|
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
|
||||||
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
||||||
<title>OpenSign™</title>
|
<title>...Loading</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root" style="touch-action:pan-x pan-y;"></div>
|
<div id="root" style="touch-action:pan-x pan-y;"></div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,73 +1,71 @@
|
|||||||
{
|
{
|
||||||
"name": "open_sign",
|
"name": "open_sign",
|
||||||
"version": "0.1.0",
|
"version": "2.35.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.2",
|
"@formkit/auto-animate": "^0.9.0",
|
||||||
"@lottiefiles/dotlottie-react": "^0.12.3",
|
"@imgly/background-removal": "^1.7.0",
|
||||||
|
"@lottiefiles/dotlottie-react": "^0.17.12",
|
||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@radix-ui/themes": "^3.1.6",
|
"@radix-ui/themes": "^3.2.1",
|
||||||
"@react-pdf/renderer": "^4.1.6",
|
"@reactour/tour": "^3.8.0",
|
||||||
"@reduxjs/toolkit": "^2.5.1",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.13.2",
|
||||||
|
"body-scroll-lock": "^4.0.0-beta.0",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^23.16.8",
|
"i18next": "^25.7.3",
|
||||||
"i18next-browser-languagedetector": "^8.0.2",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-http-backend": "^3.0.1",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nth-check": "^2.1.1",
|
"parse": "^8.0.0",
|
||||||
"parse": "^5.3.0",
|
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pkijs": "^3.3.3",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"radix-ui": "^1.0.1",
|
"prismjs": "^1.30.0",
|
||||||
"react": "^18.2.0",
|
"quill-html-edit-button": "^3.0.0",
|
||||||
"react-bootstrap": "^2.10.9",
|
"radix-ui": "^1.4.3",
|
||||||
"react-confetti": "^6.2.2",
|
"react": "^19.2.3",
|
||||||
"react-cookie": "^7.2.2",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-datepicker": "^7.6.0",
|
"react-confetti": "^6.4.0",
|
||||||
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dnd-multi-backend": "^9.0.0",
|
"react-dnd-multi-backend": "^9.0.0",
|
||||||
"react-dnd-touch-backend": "^16.0.1",
|
"react-dom": "^19.2.3",
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-gtm-module": "^2.0.11",
|
"react-gtm-module": "^2.0.11",
|
||||||
"react-helmet": "^6.1.0",
|
"react-i18next": "^16.5.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-konva": "^19.2.1",
|
||||||
"react-konva": "^18.2.10",
|
"react-pdf": "^10.2.0",
|
||||||
"react-pdf": "^9.2.1",
|
"react-quill-new": "^3.7.0",
|
||||||
"react-quill-new": "^3.3.3",
|
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-rnd": "^10.4.14",
|
"react-rnd": "^10.5.2",
|
||||||
"react-router": "^7.1.5",
|
"react-router": "^7.11.0",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"react-scrollbars-custom": "^4.1.1",
|
"react-scrollbars-custom": "^4.1.1",
|
||||||
"react-select": "^5.10.0",
|
"react-select": "^5.10.2",
|
||||||
"react-signature-canvas": "^1.0.7",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
|
||||||
"react-timezone-select": "^3.2.8",
|
"react-timezone-select": "^3.2.8",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-to-print": "^3.2.0",
|
||||||
"react-web-share": "^2.0.2",
|
"react-tooltip": "^5.30.0",
|
||||||
"reactour": "^1.19.4",
|
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-thunk": "^3.1.0",
|
"regex-parser": "^2.3.1",
|
||||||
"regex-parser": "^2.3.0",
|
"serve": "^14.2.5",
|
||||||
"serve": "^14.2.4",
|
"web-vitals": "^5.1.0",
|
||||||
"styled-components": "^5.3.0",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||||
"web-vitals": "^4.2.4",
|
|
||||||
"ws": "^8.18.0",
|
|
||||||
"xlsx": "^0.18.5"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run version && react-scripts build",
|
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||||
"start-dev": "react-scripts start",
|
"start-dev": "vite",
|
||||||
|
"dev": "vite",
|
||||||
|
"preview": "vite preview",
|
||||||
"start": "serve -s build",
|
"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": "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\"",
|
"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",
|
"build-win": "npm run version-win && vite build",
|
||||||
"test": "react-scripts test",
|
"test": "vitest run",
|
||||||
"eject": "react-scripts eject",
|
"test:watch": "vitest",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"commit": "cz"
|
"commit": "cz"
|
||||||
},
|
},
|
||||||
@@ -95,34 +93,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.8",
|
"@babel/core": "^7.28.3",
|
||||||
"@babel/preset-env": "^7.26.8",
|
"@babel/preset-env": "^7.28.3",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@babel/runtime-corejs2": "^7.26.7",
|
"@babel/runtime-corejs2": "^7.28.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"babel-loader": "^9.2.1",
|
"@testing-library/react": "^16.3.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"@vitest/ui": "^4.0.16",
|
||||||
|
"autoprefixer": "^10.4.23",
|
||||||
|
"babel-loader": "^10.0.0",
|
||||||
"commitizen": "^4.3.1",
|
"commitizen": "^4.3.1",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.2.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"daisyui": "^4.12.23",
|
"daisyui": "^4.12.24",
|
||||||
"dotenv": "^16.4.7",
|
"eslint": "^9.39.2",
|
||||||
"dotenv-webpack": "^8.1.0",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint": "^9.20.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-prettier": "^5.2.3",
|
"jsdom": "^27.4.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"lint-staged": "^16.2.7",
|
||||||
"lint-staged": "^15.4.3",
|
"postcss": "^8.5.6",
|
||||||
"mini-css-extract-plugin": "^2.9.2",
|
"prettier": "^3.7.4",
|
||||||
"postcss": "^8.5.1",
|
"pretty-quick": "^4.2.2",
|
||||||
"prettier": "^3.5.0",
|
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||||
"pretty-quick": "^4.0.0",
|
"tailwindcss": "^3.4.19",
|
||||||
"tailwindcss": "^3.4.17",
|
"tmp": "^0.2.5",
|
||||||
"terser-webpack-plugin": "^5.3.11",
|
"vite": "^7.3.0",
|
||||||
"webpack-cli": "^5.1.4"
|
"vite-plugin-svgr": "^4.5.0",
|
||||||
|
"vite-tsconfig-paths": "^6.0.3",
|
||||||
|
"vitest": "^4.0.16"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"nth-check": "$nth-check",
|
"tmp": "$tmp"
|
||||||
"ws": "$ws"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || 22"
|
"node": "18 || 20 || 22"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB |
|
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 Parse from "parse";
|
||||||
import Title from "./Title";
|
|
||||||
import Loader from "../primitives/Loader";
|
import Loader from "../primitives/Loader";
|
||||||
import {
|
import {
|
||||||
copytoData,
|
copytoData,
|
||||||
@@ -9,7 +8,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
emailRegex,
|
emailRegex,
|
||||||
} from "../constant/const";
|
} from "../constant/const";
|
||||||
import { useTranslation } from "react-i18next";
|
import {
|
||||||
|
useTranslation
|
||||||
|
} from "react-i18next";
|
||||||
|
import { withSessionValidation } from "../utils";
|
||||||
|
|
||||||
function generatePassword(length) {
|
function generatePassword(length) {
|
||||||
const characters =
|
const characters =
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
@@ -66,181 +69,70 @@ const AddUser = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Define a function to handle form submission
|
// Define a function to handle form submission
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = withSessionValidation(async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!emailRegex.test(formdata.email)) {
|
if (!emailRegex.test(formdata.email)) {
|
||||||
alert("Please enter a valid email address.");
|
alert(t("valid-email-alert"));
|
||||||
} else {
|
} else {
|
||||||
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||||
setIsFormLoader(true);
|
setIsFormLoader(true);
|
||||||
const res = await checkUserExist();
|
const res = await checkUserExist();
|
||||||
if (res) {
|
if (res) {
|
||||||
props.setIsAlert({ type: "danger", msg: t("user-already-exist") });
|
props.showAlert("danger", t("user-already-exist"));
|
||||||
setIsFormLoader(false);
|
setIsFormLoader(false);
|
||||||
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1000);
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
if (localStorage.getItem("TenantId")) {
|
||||||
const extUser = new Parse.Object("contracts_Users");
|
|
||||||
extUser.set("Name", formdata.name);
|
|
||||||
if (formdata.phone) {
|
|
||||||
extUser.set("Phone", formdata.phone);
|
|
||||||
}
|
|
||||||
extUser.set("Email", formdata.email);
|
|
||||||
extUser.set("UserRole", `contracts_${formdata.role}`);
|
|
||||||
if (formdata?.team) {
|
|
||||||
extUser.set("TeamIds", [
|
|
||||||
{
|
|
||||||
__type: "Pointer",
|
|
||||||
className: "contracts_Teams",
|
|
||||||
objectId: formdata.team
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (localUser && localUser.OrganizationId) {
|
|
||||||
extUser.set("OrganizationId", {
|
|
||||||
__type: "Pointer",
|
|
||||||
className: "contracts_Organizations",
|
|
||||||
objectId: localUser.OrganizationId.objectId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (localUser && localUser.Company) {
|
|
||||||
extUser.set("Company", localUser.Company);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localStorage.getItem("TenantId")) {
|
|
||||||
extUser.set("TenantId", {
|
|
||||||
__type: "Pointer",
|
|
||||||
className: "partners_Tenant",
|
|
||||||
objectId: localStorage.getItem("TenantId")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const timezone = usertimezone;
|
const timezone = usertimezone;
|
||||||
if (timezone) {
|
|
||||||
extUser.set("Timezone", timezone);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const _users = Parse.Object.extend("User");
|
const params = {
|
||||||
const _user = new _users();
|
name: formdata.name,
|
||||||
_user.set("name", formdata.name);
|
email: formdata.email,
|
||||||
_user.set("username", formdata.email);
|
phone: formdata.phone,
|
||||||
_user.set("email", formdata.email);
|
password: formdata.password,
|
||||||
_user.set("password", formdata.password);
|
role: formdata.role,
|
||||||
if (formdata.phone) {
|
team: formdata.team,
|
||||||
_user.set("phone", formdata.phone);
|
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();
|
||||||
}
|
}
|
||||||
|
if (props.handleUserData) {
|
||||||
const user = await _user.save();
|
if (formdata?.team) {
|
||||||
if (user) {
|
const team = teamList.find((x) => x.objectId === formdata.team);
|
||||||
const currentUser = Parse.User.current();
|
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||||
extUser.set(
|
y.objectId === team.objectId ? team : y
|
||||||
"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) {
|
props.handleUserData(parseData);
|
||||||
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: ""
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
setIsFormLoader(false);
|
||||||
|
setFormdata({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
team: "",
|
||||||
|
role: ""
|
||||||
|
});
|
||||||
|
props.showAlert("success", t("user-created-successfully"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("err ", err);
|
console.log("err", err);
|
||||||
if (err.code === 202) {
|
setIsFormLoader(false);
|
||||||
const params = { email: formdata.email };
|
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||||
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: ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} else {
|
||||||
console.log("err", err);
|
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||||
setIsFormLoader(false);
|
|
||||||
props.setIsAlert({
|
|
||||||
type: "danger",
|
|
||||||
msg: t("something-went-wrong-mssg")
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setTimeout(
|
|
||||||
() => props.setIsAlert({ type: "success", msg: "" }),
|
|
||||||
1500
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Define a function to handle the "add yourself" checkbox
|
// Define a function to handle the "add yourself" checkbox
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@@ -259,12 +151,10 @@ const AddUser = (props) => {
|
|||||||
|
|
||||||
const copytoclipboard = (text) => {
|
const copytoclipboard = (text) => {
|
||||||
copytoData(text);
|
copytoData(text);
|
||||||
props.setIsAlert({ type: "success", msg: t("copied") });
|
props.showAlert("success", t("copied"));
|
||||||
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1500); // Reset copied state after 1.5 seconds
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
||||||
<Title title={t("add-user")} />
|
|
||||||
{isFormLoader && (
|
{isFormLoader && (
|
||||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
||||||
<Loader />
|
<Loader />
|
||||||
@@ -275,7 +165,7 @@ const AddUser = (props) => {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("name")}
|
{t("name")}
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
@@ -291,12 +181,13 @@ const AddUser = (props) => {
|
|||||||
onInput={(e) => e.target.setCustomValidity("")}
|
onInput={(e) => e.target.setCustomValidity("")}
|
||||||
required
|
required
|
||||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
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>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("email")}
|
{t("email")}
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
@@ -312,10 +203,11 @@ const AddUser = (props) => {
|
|||||||
}
|
}
|
||||||
onInput={(e) => e.target.setCustomValidity("")}
|
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"
|
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>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="block text-xs text-gray-700 font-semibold">
|
<label className="block text-xs font-semibold">
|
||||||
{t("password")}
|
{t("password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
||||||
@@ -332,10 +224,9 @@ const AddUser = (props) => {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="phone"
|
htmlFor="phone"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("phone")}
|
{t("phone")}
|
||||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -349,7 +240,7 @@ const AddUser = (props) => {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="phone"
|
htmlFor="phone"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("Role")}
|
{t("Role")}
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
@@ -13,18 +13,29 @@ const BulkSendUi = (props) => {
|
|||||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
||||||
const [isSubmit, setIsSubmit] = useState(false);
|
const [isSubmit, setIsSubmit] = useState(false);
|
||||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||||
const [isBulkAvailable, setIsBulkAvailable] = useState(false);
|
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
||||||
const [isLoader, setIsLoader] = useState(false);
|
const [isLoader, setIsLoader] = useState(false);
|
||||||
|
const [signers, setSigners] = useState([]);
|
||||||
|
const [emails, setEmails] = useState([]);
|
||||||
|
const [isPrefillExist, setIsPrefillExist] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
signatureExist();
|
signatureExist();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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 signatureExist = async () => {
|
||||||
setIsBulkAvailable(true);
|
const isPrefill =
|
||||||
|
props?.Placeholders?.some((x) => x?.Role === "prefill") || false;
|
||||||
|
if (isPrefill) {
|
||||||
|
setIsPrefillExist(isPrefill);
|
||||||
|
}
|
||||||
|
setIsDisableBulkSend(false);
|
||||||
const getPlaceholder = props?.Placeholders;
|
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) =>
|
placeholderObj?.placeHolder?.some((holder) =>
|
||||||
holder?.pos?.some((posItem) => posItem?.type === "signature")
|
holder?.pos?.some((posItem) => posItem?.type === "signature")
|
||||||
)
|
)
|
||||||
@@ -47,7 +58,14 @@ const BulkSendUi = (props) => {
|
|||||||
(() => {
|
(() => {
|
||||||
if (props?.Placeholders?.length > 0) {
|
if (props?.Placeholders?.length > 0) {
|
||||||
let users = [];
|
let users = [];
|
||||||
|
let emails = [];
|
||||||
props?.Placeholders?.forEach((element) => {
|
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) {
|
if (!element.signerObjId) {
|
||||||
users = [
|
users = [
|
||||||
...users,
|
...users,
|
||||||
@@ -60,7 +78,10 @@ const BulkSendUi = (props) => {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
setEmails(emails);
|
||||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||||
|
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||||
|
setSigners(signer);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -82,10 +103,16 @@ const BulkSendUi = (props) => {
|
|||||||
|
|
||||||
function validateEmails(data) {
|
function validateEmails(data) {
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
|
let email = "";
|
||||||
for (const field of item.fields) {
|
for (const field of item.fields) {
|
||||||
if (!emailRegex.test(field.email)) {
|
if (!emailRegex.test(field.email)) {
|
||||||
alert(`Invalid email found: ${field.email}`);
|
alert(t("invalid-email-found", { email: field.email }));
|
||||||
return false;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,78 +122,75 @@ const BulkSendUi = (props) => {
|
|||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsSubmit(true);
|
setIsSubmit(true);
|
||||||
if (validateEmails(forms)) {
|
if (validateEmails(forms)) {
|
||||||
// Create a copy of Placeholders array from props.item
|
// Create a copy of Placeholders array from props.item
|
||||||
let Placeholders = [...props.Placeholders];
|
let Placeholders = [...props.Placeholders];
|
||||||
// Initialize an empty array to store updated documents
|
// Initialize an empty array to store updated documents
|
||||||
let Documents = [];
|
let Documents = [];
|
||||||
// Loop through each form
|
// Loop through each form
|
||||||
forms.forEach((form) => {
|
forms.forEach((form) => {
|
||||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||||
let existSigner = [];
|
let existSigner = [];
|
||||||
form.fields.map((data) => {
|
form.fields.map((data) => {
|
||||||
if (data.signer) {
|
if (data.signer) {
|
||||||
existSigner.push(data.signer);
|
existSigner.push(data.signer);
|
||||||
}
|
|
||||||
});
|
|
||||||
// Map through the copied Placeholders array to update email values
|
|
||||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
|
||||||
// Find the field in the current form that matches the placeholder Id
|
|
||||||
const field = form.fields.find(
|
|
||||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
|
||||||
);
|
|
||||||
// If a matching field is found, update the email value in the placeholder
|
|
||||||
const signer = field?.signer?.objectId ? field.signer : "";
|
|
||||||
if (field) {
|
|
||||||
if (signer) {
|
|
||||||
return {
|
|
||||||
...placeholder,
|
|
||||||
signerObjId: field?.signer?.objectId || "",
|
|
||||||
signerPtr: signer
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...placeholder,
|
|
||||||
email: field.email,
|
|
||||||
signerObjId: field?.signer?.objectId || "",
|
|
||||||
signerPtr: signer
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
// If no matching field is found, keep the placeholder as is
|
// Map through the copied Placeholders array to update email values
|
||||||
return placeholder;
|
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||||
});
|
// Find the field in the current form that matches the placeholder Id
|
||||||
|
const field = form.fields.find(
|
||||||
|
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||||
|
);
|
||||||
|
// If a matching field is found, update the email value in the placeholder
|
||||||
|
const signer = field?.signer?.objectId ? field.signer : "";
|
||||||
|
if (field) {
|
||||||
|
if (signer) {
|
||||||
|
return {
|
||||||
|
...placeholder,
|
||||||
|
signerObjId: field?.signer?.objectId || "",
|
||||||
|
signerPtr: signer
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...placeholder,
|
||||||
|
email: field.email,
|
||||||
|
signerObjId: field?.signer?.objectId || "",
|
||||||
|
signerPtr: signer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no matching field is found, keep the placeholder as is
|
||||||
|
return placeholder;
|
||||||
|
});
|
||||||
|
|
||||||
// Push a new document object with updated Placeholders into the Documents array
|
// Push a new document object with updated Placeholders into the Documents array
|
||||||
if (existSigner?.length > 0) {
|
if (existSigner?.length > 0) {
|
||||||
Documents.push({
|
Documents.push({
|
||||||
...props.item,
|
...props.item,
|
||||||
Placeholders: updatedPlaceholders,
|
Placeholders: updatedPlaceholders,
|
||||||
Signers: props.item.Signers
|
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||||
? [...props.item.Signers, ...existSigner]
|
});
|
||||||
: [...existSigner]
|
} else {
|
||||||
});
|
Documents.push({
|
||||||
} else {
|
...props.item,
|
||||||
Documents.push({
|
Placeholders: updatedPlaceholders,
|
||||||
...props.item,
|
SignatureType: props.signatureType,
|
||||||
Placeholders: updatedPlaceholders,
|
Signers: signers
|
||||||
SignatureType: props.signatureType
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
});
|
await batchQuery(Documents);
|
||||||
await batchQuery(Documents);
|
} else {
|
||||||
} else {
|
setIsSubmit(false);
|
||||||
setIsSubmit(false);
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const batchQuery = async (Documents) => {
|
const batchQuery = async (Documents) => {
|
||||||
const token =
|
const token =
|
||||||
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
|
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
|
||||||
const functionsUrl = `${localStorage.getItem(
|
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
|
||||||
"baseUrl"
|
|
||||||
)}functions/batchdocuments`;
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||||
@@ -175,13 +199,14 @@ const BulkSendUi = (props) => {
|
|||||||
const params = { Documents: JSON.stringify(Documents) };
|
const params = { Documents: JSON.stringify(Documents) };
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||||
// console.log("res ", res);
|
|
||||||
if (res.data && res.data.result) {
|
if (res.data && res.data.result) {
|
||||||
props.handleClose("success", Documents?.length);
|
props.handleClose("success", Documents?.length);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Err ", err);
|
const message =
|
||||||
props.handleClose("error", 0);
|
err?.response?.data?.error || err?.message || "something went wrong.";
|
||||||
|
console.error("Error sending documents:", message);
|
||||||
|
props.handleClose("error", 0, message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmit(false);
|
setIsSubmit(false);
|
||||||
}
|
}
|
||||||
@@ -199,7 +224,11 @@ const BulkSendUi = (props) => {
|
|||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isBulkAvailable ? (
|
{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 ? (
|
{props.Placeholders?.length > 0 ? (
|
||||||
isSignatureExist ? (
|
isSignatureExist ? (
|
||||||
@@ -218,7 +247,9 @@ const BulkSendUi = (props) => {
|
|||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
key={field.fieldId}
|
key={field.fieldId}
|
||||||
>
|
>
|
||||||
<label>{field.label}</label>
|
<label className="block text-xs font-semibold">
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
<SuggestionInput
|
<SuggestionInput
|
||||||
required
|
required
|
||||||
type="email"
|
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,30 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:items-center md:flex-row gap-y-3 my-2">
|
||||||
|
<span className="capitalize">{t("format")}: </span>
|
||||||
|
<select
|
||||||
|
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-2"
|
||||||
|
defaultValue={""}
|
||||||
|
onChange={(e) => handleChangeFormat(e)}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{t("select-date-format")}
|
||||||
|
</option>
|
||||||
|
{dateFormatList.map((data, ind) => {
|
||||||
|
return (
|
||||||
|
<option className="text-[13px]" value={ind} key={ind}>
|
||||||
|
{data?.date ? data?.date : "nodata"}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<span className="text-xs text-gray-400 ml-1 uppercase">{selectDate.format}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateFormat;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import ReactDatePicker from "react-datepicker";
|
||||||
|
import {
|
||||||
|
getDefaultDate,
|
||||||
|
getMonth,
|
||||||
|
getYear,
|
||||||
|
months,
|
||||||
|
years
|
||||||
|
} from "../constant/Utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const DatePicker = ({
|
||||||
|
selectDate,
|
||||||
|
format,
|
||||||
|
minDate,
|
||||||
|
maxDate,
|
||||||
|
onChange,
|
||||||
|
handleClear
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const CustomInput = forwardRef(({ value, onClick }, ref) => (
|
||||||
|
<div
|
||||||
|
className="border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content "
|
||||||
|
onClick={onClick}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
<i className={`${value ? "ml-[5px]" : ""} fa-light fa-calendar `}></i>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
CustomInput.displayName = "CustomInput";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>{t("default-date")}: </span>
|
||||||
|
<ReactDatePicker
|
||||||
|
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||||
|
<div className="flex justify-start md:ml-2">
|
||||||
|
<select
|
||||||
|
className="bg-transparent outline-none"
|
||||||
|
value={months[getMonth(date)]}
|
||||||
|
onChange={({ target: { value } }) =>
|
||||||
|
changeMonth(months.indexOf(value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{months.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="bg-transparent outline-none"
|
||||||
|
value={getYear(date)}
|
||||||
|
onChange={({ target: { value } }) => changeYear(value)}
|
||||||
|
>
|
||||||
|
{years.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
closeOnScroll={true}
|
||||||
|
selected={getDefaultDate(selectDate?.date, selectDate?.format)}
|
||||||
|
popperPlacement="top-end"
|
||||||
|
customInput={<CustomInput />}
|
||||||
|
onChange={(date) => onChange(date)}
|
||||||
|
dateFormat={
|
||||||
|
selectDate ? selectDate?.format : format ? format : "MM/dd/yyyy"
|
||||||
|
}
|
||||||
|
portalId="root-portal"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
onClick={() => handleClear()}
|
||||||
|
className="underline text-blue-500 cursor-pointer ml-2"
|
||||||
|
>
|
||||||
|
{t("clear")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatePicker;
|
||||||
@@ -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 { openInNewTab } from "../constant/Utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
|
const appName = "OpenSign™";
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showButton, setShowButton] = useState(false);
|
const [showButton, setShowButton] = useState(false);
|
||||||
const [version, setVersion] = useState("");
|
const [version, setVersion] = useState("");
|
||||||
@@ -39,7 +40,6 @@ const Footer = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const appName = "OpenSign™";
|
|
||||||
const openUrl = () => {
|
const openUrl = () => {
|
||||||
openInNewTab(
|
openInNewTab(
|
||||||
"https://github.com/OpenSignLabs/OpenSign/releases/tag/" + version
|
"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 React from "react";
|
||||||
import { Document, Page } from "react-pdf";
|
import { Document, Page } from "react-pdf";
|
||||||
import { Stage, Layer, Rect, Text } from "react-konva";
|
import { Stage, Layer, Rect, Text } from "react-konva";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
const RenderDebugPdf = (props) => {
|
const RenderDebugPdf = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="sticky top-0 p-[10px] z-10 bg-white border-[1px] border-[gray] my-[5px]">
|
<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}
|
onMouseMove={props.handleMouseMoveDiv}
|
||||||
>
|
>
|
||||||
<Document
|
<Document
|
||||||
onLoadError={() => {
|
onLoadError={() => props.setPdfLoadFail(false)}
|
||||||
props.setPdfLoadFail(false);
|
loading={t("loading-doc")}
|
||||||
}}
|
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
|
||||||
loading={"Loading Document.."}
|
|
||||||
onLoadSuccess={props.pageDetails}
|
onLoadSuccess={props.pageDetails}
|
||||||
ref={props.pdfRef}
|
ref={props.pdfRef}
|
||||||
file={props.pdfUrl}
|
file={props.pdfUrl}
|
||||||
@@ -26,7 +26,7 @@ function RotateAlert(props) {
|
|||||||
props.setShowRotateAlert({ status: false, degree: 0 })
|
props.setShowRotateAlert({ status: false, degree: 0 })
|
||||||
}
|
}
|
||||||
type="button"
|
type="button"
|
||||||
className="op-btn op-btn-ghost shadow-md"
|
className="op-btn op-btn-ghost text-base-content shadow-md"
|
||||||
>
|
>
|
||||||
{t("no")}
|
{t("no")}
|
||||||
</button>
|
</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"
|
: "cursor-default"
|
||||||
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
|
} 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">
|
<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">
|
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
|
||||||
<i
|
<i
|
||||||
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
|
|||||||
></i>
|
></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg ml-3 text-base-content">
|
<div className="text-lg ml-3">
|
||||||
{t(`sidebar.${props.Label}`)}
|
{t(`sidebar.${props.Label}`)}
|
||||||
{props.Label === "Sign yourself" && (
|
{props.Label === "Sign yourself" && (
|
||||||
<div className="text-gray-500 text-xs mt-1">
|
<div className="text-gray-500 text-xs mt-1">
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Parse from "parse";
|
import Parse from "parse";
|
||||||
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import Tooltip from "../../primitives/Tooltip";
|
import Tooltip from "../../primitives/Tooltip";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { withSessionValidation } from "../../utils";
|
||||||
|
|
||||||
const DashboardCard = (props) => {
|
const DashboardCard = (props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -14,7 +15,7 @@ const DashboardCard = (props) => {
|
|||||||
const [response, setresponse] = useState("");
|
const [response, setresponse] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const renderData = async () => {
|
const renderData = withSessionValidation(async () => {
|
||||||
if (props.Data.queryType === "function") {
|
if (props.Data.queryType === "function") {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -148,7 +149,7 @@ const DashboardCard = (props) => {
|
|||||||
);
|
);
|
||||||
let arr = [];
|
let arr = [];
|
||||||
for (const obj of listData) {
|
for (const obj of listData) {
|
||||||
const isSigner = obj.Signers.some(
|
const isSigner = obj.Signers?.some(
|
||||||
(item) => item.UserId.objectId === currentUser.id
|
(item) => item.UserId.objectId === currentUser.id
|
||||||
);
|
);
|
||||||
if (isSigner) {
|
if (isSigner) {
|
||||||
@@ -168,31 +169,27 @@ const DashboardCard = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setresponse(arr.length);
|
setresponse(arr.length);
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await axios.get(url, { headers: headers }).then((res) => {
|
await axios.get(url, { headers: headers }).then((res) => {
|
||||||
if (res.data.results.length > 0) {
|
if (res?.data?.[props.Data.key]) {
|
||||||
setLoading(false);
|
setresponse(parseInt(res.data[props.Data.key]));
|
||||||
if (props.Data.key !== "count") {
|
} else if (res?.data?.results?.length > 0) {
|
||||||
setresponse(res.data.results[0][props.Data.key]);
|
setresponse(res.data.results.length);
|
||||||
} else {
|
|
||||||
setresponse(res.data[props.Data.key]);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setresponse(0);
|
setresponse(0);
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Problem", e);
|
console.error("Problem", e);
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const filterRender = async () => {
|
const filterRender = withSessionValidation(async () => {
|
||||||
if (props.FilterData && props.FilterData.queryType === "function") {
|
if (props.FilterData && props.FilterData.queryType === "function") {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -270,7 +267,7 @@ const DashboardCard = (props) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const setFormat = (val) => {
|
const setFormat = (val) => {
|
||||||
switch (props.Format) {
|
switch (props.Format) {
|
||||||
@@ -346,7 +343,6 @@ const DashboardCard = (props) => {
|
|||||||
id={props.Label}
|
id={props.Label}
|
||||||
iconColor={"white"}
|
iconColor={"white"}
|
||||||
message={t(`tour-mssg.${props.Label}`)}
|
message={t(`tour-mssg.${props.Label}`)}
|
||||||
// {props?.Data?.tourMessage}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Parse from "parse";
|
|
||||||
import ReportTable from "../../primitives/GetReportDisplay";
|
|
||||||
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("");
|
|
||||||
const [actions, setActions] = useState([]);
|
|
||||||
const [heading, setHeading] = useState([]);
|
|
||||||
const [isNextRecord, setIsNextRecord] = useState(false);
|
|
||||||
const [isMoreDocs, setIsMoreDocs] = useState(true);
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const docPerPage = 5;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setReportName("");
|
|
||||||
getReportData(props.Record.reportId);
|
|
||||||
|
|
||||||
// Function returned from useEffect is called on unmount
|
|
||||||
return () => {
|
|
||||||
setIsLoader(true);
|
|
||||||
setList([]);
|
|
||||||
setIsNextRecord(false);
|
|
||||||
// Here it'll abort the fetch
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [props.Record.reportId]);
|
|
||||||
|
|
||||||
// below useEffect call when isNextRecord state is true and fetch next record
|
|
||||||
useEffect(() => {
|
|
||||||
if (isNextRecord) {
|
|
||||||
getReportData(props.Record.reportId, List.length, 20);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [isNextRecord]);
|
|
||||||
|
|
||||||
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
|
|
||||||
setIsLoader(true);
|
|
||||||
const json = reportJson(id);
|
|
||||||
if (json) {
|
|
||||||
setActions(json.actions);
|
|
||||||
setReportName(json.reportName);
|
|
||||||
setHeading(json.heading);
|
|
||||||
const currentUser = Parse.User.current().id;
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
|
||||||
sessiontoken: localStorage.getItem("accesstoken")
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
|
||||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
|
||||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
|
||||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
|
||||||
const res = await axios.post(url, params, {
|
|
||||||
headers: headers,
|
|
||||||
signal: abortController.signal // is used to cancel fetch query
|
|
||||||
});
|
|
||||||
if (id === "5Go51Q7T8r") {
|
|
||||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
|
||||||
let arr = [];
|
|
||||||
for (const obj of listData) {
|
|
||||||
const isSigner = obj.Signers.some(
|
|
||||||
(item) => item.UserId.objectId === currentUser
|
|
||||||
);
|
|
||||||
if (isSigner) {
|
|
||||||
let isRecord;
|
|
||||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
|
||||||
isRecord = obj?.AuditTrail.some(
|
|
||||||
(item) =>
|
|
||||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
|
||||||
item.Activity === "Signed"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
isRecord = false;
|
|
||||||
}
|
|
||||||
if (isRecord === false) {
|
|
||||||
arr.push(obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (arr.length === docPerPage) {
|
|
||||||
setIsMoreDocs(true);
|
|
||||||
} else {
|
|
||||||
setIsMoreDocs(false);
|
|
||||||
}
|
|
||||||
setList((prevRecord) =>
|
|
||||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (res.data.result.length >= docPerPage) {
|
|
||||||
setIsMoreDocs(true);
|
|
||||||
} else {
|
|
||||||
setIsMoreDocs(false);
|
|
||||||
}
|
|
||||||
setIsNextRecord(false);
|
|
||||||
if (!res.data.result.error) {
|
|
||||||
setList((prevRecord) =>
|
|
||||||
prevRecord.length > 0
|
|
||||||
? [...prevRecord, ...res.data.result]
|
|
||||||
: res.data.result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsLoader(false);
|
|
||||||
} catch (err) {
|
|
||||||
const isCancel = axios.isCancel(err);
|
|
||||||
if (!isCancel) {
|
|
||||||
console.log("err ", err);
|
|
||||||
setIsLoader(false);
|
|
||||||
}
|
|
||||||
setIsLoader(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsLoader(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isLoader ? (
|
|
||||||
<div className="h-[250px] flex justify-center items-center">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{reportName ? (
|
|
||||||
<ReportTable
|
|
||||||
ReportName={reportName}
|
|
||||||
List={List}
|
|
||||||
setList={setList}
|
|
||||||
actions={actions}
|
|
||||||
heading={heading}
|
|
||||||
setIsNextRecord={setIsNextRecord}
|
|
||||||
isMoreDocs={isMoreDocs}
|
|
||||||
docPerPage={docPerPage}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DashboardReport;
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import Parse from "parse";
|
||||||
|
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";
|
||||||
|
import { withSessionValidation } from "../../utils";
|
||||||
|
function DashboardReport(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [List, setList] = useState([]);
|
||||||
|
const [isLoader, setIsLoader] = useState(true);
|
||||||
|
const [reportName, setReportName] = useState("");
|
||||||
|
const [actions, setActions] = useState([]);
|
||||||
|
const [heading, setHeading] = useState([]);
|
||||||
|
const [isNextRecord, setIsNextRecord] = useState(false);
|
||||||
|
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("");
|
||||||
|
setSearchTerm("");
|
||||||
|
setMobileSearchOpen(false);
|
||||||
|
getReportData(props.Record.reportId, 0, 20, "");
|
||||||
|
|
||||||
|
// Function returned from useEffect is called on unmount
|
||||||
|
return () => {
|
||||||
|
setIsLoader(true);
|
||||||
|
setList([]);
|
||||||
|
setIsNextRecord(false);
|
||||||
|
// Here it'll abort the fetch
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [props.Record.reportId]);
|
||||||
|
|
||||||
|
// below useEffect call when isNextRecord state is true and fetch next record
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNextRecord) {
|
||||||
|
getReportData(props.Record.reportId, List.length, 20, searchTerm);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [isNextRecord]);
|
||||||
|
|
||||||
|
const handleSearchChange = withSessionValidation(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 = withSessionValidation(
|
||||||
|
async (id, skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||||
|
setIsLoader(true);
|
||||||
|
const json = reportJson(id);
|
||||||
|
if (json) {
|
||||||
|
setActions(json.actions);
|
||||||
|
setReportName(json.reportName);
|
||||||
|
setHeading(json.heading);
|
||||||
|
const currentUser = Parse.User.current().id;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||||
|
sessiontoken: localStorage.getItem("accesstoken")
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||||
|
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||||
|
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||||
|
if (term) {
|
||||||
|
params.searchTerm = term;
|
||||||
|
}
|
||||||
|
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||||
|
const res = await axios.post(url, params, {
|
||||||
|
headers: headers,
|
||||||
|
signal: abortController.signal // is used to cancel fetch query
|
||||||
|
});
|
||||||
|
if (id === "5Go51Q7T8r") {
|
||||||
|
const listData = res.data?.result.filter(
|
||||||
|
(x) => x.Signers.length > 0
|
||||||
|
);
|
||||||
|
let arr = [];
|
||||||
|
for (const obj of listData) {
|
||||||
|
const isSigner = obj.Signers?.some(
|
||||||
|
(item) => item.UserId.objectId === currentUser
|
||||||
|
);
|
||||||
|
if (isSigner) {
|
||||||
|
let isRecord;
|
||||||
|
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||||
|
isRecord = obj?.AuditTrail.some(
|
||||||
|
(item) =>
|
||||||
|
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||||
|
item.Activity === "Signed"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
isRecord = false;
|
||||||
|
}
|
||||||
|
if (isRecord === false) {
|
||||||
|
arr.push(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arr.length === docPerPage) {
|
||||||
|
setIsMoreDocs(true);
|
||||||
|
} else {
|
||||||
|
setIsMoreDocs(false);
|
||||||
|
}
|
||||||
|
setList((prevRecord) =>
|
||||||
|
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (res.data.result.length >= docPerPage) {
|
||||||
|
setIsMoreDocs(true);
|
||||||
|
} else {
|
||||||
|
setIsMoreDocs(false);
|
||||||
|
}
|
||||||
|
setIsNextRecord(false);
|
||||||
|
if (!res.data.result.error) {
|
||||||
|
setList((prevRecord) =>
|
||||||
|
prevRecord.length > 0
|
||||||
|
? [...prevRecord, ...res.data.result]
|
||||||
|
: res.data.result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoader(false);
|
||||||
|
} catch (err) {
|
||||||
|
const isCancel = axios.isCancel(err);
|
||||||
|
if (!isCancel) {
|
||||||
|
console.log("err ", err);
|
||||||
|
setIsLoader(false);
|
||||||
|
}
|
||||||
|
setIsLoader(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsLoader(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoader ? (
|
||||||
|
<div className="h-[250px] flex justify-center items-center">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{reportName ? (
|
||||||
|
<DocumentsReport
|
||||||
|
ReportName={reportName}
|
||||||
|
List={List}
|
||||||
|
setList={setList}
|
||||||
|
actions={actions}
|
||||||
|
heading={heading}
|
||||||
|
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-box">
|
||||||
|
<div className="text-center text-xl text-base-content">
|
||||||
|
{t("report-not-found")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardReport;
|
||||||
@@ -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";
|
import { useTranslation } from "react-i18next";
|
||||||
const DashboardButton = lazy(() => import("./DashboardButton"));
|
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
|
||||||
const DashboardCard = lazy(() => import("./DashboardCard"));
|
const DashboardCard = lazyWithRetry(() => import("./DashboardCard"));
|
||||||
const DashboardReport = lazy(() => import("./DashboardReport"));
|
const DashboardReport = lazyWithRetry(() => import("./DashboardReport"));
|
||||||
const buttonList = [
|
const buttonList = [
|
||||||
{
|
{
|
||||||
label: "Sign yourself",
|
label: "Sign yourself",
|
||||||
@@ -19,6 +20,7 @@ const buttonList = [
|
|||||||
];
|
];
|
||||||
const GetDashboard = (props) => {
|
const GetDashboard = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const Button = ({ label, redirectId, redirectType, icon }) => (
|
const Button = ({ label, redirectId, redirectType, icon }) => (
|
||||||
<DashboardButton
|
<DashboardButton
|
||||||
Icon={icon}
|
Icon={icon}
|
||||||
@@ -58,7 +60,9 @@ const GetDashboard = (props) => {
|
|||||||
<div data-tut={col.widget.data.tourSection}>
|
<div data-tut={col.widget.data.tourSection}>
|
||||||
<Suspense fallback={<div>please wait</div>}>
|
<Suspense fallback={<div>please wait</div>}>
|
||||||
<div className="mb-3 md:mb-0">
|
<div className="mb-3 md:mb-0">
|
||||||
<DashboardReport Record={col.widget} />
|
<DashboardReport
|
||||||
|
Record={col.widget}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +96,9 @@ const GetDashboard = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>please wait</div>}>
|
<Suspense fallback={<div>please wait</div>}>
|
||||||
<div className="mb-3 md:mb-0">
|
<div className="mb-3 md:mb-0">
|
||||||
<DashboardReport Record={col.widget} />
|
<DashboardReport
|
||||||
|
Record={col.widget}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import "../../styles/opensigndrive.css";
|
import "../../styles/opensigndrive.css";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as ContextMenu from "@radix-ui/react-context-menu";
|
import { ContextMenu } from "radix-ui";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import Table from "react-bootstrap/Table";
|
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 ModalUi from "../../primitives/ModalUi";
|
||||||
import FolderModal from "../shared/fields/FolderModal";
|
import FolderModal from "../shared/fields/FolderModal";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { handleDownloadPdf, isMobile } from "../../constant/Utils";
|
import { handleDownloadPdf, isMobile } from "../../constant/Utils";
|
||||||
import Parse from "parse";
|
import Parse from "parse";
|
||||||
|
import { withSessionValidation } from "../../utils";
|
||||||
|
|
||||||
function DriveBody(props) {
|
function DriveBody(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -52,13 +53,11 @@ function DriveBody(props) {
|
|||||||
props.setSkip(0);
|
props.setSkip(0);
|
||||||
};
|
};
|
||||||
//function for change doc name and update doc name in _document class
|
//function for change doc name and update doc name in _document class
|
||||||
const handledRenameDoc = async (data) => {
|
const handledRenameDoc = withSessionValidation(async (data) => {
|
||||||
setRename("");
|
setRename("");
|
||||||
const trimmedValue = renameValue.trim();
|
const trimmedValue = renameValue.trim();
|
||||||
if (trimmedValue.length > 0) {
|
if (trimmedValue.length > 0) {
|
||||||
const updateName = {
|
const updateName = { Name: renameValue };
|
||||||
Name: renameValue
|
|
||||||
};
|
|
||||||
const docId = data.objectId;
|
const docId = data.objectId;
|
||||||
const docData = props.pdfData;
|
const docData = props.pdfData;
|
||||||
const updatedData = docData.map((item) => {
|
const updatedData = docData.map((item) => {
|
||||||
@@ -71,11 +70,9 @@ function DriveBody(props) {
|
|||||||
});
|
});
|
||||||
props.setPdfData(updatedData);
|
props.setPdfData(updatedData);
|
||||||
props.sortingData(null, null, updatedData);
|
props.sortingData(null, null, updatedData);
|
||||||
await axios
|
try {
|
||||||
.put(
|
await axios.put(
|
||||||
`${localStorage.getItem(
|
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${docId}`,
|
||||||
"baseUrl"
|
|
||||||
)}classes/contracts_Document/${docId}`,
|
|
||||||
updateName,
|
updateName,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -84,20 +81,16 @@ function DriveBody(props) {
|
|||||||
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
.then(() => {
|
} catch (err) {
|
||||||
// const res = result.data;
|
console.error("Error in rename doc", err);
|
||||||
// console.log("res", res);
|
props.setIsAlert({
|
||||||
})
|
isShow: true,
|
||||||
.catch((err) => {
|
alertMessage: t("something-went-wrong-mssg")
|
||||||
console.log("Err ", err);
|
|
||||||
props.setIsAlert({
|
|
||||||
isShow: true,
|
|
||||||
alertMessage: t("something-went-wrong-mssg")
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
//function for navigate user to microapp-signature component
|
//function for navigate user to microapp-signature component
|
||||||
const checkPdfStatus = async (data) => {
|
const checkPdfStatus = async (data) => {
|
||||||
@@ -163,12 +156,10 @@ function DriveBody(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
//function for delete document
|
//function for delete document
|
||||||
const handleDeleteDocument = async (docData) => {
|
const handleDeleteDocument = withSessionValidation(async (docData) => {
|
||||||
setIsDeleteDoc({});
|
setIsDeleteDoc({});
|
||||||
const docId = docData.objectId;
|
const docId = docData.objectId;
|
||||||
const data = {
|
const data = { IsArchive: true };
|
||||||
IsArchive: true
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.put(
|
.put(
|
||||||
@@ -190,19 +181,19 @@ function DriveBody(props) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log("Err ", err);
|
console.error("Err in delete doc", err);
|
||||||
props.setIsAlert({
|
props.setIsAlert({
|
||||||
isShow: true,
|
isShow: true,
|
||||||
alertMessage: t("something-went-wrong-mssg")
|
alertMessage: t("something-went-wrong-mssg")
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
const handleMoveDocument = async (docData) => {
|
const handleMoveDocument = async (docData) => {
|
||||||
setIsOpenMoveModal(true);
|
setIsOpenMoveModal(true);
|
||||||
setSelectDoc(docData);
|
setSelectDoc(docData);
|
||||||
};
|
};
|
||||||
//function for move document from one folder to another folder
|
//function for move document from one folder to another folder
|
||||||
const handleMoveFolder = async (selectFolderData) => {
|
const handleMoveFolder = withSessionValidation(async (selectFolderData) => {
|
||||||
const selecFolderId = selectDoc?.Folder?.objectId;
|
const selecFolderId = selectDoc?.Folder?.objectId;
|
||||||
const moveFolderId = selectFolderData?.ObjectId;
|
const moveFolderId = selectFolderData?.ObjectId;
|
||||||
let updateDocId = selectDoc?.objectId;
|
let updateDocId = selectDoc?.objectId;
|
||||||
@@ -224,16 +215,12 @@ function DriveBody(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
updateData = {
|
updateData = { Folder: { __op: "Delete" } };
|
||||||
Folder: { __op: "Delete" }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.put(
|
.put(
|
||||||
`${localStorage.getItem(
|
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${updateDocId}`,
|
||||||
"baseUrl"
|
|
||||||
)}classes/contracts_Document/${updateDocId}`,
|
|
||||||
updateData,
|
updateData,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -243,9 +230,7 @@ function DriveBody(props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
.then((Listdata) => {
|
.then((Listdata) => {
|
||||||
// console.log("Listdata ", Listdata);
|
|
||||||
const res = Listdata.data;
|
const res = Listdata.data;
|
||||||
if (res) {
|
if (res) {
|
||||||
const updatedData = props.pdfData.filter(
|
const updatedData = props.pdfData.filter(
|
||||||
@@ -255,7 +240,7 @@ function DriveBody(props) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log("err", err);
|
console.error("err in move folder", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsOpenMoveModal(false);
|
setIsOpenMoveModal(false);
|
||||||
@@ -263,7 +248,7 @@ function DriveBody(props) {
|
|||||||
alert(t("folder-already-exist!"));
|
alert(t("folder-already-exist!"));
|
||||||
setIsOpenMoveModal(false);
|
setIsOpenMoveModal(false);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
const handleEnterPress = (e, data) => {
|
const handleEnterPress = (e, data) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
handledRenameDoc(data);
|
handledRenameDoc(data);
|
||||||
@@ -303,7 +288,7 @@ function DriveBody(props) {
|
|||||||
props.setPdfData(updatedData);
|
props.setPdfData(updatedData);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Err ", err);
|
console.error("Err in delete folder", err);
|
||||||
props.setIsAlert({
|
props.setIsAlert({
|
||||||
isShow: true,
|
isShow: true,
|
||||||
alertMessage: t("something-went-wrong-mssg")
|
alertMessage: t("something-went-wrong-mssg")
|
||||||
@@ -347,12 +332,14 @@ function DriveBody(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const signersName = () => {
|
const signersName = () => {
|
||||||
const getSignersName = signerExist.map((data) => data.Name);
|
const getSignersName =
|
||||||
const signerName = getSignersName.join(", ");
|
signerExist?.length > 0 && signerExist?.map((data) => data?.Name || "");
|
||||||
|
const signerName =
|
||||||
|
getSignersName?.length > 0 ? getSignersName?.join(", ") : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="text-[12px] font-medium w-[90%] break-words">
|
<span className="text-[12px] font-medium w-[90%] break-words">
|
||||||
{signerName}
|
{signerName && signerName}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -364,14 +351,14 @@ function DriveBody(props) {
|
|||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 512 512"
|
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" />
|
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-[12px] font-medium">{data.Name}</span>
|
<span className="text-[12px] font-medium">{data.Name}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>_</td>
|
<td>_</td>
|
||||||
<td>Folder</td>
|
<td>{t("folder")}</td>
|
||||||
<td>_</td>
|
<td>_</td>
|
||||||
<td>_</td>
|
<td>_</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -388,7 +375,7 @@ function DriveBody(props) {
|
|||||||
<span className="text-[12px] font-medium">{data.Name}</span>
|
<span className="text-[12px] font-medium">{data.Name}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{createddate}</td>
|
<td>{createddate}</td>
|
||||||
<td>Pdf</td>
|
<td>{t("pdf")}</td>
|
||||||
<td>{t(`drive-document-status.${status}`)}</td>
|
<td>{t(`drive-document-status.${status}`)}</td>
|
||||||
<td>
|
<td>
|
||||||
<i
|
<i
|
||||||
@@ -403,7 +390,7 @@ function DriveBody(props) {
|
|||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
) : listType === "list" && data.Type === "Folder" ? (
|
) : 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.Root>
|
||||||
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
|
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
|
||||||
{/* folder */}
|
{/* folder */}
|
||||||
@@ -419,7 +406,7 @@ function DriveBody(props) {
|
|||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 512 512"
|
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" />
|
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -445,7 +432,6 @@ function DriveBody(props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
|
||||||
<ContextMenu.Portal>
|
<ContextMenu.Portal>
|
||||||
<ContextMenu.Content
|
<ContextMenu.Content
|
||||||
className="ContextMenuContent"
|
className="ContextMenuContent"
|
||||||
@@ -457,14 +443,14 @@ function DriveBody(props) {
|
|||||||
className="ContextMenuItem"
|
className="ContextMenuItem"
|
||||||
>
|
>
|
||||||
<i className="fa-light fa-font mr-[8px]"></i>
|
<i className="fa-light fa-font mr-[8px]"></i>
|
||||||
<span>Rename</span>
|
<span>{t(`context-menu.Rename`)}</span>
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
onClick={() => handleMenuItemClick("Delete", data, data.Type)}
|
onClick={() => handleMenuItemClick("Delete", data, data.Type)}
|
||||||
className="ContextMenuItem"
|
className="ContextMenuItem"
|
||||||
>
|
>
|
||||||
<i className="fa-light fa-trash mr-[8px]"></i>
|
<i className="fa-light fa-trash mr-[8px]"></i>
|
||||||
<span>Delete</span>
|
<span>{t(`context-menu.Delete`)}</span>
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</ContextMenu.Content>
|
</ContextMenu.Content>
|
||||||
</ContextMenu.Portal>
|
</ContextMenu.Portal>
|
||||||
@@ -549,7 +535,6 @@ function DriveBody(props) {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ContextMenu.Portal>
|
<ContextMenu.Portal>
|
||||||
<ContextMenu.Content
|
<ContextMenu.Content
|
||||||
className="ContextMenuContent"
|
className="ContextMenuContent"
|
||||||
@@ -602,7 +587,6 @@ function DriveBody(props) {
|
|||||||
{signersName()}
|
{signersName()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HoverCard.Arrow className="HoverCardArrow" />
|
<HoverCard.Arrow className="HoverCardArrow" />
|
||||||
</HoverCard.Content>
|
</HoverCard.Content>
|
||||||
</HoverCard.Portal>
|
</HoverCard.Portal>
|
||||||
@@ -626,21 +610,21 @@ function DriveBody(props) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{props.pdfData.map((data, ind) => {
|
{props?.pdfData?.map((data, ind) => (
|
||||||
return (
|
<React.Fragment key={ind}>
|
||||||
<React.Fragment key={ind}>
|
{handleFolderData(data, ind, "table")}
|
||||||
{handleFolderData(data, ind, "table")}
|
</React.Fragment>
|
||||||
</React.Fragment>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
|
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
|
||||||
{props.pdfData.map((data, ind) => {
|
{props?.pdfData?.map((data, ind) => (
|
||||||
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>;
|
<React.Fragment key={ind}>
|
||||||
})}
|
{handleFolderData(data, ind, "list")}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -10,15 +10,15 @@ const AddRoleModal = (props) => {
|
|||||||
isOpen={props.isModalRole}
|
isOpen={props.isModalRole}
|
||||||
handleClose={props.handleCloseRoleModal}
|
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}>
|
<form className="flex flex-col" onSubmit={props.handleAddRole}>
|
||||||
<input
|
<input
|
||||||
value={props.roleName}
|
value={props.roleName}
|
||||||
onChange={(e) => props.setRoleName(e.target.value)}
|
onChange={(e) => props.setRoleName(e.target.value)}
|
||||||
placeholder={
|
placeholder={
|
||||||
props.signersdata.length > 0
|
props.signersdata.length > 0
|
||||||
? "User " + (props.signersdata.length + 1)
|
? "Role " + (props.signersdata.length + 1)
|
||||||
: "User 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"
|
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
|
<button
|
||||||
onClick={props.handleCloseRoleModal}
|
onClick={props.handleCloseRoleModal}
|
||||||
type="button"
|
type="button"
|
||||||
className="op-btn op-btn-ghost ml-2"
|
className="op-btn op-btn-ghost text-base-content ml-2"
|
||||||
>
|
>
|
||||||
{t("close")}
|
{t("close")}
|
||||||
</button>
|
</button>
|
||||||
@@ -4,10 +4,11 @@ import ModalUi from "../../primitives/ModalUi";
|
|||||||
|
|
||||||
function AgreementContent(props) {
|
function AgreementContent(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const appName =
|
||||||
|
"OpenSign™";
|
||||||
const h2Style = "text-base-content font-medium text-lg";
|
const h2Style = "text-base-content font-medium text-lg";
|
||||||
const ulStyle = "list-disc px-4 py-3";
|
const ulStyle = "list-disc px-4 py-3";
|
||||||
const handleOnclick = () => {
|
const handleOnclick = () => {
|
||||||
props.setIsAgreeTour(false);
|
|
||||||
props.setIsAgree(true);
|
props.setIsAgree(true);
|
||||||
props.setIsShowAgreeTerms(false);
|
props.setIsShowAgreeTerms(false);
|
||||||
props.showFirstWidget();
|
props.showFirstWidget();
|
||||||
@@ -21,78 +22,94 @@ function AgreementContent(props) {
|
|||||||
>
|
>
|
||||||
<div className="h-[100%] p-[20px]">
|
<div className="h-[100%] p-[20px]">
|
||||||
<h2 className={h2Style}>{t("term-cond-h")}</h2>
|
<h2 className={h2Style}>{t("term-cond-h")}</h2>
|
||||||
<span className="mt-2">{t("term-cond-p1")}</span>
|
<span className="mt-2">
|
||||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
{t("term-cond-p1", { appName: appName })}
|
||||||
|
</span>
|
||||||
|
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||||
<h2 className={h2Style}>{t("term-cond-h1")}</h2>
|
<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}>
|
<ul className={ulStyle}>
|
||||||
<li>{t("term-cond-p3")}</li>
|
<li>{t("term-cond-p3", { appName: appName })}</li>
|
||||||
<li>{t("term-cond-p4")}</li>
|
<li>{t("term-cond-p4")}</li>
|
||||||
</ul>
|
</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>
|
<h2 className={h2Style}>{t("term-cond-h2")}</h2>
|
||||||
<span className="mt-2">{t("term-cond-p5")}</span>
|
<span className="mt-2">{t("term-cond-p5")}</span>
|
||||||
<ul className={ulStyle}>
|
<ul className={ulStyle}>
|
||||||
<li>{t("term-cond-p6")}</li>
|
<li>{t("term-cond-p6", { appName: appName })}</li>
|
||||||
<li>{t("term-cond-p7")}</li>
|
<li>{t("term-cond-p7", { appName: appName })}</li>
|
||||||
<li>{t("term-cond-p8")}</li>
|
<li>{t("term-cond-p8")}</li>
|
||||||
</ul>
|
</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>
|
<h2 className={h2Style}>{t("term-cond-h3")}</h2>
|
||||||
<span className="mt-2">{t("term-cond-p9")}</span>
|
<span className="mt-2">{t("term-cond-p9")}</span>
|
||||||
<ul className={ulStyle}>
|
<ul className={ulStyle}>
|
||||||
<li>{t("term-cond-p10")}</li>
|
<li>{t("term-cond-p10")}</li>
|
||||||
<li>{t("term-cond-p11")}</li>
|
<li>{t("term-cond-p11")}</li>
|
||||||
</ul>
|
</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>
|
<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}>
|
<ul className={ulStyle}>
|
||||||
<li>{t("term-cond-p13")}</li>
|
<li>{t("term-cond-p13")}</li>
|
||||||
<li>{t("term-cond-p14")}</li>
|
<li>{t("term-cond-p14")}</li>
|
||||||
<li>{t("term-cond-p15")}</li>
|
<li>{t("term-cond-p15")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<span>{t("term-cond-p16")}</span>
|
<span>{t("term-cond-p16", { appName: appName })}</span>
|
||||||
<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-h5")}</h2>
|
<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>
|
<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>
|
<h2 className={h2Style}>{t("term-cond-h6")}</h2>
|
||||||
<span className="mt-2">{t("term-cond-p19")}</span>
|
<span className="mt-2">{t("term-cond-p19")}</span>
|
||||||
<ul className={ulStyle}>
|
<ul className={ulStyle}>
|
||||||
<li>{t("term-cond-p20")}</li>
|
<li>{t("term-cond-p20")}</li>
|
||||||
<li>{t("term-cond-p21")}</li>
|
<li>{t("term-cond-p21")}</li>
|
||||||
<li>{t("term-cond-p22")}</li>
|
<li>{t("term-cond-p22", { appName: appName })}</li>
|
||||||
</ul>
|
</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>
|
<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}>
|
<ul className={ulStyle}>
|
||||||
<li>{t("term-cond-p24")}</li>
|
<li>{t("term-cond-p24")}</li>
|
||||||
<li>{t("term-cond-p25")}</li>
|
<li>{t("term-cond-p25")}</li>
|
||||||
</ul>
|
</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>
|
<h2 className={h2Style}>{t("term-cond-h8")}</h2>
|
||||||
<span className="mt-2">{t("term-cond-p26")}</span>
|
<span className="mt-2">
|
||||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
{t("term-cond-p26", { appName: appName })}
|
||||||
|
</span>
|
||||||
|
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||||
<h2 className={h2Style}>{t("term-cond-h9")}</h2>
|
<h2 className={h2Style}>{t("term-cond-h9")}</h2>
|
||||||
<span className="mt-2">{t("term-cond-p27")}</span>
|
<span className="mt-2">
|
||||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
{t("term-cond-p27", { appName: appName })}
|
||||||
<span className="mt-2 font-medium">{t("term-cond-p28")}</span>
|
</span>
|
||||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||||
<span className="mt-2">{t("term-cond-p29")}</span>
|
<span className="mt-2 font-medium">
|
||||||
<a
|
{t("term-cond-p28", { appName: appName })}
|
||||||
href="www.opensignlabs.com"
|
</span>
|
||||||
target="_blank"
|
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||||
className="text-blue-700 cursor-pointer"
|
<span className="mt-2">
|
||||||
>
|
{t("term-cond-p29", { appName: appName })}
|
||||||
{" "}
|
</span>
|
||||||
www.opensignlabs.com{" "}
|
<a
|
||||||
</a>
|
href="www.opensignlabs.com"
|
||||||
<span>{t("term-cond-p30")}</span>
|
target="_blank"
|
||||||
<span className="font-medium"> support@opensignlabs.com </span>
|
className="text-blue-700 cursor-pointer"
|
||||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
>
|
||||||
|
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">
|
<div className="mt-6 flex justify-start gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOnclick()}
|
onClick={() => handleOnclick()}
|
||||||
@@ -101,7 +118,7 @@ function AgreementContent(props) {
|
|||||||
{t("agrre-button")}
|
{t("agrre-button")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="op-btn op-btn-ghost"
|
className="op-btn op-btn-ghost text-base-content"
|
||||||
onClick={() => props.setIsShowAgreeTerms(false)}
|
onClick={() => props.setIsShowAgreeTerms(false)}
|
||||||
>
|
>
|
||||||
{t("close")}
|
{t("close")}
|
||||||
@@ -4,7 +4,6 @@ import AgreementContent from "./AgreementContent";
|
|||||||
|
|
||||||
function AgreementSign(props) {
|
function AgreementSign(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isChecked, setIsChecked] = useState(false);
|
|
||||||
const [isShowAgreeTerms, setIsShowAgreeTerms] = useState(false);
|
const [isShowAgreeTerms, setIsShowAgreeTerms] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -12,26 +11,12 @@ function AgreementSign(props) {
|
|||||||
<div className="op-modal op-modal-open absolute z-[448]">
|
<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="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">
|
<div className="flex flex-row items-center">
|
||||||
<input
|
<div className="text-[11px] md:text-base text-base-content">
|
||||||
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">
|
|
||||||
<span>{t("agree-p1")}</span>
|
<span>{t("agree-p1")}</span>
|
||||||
<span
|
<span
|
||||||
className="font-bold text-blue-600 cursor-pointer"
|
className="font-bold text-blue-600 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsShowAgreeTerms(true);
|
setIsShowAgreeTerms(true);
|
||||||
props.setIsAgreeTour(false);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("agree-p2")}
|
{t("agree-p2")}
|
||||||
@@ -39,26 +24,24 @@ function AgreementSign(props) {
|
|||||||
<span> {t("agree-p3")}</span>
|
<span> {t("agree-p3")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex ml-[35px] mt-3">
|
<div className="flex mt-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isChecked) {
|
props.setIsAgree(true);
|
||||||
props.setIsAgreeTour(false);
|
props.showFirstWidget();
|
||||||
props.setIsAgree(true);
|
|
||||||
} else {
|
|
||||||
props.setIsAgreeTour(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="op-btn op-btn-primary op-btn-sm w-full md:w-auto"
|
className="op-btn op-btn-primary op-btn-sm w-full md:w-auto"
|
||||||
>
|
>
|
||||||
{t("agrre-button")}
|
{t("agrre-button")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-2 text-base-content">
|
||||||
|
<span className="text-[11px]">{t("agreement-note")}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isShowAgreeTerms && (
|
{isShowAgreeTerms && (
|
||||||
<AgreementContent
|
<AgreementContent
|
||||||
setIsAgreeTour={props.setIsAgreeTour}
|
|
||||||
setIsAgree={props.setIsAgree}
|
setIsAgree={props.setIsAgree}
|
||||||
setIsShowAgreeTerms={setIsShowAgreeTerms}
|
setIsShowAgreeTerms={setIsShowAgreeTerms}
|
||||||
showFirstWidget={props.showFirstWidget}
|
showFirstWidget={props.showFirstWidget}
|
||||||
@@ -12,12 +12,9 @@ function BorderResize(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
|
||||||
width: getHeight() || "14px",
|
|
||||||
height: getHeight() || "14px"
|
|
||||||
}}
|
|
||||||
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
|
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]`}
|
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
|
||||||
></div>
|
></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,155 @@
|
|||||||
|
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 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 = (value) => {
|
||||||
|
props?.setCustomizeMail((prev) => ({
|
||||||
|
...prev,
|
||||||
|
body: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const handleEmailSendToSigners = async () => {
|
||||||
|
setIsLoader(true);
|
||||||
|
const documentData = await contractDocument(props?.documentId);
|
||||||
|
if (documentData && documentData?.length > 0) {
|
||||||
|
props?.setDocumentDetails && 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 && 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
|
||||||
|
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(true)}
|
||||||
|
>
|
||||||
|
{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 { useTranslation } from "react-i18next";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
function DefaultSignature(props) {
|
function DefaultSignature(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
|
||||||
|
const myInitial = useSelector((state) => state.widget.myInitial);
|
||||||
const tabName = ["my-signature", "my-initials"];
|
const tabName = ["my-signature", "my-initials"];
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const confirmToaddDefaultSign = (type) => {
|
const confirmToaddDefaultSign = (type) => {
|
||||||
if (!props.isAgree) {
|
if (props?.xyPosition.length > 0) {
|
||||||
props.setIsAgreeTour(true);
|
//check signature or initial widgets exist or not for auto signing
|
||||||
} else {
|
const getCurrentSignerXY = props?.xyPosition.filter(
|
||||||
if (props?.xyPosition.length > 0) {
|
(data) => data.Id === props.uniqueId
|
||||||
//check signature or initial widgets exist or not for auto signing
|
);
|
||||||
const getCurrentSignerXY = props?.xyPosition.filter(
|
const checkIsSignInitialExist = getCurrentSignerXY?.every(
|
||||||
(data) => data.Id === props.uniqueId
|
(placeholderObj) =>
|
||||||
);
|
placeholderObj?.placeHolder?.some((placeholder) =>
|
||||||
const checkIsSignInitialExist = getCurrentSignerXY?.every(
|
placeholder?.pos?.some((posItem) => posItem?.type === type)
|
||||||
(placeholderObj) =>
|
)
|
||||||
placeholderObj?.placeHolder?.some((placeholder) =>
|
);
|
||||||
placeholder?.pos?.some((posItem) => posItem?.type === type)
|
if (checkIsSignInitialExist) {
|
||||||
)
|
props?.setDefaultSignAlert({
|
||||||
);
|
isShow: true,
|
||||||
if (checkIsSignInitialExist) {
|
alertMessage: t("default-sign-alert", { widgetsType: type }),
|
||||||
props?.setDefaultSignAlert({
|
type: type
|
||||||
isShow: true,
|
});
|
||||||
alertMessage: t("default-sign-alert", { widgetsType: type }),
|
|
||||||
type: type
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
props?.setDefaultSignAlert({
|
|
||||||
isShow: true,
|
|
||||||
alertMessage: t("defaultSign-alert", { widgetsType: type })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
props?.setDefaultSignAlert({
|
props?.setDefaultSignAlert({
|
||||||
isShow: true,
|
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
|
<img
|
||||||
alt="signature"
|
alt="signature"
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
src={props?.defaultSignImg}
|
src={defaultSignImg}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
activeTab === 1 &&
|
activeTab === 1 &&
|
||||||
(props?.myInitial ? (
|
(myInitial ? (
|
||||||
<img
|
<img
|
||||||
alt="signature"
|
alt="signature"
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
src={props?.myInitial}
|
src={myInitial}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center items-center h-full">
|
<div className="flex justify-center items-center h-full">
|
||||||
@@ -97,7 +96,7 @@ function DefaultSignature(props) {
|
|||||||
disabled={
|
disabled={
|
||||||
activeTab === 0 && !props?.isDefault
|
activeTab === 0 && !props?.isDefault
|
||||||
? true
|
? true
|
||||||
: activeTab === 1 && !props.myInitial
|
: activeTab === 1 && !myInitial
|
||||||
? true
|
? true
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
@@ -106,9 +105,7 @@ function DefaultSignature(props) {
|
|||||||
</button>
|
</button>
|
||||||
{!props.isDefault && (
|
{!props.isDefault && (
|
||||||
<div className="absolute bg-black/70 text-white w-full h-full flex items-center justify-center text-[11px] cursor-default">
|
<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">
|
<span className="-rotate-45">{t("option-disabled-by-owner")}</span>
|
||||||
Document creator has disabled this option
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -30,15 +30,19 @@ function DraftDocument() {
|
|||||||
documentData === "Error: Something went wrong!" ||
|
documentData === "Error: Something went wrong!" ||
|
||||||
(documentData.result && documentData.result.error)
|
(documentData.result && documentData.result.error)
|
||||||
) {
|
) {
|
||||||
setIsLoading({
|
if (documentData?.result?.error?.includes("deleted")) {
|
||||||
isLoader: false,
|
setIsLoading({
|
||||||
message: "Error: Something went wrong!"
|
isLoader: false,
|
||||||
});
|
message: t("document-deleted")
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setIsLoading({
|
||||||
|
isLoader: false,
|
||||||
|
message: t("something-went-wrong-mssg")
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setIsLoading({
|
setIsLoading({ isLoader: false, message: t("no-data") });
|
||||||
isLoader: false,
|
|
||||||
message: "No data found!"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import { widgets } from "../../constant/Utils";
|
||||||
import { getWidgetType, widgets } from "../../constant/Utils";
|
import getWidgetType from "./getWidgetType";
|
||||||
|
|
||||||
function DragElement(item) {
|
function DragElement(item) {
|
||||||
const getWidgets = widgets;
|
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;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ModalUi from "../../primitives/ModalUi";
|
import ModalUi from "../../primitives/ModalUi";
|
||||||
import { radioButtonWidget } from "../../constant/Utils";
|
import { radioButtonWidget } from "../../constant/Utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -7,34 +7,37 @@ import { fontColorArr, fontsizeArr } from "../../constant/Utils";
|
|||||||
function DropdownWidgetOption(props) {
|
function DropdownWidgetOption(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dropdownOptionList, setDropdownOptionList] = useState([
|
const [dropdownOptionList, setDropdownOptionList] = useState([
|
||||||
"option-1",
|
"Option-1",
|
||||||
"option-2"
|
"Option-2"
|
||||||
]);
|
]);
|
||||||
const [minCount, setMinCount] = useState(0);
|
const [minCount, setMinCount] = useState(0);
|
||||||
const [maxCount, setMaxCount] = useState(0);
|
const [maxCount, setMaxCount] = useState(0);
|
||||||
const [dropdownName, setDropdownName] = useState(props.type);
|
const [dropdownName, setDropdownName] = useState();
|
||||||
const [isReadOnly, setIsReadOnly] = useState(false);
|
const [isReadOnly, setIsReadOnly] = useState(false);
|
||||||
const [isHideLabel, setIsHideLabel] = useState(false);
|
const [isHideLabel, setIsHideLabel] = useState(false);
|
||||||
const [status, setStatus] = useState("required");
|
const [status, setStatus] = useState("required");
|
||||||
const [defaultValue, setDefaultValue] = useState("");
|
const [defaultValue, setDefaultValue] = useState("");
|
||||||
const statusArr = ["required", "optional"];
|
|
||||||
const [defaultCheckbox, setDefaultCheckbox] = useState([]);
|
const [defaultCheckbox, setDefaultCheckbox] = useState([]);
|
||||||
|
const [layout, setLayout] = useState("vertical");
|
||||||
|
const statusArr = ["required", "optional"];
|
||||||
|
const layoutArr = ["vertical", "horizontal"];
|
||||||
|
const isPrefillExist = props?.roleName === "prefill";
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
setDropdownOptionList(["option-1", "option-2"]);
|
setDropdownOptionList(["Option-1", "Option-2"]);
|
||||||
setDropdownName(props.type);
|
setDropdownName(props.currWidgetsDetails?.options?.name || props.type);
|
||||||
setIsReadOnly(false);
|
setIsReadOnly(false);
|
||||||
setIsHideLabel(false);
|
setIsHideLabel(false);
|
||||||
setMinCount(0);
|
setMinCount(0);
|
||||||
setMaxCount(0);
|
setMaxCount(0);
|
||||||
setDefaultCheckbox([]);
|
setDefaultCheckbox([]);
|
||||||
setDefaultValue("");
|
setDefaultValue("");
|
||||||
|
setLayout("vertical");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
props.currWidgetsDetails?.options?.name &&
|
props.currWidgetsDetails?.options?.name &&
|
||||||
props.currWidgetsDetails?.options?.values
|
props.currWidgetsDetails?.options?.values?.length > 0
|
||||||
) {
|
) {
|
||||||
setDropdownName(props.currWidgetsDetails?.options?.name);
|
setDropdownName(props.currWidgetsDetails?.options?.name);
|
||||||
setDropdownOptionList(props.currWidgetsDetails?.options?.values);
|
setDropdownOptionList(props.currWidgetsDetails?.options?.values);
|
||||||
@@ -49,6 +52,7 @@ function DropdownWidgetOption(props) {
|
|||||||
setStatus(props.currWidgetsDetails?.options?.status || "required");
|
setStatus(props.currWidgetsDetails?.options?.status || "required");
|
||||||
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
|
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
|
||||||
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
|
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
|
||||||
|
setLayout(props.currWidgetsDetails?.options?.layout || "vertical");
|
||||||
} else {
|
} else {
|
||||||
setStatus("required");
|
setStatus("required");
|
||||||
resetState();
|
resetState();
|
||||||
@@ -84,7 +88,7 @@ function DropdownWidgetOption(props) {
|
|||||||
const deleteOption = true;
|
const deleteOption = true;
|
||||||
const addOption = false;
|
const addOption = false;
|
||||||
const getUpdatedOptions = dropdownOptionList.filter(
|
const getUpdatedOptions = dropdownOptionList.filter(
|
||||||
(data, index) => index !== ind
|
(_, index) => index !== ind
|
||||||
);
|
);
|
||||||
setDropdownOptionList(getUpdatedOptions);
|
setDropdownOptionList(getUpdatedOptions);
|
||||||
props.handleSaveWidgetsOptions(
|
props.handleSaveWidgetsOptions(
|
||||||
@@ -99,11 +103,43 @@ function DropdownWidgetOption(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveOption = () => {
|
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 =
|
const defaultData =
|
||||||
defaultCheckbox && defaultCheckbox.length > 0
|
defaultCheckbox && defaultCheckbox.length > 0
|
||||||
? defaultCheckbox
|
? defaultCheckbox
|
||||||
: defaultValue;
|
: 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(
|
props.handleSaveWidgetsOptions(
|
||||||
dropdownName,
|
dropdownName,
|
||||||
dropdownOptionList,
|
dropdownOptionList,
|
||||||
@@ -114,18 +150,10 @@ function DropdownWidgetOption(props) {
|
|||||||
null,
|
null,
|
||||||
status,
|
status,
|
||||||
defaultData,
|
defaultData,
|
||||||
isHideLabel
|
isHideLabel,
|
||||||
|
WidgetLayout
|
||||||
);
|
);
|
||||||
// props.setShowDropdown(false);
|
resetState();
|
||||||
setDropdownOptionList(["option-1", "option-2"]);
|
|
||||||
setDropdownName(props.type);
|
|
||||||
// props.setCurrWidgetsDetails({});
|
|
||||||
setIsReadOnly(false);
|
|
||||||
setIsHideLabel(false);
|
|
||||||
setMinCount(0);
|
|
||||||
setMaxCount(0);
|
|
||||||
setDefaultCheckbox([]);
|
|
||||||
setDefaultValue("");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -137,7 +165,6 @@ function DropdownWidgetOption(props) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
|
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
|
||||||
<div className="h-full p-[15px] text-base-content">
|
<div className="h-full p-[15px] text-base-content">
|
||||||
@@ -148,25 +175,25 @@ function DropdownWidgetOption(props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[13px] font-semibold">
|
<label htmlFor="title" className="text-[13px] font-semibold">
|
||||||
{t("name")}
|
{t("name")}
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="title"
|
||||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||||
onInput={(e) => e.target.setCustomValidity("")}
|
onInput={(e) => e.target.setCustomValidity("")}
|
||||||
required
|
|
||||||
defaultValue={dropdownName}
|
|
||||||
value={dropdownName}
|
value={dropdownName}
|
||||||
onChange={(e) => setDropdownName(e.target.value)}
|
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"
|
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]">
|
<label className="text-[13px] font-semibold mt-[5px]">
|
||||||
{t("options")}
|
{t("options")}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{dropdownOptionList.map((option, index) => (
|
{dropdownOptionList?.map((option, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex flex-row mb-[5px] items-center"
|
className="flex flex-row mb-[5px] items-center"
|
||||||
@@ -210,58 +237,60 @@ function DropdownWidgetOption(props) {
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<i
|
<div>
|
||||||
onClick={handleAddInput}
|
<i
|
||||||
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
|
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
|
||||||
></i>
|
aria-label="Add option"
|
||||||
|
onClick={handleAddInput}
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{["dropdown", radioButtonWidget].includes(props.type) && (
|
{["dropdown", radioButtonWidget].includes(props.type) &&
|
||||||
<>
|
!isPrefillExist && (
|
||||||
<label className="text-[13px] font-semibold mt-[5px]">
|
<>
|
||||||
{t("default-value")}
|
<label className="text-[13px] font-semibold mt-[5px]">
|
||||||
</label>
|
{t("default-value")}
|
||||||
<select
|
</label>
|
||||||
onChange={(e) => setDefaultValue(e.target.value)}
|
<select
|
||||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
value={defaultValue}
|
||||||
name="defaultvalue"
|
onChange={(e) => setDefaultValue(e.target.value)}
|
||||||
value={defaultValue}
|
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||||
placeholder="select default value"
|
name="defaultvalue"
|
||||||
>
|
>
|
||||||
<option value="" disabled hidden className="text-[13px]">
|
<option value="" disabled hidden className="text-[13px]">
|
||||||
{t("select")}...
|
{t("select")}...
|
||||||
</option>
|
</option>
|
||||||
{dropdownOptionList.map((data, ind) => {
|
{dropdownOptionList.map((data, ind) => {
|
||||||
return (
|
return (
|
||||||
<option className="text-[13px]" key={ind} value={data}>
|
<option className="text-[13px]" key={ind} value={data}>
|
||||||
{data}
|
{data}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{props.type !== "checkbox" && props.type !== radioButtonWidget && (
|
{((props.type !== "checkbox" && !isPrefillExist) ||
|
||||||
<>
|
isPrefillExist) && (
|
||||||
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
|
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
|
||||||
{statusArr.map((data, ind) => {
|
{statusArr.map((data, ind) => (
|
||||||
return (
|
<div
|
||||||
<div
|
key={ind}
|
||||||
key={ind}
|
className="flex flex-row gap-[5px] items-center"
|
||||||
className="flex flex-row gap-[5px] items-center"
|
>
|
||||||
>
|
<input
|
||||||
<input
|
className="op-radio op-radio-xs my-1"
|
||||||
className="op-radio op-radio-xs my-1"
|
type="radio"
|
||||||
type="radio"
|
name="status"
|
||||||
name="status"
|
onChange={() => setStatus(data.toLowerCase())}
|
||||||
onChange={() => setStatus(data.toLowerCase())}
|
checked={status.toLowerCase() === data.toLowerCase()}
|
||||||
checked={status.toLowerCase() === data.toLowerCase()}
|
/>
|
||||||
/>
|
<div className="text-[13px] font-500 capitalize">
|
||||||
<div className="text-[13px] font-500">{data}</div>
|
{data}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center mt-3 mb-3">
|
<div className="flex items-center mt-3 mb-3">
|
||||||
<span>{t("font-size")} :</span>
|
<span>{t("font-size")} :</span>
|
||||||
@@ -282,8 +311,8 @@ function DropdownWidgetOption(props) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
<div className="flex flex-row gap-1 items-center ml-4 ">
|
<div className="flex flex-row gap-1 items-center ml-4">
|
||||||
<span>{t("color")} : </span>
|
<span className="capitalize">{t("color")} : </span>
|
||||||
<select
|
<select
|
||||||
value={
|
value={
|
||||||
props.fontColor ||
|
props.fontColor ||
|
||||||
@@ -316,7 +345,7 @@ function DropdownWidgetOption(props) {
|
|||||||
props.type
|
props.type
|
||||||
) && (
|
) && (
|
||||||
<div className="flex flex-row gap-5 my-2 items-center text-center">
|
<div className="flex flex-row gap-5 my-2 items-center text-center">
|
||||||
{props.isShowAdvanceFeature && (
|
{props.isShowAdvanceFeature && !isPrefillExist && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
id="isreadonly"
|
id="isreadonly"
|
||||||
@@ -325,7 +354,10 @@ function DropdownWidgetOption(props) {
|
|||||||
className="op-checkbox op-checkbox-sm"
|
className="op-checkbox op-checkbox-sm"
|
||||||
onChange={(e) => setIsReadOnly(e.target.checked)}
|
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")}
|
{t("read-only")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,15 +372,45 @@ function DropdownWidgetOption(props) {
|
|||||||
onChange={(e) => setIsHideLabel(e.target.checked)}
|
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")}
|
{t("hide-labels")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
props.type === "checkbox" && props.isShowAdvanceFeature
|
props.type === "checkbox" && props.isShowAdvanceFeature
|
||||||
@@ -367,7 +429,7 @@ function DropdownWidgetOption(props) {
|
|||||||
{props.currWidgetsDetails?.options?.values?.length > 0 && (
|
{props.currWidgetsDetails?.options?.values?.length > 0 && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="op-btn op-btn-ghost ml-1"
|
className="op-btn op-btn-ghost text-base-content ml-1"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.handleClose && props.handleClose();
|
props.handleClose && props.handleClose();
|
||||||
resetState();
|
resetState();
|
||||||
@@ -1,308 +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
|
|
||||||
});
|
|
||||||
|
|
||||||
// `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="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 React from "react";
|
||||||
import { Quill } from "react-quill-new";
|
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
|
// 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
|
// 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",
|
container: "#toolbar1",
|
||||||
handlers: { undo: undoChange, redo: redoChange }
|
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
|
// Modules object for setting up the Quill editor
|
||||||
@@ -65,7 +72,9 @@ export const module2 = {
|
|||||||
container: "#toolbar2",
|
container: "#toolbar2",
|
||||||
handlers: { undo: undoChange, redo: redoChange }
|
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
|
// Formats objects for setting up the Quill editor
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import Tooltip from "../../primitives/Tooltip";
|
import Tooltip from "../../primitives/Tooltip";
|
||||||
import ReactQuill from "react-quill-new";
|
import ReactQuill from "react-quill-new";
|
||||||
import "../../styles/quill.css";
|
import "../../styles/quill.css";
|
||||||
@@ -18,7 +17,13 @@ export function EmailBody(props) {
|
|||||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||||
onInput={(e) => e.target.setCustomValidity("")}
|
onInput={(e) => e.target.setCustomValidity("")}
|
||||||
value={props.requestSubject}
|
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}"'
|
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"
|
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||||
/>
|
/>
|
||||||
@@ -31,7 +36,6 @@ export function EmailBody(props) {
|
|||||||
theme="snow"
|
theme="snow"
|
||||||
value={props.requestBody}
|
value={props.requestBody}
|
||||||
placeholder={t("email-placeholder")}
|
placeholder={t("email-placeholder")}
|
||||||
ref={props.editorRef}
|
|
||||||
modules={module1}
|
modules={module1}
|
||||||
formats={formats}
|
formats={formats}
|
||||||
onChange={props.handleOnchangeRequest}
|
onChange={props.handleOnchangeRequest}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import axios from "axios";
|
import {
|
||||||
import { handleToPrint } from "../../constant/Utils";
|
handleToPrint,
|
||||||
import { themeColor, emailRegex } from "../../constant/const";
|
} from "../../constant/Utils";
|
||||||
|
import {
|
||||||
|
emailRegex,
|
||||||
|
} from "../../constant/const";
|
||||||
import Loader from "../../primitives/Loader";
|
import Loader from "../../primitives/Loader";
|
||||||
import ModalUi from "../../primitives/ModalUi";
|
import ModalUi from "../../primitives/ModalUi";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Parse from "parse";
|
||||||
|
|
||||||
function EmailComponent({
|
function EmailComponent({
|
||||||
isEmail,
|
isEmail,
|
||||||
pdfUrl,
|
|
||||||
setIsEmail,
|
setIsEmail,
|
||||||
setSuccessEmail,
|
setSuccessEmail,
|
||||||
pdfDetails,
|
pdfDetails,
|
||||||
sender,
|
|
||||||
setIsAlert,
|
setIsAlert,
|
||||||
extUserId,
|
|
||||||
setIsDownloadModal
|
setIsDownloadModal
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -24,81 +25,13 @@ function EmailComponent({
|
|||||||
const [emailErr, setEmailErr] = useState(false);
|
const [emailErr, setEmailErr] = useState(false);
|
||||||
const [isDownloading, setIsDownloading] = useState("");
|
const [isDownloading, setIsDownloading] = useState("");
|
||||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||||
|
|
||||||
//function for send email
|
//function for send email
|
||||||
const sendEmail = async () => {
|
const sendEmail = async () => {
|
||||||
const pdfName = pdfDetails[0]?.Name;
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
let sendMail;
|
const params = { docId: pdfDetails?.[0]?.objectId, recipients: emailList };
|
||||||
const docId = pdfDetails?.[0]?.objectId || "";
|
const sendmail = await Parse.Cloud.run("forwarddoc", params);
|
||||||
let presignedUrl = pdfUrl;
|
if (sendmail?.status === "success") {
|
||||||
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") {
|
|
||||||
setSuccessEmail(true);
|
setSuccessEmail(true);
|
||||||
setIsEmail(false);
|
setIsEmail(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -107,7 +40,8 @@ function EmailComponent({
|
|||||||
setEmailList([]);
|
setEmailList([]);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsEmail(false);
|
setIsEmail(false);
|
||||||
setIsAlert({
|
setIsAlert({
|
||||||
@@ -177,7 +111,7 @@ function EmailComponent({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between items-center py-[10px] px-[20px] border-b-[1px] border-base-content">
|
<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")}
|
{t("successfully-signed")}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
@@ -186,14 +120,14 @@ function EmailComponent({
|
|||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
handleToPrint(e, setIsDownloading, pdfDetails)
|
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>
|
<i className="fa-light fa-print" aria-hidden="true"></i>
|
||||||
{t("print")}
|
{t("print")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
setIsDownloadModal(true);
|
setIsDownloadModal(true);
|
||||||
@@ -209,12 +143,12 @@ function EmailComponent({
|
|||||||
{t("email-mssg")}
|
{t("email-mssg")}
|
||||||
</p>
|
</p>
|
||||||
{emailList.length > 0 ? (
|
{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">
|
<div className="flex flex-row flex-wrap">
|
||||||
{emailList.map((data, ind) => {
|
{emailList.map((data, ind) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
key={ind}
|
||||||
>
|
>
|
||||||
<span className="text-base-100 text-[13px]">
|
<span className="text-base-100 text-[13px]">
|
||||||
@@ -234,7 +168,7 @@ function EmailComponent({
|
|||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={emailValue}
|
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}
|
onChange={handleEmailValue}
|
||||||
onKeyDown={handleEnterPress}
|
onKeyDown={handleEnterPress}
|
||||||
onBlur={() => emailValue && handleEnterPress("add")}
|
onBlur={() => emailValue && handleEnterPress("add")}
|
||||||
@@ -251,7 +185,7 @@ function EmailComponent({
|
|||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={emailValue}
|
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}
|
onChange={handleEmailValue}
|
||||||
onKeyDown={handleEnterPress}
|
onKeyDown={handleEnterPress}
|
||||||
placeholder={t("enter-email-plaholder")}
|
placeholder={t("enter-email-plaholder")}
|
||||||
@@ -269,34 +203,22 @@ function EmailComponent({
|
|||||||
{t("email-error-1")}
|
{t("email-error-1")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<div className="mt-2">
|
||||||
className={`${
|
<button
|
||||||
emailValue ? "cursor-pointer" : "cursor-default"
|
type="button"
|
||||||
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
|
className="op-btn op-btn-secondary"
|
||||||
onClick={() => emailValue && handleEnterPress("add")}
|
onClick={() => emailList.length > 0 && sendEmail()}
|
||||||
>
|
>
|
||||||
<i className="fa-light fa-plus" aria-hidden="true"></i>
|
{t("send")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
<div className="bg-[#e3e2e1] mt-[10px] p-[5px] rounded">
|
type="button"
|
||||||
<span className="font-bold">{t("report-heading.Note")}: </span>
|
className="op-btn op-btn-ghost text-base-content ml-2"
|
||||||
<span className="text-[15px]">{t("email-error-2")}</span>
|
onClick={() => handleClose()}
|
||||||
|
>
|
||||||
|
{t("close")}
|
||||||
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</ModalUi>
|
</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 PrevNext from "./PrevNext";
|
||||||
import {
|
import {
|
||||||
base64ToArrayBuffer,
|
base64ToArrayBuffer,
|
||||||
|
decryptPdf,
|
||||||
deletePdfPage,
|
deletePdfPage,
|
||||||
|
flattenPdf,
|
||||||
|
getFileAsArrayBuffer,
|
||||||
handleDownloadCertificate,
|
handleDownloadCertificate,
|
||||||
handleDownloadPdf,
|
handleDownloadPdf,
|
||||||
handleRemoveWidgets,
|
handleRemoveWidgets,
|
||||||
handleToPrint
|
handleToPrint,
|
||||||
|
reorderPdfPages
|
||||||
} from "../../constant/Utils";
|
} from "../../constant/Utils";
|
||||||
import "../../styles/signature.css";
|
import "../../styles/signature.css";
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenu } from "radix-ui";
|
||||||
import ModalUi from "../../primitives/ModalUi";
|
import ModalUi from "../../primitives/ModalUi";
|
||||||
import Loader from "../../primitives/Loader";
|
import Loader from "../../primitives/Loader";
|
||||||
|
import PageReorderModal from "./PageReorderModal";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PDFDocument } from "pdf-lib";
|
import { PDFDocument } from "pdf-lib";
|
||||||
|
import { maxFileSize } from "../../constant/const";
|
||||||
|
|
||||||
function Header(props) {
|
function Header(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const filterPrefill =
|
|
||||||
props?.signerPos &&
|
|
||||||
props?.signerPos?.filter((data) => data.Role !== "prefill");
|
|
||||||
const isMobile = window.innerWidth < 767;
|
const isMobile = window.innerWidth < 767;
|
||||||
const [isDownloading, setIsDownloading] = useState("");
|
const [isDownloading, setIsDownloading] = useState("");
|
||||||
const [isDeletePage, setIsDeletePage] = useState(false);
|
const [isDeletePage, setIsDeletePage] = useState(false);
|
||||||
|
const [isReorderModal, setIsReorderModal] = useState(false);
|
||||||
const mergePdfInputRef = useRef(null);
|
const mergePdfInputRef = useRef(null);
|
||||||
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
|
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
|
||||||
//function for show decline alert
|
//function for show decline alert
|
||||||
const handleDeclinePdfAlert = async () => {
|
const handleDeclinePdfAlert = async () => {
|
||||||
const currentDecline = { currnt: "Sure", isDeclined: true };
|
if (props?.handleDecline) {
|
||||||
props?.setIsDecline(currentDecline);
|
props.handleDecline();
|
||||||
|
} else {
|
||||||
|
const currentDecline = { currnt: "Sure", isDeclined: true };
|
||||||
|
props?.setIsDecline(currentDecline);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleDetelePage = async () => {
|
const handleDetelePage = async () => {
|
||||||
props?.setIsUploadPdf && props?.setIsUploadPdf(true);
|
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 handleFileUpload = async (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
alert("Please upload a valid PDF file.");
|
alert(t("please-select-pdf"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!file.type.includes("pdf")) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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, {
|
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
|
||||||
ignoreEncryption: true
|
ignoreEncryption: true
|
||||||
});
|
});
|
||||||
@@ -78,6 +137,13 @@ function Header(props) {
|
|||||||
useObjectStreams: false
|
useObjectStreams: false
|
||||||
});
|
});
|
||||||
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
|
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.setPdfArrayBuffer(pdfBuffer);
|
||||||
props.setPdfBase64Url(pdfBase64);
|
props.setPdfBase64Url(pdfBase64);
|
||||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
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 (
|
return (
|
||||||
<div className="flex py-[5px]">
|
<div className="flex py-[5px]">
|
||||||
{isMobile && props?.isShowHeader ? (
|
{isMobile && props?.isShowHeader ? (
|
||||||
<div
|
<div
|
||||||
id="navbar"
|
id="navbar"
|
||||||
className="stickyHead"
|
className="stickyHead touch-none"
|
||||||
style={{
|
style={{
|
||||||
width: window.innerWidth + "px"
|
width: window.innerWidth + "px"
|
||||||
}}
|
}}
|
||||||
@@ -127,17 +224,7 @@ function Header(props) {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
className="DropdownMenuItem"
|
className="DropdownMenuItem"
|
||||||
onClick={() => {
|
onClick={() => handleDownloadBtn()}
|
||||||
if (props?.isCompleted) {
|
|
||||||
props?.setIsDownloadModal(true);
|
|
||||||
} else {
|
|
||||||
handleDownloadPdf(
|
|
||||||
props?.pdfDetails,
|
|
||||||
setIsDownloading,
|
|
||||||
props.pdfBase64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<i
|
<i
|
||||||
@@ -147,25 +234,27 @@ function Header(props) {
|
|||||||
{t("download")}
|
{t("download")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{props?.isCompleted && (
|
{
|
||||||
<DropdownMenu.Item
|
props?.isCompleted && (
|
||||||
className="DropdownMenuItem"
|
<DropdownMenu.Item
|
||||||
onClick={() =>
|
className="DropdownMenuItem"
|
||||||
handleDownloadCertificate(
|
onClick={() =>
|
||||||
props?.pdfDetails,
|
handleDownloadCertificate(
|
||||||
setIsDownloading
|
props?.pdfDetails,
|
||||||
)
|
setIsDownloading
|
||||||
}
|
)
|
||||||
>
|
}
|
||||||
<div className="border-none bg-[#fff]">
|
>
|
||||||
<i
|
<div className="border-none bg-[#fff]">
|
||||||
className="fa-light fa-award mr-[3px]"
|
<i
|
||||||
aria-hidden="true"
|
className="fa-light fa-award mr-[3px]"
|
||||||
></i>
|
aria-hidden="true"
|
||||||
{t("certificate")}
|
></i>
|
||||||
</div>
|
{t("certificate")}
|
||||||
</DropdownMenu.Item>
|
</div>
|
||||||
)}
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
{props?.isSignYourself && (
|
{props?.isSignYourself && (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
className="DropdownMenuItem"
|
className="DropdownMenuItem"
|
||||||
@@ -216,7 +305,7 @@ function Header(props) {
|
|||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!props?.isMailSend) {
|
if (!props?.isMailSend) {
|
||||||
props?.alertSendEmail();
|
props?.handleSaveDoc();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`${
|
className={`${
|
||||||
@@ -258,15 +347,23 @@ function Header(props) {
|
|||||||
className="bg-white shadow-md rounded-md px-3 py-2"
|
className="bg-white shadow-md rounded-md px-3 py-2"
|
||||||
sideOffset={5}
|
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
|
<DropdownMenu.Item
|
||||||
className="DropdownMenuItem"
|
className="DropdownMenuItem"
|
||||||
onClick={() =>
|
onClick={() => handleDownloadDoc()}
|
||||||
handleDownloadPdf(
|
|
||||||
props?.pdfDetails,
|
|
||||||
setIsDownloading,
|
|
||||||
props.pdfBase64
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<i
|
<i
|
||||||
@@ -302,6 +399,17 @@ function Header(props) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Item>
|
</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
|
<DropdownMenu.Item
|
||||||
className="DropdownMenuItem"
|
className="DropdownMenuItem"
|
||||||
@@ -375,42 +483,21 @@ function Header(props) {
|
|||||||
/>
|
/>
|
||||||
{props?.isPlaceholder ? (
|
{props?.isPlaceholder ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none">
|
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none"></div>
|
||||||
{!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">
|
<div className="flex">
|
||||||
{props?.setIsEditTemplate && (
|
{props?.setIsEditTemplate && (
|
||||||
<button
|
<button
|
||||||
onClick={() => props?.setIsEditTemplate(true)}
|
onClick={() => props?.setIsEditTemplate(true)}
|
||||||
className="outline-none border-none text-center mr-[3px]"
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
{enabledBackBtn && (
|
{enabledBackBtn && (
|
||||||
<button
|
<button
|
||||||
onClick={() => window.history.go(-2)}
|
onClick={() => window.history.go(-2)}
|
||||||
type="button"
|
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")}
|
{t("back")}
|
||||||
</button>
|
</button>
|
||||||
@@ -419,7 +506,7 @@ function Header(props) {
|
|||||||
disabled={props?.isMailSend && true}
|
disabled={props?.isMailSend && true}
|
||||||
data-tut="headerArea"
|
data-tut="headerArea"
|
||||||
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
|
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
|
||||||
onClick={() => props?.alertSendEmail()}
|
onClick={() => props?.handleSaveDoc()}
|
||||||
>
|
>
|
||||||
{props?.completeBtnTitle
|
{props?.completeBtnTitle
|
||||||
? props?.completeBtnTitle
|
? props?.completeBtnTitle
|
||||||
@@ -445,38 +532,32 @@ function Header(props) {
|
|||||||
></i>
|
></i>
|
||||||
<span className="hidden lg:block">{t("print")}</span>
|
<span className="hidden lg:block">{t("print")}</span>
|
||||||
</button>
|
</button>
|
||||||
{props?.isCompleted && (
|
{
|
||||||
<button
|
props?.isCompleted && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() =>
|
type="button"
|
||||||
handleDownloadCertificate(
|
onClick={() =>
|
||||||
props?.pdfDetails,
|
handleDownloadCertificate(
|
||||||
setIsDownloading
|
props?.pdfDetails,
|
||||||
)
|
setIsDownloading
|
||||||
}
|
)
|
||||||
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
}
|
||||||
>
|
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
||||||
<i
|
>
|
||||||
className="fa-light fa-award py-[3px]"
|
<i
|
||||||
aria-hidden="true"
|
className="fa-light fa-award py-[3px]"
|
||||||
></i>
|
aria-hidden="true"
|
||||||
<span className="hidden lg:block">{t("certificate")}</span>
|
></i>
|
||||||
</button>
|
<span className="hidden lg:block">
|
||||||
)}
|
{t("certificate")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
|
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
|
||||||
onClick={() => {
|
onClick={() => handleDownloadBtn()}
|
||||||
if (props?.isCompleted) {
|
|
||||||
props?.setIsDownloadModal(true);
|
|
||||||
} else {
|
|
||||||
handleDownloadPdf(
|
|
||||||
props?.pdfDetails,
|
|
||||||
setIsDownloading,
|
|
||||||
props.pdfBase64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="fa-light fa-download py-[3px]"
|
className="fa-light fa-download py-[3px]"
|
||||||
@@ -487,29 +568,13 @@ function Header(props) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex" data-tut="reactourFifth">
|
<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?.currentSigner && (
|
||||||
<>
|
<>
|
||||||
{props?.templateId && (
|
{props?.templateId && (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => handleDownloadDoc()}
|
||||||
handleDownloadPdf(
|
|
||||||
props?.pdfDetails,
|
|
||||||
setIsDownloading,
|
|
||||||
props.pdfBase64
|
|
||||||
)
|
|
||||||
}
|
|
||||||
type="button"
|
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>
|
<span className="hidden lg:block">{t("download")}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -525,14 +590,8 @@ function Header(props) {
|
|||||||
{!props?.templateId && (
|
{!props?.templateId && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
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]"
|
||||||
onClick={() =>
|
onClick={() => handleDownloadDoc()}
|
||||||
handleDownloadPdf(
|
|
||||||
props?.pdfDetails,
|
|
||||||
setIsDownloading,
|
|
||||||
props.pdfBase64
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<i className="fa-light fa-arrow-down font-semibold lg:hidden"></i>
|
<i className="fa-light fa-arrow-down font-semibold lg:hidden"></i>
|
||||||
<span className="hidden lg:block">{t("download")}</span>
|
<span className="hidden lg:block">{t("download")}</span>
|
||||||
@@ -551,23 +610,25 @@ function Header(props) {
|
|||||||
)
|
)
|
||||||
) : props?.isCompleted ? (
|
) : props?.isCompleted ? (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{props?.isCompleted && (
|
{
|
||||||
<button
|
props?.isCompleted && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() =>
|
type="button"
|
||||||
handleDownloadCertificate(
|
onClick={() =>
|
||||||
props?.pdfDetails,
|
handleDownloadCertificate(
|
||||||
setIsDownloading
|
props?.pdfDetails,
|
||||||
)
|
setIsDownloading
|
||||||
}
|
)
|
||||||
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
}
|
||||||
>
|
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">
|
<i className="fa-light fa-award" aria-hidden="true"></i>
|
||||||
{t("certificate")}
|
<span className="hidden lg:block ml-1">
|
||||||
</span>
|
{t("certificate")}
|
||||||
</button>
|
</span>
|
||||||
)}
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
handleToPrint(e, setIsDownloading, props?.pdfDetails)
|
handleToPrint(e, setIsDownloading, props?.pdfDetails)
|
||||||
@@ -581,7 +642,8 @@ function Header(props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
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>
|
<i className="fa-light fa-download" aria-hidden="true"></i>
|
||||||
<span className="hidden lg:block ml-1">{t("download")}</span>
|
<span className="hidden lg:block ml-1">{t("download")}</span>
|
||||||
@@ -610,7 +672,7 @@ function Header(props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => window.history.go(-2)}
|
onClick={() => window.history.go(-2)}
|
||||||
type="button"
|
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")}
|
{t("back")}
|
||||||
</button>
|
</button>
|
||||||
@@ -655,8 +717,8 @@ function Header(props) {
|
|||||||
handleClose={() => setIsDeletePage(false)}
|
handleClose={() => setIsDeletePage(false)}
|
||||||
>
|
>
|
||||||
<div className="h-[100%] p-[20px]">
|
<div className="h-[100%] p-[20px]">
|
||||||
<p className="font-medium">{t("delete-alert-2")}</p>
|
<p className="font-medium text-base-content">{t("delete-alert-2")}</p>
|
||||||
<p className="pt-3">{t("delete-note")}</p>
|
<p className="pt-3 text-base-content">{t("delete-note")}</p>
|
||||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDetelePage()}
|
onClick={() => handleDetelePage()}
|
||||||
@@ -668,12 +730,18 @@ function Header(props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsDeletePage(false)}
|
onClick={() => setIsDeletePage(false)}
|
||||||
type="button"
|
type="button"
|
||||||
className="op-btn op-btn-ghost"
|
className="op-btn op-btn-ghost text-base-content"
|
||||||
>
|
>
|
||||||
{t("no")}
|
{t("no")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ModalUi>
|
</ModalUi>
|
||||||
|
<PageReorderModal
|
||||||
|
isOpen={isReorderModal}
|
||||||
|
handleClose={() => setIsReorderModal(false)}
|
||||||
|
totalPages={props.allPages}
|
||||||
|
onSave={handleReorderSave}
|
||||||
|
/>
|
||||||
</div>
|
</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,18 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import ModalUi from "../../primitives/ModalUi";
|
import ModalUi from "../../primitives/ModalUi";
|
||||||
import {
|
import {
|
||||||
|
drawWidget,
|
||||||
handleCopyNextToWidget,
|
handleCopyNextToWidget,
|
||||||
randomId,
|
randomId
|
||||||
textWidget
|
|
||||||
} from "../../constant/Utils";
|
} from "../../constant/Utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { setPrefillImg } from "../../redux/reducers/widgetSlice";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
function PlaceholderCopy(props) {
|
function PlaceholderCopy(props) {
|
||||||
|
const { prefillImg } = useSelector((state) => state.widget);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
const copyType = [
|
const copyType = [
|
||||||
{ id: 1, type: "All pages" },
|
{ id: 1, type: "All pages" },
|
||||||
{ id: 2, type: "All pages but last" },
|
{ id: 2, type: "All pages but last" },
|
||||||
@@ -125,14 +129,19 @@ function PlaceholderCopy(props) {
|
|||||||
);
|
);
|
||||||
for (let i = 0; i < props.allPages; i++) {
|
for (let i = 0; i < props.allPages; i++) {
|
||||||
const newId = randomId();
|
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
|
//get exist placeholder position for particular page
|
||||||
const existPlaceholder = filterSignerPosition[0].placeHolder.filter(
|
const existPlaceholder = filterSignerPosition[0].placeHolder.filter(
|
||||||
(data) => data.pageNumber === newPageNumber
|
(data) => data.pageNumber === newPageNumber
|
||||||
);
|
);
|
||||||
const existPlaceholderPosition =
|
const existPlaceholderPosition =
|
||||||
existPlaceholder[0] && existPlaceholder[0].pos;
|
existPlaceholder[0] && existPlaceholder[0].pos;
|
||||||
|
|
||||||
//function for get copy to requested location of placeholder position
|
//function for get copy to requested location of placeholder position
|
||||||
const getPlaceholderObj = getCopyPlaceholderPosition(
|
const getPlaceholderObj = getCopyPlaceholderPosition(
|
||||||
type,
|
type,
|
||||||
@@ -144,6 +153,23 @@ function PlaceholderCopy(props) {
|
|||||||
newPlaceholderPosition.push(getPlaceholderObj);
|
newPlaceholderPosition.push(getPlaceholderObj);
|
||||||
}
|
}
|
||||||
newPageNumber++;
|
newPageNumber++;
|
||||||
|
if (
|
||||||
|
filterSignerPosition[0]?.Role === "prefill" &&
|
||||||
|
currentPlaceholder?.options?.response &&
|
||||||
|
(currentPlaceholder?.type === "image" ||
|
||||||
|
currentPlaceholder?.type === drawWidget)
|
||||||
|
) {
|
||||||
|
const getPrefillImg = prefillImg?.find(
|
||||||
|
(x) => x.id === currentPlaceholder?.key
|
||||||
|
);
|
||||||
|
const base64Img = getPrefillImg?.base64;
|
||||||
|
dispatch(
|
||||||
|
setPrefillImg({
|
||||||
|
id: newId,
|
||||||
|
base64: base64Img
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let updatedSignerPlaceholder;
|
let updatedSignerPlaceholder;
|
||||||
if (props?.signerObjId) {
|
if (props?.signerObjId) {
|
||||||
@@ -213,6 +239,7 @@ function PlaceholderCopy(props) {
|
|||||||
|
|
||||||
//function for getting selected type placeholder copy
|
//function for getting selected type placeholder copy
|
||||||
const handleApplyCopy = () => {
|
const handleApplyCopy = () => {
|
||||||
|
const newId = randomId();
|
||||||
if (selectCopyType === 4) {
|
if (selectCopyType === 4) {
|
||||||
const signerPosition = props.xyPosition;
|
const signerPosition = props.xyPosition;
|
||||||
let currentXYposition;
|
let currentXYposition;
|
||||||
@@ -230,13 +257,30 @@ function PlaceholderCopy(props) {
|
|||||||
);
|
);
|
||||||
//function to create new widget next to just widget
|
//function to create new widget next to just widget
|
||||||
handleCopyNextToWidget(
|
handleCopyNextToWidget(
|
||||||
|
newId,
|
||||||
currentXYposition,
|
currentXYposition,
|
||||||
props.widgetType,
|
|
||||||
props.xyPosition,
|
props.xyPosition,
|
||||||
props.pageNumber,
|
props.pageNumber,
|
||||||
props.setXyPosition,
|
props.setXyPosition,
|
||||||
props?.Id
|
props?.Id
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
filterSignerPosition[0]?.Role === "prefill" &&
|
||||||
|
currentXYposition?.options?.response &&
|
||||||
|
(currentXYposition?.type === "image" ||
|
||||||
|
currentXYposition?.type === drawWidget)
|
||||||
|
) {
|
||||||
|
const getPrefillImg = prefillImg?.find(
|
||||||
|
(x) => x.id === currentXYposition?.key
|
||||||
|
);
|
||||||
|
const base64Img = getPrefillImg?.base64;
|
||||||
|
dispatch(
|
||||||
|
setPrefillImg({
|
||||||
|
id: newId,
|
||||||
|
base64: base64Img
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const getIndex = props?.xyPosition.findIndex(
|
const getIndex = props?.xyPosition.findIndex(
|
||||||
(data) => data.pageNumber === props.pageNumber
|
(data) => data.pageNumber === props.pageNumber
|
||||||
@@ -248,8 +292,8 @@ function PlaceholderCopy(props) {
|
|||||||
);
|
);
|
||||||
//function to create new widget next to just widget
|
//function to create new widget next to just widget
|
||||||
handleCopyNextToWidget(
|
handleCopyNextToWidget(
|
||||||
|
newId,
|
||||||
currentXYposition,
|
currentXYposition,
|
||||||
props.widgetType,
|
|
||||||
props.xyPosition,
|
props.xyPosition,
|
||||||
getIndex,
|
getIndex,
|
||||||
props.setXyPosition
|
props.setXyPosition
|
||||||
@@ -260,11 +304,6 @@ function PlaceholderCopy(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleUniqueId = () => {
|
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);
|
props.setIsPageCopy(false);
|
||||||
setSelectCopyType(1);
|
setSelectCopyType(1);
|
||||||
};
|
};
|
||||||
@@ -306,7 +345,7 @@ function PlaceholderCopy(props) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="op-btn op-btn-ghost ml-2"
|
className="op-btn op-btn-ghost text-base-content ml-2"
|
||||||
onClick={() => handleUniqueId()}
|
onClick={() => handleUniqueId()}
|
||||||
>
|
>
|
||||||
{t("cancel")}
|
{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,643 @@
|
|||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
forwardRef,
|
||||||
|
useRef
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
onChangeInput,
|
||||||
|
getMonth,
|
||||||
|
getYear,
|
||||||
|
radioButtonWidget,
|
||||||
|
textInputWidget,
|
||||||
|
cellsWidget,
|
||||||
|
textWidget,
|
||||||
|
months,
|
||||||
|
years,
|
||||||
|
selectCheckbox,
|
||||||
|
isBase64,
|
||||||
|
drawWidget
|
||||||
|
} 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 uppercase"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case drawWidget:
|
||||||
|
return prefillImgLoad[props.pos?.key] ? (
|
||||||
|
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
) : widgetValue ? (
|
||||||
|
<img
|
||||||
|
alt="signature"
|
||||||
|
draggable="false"
|
||||||
|
src={widgetValue}
|
||||||
|
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={widgetCls}>
|
||||||
|
{props.pos.type && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: props.pos
|
||||||
|
? props.calculateFontsize(props.pos)
|
||||||
|
: "11px"
|
||||||
|
}}
|
||||||
|
className="font-medium"
|
||||||
|
>
|
||||||
|
{formatWidgetName()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return props.pos.SignUrl ? (
|
||||||
|
<div className="pointer-events-none">
|
||||||
|
<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,866 @@
|
|||||||
|
import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import ModalUi from "../../primitives/ModalUi";
|
||||||
|
import {
|
||||||
|
getMonth,
|
||||||
|
getYear,
|
||||||
|
radioButtonWidget,
|
||||||
|
compressedFileSize,
|
||||||
|
textWidget,
|
||||||
|
months,
|
||||||
|
changeDateToMomentFormat,
|
||||||
|
convertBase64ToFile,
|
||||||
|
generatePdfName,
|
||||||
|
drawWidget,
|
||||||
|
getBase64MimeType,
|
||||||
|
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";
|
||||||
|
import Draw from "./tab/Draw";
|
||||||
|
import PenColorComponent from "./tab/PenColorComponent";
|
||||||
|
|
||||||
|
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();
|
||||||
|
const canvasRefs = useRef({});
|
||||||
|
const [penColor, setPenColor] = useState("blue");
|
||||||
|
// Track already loaded image keys so they don't increment multiple times
|
||||||
|
const loadedSet = useRef(new Set());
|
||||||
|
const initializedRef = useRef(false); // prevent rerun on state updates
|
||||||
|
const [image, setImage] = useState(null);
|
||||||
|
const [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(() => {
|
||||||
|
setLoading(true);
|
||||||
|
//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 (base64) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const imageName = generatePdfName(16);
|
||||||
|
const imgType = image?.imgType || getBase64MimeType(base64);
|
||||||
|
const imageUrl = await convertBase64ToFile(
|
||||||
|
imageName,
|
||||||
|
base64 || image.src,
|
||||||
|
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(response);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
...(item.SignUrl !== undefined && { 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);
|
||||||
|
};
|
||||||
|
//function for set signature url
|
||||||
|
const handleSignatureChange = (drawImage, position) => {
|
||||||
|
handleWidgetDetails(position, drawImage);
|
||||||
|
};
|
||||||
|
const getCanvasRef = (widgetId) => {
|
||||||
|
if (!canvasRefs.current[widgetId]) {
|
||||||
|
canvasRefs.current[widgetId] = React.createRef();
|
||||||
|
}
|
||||||
|
return canvasRefs.current[widgetId];
|
||||||
|
};
|
||||||
|
const clearCanvasById = (widgetId) => {
|
||||||
|
const canvasRef = canvasRefs.current?.[widgetId];
|
||||||
|
|
||||||
|
if (canvasRef?.current) {
|
||||||
|
canvasRef.current.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleWidgetType = (position, id) => {
|
||||||
|
switch (position?.type) {
|
||||||
|
case "checkbox":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className={widgetTitle}>{position.options?.name}</span>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case drawWidget:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className={widgetTitle}>{position.options?.name}</span>
|
||||||
|
<Draw
|
||||||
|
penColor={penColor}
|
||||||
|
canvasRef={getCanvasRef(position.key)}
|
||||||
|
currWidgetsDetails={position}
|
||||||
|
handleSignatureChange={handleSignatureChange}
|
||||||
|
prefillCls={"prefillCanvas"}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-between mt-[10px]">
|
||||||
|
<PenColorComponent
|
||||||
|
penColor={penColor}
|
||||||
|
setPenColor={setPenColor}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
clearCanvasById(position?.key);
|
||||||
|
handleClearImage(position);
|
||||||
|
}}
|
||||||
|
className="flex justify-start text-blue-500 underline cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("clear")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return position?.SignUrl ? (
|
||||||
|
<div className="pointer-events-none">
|
||||||
|
<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) => {
|
||||||
|
//checking is there any draw widget response have base64 url then generate that base64 to url and save it
|
||||||
|
const prefillWidgets = props.xyPosition?.find((x) => x.Role === "prefill");
|
||||||
|
let updatedXyPosition = [];
|
||||||
|
if (prefillWidgets) {
|
||||||
|
const isDrawWidget = (prefillWidgets.placeHolder ?? []).some((ph) =>
|
||||||
|
(ph?.pos ?? []).some((p) => p?.type === drawWidget)
|
||||||
|
);
|
||||||
|
if (isDrawWidget) {
|
||||||
|
const getPrefill = props.xyPosition.find((x) => x?.Role === "prefill");
|
||||||
|
const getPlaceholder = getPrefill?.placeHolder ?? [];
|
||||||
|
const updatedData = await Promise.all(
|
||||||
|
getPlaceholder.map(async (page) => ({
|
||||||
|
...page,
|
||||||
|
pos: await Promise.all(
|
||||||
|
page.pos.map(async (item) => {
|
||||||
|
if (item?.type === drawWidget) {
|
||||||
|
const widgetResponse = item?.options?.response;
|
||||||
|
|
||||||
|
// Skip if no response
|
||||||
|
if (!widgetResponse) return item;
|
||||||
|
|
||||||
|
// If already URL, do not re-upload
|
||||||
|
if (
|
||||||
|
typeof widgetResponse === "string" &&
|
||||||
|
widgetResponse.startsWith("http")
|
||||||
|
) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert THIS widget's base64 → URL
|
||||||
|
const drawUrl = await handleSavePrefillImg(widgetResponse);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
options: {
|
||||||
|
...item.options,
|
||||||
|
response: drawUrl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedXyPosition = props.xyPosition.map((obj) => {
|
||||||
|
if (obj.Role === "prefill") {
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
placeHolder: updatedData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
props.setXyPosition(updatedXyPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await props.handleCreateDocument();
|
||||||
|
};
|
||||||
|
//`loadOptions` function to use show all list of signer in dropdown
|
||||||
|
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}
|
onClick={previousPage}
|
||||||
>
|
>
|
||||||
<span className="block">
|
<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>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs text-base-content font-medium mx-2 2xl:text-[20px]">
|
<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}
|
onClick={nextPage}
|
||||||
>
|
>
|
||||||
<span className="block">
|
<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>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import {
|
import {
|
||||||
color,
|
color,
|
||||||
@@ -12,6 +12,7 @@ const cursor =
|
|||||||
const RecipientList = (props) => {
|
const RecipientList = (props) => {
|
||||||
const [animationParent] = useAutoAnimate();
|
const [animationParent] = useAutoAnimate();
|
||||||
const [isHover, setIsHover] = useState();
|
const [isHover, setIsHover] = useState();
|
||||||
|
const [isPrefill, setIsPrefill] = useState(false);
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
//function for onhover signer name change background color
|
//function for onhover signer name change background color
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
@@ -31,7 +32,7 @@ const RecipientList = (props) => {
|
|||||||
e.preventDefault();
|
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) => {
|
const handleChangeSequence = (e, ind, isUp, isDown, obj) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let draggedItemId;
|
let draggedItemId;
|
||||||
@@ -80,8 +81,42 @@ const RecipientList = (props) => {
|
|||||||
});
|
});
|
||||||
props?.setSignerPos(changeOrderSignerList);
|
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 (
|
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.length > 0 &&
|
||||||
props.signersdata.map((obj, ind) => {
|
props.signersdata.map((obj, ind) => {
|
||||||
return (
|
return (
|
||||||
@@ -118,20 +153,11 @@ const RecipientList = (props) => {
|
|||||||
: color[ind % color.length]
|
: color[ind % color.length]
|
||||||
: "transparent"
|
: "transparent"
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => handleSelectRecipient(e, ind, obj)}
|
||||||
e.preventDefault();
|
|
||||||
props.setIsSelectId(ind);
|
|
||||||
props.setUniqueId(obj.Id);
|
|
||||||
props.setRoleName(obj.Role);
|
|
||||||
props.setBlockColor(obj?.blockColor);
|
|
||||||
if (props.handleModal) {
|
|
||||||
props.handleModal();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center w-full">
|
<div className="flex flex-row items-center w-full overflow-hidden pr-2">
|
||||||
<div
|
<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={{
|
style={{
|
||||||
background: obj?.blockColor
|
background: obj?.blockColor
|
||||||
? darkenColor(obj?.blockColor, 0.4)
|
? darkenColor(obj?.blockColor, 0.4)
|
||||||
@@ -142,38 +168,22 @@ const RecipientList = (props) => {
|
|||||||
{isWidgetExist(obj.Id) ? (
|
{isWidgetExist(obj.Id) ? (
|
||||||
<i className="fa-light fa-check"></i>
|
<i className="fa-light fa-check"></i>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>{getFirstLetter(obj?.Name ?? obj?.Role)}</>
|
||||||
{obj.Name
|
|
||||||
? getFirstLetter(obj.Name)
|
|
||||||
: getFirstLetter(obj.Role)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${obj.Name ? "flex-col" : "flex-row"} ${
|
||||||
obj.Name ? "flex-col" : "flex-row"
|
isSelected(ind) ? "text-[#424242]" : "text-base-content"
|
||||||
} flex items-center`}
|
} flex overflow-hidden flex-grow-0`}
|
||||||
>
|
>
|
||||||
{obj.Name ? (
|
{obj.Name ? (
|
||||||
<span
|
<span className="text-[12px] font-bold truncate whitespace-nowrap">
|
||||||
className={`${
|
|
||||||
(!isMobile && isHover === ind) ||
|
|
||||||
props.isSelectListId === ind
|
|
||||||
? "text-[#424242]"
|
|
||||||
: "text-base-content"
|
|
||||||
} text-[12px] font-bold w-[100px] whitespace-nowrap overflow-hidden text-ellipsis`}
|
|
||||||
>
|
|
||||||
{obj.Name}
|
{obj.Name}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
className={`${
|
className="text-[12px] font-bold truncate whitespace-nowrap cursor-pointer"
|
||||||
(!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`}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEdit({ [obj.Id]: true });
|
setIsEdit({ [obj.Id]: true });
|
||||||
props.setRoleName(obj.Role);
|
props.setRoleName(obj.Role);
|
||||||
@@ -182,7 +192,7 @@ const RecipientList = (props) => {
|
|||||||
{isEdit?.[obj.Id] && props.handleRoleChange ? (
|
{isEdit?.[obj.Id] && props.handleRoleChange ? (
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="bg-transparent p-[3px]"
|
className="bg-transparent p-[3px] w-full"
|
||||||
value={obj.Role}
|
value={obj.Role}
|
||||||
onChange={(e) => props.handleRoleChange(e, obj.Id)}
|
onChange={(e) => props.handleRoleChange(e, obj.Id)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@@ -204,14 +214,7 @@ const RecipientList = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{obj.Name && (
|
{obj.Name && (
|
||||||
<span
|
<span className="text-[10px] font-medium truncate whitespace-nowrap">
|
||||||
className={` ${
|
|
||||||
(!isMobile && isHover === ind) ||
|
|
||||||
props.isSelectListId === ind
|
|
||||||
? "text-[#424242]"
|
|
||||||
: "text-base-content"
|
|
||||||
} text-[10px] font-medium w-[100px] whitespace-nowrap overflow-hidden text-ellipsis`}
|
|
||||||
>
|
|
||||||
{obj?.Role || obj?.Email}
|
{obj?.Role || obj?.Email}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -247,18 +250,13 @@ const RecipientList = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{props.handleDeleteUser && (
|
{props.handleDeleteUser && obj?.Role !== "prefill" && (
|
||||||
<div
|
<div
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
props.handleDeleteUser(obj.Id);
|
props.handleDeleteUser(obj.Id);
|
||||||
}}
|
}}
|
||||||
className={`${
|
className={`${isSelected(ind) ? "text-[#424242]" : "text-base-content"} cursor-pointer`}
|
||||||
(!isMobile && isHover === ind) ||
|
|
||||||
props.isSelectListId === ind
|
|
||||||
? "text-[#424242]"
|
|
||||||
: "text-base-content"
|
|
||||||
} cursor-pointer`}
|
|
||||||
>
|
>
|
||||||
<i className="fa-light fa-trash-can 2xl:text-[22px]"></i>
|
<i className="fa-light fa-trash-can 2xl:text-[22px]"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -3,7 +3,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Document, Page } from "react-pdf";
|
import { Document, Page } from "react-pdf";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { PDFDocument } from "pdf-lib";
|
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) {
|
function RenderAllPdfPage(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -11,7 +17,7 @@ function RenderAllPdfPage(props) {
|
|||||||
const mergePdfInputRef = useRef(null);
|
const mergePdfInputRef = useRef(null);
|
||||||
const [signPageNumber, setSignPageNumber] = useState([]);
|
const [signPageNumber, setSignPageNumber] = useState([]);
|
||||||
const [bookmarkColor, setBookmarkColor] = useState("");
|
const [bookmarkColor, setBookmarkColor] = useState("");
|
||||||
const isHeader = useSelector((state) => state.showHeader);
|
const isSidebar = useSelector((state) => state.sidebar.isOpen);
|
||||||
const [pageWidth, setPageWidth] = useState("");
|
const [pageWidth, setPageWidth] = useState("");
|
||||||
|
|
||||||
//set all number of pages after load pdf
|
//set all number of pages after load pdf
|
||||||
@@ -45,7 +51,7 @@ function RenderAllPdfPage(props) {
|
|||||||
const timer = setTimeout(updateSize, 100); // match the transition duration
|
const timer = setTimeout(updateSize, 100); // match the transition duration
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
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.
|
//'function `addSignatureBookmark` is used to display the page where the user's signature is located.
|
||||||
const addSignatureBookmark = (index) => {
|
const addSignatureBookmark = (index) => {
|
||||||
const ispageNumber = signPageNumber.includes(index + 1);
|
const ispageNumber = signPageNumber.includes(index + 1);
|
||||||
@@ -62,19 +68,69 @@ function RenderAllPdfPage(props) {
|
|||||||
};
|
};
|
||||||
const pdfDataBase64 = `data:application/pdf;base64,${props?.pdfBase64Url}`;
|
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
|
// `handleFileUpload` is trigger when user click on add pages btn and is used to merge multiple pdf
|
||||||
const handleFileUpload = async (e) => {
|
const handleFileUpload = async (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
alert("Please upload a valid PDF file.");
|
alert(t("please-select-pdf"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!file.type.includes("pdf")) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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, {
|
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
|
||||||
ignoreEncryption: true
|
ignoreEncryption: true
|
||||||
});
|
});
|
||||||
@@ -91,6 +147,13 @@ function RenderAllPdfPage(props) {
|
|||||||
useObjectStreams: false
|
useObjectStreams: false
|
||||||
});
|
});
|
||||||
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
|
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.setPdfArrayBuffer(pdfBuffer);
|
||||||
props.setPdfBase64Url(pdfBase64);
|
props.setPdfBase64Url(pdfBase64);
|
||||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||||
@@ -110,6 +173,7 @@ function RenderAllPdfPage(props) {
|
|||||||
autoSignScroll hide-scrollbar max-h-[100vh] `}
|
autoSignScroll hide-scrollbar max-h-[100vh] `}
|
||||||
>
|
>
|
||||||
<Document
|
<Document
|
||||||
|
error=""
|
||||||
loading={t("loading-doc")}
|
loading={t("loading-doc")}
|
||||||
onLoadSuccess={onDocumentLoad}
|
onLoadSuccess={onDocumentLoad}
|
||||||
file={pdfDataBase64}
|
file={pdfDataBase64}
|
||||||
@@ -155,8 +219,8 @@ function RenderAllPdfPage(props) {
|
|||||||
onChange={handleFileUpload}
|
onChange={handleFileUpload}
|
||||||
/>
|
/>
|
||||||
<i className="fa-light fa-plus text-gray-500"></i>
|
<i className="fa-light fa-plus text-gray-500"></i>
|
||||||
<span className="text-xs lg:text-sm text-base-content ">
|
<span className="text-xs lg:text-sm text-base-content">
|
||||||
Add pages
|
{t("add-pages")}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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: "es", text: "Española" }, //spanish
|
||||||
{ value: "fr", text: "Français" }, //french
|
{ value: "fr", text: "Français" }, //french
|
||||||
{ value: "it", text: "Italiano" }, //italian
|
{ 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 defaultLanguage = i18next.language || "en";
|
||||||
const [lang, setLang] = useState(defaultLanguage);
|
const [lang, setLang] = useState(defaultLanguage);
|
||||||
@@ -23,14 +24,14 @@ function SelectLanguage(props) {
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
!props.isProfile && " mt-[9px] pb-2 md:pb-0 "
|
!props.isProfile && " mt-[9px] pb-2 md:pb-0 "
|
||||||
} flex justify-center items-center `}
|
} flex justify-center items-center text-base-content`}
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
value={lang}
|
value={lang}
|
||||||
onChange={handleChangeLang}
|
onChange={handleChangeLang}
|
||||||
className={`${
|
className={`${
|
||||||
!props.isProfile ? " md:w-[15%] w-[50%]" : "w-[180px]"
|
!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>
|
<option disabled>select</option>
|
||||||
{languages.map((item) => {
|
{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 (
|
return (
|
||||||
<div
|
<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) }}
|
style={{ background: checkSignerBackColor(props.obj) }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{ background: checkUserNameColor(props.obj) }}
|
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">
|
<span className="text-[12px] text-center font-bold text-black uppercase">
|
||||||
{getFirstLetter(
|
{getFirstLetter(
|
||||||
@@ -40,11 +40,11 @@ function SignerListComponent(props) {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-grow-0 flex-col overflow-hidden pr-2">
|
||||||
<span className="text-[12px] font-bold text-[#424242] w-[100px] whitespace-nowrap overflow-hidden text-ellipsis">
|
<span className="text-[12px] font-bold truncate whitespace-nowrap">
|
||||||
{props.obj?.Name || props?.obj?.Role}
|
{props.obj?.Name || props?.obj?.Role}
|
||||||
</span>
|
</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}
|
{props.obj?.Email || props.obj?.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||