Compare commits

...

2 Commits

Author SHA1 Message Date
=bhagwanSingh 14ede77a16 upd 26 sep 2025-09-26 19:56:38 +05:30
=bhagwanSingh 69778e81d9 Changes 20Sep 2025-09-20 00:01:33 +05:30
85 changed files with 5372 additions and 3112 deletions

7
.env
View File

@ -1,7 +1,10 @@
# VITE_BASE_URI="https://glider-precious-similarly.ngrok-free.app"
# VITE_BASE_URI="https://wired-terminally-muskrat.ngrok-free.app"
# VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app"
# VITE_BASE_URI="https://python-amused-adder.ngrok-free.app"
VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app"
# VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app"
# VITE_BASE_URI="https://smashing-albacore-engaging.ngrok-free.app"
VITE_BASE_URI="https://basically-polished-yeti.ngrok-free.app"
# VITE_BASE_URI="http://192.168.2.176:8083"
# VITE_BASE_URI="http://10.10.1.248:8083"

100
package-lock.json generated
View File

@ -8,6 +8,9 @@
"name": "sme-web",
"version": "0.0.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@mdx-js/react": "^3.1.0",
"@react-oauth/google": "^0.12.1",
"@reduxjs/toolkit": "^2.5.1",
@ -355,6 +358,103 @@
"node": ">=6.9.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/accessibility/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/@dnd-kit/modifiers": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",

View File

@ -11,6 +11,9 @@
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@mdx-js/react": "^3.1.0",
"@react-oauth/google": "^0.12.1",
"@reduxjs/toolkit": "^2.5.1",

BIN
public/completion-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
public/docs-made-easy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
public/forms-made-easy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/sme-colored.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/sme-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -23,6 +23,8 @@ import DocumentCompletionScreen from "./components/process/CompletionScreen/Docu
import JoyrideController from "./utlity/JoyRideTour";
import Contacts from "./components/contacts/Contacts";
import TransactionSentSuccess from "./components/process/CompletionScreen/TransactionSentSuccess";
import CompletionScreen from "./components/client/CompletionScreen";
import SessionExpired from "./components/client/SessionExpired";
const App = () => {
return (
@ -97,12 +99,17 @@ const App = () => {
</ProtectedRoute>
} />
<Route path="/completion" element={
<CompletionScreen />
} />
{/* Links work wothout Login */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/sign/:recipientId/:signType" element={<SignDocument />} />
<Route path="/sign/:recipientId/" element={<SignDocument />} />
<Route path="/view" element={<DocView />} />
<Route path="/session_expired" element={<SessionExpired />} />
{/* Links work wothout Login */}
</Routes>

View File

@ -5,6 +5,7 @@ import api from '../config/api';
import { IoSettings, IoSettingsOutline } from 'react-icons/io5';
import { useSelector } from 'react-redux';
import { RootState } from '../store/store';
import { GoBell } from 'react-icons/go';
interface AccountNavProps {
accountName?: string | null;
@ -86,10 +87,10 @@ const CommonNav: React.FC<AccountNavProps> = ({
}
useEffect(() => {
if(initial !== "") return
if (initial !== "") return
console.log(userDetailsAsync, 'line 89')
let initialN = (userDetailsAsync.firstName && userDetailsAsync.firstName[0])
initialN = initialN+""+(userDetailsAsync.lastName && userDetailsAsync.lastName[0])
initialN = initialN + "" + (userDetailsAsync.lastName && userDetailsAsync.lastName[0])
setInitial(initialN)
}, [userDetailsAsync])
@ -335,7 +336,7 @@ const CommonNav: React.FC<AccountNavProps> = ({
{/* Breadcrumb Navigation */}
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
{filteredPathnames.length === 0 ? (
<span className="text-gray-900 font-semibold">Home</span>
<span className="hidden sm:block text-sm ml-4">Welcome <span className='text-white ml-1 bg-[#34BEFF] px-2.5 py-1 rounded-2xl'>{user.firstName ? user.firstName : user.lastName}!</span></span>
) : (
filteredPathnames.map((segment, index) => {
const label = labelMap[segment] || formatSegment(segment);
@ -363,6 +364,36 @@ const CommonNav: React.FC<AccountNavProps> = ({
</div>
<div className="flex items-center space-x-2">
{/* SETINGS - START */}
<div
onClick={handleSettingsClick}
className="bg-[#F6F6F6] rounded-md py-1.5 px-2 cursor-pointer transition-all duration-200 hover:bg-[#DCDADA] mr-3">
<svg width="16" height="20" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.54">
<path d="M2 9.10996V14.88C2 17 2 17 4 18.35L9.5 21.53C10.33 22.01 11.68 22.01 12.5 21.53L18 18.35C20 17 20 17 20 14.89V9.10996C20 6.99996 20 6.99996 18 5.64996L12.5 2.46996C11.68 1.98996 10.33 1.98996 9.5 2.46996L4 5.64996C2 6.99996 2 6.99996 2 9.10996Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M11 15C12.6569 15 14 13.6569 14 12C14 10.3431 12.6569 9 11 9C9.34315 9 8 10.3431 8 12C8 13.6569 9.34315 15 11 15Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</g>
</svg>
</div>
{/* SETINGS - END */}
{/* Bell START */}
<div className="bg-[#F6F6F6] rounded-md py-1.5 px-2 cursor-pointer transition-all duration-200 hover:bg-[#DCDADA]">
<svg width="14" height="20" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.7493 8.74963C16.3353 8.74963 15.9993 8.41363 15.9993 7.99963C15.9993 4.92988 13.6087 2.30563 10.5562 2.02513C7.49653 1.7435 4.6574 3.88475 4.0994 6.9005C4.03265 7.25975 3.9989 7.6295 3.9989 7.99963C3.9989 8.41363 3.6629 8.74963 3.2489 8.74963C2.8349 8.74963 2.4989 8.41363 2.4989 7.99963C2.4989 7.538 2.5409 7.07638 2.62415 6.6275C3.32165 2.85763 6.8639 0.177876 10.6938 0.531126C14.5102 0.882126 17.4993 4.16263 17.4993 7.99963C17.4993 8.414 17.1637 8.74963 16.7493 8.74963Z" fill="#757575" />
<path d="M16.7493 17.7496C16.3353 17.7496 15.9993 17.4136 15.9993 16.9996C15.9993 16.5856 16.3353 16.2496 16.7493 16.2496C17.4873 16.2496 17.9433 15.7445 18.1237 15.2716C18.3033 14.8014 18.2996 14.1234 17.7495 13.6314C17.4408 13.3554 17.4138 12.881 17.6906 12.5724C17.9662 12.2637 18.441 12.2371 18.7492 12.5135C19.6702 13.337 19.9747 14.63 19.5247 15.8067C19.074 16.9869 17.9846 17.7496 16.7493 17.7496Z" fill="#757575" />
<path d="M16.7493 10.4686C16.3353 10.4686 15.9993 10.1326 15.9993 9.71863V8C15.9993 7.586 16.3353 7.25 16.7493 7.25C17.1633 7.25 17.4993 7.586 17.4993 8V9.71863C17.4993 10.1326 17.1637 10.4686 16.7493 10.4686Z" fill="#757575" />
<path d="M18.249 13.8226C18.0708 13.8226 17.8923 13.7596 17.7495 13.6318C16.6372 12.6369 15.9993 11.2108 15.9993 9.71863C15.9993 9.30463 16.3353 8.96863 16.7493 8.96863C17.1633 8.96863 17.4993 9.30463 17.4993 9.71863C17.4993 10.7844 17.955 11.8033 18.7492 12.5139C19.0578 12.7899 19.0848 13.2643 18.8081 13.5729C18.6603 13.7383 18.4552 13.8226 18.249 13.8226Z" fill="#757575" />
<path d="M3.24927 10.4686C2.83527 10.4686 2.49927 10.1326 2.49927 9.71863V8C2.49927 7.586 2.83527 7.25 3.24927 7.25C3.66327 7.25 3.99927 7.586 3.99927 8V9.71863C3.99927 10.1326 3.66364 10.4686 3.24927 10.4686Z" fill="#757575" />
<path d="M1.74974 13.8226C1.54349 13.8226 1.33837 13.7382 1.19062 13.5725C0.914245 13.2639 0.94087 12.7895 1.24949 12.5135C2.04374 11.8029 2.49937 10.7844 2.49937 9.71825C2.49937 9.30425 2.83537 8.96825 3.24937 8.96825C3.66337 8.96825 3.99937 9.30425 3.99937 9.71825C3.99937 11.2104 3.36149 12.6365 2.24924 13.6314C2.10637 13.7596 1.92787 13.8226 1.74974 13.8226Z" fill="#757575" />
<path d="M9.99933 21.4996C7.55995 21.4996 5.49933 19.439 5.49933 16.9996C5.49933 16.5856 5.83533 16.2496 6.24933 16.2496C6.66333 16.2496 6.99933 16.5856 6.99933 16.9996C6.99933 18.6256 8.37295 19.9996 9.99933 19.9996C11.6257 19.9996 12.9993 18.6256 12.9993 16.9996C12.9993 16.5856 13.3353 16.2496 13.7493 16.2496C14.1633 16.2496 14.4993 16.5856 14.4993 16.9996C14.4993 19.439 12.4387 21.4996 9.99933 21.4996Z" fill="#757575" />
<path d="M3.24931 17.7496C2.01406 17.7496 0.925435 16.9888 0.47581 15.8113C0.02506 14.6315 0.32881 13.337 1.24944 12.5139C1.55844 12.2379 2.03244 12.2641 2.30844 12.5731C2.58444 12.8821 2.55781 13.3558 2.24919 13.6321C1.69944 14.1238 1.69644 14.804 1.87681 15.2765C2.05644 15.7471 2.51169 16.2504 3.24931 16.2504C3.66331 16.2504 3.99931 16.5864 3.99931 17.0004C3.99931 17.4144 3.66369 17.7496 3.24931 17.7496Z" fill="#757575" />
<path d="M16.7493 17.7496H3.24927C2.83527 17.7496 2.49927 17.4136 2.49927 16.9996C2.49927 16.5856 2.83527 16.2496 3.24927 16.2496H16.7493C17.1633 16.2496 17.4993 16.5856 17.4993 16.9996C17.4993 17.4136 17.1636 17.7496 16.7493 17.7496Z" fill="#757575" />
</svg>
</div>
{/* Bell END */}
<div className="relative" ref={dropdownRef}>
<button
id='user-details-avatar'
@ -371,7 +402,7 @@ const CommonNav: React.FC<AccountNavProps> = ({
aria-expanded={open}
aria-haspopup="true"
>
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
<div className="w-8 h-8 rounded-md overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
{user ? (
<span className="w-full h-full object-cover p-1.5 font-bold">
{initial}

View File

@ -8,6 +8,7 @@ import Cards from "../home/Cards";
import AddInviteModal from "./AddInviteModal";
import { Bounce, toast } from "react-toastify";
import { FiFileText, FiClock, FiAlertTriangle, FiCheckCircle } from "react-icons/fi";
import { CgSandClock } from "react-icons/cg";
import TransactionDataTable from "./TransactionDataTable";
import { useSelector } from "react-redux";
import { RootState } from "../../store/store";
@ -45,7 +46,7 @@ const Account = () => {
{
title: "Expiring Soon",
count: data.expiring || 0,
icon: <FiAlertTriangle className="w-5 h-5 text-red-500" />,
icon: <CgSandClock className="w-5 h-5 text-red-500" />,
iconBg: "bg-red-100 border-red-200",
textColor: "text-red-600",
gradient: "bg-white",
@ -60,47 +61,6 @@ const Account = () => {
gradient: "bg-white",
shadowColor: "shadow-green-100",
},
// {
// title: 'Waiting for Others',
// count: data.waitingForOthers || 0,
// icon: <FaClock size={18} className="text-stone-600" />,
// gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
// shadowColor: 'shadow-blue-200/50',
// iconBg: 'bg-blue-100 border-blue-200',
// textColor: 'text-blue-700',
// trend: 'down'
// },
// {
// title: 'declined',
// count: data.declined || 0,
// icon: <FaClock size={18} className="text-purple-600" />,
// gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
// shadowColor: 'shadow-purple-200/50',
// iconBg: 'bg-purple-100 border-purple-200',
// textColor: 'text-purple-700',
// trend: 'down'
// },
// {
// title: 'cancelled',
// count: data.cancelled || 0,
// icon: <FaClock size={18} className="text-green-600" />,
// gradient: 'bg-gradient-to-br from-green-50 to-green-100',
// shadowColor: 'shadow-green-200/50',
// iconBg: 'bg-green-100 border-green-200',
// textColor: 'text-green-700',
// trend: 'down'
// },
// {
// title: 'draft',
// count: data.draft || 0,
// icon: <FaClock size={18} className="text-stone-600" />,
// gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
// shadowColor: 'shadow-stone-200/50',
// iconBg: 'bg-stone-100 border-stone-200',
// textColor: 'text-stone-700',
// trend: 'down'
// }
// Add more cards here if needed
];
};
@ -247,8 +207,9 @@ const Account = () => {
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
isLoading={isLoading} />
<section className="my-4 mx-3">
<TransactionDataTable activeAccountId={activeAccountId}/>
<section className="my-4 mx-3 px-4">
<div className="h-[10px]"></div>
<TransactionDataTable activeAccountId={activeAccountId} />
</section>
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}

View File

@ -112,7 +112,6 @@ const Accounts = () => {
return (
<>
{/* <Cards type="account" onOpenModal={handleOpenModal} /> */}
{allAccounts.length > 0 && (
<div className="overflow-x-auto py-2">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">

View File

@ -0,0 +1,73 @@
import React from 'react'
import { IoMdClose } from 'react-icons/io'
import downloadDocumentUtils from '../../utlity/downloadDocumentsUtils';
import { DownloadTypesEnum } from './TransactionDataTable';
interface DownloadDocumentInt {
showMenu: { open: boolean, transactionId: string | null },
setShowMenu: React.Dispatch<{ open: boolean, transactionId: string | null }>,
accountId: string
}
const DownloadDocument: React.FC<DownloadDocumentInt> = ({ showMenu, setShowMenu, accountId }) => {
function handleDownloadFormSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const docType = formData.get('download-doc-type');
const includeCertificate = formData.get('download-doc-type-include-cert');
downloadDocumentUtils(accountId, showMenu.transactionId as string, docType as string, includeCertificate as string | null)
}
return (
<>
{showMenu.open && (
<form onSubmit={(e) => handleDownloadFormSubmit(e)} className='fixed inset-0 z-50'>
<div className='absolute inset-0 bg-black/40' onClick={() => setShowMenu({ open: false, transactionId: null })}></div>
<div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90%] max-w-md rounded-lg bg-white shadow-lg'>
<div className='flex items-center justify-between px-5 py-4 border-b border-slate-200'>
<p className='font-semibold text-lg'>Download Document</p>
<button aria-label='Close download dialog' className='p-1 rounded hover:bg-slate-100' onClick={() => setShowMenu({ open: false, transactionId: null })}>
<IoMdClose className='text-xl cursor-pointer' />
</button>
</div>
<div className='px-5 py-4'>
<fieldset>
<legend className='text-sm font-medium text-slate-700 mb-3'>Choose file to download</legend>
<div className='space-y-2'>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' value={DownloadTypesEnum.ALL} />
<span>All</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' defaultChecked value={DownloadTypesEnum.COMBINED} />
<span>Combined</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' defaultChecked value={DownloadTypesEnum.CERTIFICATE} />
<span>Certificate</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='checkbox' className='cursor-pointer' name='download-doc-type-include-cert' value='?include_certificate=true' />
<span>Include Certificate</span>
</label>
</div>
</fieldset>
<div className='flex justify-end gap-2 mt-5'>
<button className='cursor-pointer px-4 py-2 rounded border border-slate-200 text-slate-700 hover:bg-slate-50' onClick={() => setShowMenu({ open: false, transactionId: null })}>Cancel</button>
<button className='cursor-pointer px-4 py-2 rounded bg-[var(--mainPrimaryBtn)] text-white hover:opacity-90'>Download</button>
</div>
</div>
</div>
</form>
)}
</>
)
}
export default DownloadDocument

View File

@ -3,12 +3,12 @@ import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import api from '../../config/api';
import { Transaction } from '../transactions/Transactions';
import { useLocation, useNavigate } from 'react-router-dom';
import { FaCheck } from 'react-icons/fa';
import LoaderUi from '../../helper/LoaderUi';
import logoRecord from '../../assets/record.svg';
import { MdOutlineFileDownload } from 'react-icons/md';
import downloadDocumentUtils from '../../utlity/downloadDocumentsUtils';
import { IoMdClose } from 'react-icons/io';
import DownloadDocument from './DownloadDocument';
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
@ -21,15 +21,33 @@ const statusColorMap = {
EXPIRED: "bg-red-100 text-red-600",
};
export enum DownloadTypesEnum {
ALL = "archived",
COMBINED = "combined",
CERTIFICATE = "certificate"
}
const TransactionDataTable = ({activeAccountId}) => {
const TransactionDataTable = ({ activeAccountId }) => {
const navigate = useNavigate()
const loaction = useLocation()
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [showMenu, setShowMenu] = useState<{ open: boolean, transactionId: string | null }>({
open: false,
transactionId: null
});
const [loading, setLoading] = useState(false)
const [downloadDocumentType, setDownloadDocumentType] = useState({
type: DownloadTypesEnum.ALL,
includeCertificate: false
})
const fetchTransactions = async () => {
console.log("fetchTransactions")
setLoading(true)
@ -81,36 +99,39 @@ const TransactionDataTable = ({activeAccountId}) => {
return statusColorMap[status] || "bg-gray-100 text-gray-600";
}
function handleDownloadFormSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const docType = formData.get('download-doc-type');
const includeCertificate = formData.get('download-doc-type-include-cert');
downloadDocumentUtils(activeAccountId, showMenu.transactionId as string, docType as string, includeCertificate as string | null)
}
return (
<>
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div className={`bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden px-5 pb-5`}>
{/* Header */}
{location.pathname !== '/transactions' &&
<div className="flex justify-between items-center px-6 py-4 border-b border-gray-100">
<h2 className="text-sm font-semibold text-gray-800">Recent Transactions</h2>
<span className=' text-[var(--primary)] text-xs hover:underline cursor-pointer font-semibold'>View all</span>
</div>}
{location.pathname !== '/transactions' &&
<div className="flex justify-between items-center pt-4">
<h2 className="text-lg font-bold text-gray-800">Recent Transactions</h2>
<span className=' text-[var(--primary)] text-xs hover:underline cursor-pointer font-semibold'>View all</span>
</div>}
{/* Scrollable Table Container */}
<div className={`overflow-auto no-scrollbar ${location.pathname === '/transactions' ? "min-h-[76vh] max-h-[76vh]" : "min-h-[70vh] max-h-[70vh]"}`}>
<div className={`overflow-auto my-4 no-scrollbar ${location.pathname === '/transactions' ? "min-h-[76vh] max-h-[76vh]" : "min-h-[30vh] max-h-[46vh]"}`}>
<table className="w-full text-sm">
{/* Sticky Header */}
<thead className="bg-gray-50 sticky top-0 z-10">
<tr className="text-gray-600 text-xs uppercase tracking-wider border-b border-gray-200">
<th className="px-6 py-4 text-left font-semibold">Document</th>
<th className="px-6 py-4 text-center font-semibold">Status</th>
{location.pathname === '/transactions' && (
<>
<th className="px-6 py-4 text-center font-semibold">Signed By</th>
<th className="px-6 py-4 text-center font-semibold">Last Change</th>
<th className="px-6 py-4 text-center font-semibold">Expire After (Days)</th>
<th className="px-6 py-4 text-center font-semibold">Actions</th>
</>
)}
<thead className="sticky top-0 z-10">
<tr className="text-xs">
<th className="text-left bg-[#F8F9FC] rounded-tl-lg rounded-bl-lg px-6 py-4 font-bold text-nowrap">Transaction</th>
<th className="text-left bg-[#F8F9FC] px-6 py-4 font-bold text-nowrap">Status</th>
<th className="text-left bg-[#F8F9FC] px-6 py-4 font-bold text-nowrap">Signed By</th>
<th className="text-left bg-[#F8F9FC] px-6 py-4 font-bold text-nowrap">Last Change</th>
<th className="text-left bg-[#F8F9FC] px-6 py-4 font-bold text-nowrap">Expire After (Days)</th>
<th className="text-left bg-[#F8F9FC] rounded-tr-lg rounded-br-lg px-6 py-4 font-bold text-nowrap">Actions</th>
</tr>
</thead>
{loading ? (
@ -124,19 +145,20 @@ const TransactionDataTable = ({activeAccountId}) => {
) : transactions.length > 0 ? (
<>
{/* Scrollable Body */}
<tbody className="bg-white divide-y divide-gray-100">
{transactions.map((tx) => (
<tbody>
{transactions.map((tx, idx) => (
<tr
key={tx.id}
onClick={() => handleTransactionClick(tx.id)}
className="hover:bg-gray-50 transition-colors duration-150 cursor-pointer"
// onClick={() => handleTransactionClick(tx.id)}
>
<td className="px-6 py-4 text-left">
<div className="text-gray-800 font-medium truncate max-w-xs">
<td className={`${idx % 2 !== 0 ? "bg-[#F8F9FC]" : ""} font-bold px-6 py-4 text-left`}>
<div className="text-gray-800 font-medium truncate max-w-xs cursor-pointer"
onClick={() => handleTransactionClick(tx.id)}
>
{tx?.subject || "Untitled"}
</div>
</td>
<td className="px-6 py-4 text-center">
<td className={`${idx % 2 !== 0 ? "bg-[#F8F9FC]" : ""} text-left font-bold px-6 py-4`}>
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${getStatusColorClass(
tx.status
@ -146,50 +168,94 @@ const TransactionDataTable = ({activeAccountId}) => {
</span>
</td>
{location.pathname === '/transactions' && (
<>
<td className="px-6 py-4">
<div className="flex items-center justify-center">
{tx.signedBy ? (
<div className="flex items-center gap-2">
<div
className="w-7 h-7 rounded-full flex items-center justify-center text-xs font-medium text-white flex-shrink-0"
style={{ backgroundColor: "#d8b4fe" }}
>
{tx.signedByInitials}
</div>
<span className="text-gray-700 truncate max-w-[120px]">
{tx.signedBy}
</span>
</div>
) : (
<span className="text-gray-400">-</span>
)}
<td className={`${idx % 2 !== 0 ? "bg-[#F8F9FC]" : ""} text-left font-bold px-6 py-4`}>
<div className="flex items-center justify-left">
{tx.signedBy ? (
<div className="flex items-center gap-2">
<div
className="w-7 h-7 rounded-full flex items-center justify-center text-xs font-medium text-white flex-shrink-0"
style={{ backgroundColor: "#d8b4fe" }}
>
{tx.signedByInitials}
</div>
<span className="text-gray-700 truncate max-w-[120px]">
{tx.signedBy}
</span>
</div>
</td>
<td className="px-6 py-4 text-center">
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(tx.lastModifiedAt)}
</span>
</td>
<td className="px-6 py-4 text-center">
<span className="text-gray-700">
{tx.expireAfter ?? "-"}
</span>
</td>
<td className="px-6 py-4 text-center">
<button
className="text-gray-400 hover:text-gray-600 transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
onClick={(e) => {
e.stopPropagation();
// Handle actions menu
}}
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button>
</td>
</>
)}
) : (
<>
-
</>
)}
</div>
</td>
<td className={`${idx % 2 !== 0 ? "bg-[#F8F9FC]" : ""} font-bold px-6 py-4 text-left`}>
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(tx.lastModifiedAt)}
</span>
</td>
<td className={`${idx % 2 !== 0 ? "bg-[#F8F9FC]" : ""} font-bold px-6 py-4 text-left`}>
<span className="text-gray-700">
{tx.expireAfter ?? "-"}
</span>
</td>
<td className={`${idx % 2 !== 0 ? "bg-[#F8F9FC]" : ""} font-semibold px-6 py-4 text-left text-[var(--mainPrimaryBtn)]`}>
<div className='flex justify-start items-center gap-6'>
<div className='flex gap-1 relative'
tabIndex={0}
// onBlur={(e) => {
// if (!e.currentTarget.contains(e.relatedTarget)) {
// setShowMenu(null);
// }
// }}
onClick={() =>
setShowMenu({
open: true,
transactionId: tx.id
})
}
>
<MdOutlineFileDownload className='text-xl' />
<p>
Download
</p>
{/* {showMenu === idx && (
<div className="absolute top-7.5 right-0 bg-white border border-gray-200 rounded-md shadow-lg z-20 min-w-[140px] overflow-hidden animate-fade-in">
<button
onClick={() => downloadDocumentUtils(activeAccountId, tx.id, "combined")}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-slate-600 hover:bg-slate-50 transition-colors whitespace-nowrap"
>
Download Signed
</button>
<button
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-slate-600 hover:bg-slate-50 transition-colors whitespace-nowrap"
>
Download Original
</button>
</div>
)} */}
</div>
{/* <button
className="duration-150 p-1 rounded-md rotate-[90deg] text-black"
onClick={(e) => {
e.stopPropagation();
// Handle actions menu
}}
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button> */}
</div>
</td>
</tr>
@ -212,8 +278,28 @@ const TransactionDataTable = ({activeAccountId}) => {
)}
</table>
</div>
</div>
{showMenu.open && (
<DownloadDocument
showMenu={showMenu}
setShowMenu={setShowMenu}
accountId={activeAccountId}
/>
)}
</>
)
}

View File

@ -8,6 +8,7 @@ import OAuthGoogle from "./OAuthGoogle";
// import Cookies from "js-cookie";
import { BsEye } from "react-icons/bs";
import { FiEyeOff } from "react-icons/fi";
import { TokenStorage } from "../../config/api";
// import api from "../../config/api";
interface FormDataState {
@ -115,8 +116,9 @@ const LoginForm = ({ onSwitchToRegister }) => {
);
if (res.status === 200) {
navigate("/");
TokenStorage.setTokens(res.data.accessToken, res.data.refreshToken)
formErrorRef.current = { message: "Login Successfully", error: false }
navigate("/");
}
} catch (err) {
if (err.response?.status === 400 || err.response?.status === 401) {

View File

@ -0,0 +1,78 @@
import React from 'react'
import { IoMdClose } from 'react-icons/io'
import downloadDocumentUtils from '../../utlity/downloadDocumentsUtils';
interface DownloadDocumentInt {
showMenuDownload: { open: boolean, transactionId: string | null },
setShowMenuDownload: React.Dispatch<{ open: boolean, transactionId: string | null }>,
accountId: string
}
enum DownloadTypesEnum {
ALL = "archived",
COMBINED = "combined",
CERTIFICATE = "certificate"
}
const ClientDownloadDocument: React.FC<DownloadDocumentInt> = ({ showMenuDownload, setShowMenuDownload, accountId }) => {
function handleDownloadFormSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const docType = formData.get('download-doc-type');
const includeCertificate = formData.get('download-doc-type-include-cert');
downloadDocumentUtils(accountId, showMenuDownload.transactionId as string, docType as string, includeCertificate as string | null)
}
return (
<>
{showMenuDownload.open && (
<form onSubmit={(e) => handleDownloadFormSubmit(e)} className='fixed inset-0 z-50'>
<div className='absolute inset-0 bg-black/40' onClick={() => setShowMenuDownload({ open: false, transactionId: null })}></div>
<div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90%] max-w-md rounded-lg bg-white shadow-lg'>
<div className='flex items-center justify-between px-5 py-4 border-b border-slate-200'>
<p className='font-semibold text-lg'>Download Document</p>
<button aria-label='Close download dialog' className='p-1 rounded hover:bg-slate-100' onClick={() => setShowMenuDownload({ open: false, transactionId: null })}>
<IoMdClose className='text-xl cursor-pointer' />
</button>
</div>
<div className='px-5 py-4'>
<fieldset>
<legend className='text-sm font-medium text-slate-700 mb-3'>Choose file to download</legend>
<div className='space-y-2'>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' value={DownloadTypesEnum.ALL} />
<span>All</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' defaultChecked value={DownloadTypesEnum.COMBINED} />
<span>Combined</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' defaultChecked value={DownloadTypesEnum.CERTIFICATE} />
<span>Certificate</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='checkbox' className='cursor-pointer' name='download-doc-type-include-cert' value='?include_certificate=true' />
<span>Include Certificate</span>
</label>
</div>
</fieldset>
<div className='flex justify-end gap-2 mt-5'>
<button className='cursor-pointer px-4 py-2 rounded border border-slate-200 text-slate-700 hover:bg-slate-50' onClick={() => setShowMenuDownload({ open: false, transactionId: null })}>Cancel</button>
<button className='cursor-pointer px-4 py-2 rounded bg-[var(--mainPrimaryBtn)] text-white hover:opacity-90'>Download</button>
</div>
</div>
</div>
</form>
)}
</>
)
}
export default ClientDownloadDocument

View File

@ -0,0 +1,136 @@
import React from "react";
import { RiFileList3Line } from "react-icons/ri";
interface CompletionScreenProps {
hit?: string;
title?: string;
message?: string;
textColorSuccess?: boolean;
}
const CompletionScreen: React.FC<CompletionScreenProps> = ({
hit = "All Done! ",
title = "Thanks for signing",
message = "Your document is now complete and securely stored. We’ve emailed you a copy for your reference.",
textColorSuccess = true
}) => {
return (
<div className="relative min-h-screen flex flex-col bg-gradient-to-b from-white to-blue-50 overflow-hidden">
<div>
<img className="absolute z-0 top-[5%] right-0" src="/completion-screen-frame-1.png" alt="" />
<img className="absolute z-0 top-[5%] left-0 rotate-y-180" src="/completion-screen-frame-1.png" alt="" />
</div>
{/* Header */}
<header className="px-4 py-3 flex justify-center sm:justify-start items-center bg-white shadow-md z-10">
<div className="flex items-center gap-2">
<RiFileList3Line className="text-[var(--mainPrimaryBtn)] text-2xl" />
<span className="text-md font-semibold text-[var(--mainPrimaryBtn)]">
Sign made easy
</span>
</div>
</header>
{/* Main content */}
<main className="flex flex-col items-center text-center px-4 flex-grow">
<h1 className={`text-xl md:text-3xl 2xl:text-5xl 2xl:mt-10 font-bold mt-6`}>
<span className={`${textColorSuccess ? "text-[var(--mainPrimaryBtn)]" : "text-red-400"}`}>{hit}</span>{title}
</h1>
<p className="mt-2 max-w-xl 2xl:mt-4">
{message}
</p>
{/* <button className="flex items-center gap-2 mt-6 bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 transition">
<FiDownload />
Download
</button> */}
{/* <button className="mt-6 flex items-center gap-2 bg-orange-500 text-white px-6 py-2 rounded-md hover:bg-orange-600 transition">
<FiZap /> Login
</button> */}
<p className="font-semibold mt-4 2xl:mt-6 text-md 2xl:text-xl">
Access your documents anytime — just <a href="/login" className="text-white bg-[var(--mainPrimaryBtn)] px-3 py-1 rounded-sm">Login</a>
</p>
{/* Illustration placeholder */}
<div className="mt-6 2xl:mt-10">
{textColorSuccess ? <img
src="/completion-screen-1.png"
alt="Document completed"
className="w-30 md:w-34 lg:w-42 2xl:w-70"
/> :
<img
src="/completion-screen-2.png"
alt="Document completed"
className="w-30 md:w-34 lg:w-42 2xl:w-70"
/>}
</div>
{/* Apps Section */}
<section className="relative z-10 top-6 2xl:top-10 w-full">
<h2 className="text-xl font-semibold text-gray-800 mb-4 2xl:text-2xl">
Boost Efficiency with Our Apps
</h2>
<div className="w-full flex flex-col sm:flex-row gap-2 justify-center items-center 2xl:gap-4 2xl:mt-6">
{/* Forms Made Easy */}
<div className="bg-white rounded-lg shadow p-4 2xl:p-6">
<div className="">
<p className="text-sm font-semibold text-left mb-2 2xl:text-lg">
Generate Forms with Ease
</p>
<div className="w-full">
<div className="w-full flex flex-col justify-start items-start sm:flex-row sm:justify-between sm:items-center">
<img src="/forms-made-easy.png" className="w-[40%] 2xl:w-[60%]" alt="Docs Made Easy" />
<button className="text-xs sm:text-sm md:text-md sm:mt-0 bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 transition">
Try Now
</button>
</div>
</div>
</div>
</div>
{/* Docs Made Easy */}
<div className="bg-white rounded-lg shadow p-4 2xl:p-6">
<div className="">
<p className="text-sm font-semibold text-left mb-2 2xl:text-lg">
Generate Documents (Invoices, Reports, Quotes)
</p>
<div className="w-full">
<div className="w-full flex flex-col justify-start items-start sm:flex-row sm:justify-between sm:items-center">
<img src="/docs-made-easy.png" className="w-[40%] 2xl:w-[60%]" alt="Docs Made Easy" />
<button className="text-xs sm:text-sm md:text-md sm:mt-0 bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 transition">
Try Now
</button>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
{/* Footer */}
<footer className="relative bottom-0 text-center text-sm bg-[#fff] opacity-100 py-2 z-50 2xl:py-3">
<p className="px-4 font-semibold text-xs 2xl:text-sm">
Copyright 2025 Sign Made Easy. All Rights Reserved.{" "}
<a href="#" className="text-[var(--mainPrimaryBtn)] hover:underline">
Privacy Policy
</a>{" "}
|{" "}
<a href="#" className="text-[var(--mainPrimaryBtn)] hover:underline">
Reasonable Use Policy
</a>
</p>
</footer>
</div>
);
}
export default CompletionScreen;

View File

@ -14,8 +14,8 @@ export default function ConsentScreen({ onConsent, onDecline }: ConsentScreenPro
return (
<>
<div className="absolute top-0 left-0 w-[100vw] h-[100vh] bg-slate-100 opacity-50 backdrop-filter backdrop-blur-2xl"></div>
<div className="absolute top-0 left-0 w-full h-screen flex items-center justify-center p-6">
<div className="absolute top-0 left-0 w-[100vw] h-[100vh] bg-slate-100 opacity-50 backdrop-filter backdrop-blur-2xl z-50"></div>
<div className="absolute top-0 left-0 w-full h-screen flex items-center justify-center p-6 z-50">
<div className="bg-white shadow-xl rounded-2xl p-8 max-w-lg w-full border border-gray-200">
{/* Header */}
<div className="flex items-center gap-3 mb-6">

View File

@ -1,14 +1,35 @@
import { useState } from "react";
import { FaTimesCircle } from "react-icons/fa";
interface DeclineModalProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
onConfirm: (data: { reason: string; comments: string }) => void;
}
export default function DeclineModal({ open, onClose, onConfirm }: DeclineModalProps) {
const REASONS = [
"Incorrect recipient",
"Wrong or missing information",
"Not intended for me",
"I don’t agree with the terms",
"Other",
];
export default function DeclineModal({
open,
onClose,
onConfirm,
}: DeclineModalProps) {
const [reason, setReason] = useState("Other");
const [comments, setComments] = useState("");
if (!open) return null;
const handleConfirm = () => {
onConfirm({ reason, comments });
onClose();
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6 relative">
@ -21,10 +42,48 @@ export default function DeclineModal({ open, onClose, onConfirm }: DeclineModalP
</div>
{/* Message */}
<p className="text-sm text-gray-600 mb-6">
<p className="text-sm text-gray-600 mb-4">
Are you sure you want to decline this document? This action cannot be undone.
</p>
{/* Reasons (Radio buttons) */}
<div className="mb-4">
<p className="text-sm font-medium text-gray-700 mb-2">
Please select a reason:
</p>
<div className="space-y-2">
{REASONS.map((r) => (
<label key={r} className="flex items-center gap-2 text-sm">
<input
type="radio"
name="declineReason"
value={r}
checked={reason === r}
onChange={() => setReason(r)}
className="h-4 w-4 text-red-500 border-gray-300 focus:ring-red-500"
/>
{r}
</label>
))}
</div>
</div>
{/* Comments (only for Other) */}
{reason === "Other" && (
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-1">
Additional Comments
</label>
<textarea
value={comments}
onChange={(e) => setComments(e.target.value)}
placeholder="Please provide more details..."
rows={3}
className="w-full border rounded-md p-2 text-sm focus:ring-red-500 focus:border-red-500"
/>
</div>
)}
{/* Buttons */}
<div className="flex justify-end gap-3">
<button
@ -34,10 +93,7 @@ export default function DeclineModal({ open, onClose, onConfirm }: DeclineModalP
Cancel
</button>
<button
onClick={() => {
onConfirm();
onClose();
}}
onClick={handleConfirm}
className="px-4 py-2 rounded-md bg-red-500 text-white hover:bg-red-600"
>
Decline

View File

@ -14,13 +14,12 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
const [currentPage, setCurrentPage] = useState(0);
return (
<div className='p-4'>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 pb-[6rem]">
{selectedDocuments && selectedDocuments.length && selectedDocuments.map((document, indx) => {
return (
<div
key={indx}
// tabIndex={1}
// onBlur={() => setDropDownTogglePage('')}
>
@ -34,7 +33,7 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
document.pages != undefined && <img
key={indx}
className='rounded-sm shadow'
src={"data:image/png;base64," + document.thumbnail}
src={document.pages[0].src}
alt="" />
}
@ -102,7 +101,6 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
)
})}
</div>
</div>
)
}

View File

@ -4,13 +4,14 @@ import { FaUserEdit } from "react-icons/fa";
interface ReassignModalProps {
open: boolean;
onClose: () => void;
onConfirm: (data: { firstName: string; lastName: string; email: string }) => void;
onConfirm: (data: { firstName: string; lastName: string; email: string; message: string }) => void;
}
export default function ReassignModal({ open, onClose, onConfirm }: ReassignModalProps) {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [errors, setErrors] = useState<{ lastName?: string; email?: string }>({});
@ -27,7 +28,7 @@ export default function ReassignModal({ open, onClose, onConfirm }: ReassignModa
const handleSubmit = () => {
if (!validate()) return;
onConfirm({ firstName, lastName, email });
onConfirm({ firstName, lastName, email, message });
onClose();
};
@ -85,10 +86,24 @@ export default function ReassignModal({ open, onClose, onConfirm }: ReassignModa
/>
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
</div>
{/* Message Textarea */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Message (optional)
</label>
<textarea
placeholder="Write a message for the recipient..."
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={3}
className="w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 resize-none"
/>
</div>
</div>
{/* Buttons */}
<div className="flex justify-end gap-3 mt-6">
<div className="flex justify-end gap-3 mt-6 flex-wrap">
<button
onClick={onClose}
className="px-4 py-2 rounded-md border border-gray-300 text-gray-600 hover:bg-gray-100 text-sm"

View File

@ -0,0 +1,37 @@
import { FaCheckCircle } from "react-icons/fa";// or replace with your button
interface ReassignSuccessScreenProps {
reassignedTo: { firstName: string | null; lastName: string; email: string };
onBackHome?: () => void;
}
export default function ReassignSuccess({
reassignedTo,
onBackHome,
}: ReassignSuccessScreenProps) {
return (
<div className="flex flex-col items-center justify-center h-screen w-full bg-slate-50 px-4">
<FaCheckCircle className="text-green-500 text-6xl mb-4" />
<h1 className="text-2xl font-semibold text-gray-800 mb-2 text-center">
Reassignment Complete
</h1>
<p className="text-gray-600 text-center max-w-md mb-6">
You have successfully reassigned the transaction to:
</p>
<div className="bg-white border border-gray-200 rounded-lg shadow-md p-4 w-full max-w-sm mb-8">
<p className="text-gray-800 font-medium text-lg">
{reassignedTo.firstName} {reassignedTo.lastName}
</p>
<p className="text-gray-500 text-sm">{reassignedTo.email}</p>
</div>
<button
onClick={onBackHome}
className="bg-[var(--primary)] text-white px-6 py-2 rounded-lg"
>
Visit SME
</button>
</div>
);
}

View File

@ -0,0 +1,19 @@
import React from 'react'
import CompletionScreen from './CompletionScreen'
const SessionExpired = () => {
return (
<>
<CompletionScreen
hit={"Session Expired!"}
title={" Your Session is expired"}
message={"Revisit OR request new URL"}
textColorSuccess={false}
/>
</>
)
}
export default SessionExpired

View File

@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from "react";
import { FaSignature, FaDownload, FaUserEdit, FaTimesCircle } from "react-icons/fa";
import { FaSignature, FaDownload, FaUserEdit, FaTimesCircle, FaAngleLeft, FaAngleRight } from "react-icons/fa";
import { BsThreeDotsVertical } from "react-icons/bs";
import ClientDownloadDocument from "./ClientDownloadDocument";
interface Props {
transparentTags?: boolean;
@ -22,6 +23,11 @@ export default function SignButton({
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const [showMenuDownload, setShowMenuDownload] = useState<{ open: boolean, transactionId: string | null }>({
open: false,
transactionId: null
});
// Close menu on outside click
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
@ -34,79 +40,95 @@ export default function SignButton({
}, []);
return (
<div className="relative inline-block" ref={menuRef}>
{(!transparentTags && !hideTags) ? (
<div className="flex items-center">
{/* Sign Button */}
<button
onClick={handleFormSubmit}
className="flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-md bg-[var(--primary)] hover:opacity-90 transition text-white outline-none"
>
<span id="sign-client-side-btn" className="tracking-wide">
Sign
</span>
<FaSignature className="text-md" />
</button>
{/* Three Dots Dropdown Trigger */}
<button
onClick={() => setMenuOpen((prev) => !prev)}
className="ml-2 p-2 rounded-full hover:bg-gray-100 focus:outline-none transition"
>
<BsThreeDotsVertical className="text-gray-600 text-lg cursor-pointer" />
</button>
{/* Dropdown Menu */}
{menuOpen && (
<div
tabIndex={0}
onBlur={() => setMenuOpen(false)} // close on blur
className="absolute right-0 top-10 w-56 bg-white border border-gray-200 rounded-lg shadow-lg z-10"
<>
<div className="relative inline-block" ref={menuRef}>
{(!transparentTags && !hideTags) ? (
<div className="flex items-center">
{/* Sign Button */}
{/* <button
onClick={handleFormSubmit}
className="flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-md bg-[var(--primary)] hover:opacity-90 transition text-white outline-none"
>
<ul className="py-1 text-sm text-gray-700">
<li>
<button
onClick={() => {
onDownloadClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<FaDownload className="text-blue-500" />
<span>Download Document</span>
</button>
</li>
<li>
<button
onClick={() => {
onReassignClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<FaUserEdit className="text-yellow-500" />
<span>Reassign</span>
</button>
</li>
<li>
<button
onClick={() => {
onDeclineClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100 text-red-500 border-t border-gray-200"
>
<FaTimesCircle />
<span>Decline</span>
</button>
</li>
</ul>
</div>
)}
</div>
) : (
<p className="text-xs text-green-600 font-medium">Signed Successfully</p>
<span id="sign-client-side-btn" className="tracking-wide">
Sign
</span>
<FaSignature className="text-md" />
</button> */}
{/* Three Dots Dropdown Trigger */}
<button
onClick={() => setMenuOpen((prev) => !prev)}
className="py-2 rounded-full focus:outline-none transition ml-2"
>
<BsThreeDotsVertical className="text-white text-lg cursor-pointer" />
</button>
{/* Dropdown Menu */}
{menuOpen && (
<div
tabIndex={0}
onBlur={() => setMenuOpen(false)} // close on blur
className="absolute right-0 top-10 w-56 bg-white border border-gray-200 rounded-lg shadow-lg z-10"
>
<ul className="py-1 text-sm text-gray-700">
{/* <li>
<button
onClick={() => {
onDownloadClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<FaDownload className="" />
<span>Download Document</span>
</button>
</li> */}
<li>
<button
onClick={() => {
onReassignClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<FaUserEdit className="" />
<span>Reassign</span>
</button>
</li>
<li>
<button
onClick={() => {
onDeclineClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100 text-red-500 border-t border-gray-200"
>
<FaTimesCircle />
<span>Decline</span>
</button>
</li>
</ul>
</div>
)}
</div>
) : (
<p className="text-xs text-green-600 font-medium">Signed Successfully</p>
)}
</div>
{showMenuDownload.open && (
<ClientDownloadDocument
showMenuDownload={showMenuDownload}
setShowMenuDownload={setShowMenuDownload}
accountId={activeAccountId}
/>
)}
</div>
</>
);
}

View File

@ -6,14 +6,14 @@ import { Link, useNavigate, useParams } from "react-router-dom"
import { DroppedItem, TagsTypes } from "../../utlity/types";
import Loader from "../dashboard/Loader";
import SignCon from "../signature/SignCon";
import ClientSignCon from "../signature/ClientSignCon";
import { Bounce, toast } from "react-toastify";
import { useDispatch, useSelector } from "react-redux";
import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
import { FaSignature, FaCheck, FaQuestionCircle } from 'react-icons/fa'
import { FaSignature, FaCheck, FaQuestionCircle, FaDownload, FaAngleLeft, FaAngleRight } from 'react-icons/fa'
import { IoIosDocument, IoMdHome } from 'react-icons/io'
import { MdKeyboardArrowDown } from 'react-icons/md'
import { MdKeyboardArrowDown, MdLabel } from 'react-icons/md'
import confetti from 'canvas-confetti'
import dayjs from "dayjs";
@ -30,6 +30,11 @@ import ConsentScreen from "./ConsentScreen";
import SignButton from "./SignButton";
import DeclineModal from "./DeclineModal";
import ReassignModal from "./ReassignModal";
import { ImCancelCircle } from "react-icons/im";
import ReassignSuccess from "./ReassignSuccess";
import CompletionScreen from "./CompletionScreen";
import { BsPrinter } from "react-icons/bs";
import { GrPrint } from "react-icons/gr";
@ -92,6 +97,7 @@ export interface SaveType {
save: boolean;
signature: string | null;
initial: string | null;
isDefaultAndClicked?: boolean;
}
interface TagsSubmitDataObj {
tags: TagSubmitData[];
@ -142,6 +148,7 @@ enum RightSideNav {
}
const SignDocument = () => {
const windowWidth = window.innerWidth;
const { recipientId } = useParams();
const dispatch = useDispatch();
@ -157,6 +164,11 @@ const SignDocument = () => {
const [signTagId, setSignTagId] = useState<number>(0)
const [showConfetti, setShowConfetti] = useState<boolean>(false)
const [pageScales, setPageScales] = useState<Record<string, { x: number; y: number }>>({});
const [requiredFilled, setRequiredFilled] = useState<boolean>(false);
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
const [signerConsent, setSignerConsent] = useState<{ consent: boolean; decline: boolean }>({
@ -196,6 +208,14 @@ const SignDocument = () => {
initial: null,
});
enum NavigationTagPosEnum {
ABOVE = "above",
BELOW = "below",
VISIBLE = "VISIBLE"
}
const [navigationTagPos, setNavigationTagPos] = useState<NavigationTagPosEnum>(NavigationTagPosEnum.VISIBLE);
const navigate = useNavigate();
@ -234,17 +254,17 @@ const SignDocument = () => {
if (focusElement === null || !("focusElementIndx" in focusElement)) return;
const docCon = document.getElementById("doc-con");
document.addEventListener("keydown", (e) => {
if (e.key === "Tab") {
e.preventDefault();
// const docCon = document.getElementById("doc-con");
// document.addEventListener("keydown", (e) => {
// if (e.key === "Tab") {
// e.preventDefault();
console.log("Listening Tab Key Press")
let focusIndx = focusElement?.focusElementIndx;
if (focusIndx >= droppedItems.length - 1) focusIndx = -1
setFocusElement({ ...droppedItems[++focusIndx], focusElementIndx: ++focusIndx })
}
})
// console.log("Listening Tab Key Press")
// let focusIndx = focusElement?.focusElementIndx;
// if (focusIndx >= droppedItems.length - 1) focusIndx = -1
// setFocusElement({ ...droppedItems[++focusIndx], focusElementIndx: ++focusIndx })
// }
// })
}
}, [droppedItems])
@ -307,18 +327,22 @@ const SignDocument = () => {
}, [saved])
const fetchData = (isSign: boolean = false) => {
axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign?recipientId=${recipientId}`,
axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/${recipientId}`,
{
headers:
{ "ngrok-skip-browser-warning": "69420", }
})
.then((res) => {
if (res.status === 200) {
setSelectedDocuments(res?.data?.documents)
setRecipient(res?.data?.details)
setDroppedItems(res?.data?.tagJsons)
const sortedDroppedItems = res?.data?.tagJsons.sort((a, b) => a.navigationOrder - b.navigationOrder);
setDroppedItems(sortedDroppedItems)
setFocusElement({ ...sortedDroppedItems[0], focusElementIndx: 0 });
setTransaction(res?.data?.transaction)
setRecSignStatus(res?.data?.status)
@ -328,13 +352,17 @@ const SignDocument = () => {
setProvideTags(true)
}
if (res?.data?.status === "COMPLETED") {
if (windowWidth > 638) {
setIsFields(false)
}
if (res?.data?.status === "COMPLETED" || res?.data?.status === "DECLINED") {
setShowCompletionScreen(true)
}
setTimeout(() => {
dispatch(hideLoader());
}, 600);
dispatch(hideLoader());
// setTimeout(() => {
// }, 600);
}
@ -359,7 +387,7 @@ const SignDocument = () => {
});
}
const handleFormSubmit = () => {
const handleFormSubmit = async () => {
const submitData: TagsSubmitDataObj = {
tags: []
}
@ -388,9 +416,9 @@ const SignDocument = () => {
const tagData: TagSubmitData = {
id: tag.id as string,
type: tag.type,
subType: saved.saveType as string,
// subType: saved.saveType as string,
signed: true,
value: ""
value: saved.signatureId as string
}
submitData.tags.push(tagData)
} else {
@ -406,39 +434,45 @@ const SignDocument = () => {
}
// return;
try {
dispatch(showLoader("Signing ..."));
await axios.put(`${import.meta.env.VITE_BASE_URI}/api/sign/${recipientId}/tags`, submitData, { headers: { "ngrok-skip-browser-warning": "69420", } })
dispatch(showLoader("Signing ..."));
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign?recipientId=${recipientId}`, submitData, { headers: { "ngrok-skip-browser-warning": "69420", } })
.then((res) => {
if (res.status === 200) {
setTransparentTags(true)
setShowConfetti(true)
}
})
.catch((err) => {
const signRes = await axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/${recipientId}`, {}, { headers: { "ngrok-skip-browser-warning": "69420", } })
dispatch(hideLoader());
if (signRes.status === 200) {
dispatch(hideLoader());
setTransparentTags(true)
setShowConfetti(true)
setShowCompletionScreen(true)
setSelectedTag(null)
}
)
} catch (e) {
dispatch(hideLoader());
}
};
const handleTagNavClick = (action: "N" | "P") => {
if (!tagJson || !tagJson.length) return
// debugger
if (!droppedItems || !droppedItems.length) return
// If FocusElement is Null Setting up 0 th Tag
if (!focusElement) {
setFocusElement({ ...tagJson[0], focusElementIndx: 0 });
setFocusElement({ ...droppedItems[0], focusElementIndx: 0 });
return;
}
// If FocusElement is present updating in Based on Next and Prev
const currFocusElemIndx = focusElement.focusElementIndx;
if (currFocusElemIndx === undefined) return;
const updIndx = (action === "N") ? (currFocusElemIndx + 1) : (currFocusElemIndx - 1)
let updIndx = (action === "N") ? (currFocusElemIndx + 1) : (currFocusElemIndx - 1)
if (updIndx >= tagJson.length || updIndx < 0 || !tagJson[updIndx].id) return
if (updIndx >= droppedItems.length || updIndx < 0 || !droppedItems[updIndx].id) updIndx = 0;
scrollToElement(tagJson[updIndx].id)
setFocusElement({ ...tagJson[updIndx], focusElementIndx: updIndx })
scrollToElement(droppedItems[updIndx].navigationOrder)
setFocusElement({ ...droppedItems[updIndx], focusElementIndx: updIndx })
}
const scrollToElement = (elementId: string) => {
@ -453,7 +487,7 @@ const SignDocument = () => {
const fetchSignatureData = async (signatureId: string) => {
try {
const response = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/signatures/${signatureId}?recipientId=${recipientId}`, {
const response = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/${recipientId}signatures/${signatureId}`, {
headers: { "ngrok-skip-browser-warning": "69420" }
});
if (response.status === 200) {
@ -516,6 +550,17 @@ const SignDocument = () => {
}, [droppedItems.length])
useEffect(() => {
const allRequiredFields = droppedItems.filter((item, idx) => {
if (!item.required) return false
if ((item.required && item.value) || idx in signConfObj) return false
return true
})
setRequiredFilled(allRequiredFields.length ? false : true)
}, [droppedItems, signConfObj])
// HANDLE DROP - START
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
event.preventDefault();
@ -615,6 +660,112 @@ const SignDocument = () => {
});
}
const handleDeclineConfirm = async (reason: string, comments: string) => {
try {
await axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/decline/${recipientId}`, { reason: reason + ", " + comments });
setSignerConsent((prev) => ({ ...prev, decline: true }));
setRecSignStatus("DECLINED");
setShowCompletionScreen(true);
} catch (e) { }
}
const handleReassignConfirm = (data: { firstName: string; lastName: string; email: string; message: string }) => {
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/reassign/${recipientId}`, {
name: data.firstName ? data.firstName + " " + data.lastName : data.lastName,
email: data.email,
message: data.message,
language: "en"
})
.then((res) => {
if (res.status === 202) {
setShowCompletionScreen(true);
}
})
.catch((err) => {
console.log(err)
});
}
// Simple throttle
function throttle(fn: Function, wait: number) {
let lastTime = 0;
return (...args: any[]) => {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
fn(...args);
}
};
}
// Utility to check if element is out of viewport
function isOutOfViewport(el: HTMLElement) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
return (
rect.right < 0 ||
rect.bottom < 0 ||
rect.left > windowWidth ||
rect.top > windowHeight
);
}
function printDocConDiv() {
var printContents = document.getElementById(divId).innerHTML;
var originalContents = document.body.innerHTML;
document.body.innerHTML = printContents;
window.print();
document.body.innerHTML = originalContents;
}
useEffect(() => {
const docCon = document.getElementById("doc-con");
if (!docCon) return;
const checkPosition = throttle(() => {
let el = document.getElementById("tag-navigation-btn");
if (!el) return;
const elRect = el.getBoundingClientRect();
const containerRect = docCon.getBoundingClientRect();
if (elRect.bottom < containerRect.top) {
setNavigationTagPos(NavigationTagPosEnum.ABOVE)
} else if (elRect.top > containerRect.bottom) {
setNavigationTagPos(NavigationTagPosEnum.BELOW)
} else {
setNavigationTagPos(NavigationTagPosEnum.VISIBLE)
}
}, 150);
// Initial check
checkPosition();
docCon.addEventListener("scroll", checkPosition);
window.addEventListener("resize", checkPosition); // container may resize
return () => {
docCon.removeEventListener("scroll", checkPosition);
window.removeEventListener("resize", checkPosition);
};
}, []);
if (showCompletionScreen) {
if (recSignStatus === "COMPLETED") {
return <CompletionScreen />
} else if (recSignStatus === "DECLINED") {
return <CompletionScreen hit="Action Canceled! " title="Maybe Next Time!" message={"Your request has been declined successfully. You can revisit anytime if you change your mind."} textColorSuccess={false} />
} else {
return <CompletionScreen />
}
}
return (
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>
{!showCompletionScreen && <>
@ -623,68 +774,12 @@ const SignDocument = () => {
<div className="bg-slate-100"
// onSubmit={(e: React.FormEvent<HTMLFormElement>) => handleFormSubmit(e)}
>
{/* Nav Two - Options - START */}
<div className='flex justify-between items-center px-4 py-1 bg-white'>
{/* Left Side */}
<div className='flex justify-start items-center gap-1'>
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide font-semibold text-[1rem] text-[var(--primary)]'>Sign Made Easy</p>
</div>
{/* FIT - START */}
<div
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>Fit</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute bg-white rounded-sm z-50'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
onClick={() => setSelFitIndx(indx)}
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
<div
className='flex items-center justify-between w-full'>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
</div>
</div>)
})
}
</div>}
</div>
{/* FIT - END */}
{provideTags && <div
onClick={() => handleSetIsFields(true)}
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${isFields ? 'bg-slate-100' : ''}`}>
<IoIosDocument />
<p className='tracking-wide'>Fields</p>
</div>}
<div
onClick={() => handleSetIsFields(provideTags ? false : !isFields)}
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${!isFields ? 'bg-slate-100' : ''}`}>
<IoIosDocument />
<p className='tracking-wide'>Pages</p>
</div>
</div>
<p className="text-xs text-slate-600">Hi! {recipient && recipient.firstName} {recipient && recipient.lastName}</p>
<div className='flex justify-between items-center px-4 py-1 bg-[var(--primary)]'>
<img className="w-[10%] max-w-max py-1 pr-3.5" src="/sme-white.png" alt="" />
{/* Right Side - STARTS */}
<div className='flex justify-center items-center'>
@ -692,12 +787,14 @@ const SignDocument = () => {
{/* Documnt Nav */}
<>
{selectedDocuments !== null &&
<div tabIndex={1}
<div
className=""
tabIndex={1}
onBlur={() => setNav2DocToggle(false)}>
<div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>{selectedDocuments && selectedDocuments.length} Documents</p>
{/* <div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='bg-white flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide flex gap-1'><span>{selectedDocuments && selectedDocuments.length}</span> <span>Documents</span></p>
<MdKeyboardArrowDown className={`${nav2DocToggle ? 'rotate-180' : ''} transition-all`} />
</div>
</div> */}
<div
@ -718,6 +815,16 @@ const SignDocument = () => {
}
</>
{/* PRINT _ START */}
<GrPrint
onClick={printDocConDiv}
className="text-white mr-2 cursor-pointer" />
{/* PRINT _ END */}
{/* Send */}
{/* {(!transparentTags && !hideTags) ? <button
onClick={handleFormSubmit}
@ -735,21 +842,99 @@ const SignDocument = () => {
<p className="text-xs">Signed Successfully</p>
} */}
<SignButton
{recipient?.type !== "CC" ? <SignButton
transparentTags={transparentTags}
hideTags={hideTags}
handleFormSubmit={handleFormSubmit}
onDeclineClick={() => setDeclineOpen(true)}
onReassignClick={() => setReassignOpen(true)}
/>
/> : <button
className="text-xs bg-[var(--primary)] rounded-md text-white flex items-center gap-1 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<FaDownload className="sm" />
<span>Download</span>
</button>}
<>
<div className={`absolute z-50 w-full shadow-[0_20px_35px_rgba(0,0,0,0.25)] bottom-0 left-0 bg-white px-4 py-2 flex justify-between items-center gap-4`}>
<p className='text-sm text-slate-600'>By Signing I agree Sign Made Easy <a href="" className="text-[var(--mainPrimaryBtn)]">Terms & Conditions.</a></p>
{/* <div className='flex items-center gap-8 cursor-pointer text-xs px-2 py-1 rounded-sm outline-none'>
<FaAngleLeft
onClick={() => handleTagNavClick("P")}
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
<div>
<span className="text-[var(--mainPrimaryBtn)]">{focusElement?.navigationOrder}</span>
<span> / </span>
<span>{droppedItems.length}</span>
<span> Fields</span>
</div>
<FaAngleRight
onClick={() => handleTagNavClick("N")}
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
</div> */}
<div></div>
{/* <p className='cursor-pointer text-sm text-white bg-[var(--mainPrimaryBtn)] px-4 py-2 rounded'
onClick={handleFormSubmit}
>Sign</p> */}
<div>
{/* FIT - START */}
<div
className="hidden sm:block"
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='bg-white flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ml-2'>
<p className='tracking-wide'>Fit</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute top-[120px] bg-white rounded-sm z-50'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
onClick={() => setSelFitIndx(indx)}
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
<div
className='flex items-center justify-between w-full'>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
</div>
</div>)
})
}
</div>}
</div>
{/* FIT - END */}
</div>
</div>
</>
{requiredFilled && <>
<div className={`absolute z-50 ${requiredFilled ? "bottom-0" : "bottom-[-100px] hidden"} left-0 bg-[var(--primary)] w-full flex justify-around items-center transition-all duration-300 px-2.5`}>
<div className="py-6 text-white">
<p className="text-2xl font-semibold">All Set to Wrap This Up?</p>
<p className="text-sm">You've completed the required fields. Review your work, then select Finish.</p>
</div>
<button
onClick={handleFormSubmit}
className="font-semibold text-sm px-8 py-2 bg-slate-100 rounded-md text-[var(--primary)]">Finish</button>
</div>
</>}
</div>
{/* Right Side - ENDS */}
</div>
{/* Nav Two - Options - End */}
{/* Document Container Start */}
<div className="grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-48px)] overflow-hidden">
@ -757,8 +942,30 @@ const SignDocument = () => {
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
<div
id="tags-list-client-side"
className={`col-span-2 h-full ${isFields ? "p-4" : ""} overflow-y-auto bg-white ${!provideTags && isFields ? "hidden" : ""}`}>
{isFields && recipientId && provideTags && <FieldList color={"#8f87f1"} recipientId={Number(recipientId)} recipientIndx={0} acntId={"1"} />}
className={`absolute w-[220px] left-0 top-[42px] border-r-2 sm:border-none border-slate-200 z-10 sm:static sm:w-auto sm:col-span-2 h-[100vh] p-4 overflow-y-auto ${recipient?.type !== "CC" ? "bg-white" : ""} ${!provideTags && isFields ? "hidden" : ""}`}>
<div className='flex flex-col md:flex-row justify-between items-center bg-slate-100 p-1.5 rounded-2xl border-1 border-gray-200 mb-3'>
{provideTags && recipient?.type !== "CC" && <div
onClick={() => handleSetIsFields(true)}
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isFields ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300'}`}
>
<IoIosDocument />
<p className='tracking-wide'>Tags</p>
</div>}
<div
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${!isFields ? 'bg-[var(--primary)] text-white' : ''} ${!provideTags ? "sm:hidden" : ""} transition-all duration-300`}
onClick={() => handleSetIsFields(provideTags ? false : !isFields)}
>
<IoIosDocument />
<p className='tracking-wide'>Pages</p>
</div>
{/* <p className="hidden sm:block text-xs text-white">Hi! {recipient && recipient.firstName} {recipient && recipient.lastName}</p> */}
</div>
{isFields && recipientId && provideTags && recipient?.type !== "CC" && <FieldList color={"#8f87f1"} recipientId={Number(recipientId)} recipientIndx={0} acntId={"1"} />}
{!isFields && <DocumentPages
selectedDocuments={selectedDocuments}
@ -771,7 +978,7 @@ const SignDocument = () => {
{/* Main container */}
<div
id="doc-con"
className={`zoom-con relative ${!isFields || (isFields && provideTags) ? "col-span-8" : "col-span-12"} px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth transition-all duration-200`}
className={`zoom-con relative ${provideTags ? (isFields && selectedTag !== null ? "col-span-8" : "col-span-10") : (isFields ? "col-span-12" : "col-span-10")} sm:px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth transition-all duration-200 pb-[4rem]`}
>
{
selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => {
@ -786,8 +993,8 @@ const SignDocument = () => {
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
const key = `${doc.documentId}_${page.index}`;
// const tagJsonData = [...droppedItems];
// debugger
const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index)
let scale: { x: number | null, y: number | null } = { x: null, y: null }
return (
<div
key={indx}
@ -797,8 +1004,27 @@ const SignDocument = () => {
className='drop-page-in shadow-md relative'
>
<img
// style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
style={{ "width": page.width, "height": page.height, "maxWidth": "100%", "maxHeight": "100%" }}
draggable="false"
onLoad={(imgElem) => {
setPageScales((prev) => ({
...prev,
[page.index]: {
x: imgElem.clientWidth / page.width,
y: imgElem.clientHeight / page.height,
},
}));
}}
width={page.width}
height={page.height}
src={page.src}
/>
{!hideTags && <>
<TagsCon
scale={pageScales[page.index] || { x: 1, y: 1 }}
currDocTags={currDocTags}
pageIdkey={key}
provideTags={provideTags}
@ -811,43 +1037,68 @@ const SignDocument = () => {
droppedItems={droppedItems}
saved={saved}
signConfObj={signConfObj}
recipientId={recipientId as string}
setSaved={setSaved}
setStamp={setStamp}
setSignConfObj={setSignConfObj}
setOpenSign={setOpenSign}
setSignTagId={setSignTagId}
setDroppedItems={setDroppedItems}
setSelectedTag={setSelectedTag}
setFocusElement={setFocusElement} />
setFocusElement={setFocusElement}
handleTagNavClick={handleTagNavClick}
/>
</>}
<img
style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
draggable="false"
width={page.width}
height={page.height}
src={page.src}
/>
{/* Side details of documents Pages and document Name - START */}
< p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-60px] text-slate-600' >
< p className='hidden sm:block absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-50px] text-slate-600' >
page {indx + 1} of {doc.pages && doc.pages.length}
</p>
{/* Side details of documents Pages and document Name - END */}
{focusElement !== null && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index &&
<div
id="tag-navigation-btn"
className={`absolute right-0 translate-x-[100%] flex flex-col justify-center items-center z-50 cursor-pointer transition-all duration-300`}
style={{ top: (focusElement.dimensions.top + focusElement.dimensions.height / 2) + "px" }}
onClick={() => handleTagNavClick("N")}
>
{/* NEXT Button - START */}
<svg width="70" height="45" viewBox="0 0 112 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.87927 20.8542L15.0554 2.53357C15.7668 1.54438 16.9108 0.958008 18.1292 0.958008H108.049C110.14 0.958008 111.835 2.65317 111.835 4.74426V41.0235C111.835 43.1146 110.14 44.8098 108.049 44.8098H18.1084C16.9016 44.8098 15.7671 44.2345 15.054 43.261L1.89871 25.3024C0.930133 23.9802 0.92229 22.1849 1.87927 20.8542Z" fill="#005AB5" />
<path d="M53.2649 16.7474V28.3838H51.3899L45.907 20.4577H45.8104V28.3838H43.7024V16.7474H45.5888L51.0661 24.6792H51.1683V16.7474H53.2649ZM58.8545 28.5542C57.9795 28.5542 57.2238 28.3724 56.5875 28.0088C55.9549 27.6414 55.4682 27.1224 55.1272 26.452C54.7863 25.7777 54.6159 24.9842 54.6159 24.0713C54.6159 23.1736 54.7863 22.3857 55.1272 21.7077C55.4719 21.0258 55.953 20.4955 56.5704 20.1167C57.1879 19.7342 57.9132 19.5429 58.7466 19.5429C59.2844 19.5429 59.792 19.63 60.2693 19.8042C60.7504 19.9747 61.1746 20.2398 61.542 20.5997C61.9132 20.9595 62.2049 21.4179 62.417 21.9747C62.6291 22.5277 62.7352 23.1868 62.7352 23.952V24.5827H55.5818V23.1963H60.7636C60.7598 22.8023 60.6746 22.452 60.5079 22.1452C60.3413 21.8345 60.1083 21.5902 59.8091 21.4122C59.5136 21.2342 59.1689 21.1452 58.775 21.1452C58.3545 21.1452 57.9852 21.2474 57.667 21.452C57.3488 21.6527 57.1007 21.9179 56.9227 22.2474C56.7485 22.5732 56.6594 22.9311 56.6557 23.3213V24.5315C56.6557 25.0391 56.7485 25.4747 56.9341 25.8383C57.1197 26.1982 57.3791 26.4747 57.7125 26.6679C58.0458 26.8573 58.436 26.952 58.8829 26.952C59.1822 26.952 59.453 26.9103 59.6954 26.827C59.9379 26.7398 60.1481 26.613 60.3261 26.4463C60.5041 26.2796 60.6386 26.0732 60.7295 25.827L62.65 26.0429C62.5288 26.5505 62.2977 26.9936 61.9568 27.3724C61.6197 27.7474 61.1879 28.0391 60.6613 28.2474C60.1348 28.452 59.5325 28.5542 58.8545 28.5542ZM65.4896 19.6565L67.2509 22.8781L69.0407 19.6565H71.2168L68.5862 24.0202L71.2623 28.3838H69.0975L67.2509 25.2417L65.4214 28.3838H63.2396L65.8987 24.0202L63.3078 19.6565H65.4896ZM76.6857 19.6565V21.2474H71.6687V19.6565H76.6857ZM72.9073 17.5656H74.9641V25.7588C74.9641 26.0353 75.0058 26.2474 75.0891 26.3952C75.1762 26.5391 75.2899 26.6376 75.43 26.6906C75.5702 26.7436 75.7255 26.7702 75.8959 26.7702C76.0247 26.7702 76.1421 26.7607 76.2482 26.7417C76.3581 26.7228 76.4414 26.7058 76.4982 26.6906L76.8448 28.2986C76.735 28.3364 76.5778 28.3781 76.3732 28.4236C76.1725 28.469 75.9262 28.4955 75.6346 28.5031C75.1194 28.5183 74.6554 28.4406 74.2425 28.2702C73.8296 28.0959 73.502 27.827 73.2596 27.4633C73.0209 27.0997 72.9035 26.6452 72.9073 26.0997V17.5656Z" fill="white" />
</svg>
{/* NEXT Button - END */}
</div>}
</div>
)
})
}
</div>
</div>
</div>
)
})
}
{(navigationTagPos === NavigationTagPosEnum.ABOVE || navigationTagPos === NavigationTagPosEnum.BELOW) &&
<div
onClick={() => scrollToElement(focusElement?.navigationOrder)}
className={`cursor-pointer fixed ${navigationTagPos === NavigationTagPosEnum.ABOVE ? "top-[45px]" : "bottom-[45px] rotate-[180deg]"} right-[8%] flex flex-col items-center`}>
{/* Arrow pointing up */}
<div className="w-0 h-0 border-l-6 border-r-6 border-b-6 border-l-transparent border-r-transparent border-b-[var(--mainPrimaryBtn)]"></div>
{/* Label box */}
<div
className="bg-[var(--mainPrimaryBtn)] text-white text-sm px-3 py-1 rounded-md shadow-md flex items-center gap-1">
<span>Next</span>
</div>
</div>}
</div>
{provideTags && selectedTag && selectedTag !== null && "key" in selectedTag && "index" in selectedTag &&
@ -866,174 +1117,16 @@ const SignDocument = () => {
}
</div>
{/* Document Container End */}
</div>
</>}
{signerConsent && <>
<div className='w-full h-full'>
<div className="flex items-center space-x-4 bg-white w-full py-2 px-5 shadow">
<Link to={'/'} className="flex items-center space-x-2 text-gray-600 hover:text-[var(--primary)] transition-colors duration-200 group">
{/* <IoMdHome
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
/> */}
SME
</Link>
<div className="h-8 w-px bg-gray-300"></div>
<h1 className="text-md text-gray-800">🎉 The document has been signed successfully!</h1>
</div>
<div className='flex flex-col p-4 justify-center items-center w-full'>
<div className='mx-auto max-w-max'>
<div className='flex items-center gap-4 mb-3 ml-0 bg-slate-100 p-4 rounded-md'>
<div className="w-12 h-12 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center shadow-sm">
<FaCheck className="text-white text-2xl" />
</div>
<div>
<p className='text-2xl font-medium text-[var(--primary)]'>You have successfully signed the document.</p>
{/* <p className='text-sm text-slate-600'>All signers will be notified via email</p> */}
</div>
</div>
<div className='flex gap-4'>
<div className='pb-4 bg-slate-100'>
<div className='w-full overflow-auto h-[calc(100vh-200px)]'>
<div className='py-2 bg-white pl-4 pr-4 shadow-sm rounded-sm border-1 border-slate-200 sticky top-0 w-full z-1'>
<p className='text-sm font-medium'>QuickPreview</p>
</div>
{/* <NavTwo isPreview={true} aId={accountId} tId={transactionId} /> */}
<div className='pl-4 pr-4'>
{/* SIGN Documen Container - START */}
<div
id="doc-con"
className={`relative px-4 bg-slate-100 max-w-max h-full w-full overflow-auto scroll-smooth transition-all duration-200`}
>
{
selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => {
return (
<div key={doc.documentId} className='flex flex-col pointer-events-none'>
<div className='py-2 text-sm'>
<p className='text-center'>{doc.name}</p>
</div>
<div className='flex flex-col gap-3 max-w-max relative'>
{
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
const key = `${doc.documentId}_${page.index}`;
// const tagJsonData = [...droppedItems];
// debugger
const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index)
return (
<div
key={indx}
id={doc.documentId + indx}
onDrop={(e) => handleDrop(e, doc.documentId, doc.documentId, page.index)}
onDragOver={(e) => handleDragOver(e)}
className='drop-page-in shadow-md relative'
>
{!hideTags && <>
<TagsCon
currDocTags={currDocTags}
pageIdkey={key}
provideTags={provideTags}
documentId={doc.documentId}
pageIndex={page.index}
hideTags={hideTags}
transparentTags={transparentTags}
focusElement={focusElement}
selectedTag={selectedTag}
droppedItems={droppedItems}
saved={saved}
signConfObj={signConfObj}
setStamp={setStamp}
setSignConfObj={setSignConfObj}
setOpenSign={setOpenSign}
setSignTagId={setSignTagId}
setDroppedItems={setDroppedItems}
setSelectedTag={setSelectedTag}
setFocusElement={setFocusElement} />
</>}
<img
style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
draggable="false"
width={page.width}
height={page.height}
src={page.src}
/>
{/* Side details of documents Pages and document Name - START */}
< p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-60px] text-slate-600' >
page {indx + 1} of {doc.pages && doc.pages.length}
</p>
{/* Side details of documents Pages and document Name - END */}
</div>
)
})
}
</div>
</div>
)
})
}
</div>
{/* SIGN Documen Container - END */}
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<Link to={"/"} className="px-4 py-2 rounded-sm text-white bg-[var(--primary)] lg:min-w-[240px] text-center">Download Document</Link>
<Link to={"/"} className="px-4 py-2 rounded-sm text-white bg-[var(--primary)] lg:min-w-[240px] text-center">Visit Sign Made Easy</Link>
</div>
{/* Right Side */}
{/* <div className={`transition-all rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}> */}
{/* <div className="text-center pt-1 max-h-[25vh]">
<div className="w-20 h-20 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-3 mx-auto shadow-lg ">
<FaCheck className="text-white text-4xl" />
</div>
</div> */}
{/* <div className='text-center py-1'>
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
</div> */}
{/* <div className='max-h-[30vh]'> */}
{/* <SignersList signers={selectedRecipients} /> */}
{/* </div> */}
{/* <div className='mt-4 flex flex-col gap-2'>
{selfUserSign.sign && <Link to={`/sign/${selfUserSign.recipientId}`} className={`p-2 text-center rounded-md bg-[var(--primary)] text-white hover:shadow-sm transition-all duration-150`}>Sign Now</Link>}
<Link to={"/"} className={`p-2 text-center rounded-md bg-slate-200 hover:shadow-sm transition-all duration-150`}>Home</Link>
</div> */}
{/* <div className='border border-b border-gray-50'></div> */}
{/* <div className='min-h-[30vh]'>
<SigningOrderDiagram signers={selectedRecipients} />
</div> */}
{/* <div className='border border-b border-gray-50'></div> */}
{/* </div> */}
</div>
</div>
</div>
</div>
</>}
{/* {showCompletionScreen && recSignStatus === "COMPLETED" && <CompletionScreen />}
{showCompletionScreen && recSignStatus === "DECLINED" && <CompletionScreen title="No worries! you declined the signing request." message={"Your signing request has been declined. No changes were made to the document."} textColorSuccess={false} />} */}
<div style={{ display: `${openSign ? 'block' : 'none'}` }}>
{recipient &&
<SignCon
<ClientSignCon
recipient={recipient}
recipientId={recipientId as string}
setOpenSign={setOpenSign} saved={saved}
@ -1050,21 +1143,23 @@ const SignDocument = () => {
{globalLoader.isLoading && <Loader />}
{!showCompletionScreen && !signerConsent.consent && <ConsentScreen
onConsent={() => handleOnConsent()}
// onDecline={() => console.log("User declined")}
/>}
{
selectedDocuments !== null && !showCompletionScreen && !signerConsent.consent && <ConsentScreen
onConsent={() => handleOnConsent()}
// onDecline={() => console.log("User declined")}
/>
}
<DeclineModal
open={declineOpen}
onClose={() => setDeclineOpen(false)}
onConfirm={() => setSignerConsent((prev) => ({ ...prev, decline: false }))}
onConfirm={({ reason, comments }) => handleDeclineConfirm(reason, comments)}
/>
<ReassignModal
open={reassignOpen}
onClose={() => setReassignOpen(false)}
onConfirm={(email) => console.log("Reassigned to:", email)}
onConfirm={({ firstName, lastName, email, message }) => handleReassignConfirm({ firstName, lastName, email, message })}
/>
</div >

View File

@ -5,15 +5,16 @@ import { IoMdClose } from 'react-icons/io';
import maskUtils from '../../utlity/maskUtils';
import { FaFileAlt, FaSignature, FaStamp } from 'react-icons/fa';
import { TbSignature } from 'react-icons/tb';
import { BsFileArrowDownFill } from 'react-icons/bs';
import { handleGroupBorderPosition } from '../../utlity/tagGroupConUtils';
import api from '../../config/api';
import { SaveType, SignConfTypeObj, StampTypeObj } from './SignDocument';
import dayjs from 'dayjs';
import typeToIconMap from '../../helper/iconMap';
import axios from 'axios';
interface TagsConProps {
scale: { x: number | null, y: number | null }
currDocTags: DroppedItem[]
pageIdkey: string;
provideTags: boolean
@ -26,6 +27,8 @@ interface TagsConProps {
droppedItems: DroppedItem[]
saved: SaveType;
signConfObj: SignConfTypeObj | {};
recipientId: string;
setSaved: React.Dispatch<React.SetStateAction<SaveType>>,
setStamp: React.Dispatch<React.SetStateAction<StampTypeObj>>;
setSignConfObj: React.Dispatch<React.SetStateAction<SignConfTypeObj | {}>>;
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>,
@ -33,9 +36,11 @@ interface TagsConProps {
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItem[]>>;
setSelectedTag: React.Dispatch<React.SetStateAction<{ key: string; index: number } | null>>
setFocusElement: React.Dispatch<React.SetStateAction<DroppedItem | null>>
handleTagNavClick: (action: "N" | "P") => void
}
const TagsCon: React.FC<TagsConProps> = ({
scale,
currDocTags,
pageIdkey,
provideTags,
@ -48,13 +53,16 @@ const TagsCon: React.FC<TagsConProps> = ({
droppedItems,
saved,
signConfObj,
recipientId,
setSaved,
setStamp,
setSignConfObj,
setOpenSign,
setSignTagId,
setDroppedItems,
setSelectedTag,
setFocusElement
setFocusElement,
handleTagNavClick
}) => {
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
@ -131,7 +139,7 @@ const TagsCon: React.FC<TagsConProps> = ({
(async () => {
if ("id" in tag) {
try {
const response = await api.delete(`/api/sign/tags?recipientId=${recipientId}`, { data: [{ tagId: tag.id }] })
const response = await api.delete(`/api/sign/${recipientId}/tags/`, { data: [{ tagId: tag.id }] })
if (response.status !== 200) throw "Tag Deletion error"
} catch (err) {
return;
@ -230,10 +238,24 @@ const TagsCon: React.FC<TagsConProps> = ({
// });
}
const handleSignClick = async (index: number, id: number) => {
const handleSignClick = (index: number, id: number) => {
setSignTagId(id)
if (saved.save) {
if (saved.isDefaultAndClicked) {
try {
const res = await axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/${recipientId}/signatures`, {
signatureId: saved.signatureId,
tagId: id
}, { headers: { "ngrok-skip-browser-warning": "69420", } })
if (res.status === 202) {
setSaved({ ...saved, signatureId: res.data.id, isDefaultAndClicked: false });
}
} catch (error) { }
}
if (index in signConfObj) {
setOpenSign(true);
return
@ -246,9 +268,16 @@ const TagsCon: React.FC<TagsConProps> = ({
}
})
return
}
setOpenSign(true);
// setDroppedItems((prev) => {
// return prev.map((obj, idx) => {
// if (idx === index && obj.id === id) return { ...obj, valus: saved.signatureId }
// return obj;
// })
// })
const newObj = {
[index]: {
"accepted": false,
@ -256,7 +285,6 @@ const TagsCon: React.FC<TagsConProps> = ({
}
}
setSignConfObj({ ...newObj })
}
const handleStampClick = (index: number) => {
@ -279,25 +307,37 @@ const TagsCon: React.FC<TagsConProps> = ({
if (item.pageIndex !== pageIndex || item.documentId != documentId) return <></>;
return (
<div
tabIndex={index+1}
tabIndex={index + 1}
onFocus={() => {
setSelectedTag({ key: pageIdkey, index })
// setFocusElement({ ...item, focusElementIndx: index })
setFocusElement({ ...item, focusElementIndx: index })
}
}
key={index}
// onPointerDown={(e) => handleTagMouseDown(e, key, index)}
className="absolute rounded dropped-item text-sm"
// onClick={() => handleTagClick(key, index)}
// style={{
// left: `${item.dimensions.left}px`,
// top: `${item.dimensions.top}px`,
// width: `${item.dimensions.width}px`,
// height: `${item.dimensions.height}px`,
// border: `${((!hideTags && focusElement) && !transparentTags && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) ? '2px dashed #1c398e' : 'none'}`,
// // display: `${hideTags ? 'none' : 'block'}`
// }}
style={{
left: `${item.dimensions.left}px`,
top: `${item.dimensions.top}px`,
width: `${item.dimensions.width}px`,
height: `${item.dimensions.height}px`,
border: `${((!hideTags && focusElement) && !transparentTags && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) ? '2px dashed #1c398e' : 'none'}`,
// display: `${hideTags ? 'none' : 'block'}`
left: item.dimensions.left * scale.x,
top: item.dimensions.top * scale.y,
width: item.dimensions.width * scale.x,
height: item.dimensions.height * scale.y,
}}
>
{item.required && <div
className='absolute top-[-16px] right-[-8px] text-lg text-red-600'
>*</div>}
{provideTags && !transparentTags && !hideTags && <div
onPointerDown={(e) => handleTagMouseDown(e, pageIdkey, index)}
// className="absolute top-[-15px] left-[-15px] w-4 h-4 bg-blue-600 rounded-tr cursor-move z-30"
@ -309,7 +349,7 @@ const TagsCon: React.FC<TagsConProps> = ({
</div>}
<>
{selectedTag?.key && selectedTag.key === pageIdkey && selectedTag.index === index &&
{selectedTag !== null && selectedTag?.key && selectedTag.key === pageIdkey && selectedTag.index === index &&
<div className='absolute flex justify-between items-center w-full max-w-max bg-white top-[-22px] shadow z-20 text-sm rounded-t-sm' >
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
{/* <p className='w-2.5 h-2.5 rounded'
@ -317,8 +357,8 @@ const TagsCon: React.FC<TagsConProps> = ({
>
</p> */}
{/* <p
className=''>{IconComponent}
</p> */}
className=''>{IconComponent}
</p> */}
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
{provideTags && <IoMdClose onClick={(e) => {
e.stopPropagation();
@ -332,7 +372,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "TEXT" &&
<div
id={item.id}
id={item.navigationOrder}
tabIndex={1}
className={`max-h-max text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
// style={{ height: item.dimensions.height + "px" }}
@ -401,7 +441,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "EMAIL" &&
<div
id={item.id}
id={item.navigationOrder}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
@ -454,7 +494,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "NAME" &&
<div
id={item.id}
id={item.navigationOrder}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
@ -490,7 +530,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "NUMBER" &&
<div
id={item.id}
id={item.navigationOrder}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
@ -514,7 +554,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "COMPANY" &&
<div
id={item.id}
id={item.navigationOrder}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
@ -548,7 +588,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "CHECKBOX" &&
<div
id={item.id}
id={item.navigationOrder}
className={`h-full w-full rounded flex justify-center items-center ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
@ -567,7 +607,7 @@ const TagsCon: React.FC<TagsConProps> = ({
item.type === "SIGNATURE" &&
<div
tabIndex={1}
id={item.id}
id={item.navigationOrder}
onClick={() => handleSignClick(index, item.id)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none bg-transparent" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
@ -598,7 +638,7 @@ const TagsCon: React.FC<TagsConProps> = ({
item.type === "INITIALS" &&
<div
tabIndex={1}
id={item.id}
id={item.navigationOrder}
onClick={() => handleSignClick(index, item.id)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
@ -626,7 +666,7 @@ const TagsCon: React.FC<TagsConProps> = ({
item.type === "STAMP" &&
<div
tabIndex={1}
id={item.id}
id={item.navigationOrder}
onClick={() => handleStampClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
@ -645,7 +685,7 @@ const TagsCon: React.FC<TagsConProps> = ({
item.type === "FILE" &&
<div
tabIndex={1}
id={item.id}
id={item.navigationOrder}
// onClick={() => handleSignClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
@ -664,7 +704,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "SELECT" &&
<div
id={item.id}
id={item.navigationOrder}
tabIndex={1}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100"}`}
>
@ -701,7 +741,7 @@ const TagsCon: React.FC<TagsConProps> = ({
{
item.type === "TEMPORAL" &&
<div
id={item.id}
id={item.navigationOrder}
tabIndex={1}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100"}`}
>
@ -752,7 +792,7 @@ const TagsCon: React.FC<TagsConProps> = ({
item.type.toLowerCase() === TagsTypes.ATTACHMENT &&
<div
tabIndex={1}
id={item.id}
id={item.navigationOrder}
// onClick={() => handleStampClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
@ -786,14 +826,26 @@ const TagsCon: React.FC<TagsConProps> = ({
>
<p>{focusElement.label}</p>
</div> */}
<div
className="absolute left-[-10px] z-50 flex max-w-max"
style={{ left: -(item.dimensions.left + 20) + "px", top: ((item.dimensions.height / 2) - 18) + "px" }}
{/* <div
className="absolute z-[1000] flex flex-col items-center max-w-max bg-[var(--primary)] rounded-tl-md rounded-bl-md"
style={{ right: 0 + "px", top: ((item.dimensions.height / 2) - 43) + "px" }}
>
{/* <FaArrowRight className="text-[var(--primary)] text-xl" />
*/}
<BsFileArrowDownFill className="text-blue-900 text-4xl rotate-270" />
</div>
<span className='pt-2 pb-1 text-white cursor-pointer'
onClick={() => {
handleTagNavClick("P")
}}
>
<TiArrowSortedUp />
</span>
<p className='p-2 border-t-1 border-b-1 border-slate-700 text-white text-xs'><span>{focusElement.navigationOrder}</span><span>/{droppedItems.length}</span></p>
<span className='pb-2 pt-1 text-white cursor-pointer'
onClick={() => {
handleTagNavClick("N")
}}
>
<TiArrowSortedUp className='rotate-180' />
</span>
</div> */}
</>
}
</div>

View File

@ -12,7 +12,7 @@ interface TermsModalProps {
const TermsModal: React.FC<TermsModalProps> = ({ showTerms, setShowTerms, recipientId }) => {
const onAccept = () => {
axios.put(`${import.meta.env.VITE_BASE_URI}/api/sign/accept?recipientId=${recipientId}`, { headers: { "ngrok-skip-browser-warning": "69420" } })
axios.put(`${import.meta.env.VITE_BASE_URI}/api/sign/accept/${recipientId}`, { headers: { "ngrok-skip-browser-warning": "69420" } })
.then(response => {
setShowTerms(false);
console.log(response, "Accept");

View File

@ -0,0 +1,179 @@
{showCompletionScreen && (
<div className="w-full h-full">
{/* Header */}
<div className="flex flex-wrap items-center gap-2 sm:gap-4 bg-white w-full py-2 px-4 sm:px-5 shadow">
<Link
to={"/"}
className="flex items-center space-x-2 text-gray-600 hover:text-[var(--primary)] transition-colors duration-200 group text-sm sm:text-base"
>
SME
</Link>
<div className="h-6 sm:h-8 w-px bg-gray-300"></div>
{recSignStatus === "COMPLETED" && (
<h1 className="text-sm sm:text-md text-gray-800">
🎉 The document has been signed successfully!
</h1>
)}
{recSignStatus === "DECLINED" && (
<h1 className="text-sm sm:text-md text-gray-800">
❌ The document has been declined by you.
</h1>
)}
</div>
{/* Content */}
<div className="flex flex-col p-4 justify-center items-center w-full">
<div className="mx-auto w-full max-w-6xl">
{/* Banner */}
<div className="flex items-center gap-3 sm:gap-4 mb-4 bg-slate-100 p-4 rounded-md">
{recSignStatus === "COMPLETED" && (
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center shadow-sm">
<FaCheck className="text-white text-xl sm:text-2xl" />
</div>
)}
{recSignStatus === "DECLINED" && (
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-red-400 to-red-500 rounded-full flex items-center justify-center shadow-sm">
<ImCancelCircle className="text-white text-xl sm:text-2xl" />
</div>
)}
<div>
{recSignStatus === "COMPLETED" && (
<p className="text-lg sm:text-2xl font-medium text-[var(--primary)]">
You have successfully signed the document.
</p>
)}
{recSignStatus === "DECLINED" && (
<p className="text-lg sm:text-2xl font-medium text-[var(--primary)]">
You have declined the document.
</p>
)}
</div>
</div>
{/* Main responsive layout */}
<div className="flex flex-col lg:flex-row gap-4">
{/* Left: Quick Preview */}
<div className="flex-1 bg-slate-100 rounded-md overflow-hidden">
<div className="w-full h-[60vh] lg:h-[calc(100vh-200px)] overflow-auto">
<div className="py-2 bg-white px-4 shadow-sm rounded-sm sticky top-0 z-10">
<p className="text-sm font-medium">Quick Preview</p>
</div>
<div className="px-4">
{/* SIGN Document Container */}
<div
id="doc-con"
className="relative px-2 sm:px-4 bg-slate-100 w-full overflow-auto scroll-smooth transition-all duration-200"
>
{selectedDocuments &&
selectedDocuments.length > 0 &&
selectedDocuments.map((doc) => (
<div
key={doc.documentId}
className="flex flex-col pointer-events-none"
>
<div className="py-2 text-xs sm:text-sm">
<p className="text-center">{doc.name}</p>
</div>
<div className="flex flex-col gap-3 relative">
{doc.pages &&
doc.pages.length > 0 &&
doc.pages.map((page, indx) => {
const key = `${doc.documentId}_${page.index}`
const currDocTags = droppedItems.filter(
(val) => val.pageIndex === page.index
)
return (
<div
key={indx}
id={doc.documentId + indx}
onDrop={(e) =>
handleDrop(
e,
doc.documentId,
doc.documentId,
page.index
)
}
onDragOver={(e) => handleDragOver(e)}
className="drop-page-in shadow-md relative"
>
{!hideTags && (
<TagsCon
currDocTags={currDocTags}
pageIdkey={key}
provideTags={provideTags}
documentId={doc.documentId}
pageIndex={page.index}
hideTags={hideTags}
transparentTags={transparentTags}
focusElement={focusElement}
selectedTag={selectedTag}
droppedItems={droppedItems}
saved={saved}
signConfObj={signConfObj}
setStamp={setStamp}
setSignConfObj={setSignConfObj}
setOpenSign={setOpenSign}
setSignTagId={setSignTagId}
setDroppedItems={setDroppedItems}
setSelectedTag={setSelectedTag}
setFocusElement={setFocusElement}
/>
)}
<img
style={{
minWidth: page.width,
minHeight: page.height,
maxWidth: page.width,
maxHeight: page.height,
}}
draggable="false"
width={page.width}
height={page.height}
src={page.src}
/>
{/* Page info */}
<p className="absolute top-1/2 -translate-y-1/2 -rotate-90 text-[10px] sm:text-xs left-[-50px] sm:left-[-60px] text-slate-600">
page {indx + 1} of{" "}
{doc.pages && doc.pages.length}
</p>
</div>
)
})}
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Right: Actions */}
<div className="flex flex-col gap-2 w-full lg:w-[280px]">
<Link
to={"/"}
className="px-4 py-2 rounded-sm text-white bg-[var(--primary)] w-full text-center"
>
Download Document
</Link>
<Link
to={"/"}
className="px-4 py-2 rounded-sm text-white bg-[var(--primary)] w-full text-center"
>
Visit Sign Made Easy
</Link>
{recSignStatus === "DECLINED" && (
<p className="italic text-slate-400 text-center mt-2 cursor-pointer hover:underline">
Report Sender ?
</p>
)}
</div>
</div>
</div>
</div>
</div>
)}

View File

@ -24,10 +24,13 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
}, [sendForSignDropdown])
const user = useSelector((state: RootState) => state.user);
if (!user.account || user.account === null) {
window.location.href = "/login";
return;
}
useEffect(() => {
if (!user.account || user.account === null) {
window.location.href = "/login";
return;
}
}, [])
const defaultAccountId = user.account.id;
@ -118,7 +121,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
transition-all duration-300 ease-in-out w-full
rounded
`}
style={{ whiteSpace: "nowrap" }}
style={{ whiteSpace: "nowrap" }}
>
{showSideBar ? (
<p className="text-sm select-none">Send for signature</p>

View File

@ -70,7 +70,8 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar, isMobile
{/* Menu */}
<div className={`absolute top-0 ${showSideBar ? 'p-4' : 'py-4'} w-full max-h-max ${showSideBar ? '' : 'text-center'}`}>
<h1 className='w-full text-lg text-center tracking-widest font-medium text-gray-800'>
{showSideBar ? <img src={logoSME} loading='lazy' className='h-7 ms-6' /> : <p className='text-white'>SME</p>}
<img className={`${!showSideBar ? "hidden" : ""} h-7 ms-6`} src={logoSME} loading='lazy' />
<p className={`text-white ${showSideBar ? "hidden" : ""}`}>SME</p>
</h1>
<div

View File

@ -98,8 +98,6 @@ const accounts = ['All Accounts', 'Account 1', 'Account 2', 'Account 3'];
const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRangeChange, cards, isLoading }) => {
const [selectedFilter, setSelectedFilter] = useState<string>('Last 7 Days');
const [selectedAccount, setSelectedAccount] = useState<string>('All Accounts');
const [showCards, setShowCards] = useState<CardData[]>([]);
// const [isLoading, setIsLoading] = useState<boolean>(true);
// useEffect(() => {
@ -188,110 +186,49 @@ const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRa
);
};
// if (isLoading) {
// return (
// <div className="w-full [h-50vh] px-6 py-8">
// <div className="animate-pulse">
// <div className="h-8 bg-gray-200 rounded w-32 mb-8"></div>
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
// {[...Array(7)].map((_, i) => (
// <div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
// ))}
// </div>
// </div>
// </div>
// );
// }
return (
<div className="w-full">
{/* Cards */}
{isLoading ? (
<div className="w-full h-[10vh] px-6 py-3">
<div className="animate-pulse">
{/* <div className="h-8 bg-gray-200 rounded w-32 mb-8"></div> */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-10 bg-gray-200 border border-gray-100"></div>
{/* Cards */}
{isLoading ? (
<div className="w-full h-[10vh] px-6 py-3">
<div className="animate-pulse">
{/* <div className="h-8 bg-gray-200 rounded w-32 mb-8"></div> */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-10 bg-gray-200 border border-gray-100"></div>
))}
</div>
</div>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 shadow">
{cards.map((card, index) => (
<div
key={card.title}
className={`flex items-center justify-between bg-white px-4 py-3.5 transition-all duration-300 cursor-pointer ${index > 0 ? "border-l-1 border-gray-200" : ""}`}
style={{
animationDelay: `${index * 100}ms`,
}}
>
{/* Left side: Icon + Title */}
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded-lg ${card.iconBg}`}>
{card.icon}
</div>
<h3 className="text-xs font-medium text-gray-600 text-nowrap">{card.title}</h3>
</div>
{/* Count */}
<p className={`text-sm font-bold`}>
{card.count}
</p>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{cards.map((card, index) => (
<div
key={card.title}
className="flex items-center justify-between border border-gray-200 bg-white px-4 py-1.5 hover:shadow-md transition-all duration-300 cursor-pointer"
style={{
animationDelay: `${index * 100}ms`,
}}
>
{/* Left side: Icon + Title */}
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${card.iconBg}`}>
{card.icon}
</div>
<h3 className="text-xs font-medium text-gray-600">{card.title}</h3>
</div>
{/* Count */}
<p className={`text-sm font-bold ${card.textColor}`}>
{card.count}
</p>
</div>
))}
</div>
)}
</div>
);
};
export default Cards;
{/* <div className="flex flex-wrap gap-3 w-full lg:w-auto">
{type === 'dashboard' && <>
Account Selector
<div className="relative">
<select
value={selectedAccount}
onChange={(e) => setSelectedAccount(e.target.value)}
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
>
{accounts.map((account) => (
<option key={account} value={account}>{account}</option>
))}
</select>
<FaChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
</div>
Date Filter
<div className="relative">
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
>
{filters.map((filter) => (
<option key={filter} value={filter}>{filter}</option>
))}
</select>
<FaCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
</div>
</>}
Action Button
{buttonConfig && (
<button
className={`flex items-center gap-1 px-3 py-3 text-sm font-semibold text-white rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95 cursor-pointer ${buttonConfig.gradient}`}
onClick={onOpenModal}
>
{buttonConfig.icon}
{buttonConfig.text}
</button>
)}
</div> */}
export default Cards;

View File

@ -4,6 +4,7 @@ import Cards from './Cards';
import StartSection from './StartSection';
import api from '../../config/api';
import { FiFileText, FiClock, FiAlertTriangle, FiCheckCircle } from "react-icons/fi";
import { CgSandClock } from "react-icons/cg";
import { useSelector } from 'react-redux';
import { RootState } from '../../store/store';
@ -25,8 +26,8 @@ const Home = () => {
{
title: "Action Required",
count: data.actionNeeded || 0,
icon: <FiFileText className="w-5 h-5 text-sky-500" />,
iconBg: "bg-sky-100 border-sky-200",
icon: <FiFileText className="w-5 h-5 text-[#0080FF]" />,
iconBg: "bg-[#F3F9FF]",
textColor: "text-sky-600",
gradient: "bg-white",
shadowColor: "shadow-sky-100",
@ -35,7 +36,7 @@ const Home = () => {
title: "Waiting for Response",
count: data.waitingForOthers || 0,
icon: <FiClock className="w-5 h-5 text-yellow-500" />,
iconBg: "bg-yellow-100 border-yellow-200",
iconBg: "bg-[#FFF9EB]",
textColor: "text-yellow-600",
gradient: "bg-white",
shadowColor: "shadow-yellow-100",
@ -43,8 +44,8 @@ const Home = () => {
{
title: "Expiring Soon",
count: data.expiring || 0,
icon: <FiAlertTriangle className="w-5 h-5 text-red-500" />,
iconBg: "bg-red-100 border-red-200",
icon: <CgSandClock className="w-5 h-5 text-red-500" />,
iconBg: "bg-[#FFF8F8]",
textColor: "text-red-600",
gradient: "bg-white",
shadowColor: "shadow-red-100",
@ -53,7 +54,7 @@ const Home = () => {
title: "Completed",
count: data.completed || 0,
icon: <FiCheckCircle className="w-5 h-5 text-green-500" />,
iconBg: "bg-green-100 border-green-200",
iconBg: "bg-[#F1FFEF]",
textColor: "text-green-600",
gradient: "bg-white",
shadowColor: "shadow-green-100",
@ -83,7 +84,7 @@ const Home = () => {
try {
const res = await api.get(`/api/accounts/${userData.account.id}/dashboard`, { params: payload });
setDashboardData(res.data);
const cardDataArray = mapApiDataToCards(res.data);
setCards(cardDataArray);
} catch (error) {
@ -154,7 +155,9 @@ const Home = () => {
cards={cards}
isLoading={isLoading}
/>
<StartSection />
<div className='px-4 pt-2'>
<StartSection />
</div>
</>
);
};

File diff suppressed because one or more lines are too long

View File

@ -115,7 +115,7 @@ const StartSection = () => {
return (
<section className="px-3 py-2 w-full mt-2">
<h2 className="text-sm font-semibold text-gray-800 px-1 pb-2">
<h2 className="text-lg font-bold text-gray-800 px-1 pb-3">
Upload Document
</h2>

View File

@ -0,0 +1,65 @@
import { FiX, FiCheckCircle, FiGrid, FiLayers } from "react-icons/fi";
interface TransactionChoiceModalProps {
open: boolean;
onClose: () => void;
onCreateTransaction: () => void;
onCreateSeparateTransactions: () => void;
}
export default function TransactionChoiceModal({ open, onClose, onCreateTransaction, onCreateSeparateTransactions }: TransactionChoiceModalProps) {
if (!open) return null;
return (
<div className="fixed inset-0 justify-center z-50">
<div className="w-full h-full bg-gray-200 absolute top-0 left-0 opacity-55"></div>
<div className="bg-white rounded-lg shadow-lg w-[320px] sm:w-[620px] p-5 relative top-[40%] translate-y-[-40%] left-[50%] translate-x-[-50%]">
{/* Close button */}
<button
onClick={onClose}
className="absolute top-3 right-3 text-gray-500 hover:text-gray-700"
>
<FiX size={20} />
</button>
{/* Header */}
<div className="flex items-center gap-2 mb-4">
<FiCheckCircle className="text-green-500" size={22} />
<div>
<h2 className="text-lg font-semibold">Create Transaction</h2>
</div>
</div>
{/* Options */}
<div className="flex flex-col wm sm:flex-row justify-center items-center gap-4 mt-6">
{/* Create Transaction (combine) */}
<button
onClick={onCreateTransaction}
className="w-[200px] sm:w-auto flex flex-col items-center justify-center border-1 border-dashed border-gray-200 cursor-pointer rounded-lg px-5 py-8 hover:bg-blue-50 transition shadow"
>
<FiLayers className="text-[var(--mainPrimaryBtn)] mb-2" size={28} />
<span className="font-medium text-[var(--mainPrimaryBtn)]">Create Transaction</span>
<span className="text-xs text-gray-500 mt-1 text-center">
All documents combined into a single transaction
</span>
</button>
{/* Create Separate Transactions */}
<button
onClick={onCreateSeparateTransactions}
className="w-[200px] sm:w-auto flex flex-col items-center justify-center border-1 border-dashed border-gray-200 cursor-pointer rounded-lg px-5 py-8 hover:bg-blue-50 transition shadow"
>
<FiGrid className="text-[var(--mainPrimaryBtn)] mb-2" size={28} />
<span className="font-medium text-[var(--mainPrimaryBtn)]">
Create Separate Transactions
</span>
<span className="text-xs text-gray-500 mt-1 text-center">
Each document will be its own transaction
</span>
</button>
</div>
</div>
</div>
);
}

View File

@ -3,11 +3,8 @@ import { useDispatch } from "react-redux";
import { AppDispatch } from "../../store/store";
import { FaCloudUploadAlt } from "react-icons/fa";
import { MdCancel } from "react-icons/md";
import { uploadDocuments } from "../../store/slices/documentSlice";
import { Bounce, toast } from "react-toastify";
const UploadDoc: React.FC = () => {
const [files, setFiles] = useState<File[]>([]);
const dispatch = useDispatch<AppDispatch>();

View File

@ -4,20 +4,20 @@ import { Signer } from "./DocumentCompletionScreen";
export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
return (
// <div className="bg-white/60 rounded-md shadow-sm border border-gray-100 ">
<div className="p-4 bg-slate-100 rounded-sm shadow-sm">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-[var(--primary)] flex items-center gap-2 ">
<div className="px-2">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold flex items-center gap-2 mb-4">
{/* <FaUsers className="text-blue-600" /> */}
Recipients
</h3>
<span className="text-sm text-gray-600">
{signers.filter(s => s.completed).length} of {signers.length} signed
{signers.filter(s => s.completed).length}/{signers.length} Signed
</span>
</div>
<div className="max-h-[24vh] overflow-y-auto no-scrollbar">
{signers.map((signer) => (
<div key={signer.id} className="text-sm flex items-center gap-4 rounded-lg">
<div className="overflow-y-auto no-scrollbar">
{signers.map((signer, indx) => (
<div key={signer.id} className={`text-sm flex items-center gap-4 ${indx !== signers.length-1 ? "border-b-1 border-gray-200" : ""} py-4`}>
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium">
{/* {signer.name.split(" ").length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.name[0].toUpperCase()} */}
@ -31,13 +31,15 @@ export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
</div>
<div className="flex items-center gap-2">
{signer.completed ? (
<div className="flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-full text-xs">
<FaCheck className="w-3 h-3" />
<div className="flex items-center gap-2 px-3 py-1 text-green-700 rounded-full text-sm">
{/* <FaCheck className="w-3 h-3" /> */}
<div className="w-2.5 h-2.5 bg-green-700 rounded-full"></div>
Signed
</div>
) : (
<div className="flex items-center gap-1 px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-xs">
<FaClock className="w-3 h-3" />
<div className="flex items-center gap-2 px-3 py-1 text-red-700 rounded-full text-sm">
{/* <FaClock className="w-3 h-3" /> */}
<div className="w-2.5 h-2.5 bg-red-700 rounded-full"></div>
Pending
</div>
)}

View File

@ -9,145 +9,185 @@ import { fetchRecipients, recipientType } from '../../../store/slices/selectedRe
import { fetchDocuments } from '../../../store/slices/documentSlice'
import { SigningOrderDiagram } from './SigningOrderDiagram'
import { SignersList } from './SignerList'
import { IoCheckmarkDoneCircle } from 'react-icons/io5'
import signSessionRedirect from '../../../utlity/signSessionRedirect'
const TransactionSentSuccess = () => {
const [isVisible, setIsVisible] = useState(false);
const [isSignNow, setIsSignNow] = useState(false);
const [isVisible, setIsVisible] = useState(false)
const dispatch = useDispatch<AppDispatch>()
const dispatch = useDispatch<AppDispatch>();
const { accountId, transactionId } = useParams<{ accountId: string; transactionId: string }>()
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const userDetails = useSelector((state: RootState) => state.user)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
const userDetails = useSelector((state: RootState) => state.user);
const [selfUserSign, setSelfUserSign] = useState({ sign: false, recipientId: 0 });
const [selfUserSign, setSelfUserSign] = useState({ sign: false, recipientId: 0 })
if (!accountId || !transactionId) {
window.location.href = "/";
return null;
if (!accountId || !transactionId) {
window.location.href = '/'
return null
}
useEffect(() => {
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }))
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }))
setIsVisible(true)
}, [])
useEffect(() => {
if (selectedRecipients.length === 0) return
let minRoutingOrder: number = Infinity
let userEmailPresent = {
present: false,
routingOrder: 0,
recipientId: 0,
type: recipientType.CC,
}
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
// const [transactionData, setTransactionData] = useState({})
for (let i = 0; i < selectedRecipients.length; i++) {
const recipient = selectedRecipients[i]
if (recipient.routingOrder < minRoutingOrder) {
minRoutingOrder = recipient.routingOrder
}
if (recipient.email === userDetails.email) {
userEmailPresent.present = true
userEmailPresent.routingOrder = recipient.routingOrder
userEmailPresent.recipientId = Number(recipient.id)
userEmailPresent.type = recipient.type
}
}
if (
userEmailPresent.present &&
userEmailPresent.routingOrder === minRoutingOrder &&
userEmailPresent.type !== recipientType.CC &&
userEmailPresent.type !== recipientType.INPERSONSIGNER
) {
setSelfUserSign({ sign: true, recipientId: userEmailPresent.recipientId })
}
}, [selectedRecipients])
return (
<div className="relative w-full overflow-hidden">
<div>
<img className="absolute z-0 top-[5%] right-0" src="/completion-screen-frame-1.png" alt="" />
<img className="absolute z-0 top-[5%] left-0 rotate-y-180" src="/completion-screen-frame-1.png" alt="" />
</div>
{/* Header */}
<div className="flex items-center space-x-3 bg-white w-full py-2 px-4 sm:px-5 shadow">
<Link
to={'/'}
className="flex items-center text-gray-600 hover:text-[var(--primary)] transition-colors duration-200"
>
{/* <IoMdHome className="text-xl sm:text-2xl text-slate-500 cursor-pointer" /> */}
<img src="/sme-colored.png" className='py-2 px-8' alt="" />
</Link>
{/* <div className="h-6 sm:h-8 w-px bg-gray-300"></div> */}
{/* <h1 className="text-sm sm:text-md text-gray-800">
🎉 Awesome! The document’s been sent for signature. 🎉
</h1> */}
</div>
{/* Content */}
<div className="flex flex-col p-4 justify-center items-center w-full">
<div className="mx-auto w-full max-w-6xl">
{/* Success banner */}
<div className="flex flex-col justify-center items-center gap-3 mb-4 p-4 rounded-md">
{/* <div className="w-12 h-12 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center shadow-sm">
<FaCheck className="text-white text-2xl" />
</div> */}
useEffect(() => {
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
setIsVisible(true)
}, [])
<svg width="70" height="70" viewBox="0 0 114 109" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M114 54.3837L101.095 40.1811L103.115 21.1673L84.2441 17.2075L74.6181 0.631836L57 8.43682L54.6221 54.3837L57 100.33L74.6183 108.135L84.2441 91.56L103.115 87.5999L101.095 68.5861L114 54.3837Z" fill="#0ED678" />
<path d="M39.3816 0.631836L29.7558 17.2075L10.8847 21.1675L12.9053 40.1811L0 54.3837L12.9053 68.5861L10.8847 87.5999L29.7558 91.56L39.3819 108.135L57.0001 100.33V8.43682L39.3816 0.631836Z" fill="#04EB84" />
<path d="M80.8957 42.013L75.9064 37.5986L57 58.5946L54.6221 63.8124L57 68.5499L80.8957 42.013Z" fill="#F7F0EB" />
<path d="M37.1311 49.3916L32.4023 54.0791L52.2842 73.7868L57.0003 68.5495V58.5942L52.0082 64.138L37.1311 49.3916Z" fill="#FFFBF5" />
</svg>
useEffect(() => {
if (selectedRecipients.length === 0) return;
let minRoutingOrder: number = Infinity;
let userEmailPresent = {
present: false,
routingOrder: 0,
recipientId: 0,
type: recipientType.CC
};
// Getting Minimum RoutingOrder
for (let i = 0; i < selectedRecipients.length; i++) {
const recipient = selectedRecipients[i]
if (recipient.routingOrder < minRoutingOrder) {
minRoutingOrder = recipient.routingOrder;
}
if (recipient.email === userDetails.email) {
userEmailPresent.present = true;
userEmailPresent.routingOrder = recipient.routingOrder;
userEmailPresent.recipientId = Number(recipient.id);
userEmailPresent.type = recipient.type;
}
}
if (userEmailPresent.present && userEmailPresent.routingOrder === minRoutingOrder && userEmailPresent.type !== recipientType.CC && userEmailPresent.type !== recipientType.INPERSONSIGNER) {
setSelfUserSign({ sign: true, recipientId: userEmailPresent.recipientId })
}
}, [selectedRecipients])
return (
<>
<div className='w-full'>
<div className="flex items-center space-x-4 bg-white w-full py-2 px-5 shadow">
<Link to={'/'} className="flex items-center space-x-2 text-gray-600 hover:text-[var(--primary)] transition-colors duration-200 group">
<IoMdHome
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
/>
</Link>
<div className="h-8 w-px bg-gray-300"></div>
<h1 className="text-md text-gray-800">🎉 Awesome! The document’s been sent for signature. 🎉</h1>
</div>
<div className='flex flex-col p-4 justify-center items-center w-full'>
<div className='mx-auto max-w-max'>
<div className='flex items-center gap-4 mb-3 ml-0 bg-slate-100 p-4 rounded-md'>
<div className="w-12 h-12 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center shadow-sm">
<FaCheck className="text-white text-2xl" />
</div>
<div>
<p className='text-2xl font-medium text-[var(--primary)]'>Your Document is send for signature</p>
<p className='text-sm text-slate-600'>All signers will be notified via email</p>
</div>
</div>
<div className='flex gap-4'>
<div className='pb-4 bg-slate-100'>
<div className='w-full overflow-auto h-[calc(100vh-200px)]'>
<div className='py-2 bg-white pl-4 pr-4 shadow-sm rounded-sm border-1 border-slate-200 sticky top-0 w-full z-1'>
<p className='text-sm font-medium'>QuickPreview</p>
</div>
{/* <NavTwo isPreview={true} aId={accountId} tId={transactionId} /> */}
<div className='pl-4 pr-4'>
<DocumentContainer isPreview={true} aId={accountId} tId={transactionId} />
</div>
</div>
</div>
{/* Right Side */}
<div className={`transition-all rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
{/* <div className="text-center pt-1 max-h-[25vh]">
<div className="w-20 h-20 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-3 mx-auto shadow-lg ">
<FaCheck className="text-white text-4xl" />
</div>
</div> */}
{/* <div className='text-center py-1'>
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
</div> */}
<div className='max-h-[30vh]'>
<SignersList signers={selectedRecipients} />
</div>
<div className='mt-4 flex flex-col gap-2'>
{selfUserSign.sign && <Link to={`/sign/${selfUserSign.recipientId}`} className={`p-2 text-center rounded-md bg-[var(--primary)] text-white hover:shadow-sm transition-all duration-150`}>Sign Now</Link>}
<Link to={"/"} className={`p-2 text-center rounded-md bg-slate-200 hover:shadow-sm transition-all duration-150`}>Home</Link>
</div>
{/* <div className='border border-b border-gray-50'></div> */}
{/* <div className='min-h-[30vh]'>
<SigningOrderDiagram signers={selectedRecipients} />
</div> */}
{/* <div className='border border-b border-gray-50'></div> */}
</div>
</div>
</div>
</div>
<div>
<p className="text-lg sm:text-4xl font-bold text-[var(--mainPrimaryBtn)]">
Your Document is sent for signature
</p>
<p className="text-lg text-slate-600 text-center font-medium">All signers will be notified via email</p>
</div>
</>
)
</div>
{/* Main two-column layout */}
<div className="flex flex-col lg:flex-row gap-4 mt-6 lg:justify-center lg:items-stretch">
{/* Left: Document Preview */}
<div className="flex-1 bg-slate-100 rounded-md overflow-hidden">
<div className="w-full h-[50vh] overflow-auto">
<div className="py-2 bg-white px-4 shadow-sm rounded-sm sticky top-0 z-10 border-1 border-gray-200">
<p className="text-sm font-medium">Quick Preview</p>
</div>
<div className="px-4">
<DocumentContainer isPreview={true} aId={accountId} tId={transactionId} />
</div>
</div>
</div>
{/* Right: Signers list + actions */}
<div
className={`flex-1 transition-all rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'
}`}
>
<div className="p-4 h-full bg-white rounded-md shadow-2xl border-1 border-gray-100">
<div className='overflow-auto'>
<SignersList signers={selectedRecipients} />
</div>
<div className="flex flex-col sm:flex-row gap-2 max-h-max relative top-auto bottom-0">
<Link
to="/"
className="flex-1 p-2 text-center rounded-md bg-slate-200 hover:shadow-sm transition-all duration-150"
>
Back to Home
</Link>
{selfUserSign.sign && (
<p
onClick={() => signSessionRedirect(accountId, transactionId, selfUserSign.recipientId + "")}
className="flex-1 p-2 text-center rounded-md bg-[var(--mainPrimaryBtn)] text-white hover:shadow-sm transition-all duration-150 cursor-pointer"
>
Sign Now
</p>
)}
</div>
</div>
{/* Optional: Signing Order */}
{/* <div className="mt-4">
<SigningOrderDiagram signers={selectedRecipients} />
</div> */}
</div>
</div>
</div>
</div>
{/* Footer */}
<footer className="text-center text-sm py-6 bg-white mt-[80px] ">
<p className="px-4 font-semibold">
Copyright 2025 Sign Made Easy. All Rights Reserved.{" "}
<a href="#" className="text-[var(--mainPrimaryBtn)] hover:underline">
Privacy Policy
</a>{" "}
|{" "}
<a href="#" className="text-[var(--mainPrimaryBtn)] hover:underline">
Reasonable Use Policy
</a>
</p>
</footer>
</div>
)
}
export default TransactionSentSuccess
export default TransactionSentSuccess

View File

@ -1,94 +1,206 @@
import { useState } from "react";
import { FiLock } from "react-icons/fi";
import api from "../../../config/api";
import { VscClose } from "react-icons/vsc";
import { FaCaretRight } from "react-icons/fa";
export default function AdvancedOptions({setShowAdvancedOptions}) {
const [autoReminders, setAutoReminders] = useState(false);
const [expireDays, setExpireDays] = useState(120);
const [alertDays, setAlertDays] = useState(0);
interface TransactionDetailsCreatedByInt {
userId: number,
firstName: string,
lastName: string,
name: string,
email: string
}
interface TransactionDetailsReminderConfigInt {
reminderEnabled: boolean,
reminderFrequency: number,
reminderDelay: number,
expireReminder: number
}
interface TransactionDetailsInt {
active: boolean;
allowReassign: true
allowViewAudit: true
createdAt: string
createdBy: TransactionDetailsCreatedByInt
currentRoutingOrder: 0
enableIdStamping: boolean
enableMobileSigning: boolean
enableSignatureIdStamping: boolean
expireAfter: number
expireEnabled: boolean
id: number
lastModifiedAt: string
lastModifiedBy: TransactionDetailsCreatedByInt
reminderConfig: TransactionDetailsReminderConfigInt
status: string;
}
interface AdvancedOptionsProps {
accountId: string;
setShowAdvancedOptions: React.Dispatch<React.SetStateAction<boolean>>;
transactionDetails?: TransactionDetailsInt;
}
const AdvancedOptions: React.FC<AdvancedOptionsProps> = ({ accountId, setShowAdvancedOptions, transactionDetails }) => {
if (!transactionDetails || !transactionDetails.id) return null;
const [transactionFormData, setTransactionFormData] = useState<TransactionDetailsInt>(transactionDetails);
const handleSaveBtnClick = async () => {
if(!transactionDetails) return
let changes : any = {}
for (const [key, val] of Object.entries(transactionDetails)) {
if(transactionFormData[key as keyof TransactionDetailsInt] === transactionDetails[key as keyof TransactionDetailsInt]) continue
changes[key as keyof TransactionDetailsInt] = val
}
debugger
console.log(changes)
await api.put(`/api/accounts/${accountId}/transactions/${transactionDetails.id}`, {
})
}
return (
<div className="w-full h-screen bg-white flex flex-col">
{/* Header */}
<div className="flex justify-between items-center border-b px-6 py-4">
<h2 className="text-xl font-semibold">Advanced Options</h2>
<button
onClick={() => setShowAdvancedOptions(false)}
className="text-gray-500 hover:text-black">✕</button>
<div className="flex justify-start gap-4 items-center border-b border-gray-200">
{/* <button
onClick={() => setShowAdvancedOptions(false)}
className="text-gray-500 hover:text-black border-r-1 border-gray-200 px-4 py-3">✕</button> */}
<button className="px-4 py-3 border-r-1 border-gray-300">
<VscClose
onClick={() => setShowAdvancedOptions(false)}
className='text-2xl font-thin cursor-pointer hover:text-red-500 transition self-start sm:self-auto'
/>
</button>
<h2 className="font-semibold text-black">Advanced Options</h2>
</div>
{/* Content */}
<div className="flex flex-1 overflow-hidden">
{/* Sidebar */}
<div className="w-60 border-r px-6 py-4 space-y-4 text-sm font-medium text-gray-700">
{/* <div className="w-60 border-r px-6 py-4 space-y-4 text-sm font-medium text-gray-700">
<div className="cursor-pointer hover:text-black">Recipient Privileges</div>
<div className="cursor-pointer hover:text-black">Reminders</div>
<div className="cursor-pointer hover:text-black">Expiration</div>
</div>
</div> */}
{/* Main Content */}
<div className="flex-1 px-8 py-6 overflow-y-auto space-y-10">
<div className="flex-1 px-6 sm:px-34 md:px-44 py-4 overflow-y-auto space-y-10">
{/* Recipient Privileges */}
<div>
<h3 className="text-lg font-semibold mb-3">Recipient Privileges</h3>
<p className="text-sm text-gray-600 mb-4">Give recipients options for how they sign.</p>
<h3 className="relative flex justify-start items-center text-lg font-semibold mb-3">
<FaCaretRight className="absolute text-2xl left-[-25px]" />
<span>
Recipient Privileges
</span>
</h3>
{/* <p className="text-sm text-gray-600 mb-2">Give recipients options for how they sign.</p> */}
<div className="flex flex-col gap-2">
{/* <label className="flex items-center gap-2 text-gray-700">
<input type="checkbox" checked readOnly className="accent-purple-600" />
Recipients can sign on paper
</label> */}
<label className="flex items-center gap-2 text-gray-400">
<input type="checkbox" disabled className="accent-purple-600" />
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={transactionFormData.allowReassign ? true : false}
onChange={(e) => setTransactionFormData({
...transactionFormData,
allowReassign: e.target.checked
})}
className="accent-[var(--mainPrimaryBtn)]" />
Recipients can change signing responsibility or assign a delegate
</label>
</div>
<FiLock className="text-gray-400 mt-2" />
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={transactionFormData.enableMobileSigning ? true : false}
onChange={(e) => setTransactionFormData({
...transactionFormData,
enableMobileSigning: e.target.checked
})}
className="accent-[var(--mainPrimaryBtn)]" />
Recipients can sign from mobile device
</label>
</div>
</div>
{/* Reminders */}
<div>
{false && <div>
<h3 className="text-lg font-semibold mb-3">Reminders</h3>
<p className="text-sm text-gray-600 mb-4">
Follow up with automatic reminders. Signers will receive emails until they sign or decline the envelope.
</p>
<div>
<div className="flex">
<button
onClick={() => setAutoReminders(!autoReminders)}
className={`relative w-14 h-7 flex items-center rounded-full p-1 transition-colors duration-200 ${autoReminders ? "bg-purple-600" : "bg-gray-300"
onClick={() => setTransactionFormData({
...transactionFormData,
reminderConfig: {
...transactionFormData.reminderConfig,
reminderEnabled: !transactionFormData.reminderConfig.reminderEnabled
}
})}
className={`relative w-9 h-5 flex items-center rounded-full p-1 transition-colors duration-200 ${transactionFormData?.reminderConfig.reminderEnabled ? "bg-[var(--mainPrimaryBtn)]" : "bg-gray-300"
}`}
>
<span
className={`bg-white w-5 h-5 rounded-full shadow-md transform transition-transform duration-200 ${autoReminders ? "translate-x-7" : "translate-x-0"
className={`bg-white w-3.5 h-3.5 rounded-full shadow-md transform transition-transform duration-200 ${transactionFormData?.reminderConfig.reminderEnabled ? "translate-x-3.5" : "translate-x-0"
}`}
/>
</button>
<span className="ml-3 text-sm">Turn on auto reminders</span>
</div>
</div>
</div>}
{/* Expiration */}
<div>
<h3 className="text-lg font-semibold mb-3">Expiration</h3>
<h3 className="relative flex justify-start items-center text-lg font-semibold mb-3">
<FaCaretRight className="absolute text-2xl left-[-25px]" />
<span>
Expiration
</span>
</h3>
<p className="text-sm text-gray-600 mb-4">
By default, envelopes expire after 120 days. Recipients can no longer view or sign an envelope after it expires.
</p>
<div className="flex items-center gap-4 mb-3">
<select className="border rounded px-3 py-2 text-sm">
<option>Custom days</option>
</select>
<input
type="number"
value={expireDays}
onChange={(e) => setExpireDays(Number(e.target.value))}
className="border rounded px-3 py-2 text-sm w-20"
value={transactionFormData.expireAfter}
onChange={(e) => {
if (Number(e.target.value) >= 999) return;
if (Number(e.target.value) == 0) e.target.value = "1";
setTransactionFormData({
...transactionFormData,
expireAfter: Number(e.target.value)
})
}}
className="border border-slate-300 rounded px-3 py-2 text-sm w-20 outline-0"
/>
</div>
<p className="text-sm text-gray-600 mb-6">
Envelopes will expire on: <span className="font-medium">1/3/2026</span>
Envelopes will expire on: <span className="font-medium">
{new Date(new Date(transactionDetails.createdAt).getTime() + (transactionFormData.expireAfter * 24 * 60 * 60 * 1000)).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
</p>
<div className="flex items-center gap-4">
{/* <div className="flex items-center gap-4">
<select className="border rounded px-3 py-2 text-sm">
<option>Custom days</option>
</select>
@ -98,17 +210,21 @@ export default function AdvancedOptions({setShowAdvancedOptions}) {
onChange={(e) => setAlertDays(Number(e.target.value))}
className="border rounded px-3 py-2 text-sm w-20"
/>
</div>
</div> */}
</div>
</div>
</div>
{/* Footer */}
<div className="border-t p-4 flex justify-end">
<button className="bg-purple-600 text-white px-6 py-2 rounded hover:bg-purple-700 transition">
<button
onClick={handleSaveBtnClick}
className="bg-[var(--mainPrimaryBtn)] text-white px-6 py-2 rounded hover:bg-purple-700 transition">
Save
</button>
</div>
</div>
);
}
export default AdvancedOptions;

View File

@ -1,24 +1,24 @@
import React, { useEffect, useState } from 'react'
import { FaFileAlt, FaCheckCircle, FaUpload } from 'react-icons/fa'
import { MdFileUpload } from 'react-icons/md'
import { FaFileAlt } from 'react-icons/fa'
import { RiDraggable } from 'react-icons/ri'
import { deleteDocument, setDocProgComplete } from '../../../store/slices/documentSlice'
import { useDispatch } from 'react-redux'
import { AppDispatch } from '../../../store/store'
import { IoMdClose } from 'react-icons/io'
import { VscClose } from "react-icons/vsc";
import api from '../../../config/api'
import { useParams } from 'react-router-dom'
import axios from 'axios'
import { TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'
import { BiSolidFilePdf } from 'react-icons/bi'
interface DocumentCardProps {
id: string;
name: string;
isCompleted: boolean;
transactionTemplateToggle:boolean;
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE;
}
const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted , transactionTemplateToggle }) => {
const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted, transactionTemplateToggle }) => {
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
const accountId = params.accountId;
const transactionId = params.transactionId || params.templateId;
@ -32,14 +32,15 @@ const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted , tra
const [documentDel, setDocumentDel] = useState<{ docId: string | null, isDel: boolean }>({ docId: null, isDel: false })
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
let isCompleted = false;
if(isCompleted) return;
isCompleted = false;
let updateTimeout: ReturnType<typeof setTimeout> | null = null;
let lastUpdate = 0;
const delay = 1000;
const docProgSSE = new EventSource(`http://192.168.2.176:8083/api/progress/${id}`);
const docProgSSE = new EventSource(`http://10.10.1.99:8083/api/progress/${id}`);
docProgSSE.addEventListener('progress', (event) => {
const jsonData = JSON.parse(event.data);
@ -110,21 +111,21 @@ const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted , tra
<div
key={id}
className="relative w-[160px] h-[170px] bg-white rounded-xl shadow-sm hover:shadow-md transition p-4 flex flex-col items-center justify-evenly"
className="relative min-w-[260px] max-w-max bg-gray-100 py-0.5 rounded-md hover:shadow-sm transition flex justify-start items-center"
draggable
>
<IoMdClose
<VscClose
onClick={() => setDocumentDel({ docId: id, isDel: false })}
className='absolute right-2.5 top-2.5 text-slate-400 text-xl hover:text-red-500 cursor-pointer transition-all' />
className='absolute right-2 top-2.5 text-lg hover:text-red-500 cursor-pointer transition-all' />
{/* Draggable handle */}
<RiDraggable
draggable
onDragStart={(e) => e.dataTransfer.setData('text/plain', id)}
className="absolute top-2 left-2 text-gray-400 text-lg cursor-grab drag-handle"
className="mx-2 text-gray-400 text-lg cursor-grab drag-handle"
/>
{/* Main icon or circular loader */}
<div className="flex flex-col items-center mt-2 mb-1">
<div className="flex items-center mt-2 mb-1">
{!isCompleted ? (
<div className="relative w-16 h-16">
<svg width="64" height="64" className="rotate-[-90deg]">
@ -154,24 +155,24 @@ const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted , tra
</div>
</div>
) : (
<FaFileAlt className="text-[var(--primary)] text-5xl mb-1" />
<BiSolidFilePdf className="text-red-600 text-xl mb-1" />
)}
</div>
{/* Document name */}
<span className="text-center text-sm text-gray-800 font-medium truncate max-w-[14ch] leading-snug">
<span className={`text-center text-xs ml-2 text-gray-800 font-medium truncate ${isCompleted ? "max-w-[25ch]" : "max-w-[18ch]"} leading-snug`}>
{name}
</span>
{/* Status message */}
<span className="text-xs text-gray-500 mt-1">{message}</span>
{/* <span className="text-xs text-gray-500 mt-1">{message}</span> */}
</div>
{
documentDel.docId !== null && <div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-80">
<div className="bg-white rounded-lg p-6 w-80">
<h3 className="text-lg font-medium text-gray-800 mb-4">Confirm Delete</h3>
<p className="text-gray-600 mb-6">Are you sure you want to delete?</p>
<div className="flex justify-end gap-3">
@ -193,9 +194,6 @@ const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted , tra
}
</>
)
}

View File

@ -0,0 +1,138 @@
import { useEffect, useState } from "react";
import { IoMdClose } from "react-icons/io";
import { UserState } from "../../../store/slices/userSlice";
import { TRANSACTION_TEMPLATE_TOGGLE } from "../../../store/slices/selectedRecipientsSlice";
import api from "../../../config/api";
interface MessageConIntProps {
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE;
accountId: string;
transactionId: string;
messageModal: boolean;
setMessageModal: React.Dispatch<boolean>;
}
const MessageCon: React.FC<MessageConIntProps> = ({ transactionTemplateToggle, accountId, transactionId, messageModal, setMessageModal }) => {
interface EmailDtls {
subject: string;
message: string;
}
const [emailDtls, setEmailDtls] = useState<EmailDtls>({
subject: "Documents for Signature",
message: "",
});
const [templateName, setTemplateName] = useState('Untitled');
const fetchEmailDetails = async () => {
try {
const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`);
if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
setTemplateName(res.data.name)
}
setEmailDtls(
{ subject: res.data.subject ? res.data.subject : "Documents for Signature", message: res.data.message ? res.data.message : "" }
)
} catch (e) {
}
}
const handleMessageSaveClick = async () => {
await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, {
name: templateName,
subject: emailDtls.subject, message: emailDtls.message,
status: "CREATED"
})
}
useEffect(() => {
fetchEmailDetails()
}, [messageModal])
return (
<section className="w-full">
{/* Header */}
<div className="flex justify-between items-center mb-6">
<h2 className="text-lg font-semibold text-gray-800">Message</h2>
<IoMdClose
onClick={() => {
// handleRecipientAdd();
setMessageModal(false)
}}
className="text-2xl text-gray-400 hover:text-gray-600 cursor-pointer transition"
/>
</div>
{/* Subject */}
<div className="flex flex-col mb-6">
<label
htmlFor="subject"
className="text-sm font-medium text-gray-700 mb-2"
>
Subject
</label>
<input
id="subject"
type="text"
required
value={emailDtls.subject}
onChange={(e) =>
setEmailDtls({ ...emailDtls, subject: e.target.value })
}
placeholder="Enter email subject"
className="w-full lg:w-4xl px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-[var(--primary)] text-base"
/>
</div>
{/* Message */}
<div className="flex flex-col">
<label
htmlFor="message"
className="text-sm font-medium text-gray-700 mb-2"
>
Message
</label>
{/* <div>
<p>Add Placeholder</p>
<div>
<span>Sender Name</span>
<span>Recipient Name</span>
<span>Recipient Email</span>
</div>
</div> */}
<textarea
id="message"
rows={6}
value={emailDtls.message}
onChange={(e) =>
setEmailDtls({ ...emailDtls, message: e.target.value })
}
placeholder="Write your message..."
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-[var(--primary)] text-base resize-none"
/>
</div>
<div className="flex flex-row-reverse gap-2 mt-4">
<button
onClick={() => {
setMessageModal(false)
handleMessageSaveClick()
}}
className="bg-[var(--primary)] px-3 py-1 rounded-sm text-white opacity-80 hover:bg-[var(--primary)] hover:opacity-100 transition-all duration-200 cursor-pointer">Save & Close</button>
<button
onClick={() => setMessageModal(false)}
className="bg-slate-400 px-3 py-1 rounded-sm text-white hover:bg-slate-500 transition-all duration-200 cursor-pointer">Close</button>
</div>
</section>
);
};
export default MessageCon;

View File

@ -8,38 +8,51 @@ import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../store/store";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { setImages } from "../../../store/slices/selectedDocsSlice";
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, removeAllRecipients, removeRecipientAsync, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
import { addRecipientAsync, fetchRecipients, recipientType, removeAllRecipients, removeRecipientAsync, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from "../../../store/slices/selectedRecipientsSlice";
import { TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
import { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
import Loader from "../../dashboard/Loader";
import { Bounce, toast } from "react-toastify";
import { fetchDocuments, resetDocuments, setDocuments, uploadDocuments } from "../../../store/slices/documentSlice";
import { fetchDocuments, resetDocuments, setDocuments } from "../../../store/slices/documentSlice";
import { SignTypeEnum } from "../../../utlity/types";
import { RiPencilFill } from "react-icons/ri";
import SelfSignWarnModal from "./SelfSignWarnModal";
import AdvancedOptions from "./AdvancedOptions";
import getMinRoutingOrder from "../../../utlity/commonUtils";
import MessageCon from "./MessageCon";
import RecipientsCon from "./RecipientsCon";
import { VscClose } from "react-icons/vsc";
import signSessionRedirect from "../../../utlity/signSessionRedirect";
interface EmailDtls {
subject: string;
message: string;
}
export interface FormSubmitInt {
documents: boolean;
recipients: boolean;
subject: boolean;
invalidRecipientEmail: boolean;
invalidRecipientLastName: boolean;
duplicateEmailSameRoutingOrder: boolean;
email: string;
showError: boolean;
allValid: boolean;
}
const ProcessStepOne = () => {
interface EmailDtls {
subject: string;
message: string;
}
interface FormSubmitInt {
documents: boolean,
recipients: boolean,
subject: boolean,
recipientEmailAndLastName: boolean,
duplicateEmailSameRoutingOrder: boolean
}
const FormSubmitIntitialState = {
documents: false,
recipients: false,
subject: false,
recipientEmailAndLastName: false,
duplicateEmailSameRoutingOrder: false
invalidRecipientEmail: false,
invalidRecipientLastName: false,
duplicateEmailSameRoutingOrder: false,
email: "",
showError: false,
allValid: false
}
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
@ -52,7 +65,7 @@ const ProcessStepOne = () => {
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const [searchParams] = useSearchParams();
const isTamplate = searchParams.get('useTemplate')
const isTemplate = searchParams.get('useTemplate')
const navigate = useNavigate();
const dispatch = useDispatch();
@ -128,22 +141,15 @@ const ProcessStepOne = () => {
if (submitError.recipients) setSubmitError(prev => ({ ...prev, recipients: false }))
}, [selectedRecipients])
const getMinRoutingOrder = () => {
return selectedRecipients.reduce((min, recipient) => {
return Math.min(min, recipient.routingOrder ?? Infinity);
}, Infinity);
};
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const handleNextClick = async (saveAndClose: boolean = false) => {
const validationCheck = () => {
let formError = { ...FormSubmitIntitialState }
const recipientsData: { [key: string | number]: string[] } = {}
for (const recipient of selectedRecipients) {
if (recipientsData[recipient.routingOrder] && recipientsData[recipient.routingOrder].includes(recipient.email)) {
formError.duplicateEmailSameRoutingOrder = true;
formError.email = recipient.email
break;
}
@ -162,26 +168,53 @@ const ProcessStepOne = () => {
formError.recipients = true;
} else {
// Check if all recipients have email and lastName
const isValid = selectedRecipients.every(
(item) => item.email?.trim() !== '' && item.lastName?.trim() !== ''
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const invalidRecipients = selectedRecipients.filter(
(item) =>
!item.email?.trim() ||
!emailRegex.test(item.email.trim())
);
if (!isValid) {
const invalidRecipientslastName = selectedRecipients.filter(
(item) =>
!item.lastName?.trim()
);
if (invalidRecipientslastName.length > 0) {
// alert('All recipients must have both Email and Last Name filled.');
formError.recipientEmailAndLastName = true;
formError.invalidRecipientLastName = true;
}
if (invalidRecipients.length > 0) {
// alert('All recipients must have both Email and Last Name filled.');
formError.invalidRecipientEmail = true;
formError.email = invalidRecipients[0].email
}
}
if (!emailDtls.subject.length) {
formError.subject = true
}
if (SignTypeEnum.selfSign === signType) formError.recipients = false;
if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName || formError.duplicateEmailSameRoutingOrder) {
setSubmitError(formError)
return;
if (formError.documents || formError.recipients || formError.subject || formError.invalidRecipientLastName || formError.invalidRecipientEmail || formError.duplicateEmailSameRoutingOrder) {
setSubmitError({ ...formError, allValid: false })
return false
}
setSubmitError({ ...formError, allValid: true })
return true
}
}
const handleNextClick = async (saveAndClose: boolean = false) => {
if (!validationCheck() && transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
setSubmitError((prev) => ({ ...prev, showError: true }))
return
}
try {
@ -195,7 +228,7 @@ const ProcessStepOne = () => {
await api.patch(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents`, { "documents": documentsData });
await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents?include_images=true`).then(res => {
await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents?include_pages=true`).then(res => {
if (res.status === 200) {
dispatch(setDocuments(res.data));
}
@ -203,9 +236,16 @@ const ProcessStepOne = () => {
console.error(err)
});
dispatch(showLoader("Processing Message..."));
await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, {
name: templateName,
subject: emailDtls.subject, message: emailDtls.message,
status: "CREATED"
})
dispatch(showLoader("Processing Recipients..."));
let minRoutingOrder = getMinRoutingOrder();
let minRoutingOrder = getMinRoutingOrder({ recipients: selectedRecipients });
const recipientsData = selectedRecipients.map((recipient, index) => ({
id: "id" in recipient ? recipient.id : null,
@ -283,16 +323,7 @@ const ProcessStepOne = () => {
if (!accountId || !transactionId) return;
dispatch(addRecipientAsync({ recipientsData: recipientsDataTemp, accountId, transactionId }));
}
dispatch(showLoader("Processing Message..."));
let res = await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, {
name: templateName,
subject: emailDtls.subject, message: emailDtls.message,
status: "CREATED"
})
dispatch(resetDocuments())
dispatch(resetRecipients())
if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
dispatch(hideLoader());
if (!saveAndClose) {
@ -306,8 +337,8 @@ const ProcessStepOne = () => {
(async () => {
try {
await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" })
if(accountId && transactionId) signSessionRedirect(accountId, transactionId, recipientId);
dispatch(hideLoader());
navigate(`/sign/${recipientId}/self_sign`);
} catch (err) {
console.log("Error Transaction Sent", err.message);
}
@ -339,11 +370,14 @@ const ProcessStepOne = () => {
navigate('/')
}
useEffect(() => {
validationCheck()
}, [selectedDocuments, selectedRecipients, emailDtls])
const handleSelfSignCb = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.checked) {
navigate("self_sign");
setShowSelfSignWarnModal((prev) => !prev)
if (selectedRecipients.length) setShowSelfSignWarnModal((prev) => !prev)
} else {
navigate("./../");
}
@ -352,9 +386,12 @@ const ProcessStepOne = () => {
const globalLoader = useSelector((state: RootState) => state.loader)
if (showAdvancedOptions) {
if (showAdvancedOptions && accountId) {
return (
<AdvancedOptions setShowAdvancedOptions={setShowAdvancedOptions} />
<AdvancedOptions
accountId={accountId}
transactionDetails={transactionDetails}
setShowAdvancedOptions={setShowAdvancedOptions} />
)
}
@ -370,25 +407,27 @@ const ProcessStepOne = () => {
{/* Left Section */}
<div className='flex sm:flex-row items-start sm:items-center gap-2 sm:gap-4 w-full sm:w-auto'>
<IoMdClose
<VscClose
onClick={handleClose}
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition self-start sm:self-auto'
className='text-2xl font-thin cursor-pointer hover:text-red-500 transition self-start sm:self-auto'
/>
<p className="text-base font-normal text-gray-700 border-l pl-4 relative w-full sm:w-auto">
<div className="hidden md:block absolute left-[55px] w-[1px] h-[62px] bg-gray-300"></div>
<p className="text-base font-normal text-gray-700 relative w-full sm:w-auto">
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? (
<>
<input
value={templateName}
onChange={handleChange}
className="w-full sm:w-[250px] px-4 py-1.5 border-2 border-dashed rounded-md text-sm focus:outline-none transition border-gray-300 focus:ring-2 focus:border-dashed focus:ring-[var(--primary)]"
className="w-full ml-4 sm:w-[250px] px-4 py-1.5 border-2 border-dashed rounded-md text-sm focus:outline-none transition border-gray-300 focus:ring-2 focus:border-dashed focus:ring-[var(--primary)]"
/>
{!isTyping && (
<RiPencilFill className="absolute top-2.5 right-2 text-[17px]" />
)}
</>
) : (
"Add Recipient and Message"
<span className="font-semibold text-black ml-5">Add Recipient and Message</span>
)}
</p>
</div>
@ -398,27 +437,25 @@ const ProcessStepOne = () => {
id="add-tags-btn-step-one"
className='flex sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3 w-full sm:w-auto '>
<button
onClick={() => setShowAdvancedOptions(true)}
className="text-sm w-full sm:w-auto px-4 py-2 rounded text-slate-800 border shadow hover:bg-[var(--primary)] hover:text-white transition duration-300 cursor-pointer"
>
Advanced Options
</button>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<button
onClick={() => handleNextClick(true)}
className="text-sm w-full sm:w-auto px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
className="text-sm w-full sm:w-auto px-4 py-2 rounded bg-[var(--primary)] text-white shadow transition duration-300 cursor-pointer"
>
Save & Close
</button>
)}
<button
onClick={() => setShowAdvancedOptions(true)}
className="text-sm w-full sm:w-auto px-4 py-2 rounded text-slate-800 border shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
>
Advanced Options
</button>
<button
onClick={() => handleNextClick(false)}
className="text-sm w-full sm:w-auto px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
className={`text-sm w-full sm:w-auto px-4 py-2 rounded bg-[var(--primary)] text-white shadow transition duration-300 cursor-pointer ${transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES || submitError.allValid || (signType === SignTypeEnum.selfSign && selectedDocuments.length) ? "opacity-100" : "opacity-60"}`}
>
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"}
</button>
@ -428,22 +465,22 @@ const ProcessStepOne = () => {
{/* Main Content - Scrollable */}
<div className="h-screen pt-[80px] overflow-y-auto no-scrollbar">
<div className="h-screen pt-[95px] overflow-y-auto no-scrollbar">
<div className="max-w-4xl rounded-xl shadow-md mx-auto px-6 pb-10 bg-[var(--documentUploadBg)] mb-5">
{/* Documents */}
<section>
<h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2>
<div id="documents_con" className={`${submitError.documents ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
<div id="documents_con" className={`${submitError && submitError.documents && submitError.showError ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} />
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.documents && "At least one document is required."}</p>
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError && submitError.documents && submitError.showError && "At least one document is required."}</p>
</div>
</section>
{/* Recipients */}
<section className="mt-8">
<p className="border-t border-gray-200"></p>
{!isTamplate &&
{!isTemplate &&
transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES &&
<div className="flex justify-between items-center">
<h2 className=" text-lg font-medium mb-4 pt-6 text-[var(--processStepOneTextHead)]">Signers</h2>
@ -451,41 +488,27 @@ const ProcessStepOne = () => {
<input
checked={signType === SignTypeEnum.selfSign}
onChange={(e) => handleSelfSignCb(e)}
id="default-checkbox" type="checkbox" value="" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm outline-none"
id="default-checkbox" type="checkbox" value="" className="w-4 h-4 checked:bg-[var(--mainPrimaryBtn)] bg-gray-100 border-gray-300 rounded-sm outline-none"
/>
<label htmlFor="default-checkbox" className="ms-2 text-sm flex gap-1 text-slate-600">
<span>SELF </span><span>SIGN</span></label>
I AM THE ONLY SIGNER</label>
</div>
</div>
}
{
(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) &&
<div className="flex flex-col gap-4 align-start">
<div id="recipient_cards_con" className={`${submitError.duplicateEmailSameRoutingOrder ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
<RecipientCards
userEmail={userData.email}
getMinRoutingOrder={getMinRoutingOrder}
transactionTemplateToggle={transactionTemplateToggle}
setEditRecipient={setEditRecipient}
setEditCase={setEditCase}
editRecipient={editRecipient}
setEditRecipientCurrentEmail={setEditRecipientCurrentEmail}
editRecipientCurrentEmail={editRecipientCurrentEmail}
/>
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.duplicateEmailSameRoutingOrder && "Each Signing order must have unique emails."}</p>
</div>
<div
ref={topRef}
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none' : ''} ${submitError.recipients ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}
>
<RecipientAddCard userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.recipients && "Each Signing order must have unique emails."}</p>
</div>
</div>
<>
<RecipientsCon
transactionTemplateToggle={transactionTemplateToggle}
accountId={accountId}
transactionId={transactionId}
userData={userData}
// setEditRecipientsModal={setEditRecipientsModal}
submitError={submitError}
isModal={false}
/>
</>
}
</section>
@ -494,34 +517,40 @@ const ProcessStepOne = () => {
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2>
{/* Subject */}
<div className={`${submitError.subject ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""} flex flex-col mb-4`}>
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
<input
id="subject"
type="text"
required
value={emailDtls.subject}
onChange={(e) => {
setEmailDtls({ ...emailDtls, subject: e.target.value })
submitError.subject && setSubmitError(prev => ({ ...prev, subject: false }))
}}
placeholder="Email Subject"
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.subject && "Enter a subject."}</p>
<div className="border-1 rounded-xl border-gray-300 px-6 py-8">
<div className={`${submitError.subject && submitError.showError ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""} flex flex-col mb-4`}>
<label className='text-sm font-medium mb-1'>Subject</label>
<input
id="subject"
type="text"
required
value={emailDtls.subject}
onChange={(e) => {
setEmailDtls({ ...emailDtls, subject: e.target.value })
submitError.subject && setSubmitError(prev => ({ ...prev, subject: false }))
}}
placeholder="Email Subject"
className="w-full px-3 py-2 rounded-lg border border-gray-200 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
{submitError.subject && submitError.showError && <p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.subject && "Enter a subject."}</p>}
</div>
{/* Message */}
<div className='flex flex-col'>
<label className='text-sm font-medium mb-1'>Message for signers</label>
<textarea
rows={4}
value={emailDtls.message}
onChange={(e) => setEmailDtls({ ...emailDtls, message: e.target.value })}
placeholder="Message"
className="w-full px-3 py-2 rounded-lg border border-gray-200 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
</div>
</div>
{/* Message */}
<div className='flex flex-col'>
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Message</label>
<textarea
rows={4}
value={emailDtls.message}
onChange={(e) => setEmailDtls({ ...emailDtls, message: e.target.value })}
placeholder="Message"
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
</div>
</section>
</div>

View File

@ -25,21 +25,20 @@ export interface ContactTypeObj {
setEditRecipientCurrentEmail: any
}
const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase, setEditRecipient, setEditCase, topRef, setEditRecipientCurrentEmail, showCard = false, recipientData = null }) => {
const [activeAccountId, setActiveAccountId] = useState<string | null>(
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] });
const [openContactModal, setOpenContactModal] = useState(false);
const dispatch = useDispatch<AppDispatch>();
const formRef = useRef<HTMLFormElement | null>(null);
const [recipient, setRecipients] = useState<Recipient>(recipientData !== null ? {...recipientData} : { uuid: null, newOne: false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: true, allowSenderToDelete: true } );
const [recipient, setRecipients] = useState<Recipient>(recipientData !== null ? { ...recipientData } : { uuid: null, newOne: false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: true, allowSenderToDelete: true });
const [showAdvanceSettings, setShowAdvanceSettings] = useState(false)
@ -216,7 +215,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
updatedRecipient.hostEmail = "";
}
if (editCase === "Edit") {
dispatch(updateRecipient(updatedRecipient));
} else {
@ -295,7 +294,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
useDebouncedEffect(() => {
if (!activeField || !manualFocusTriggered) return;
const inputValue = recipient[activeField].trim().toLowerCase();
console.log(inputValue, "Line 297", activeField)
@ -324,7 +323,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
if (lastQuery !== inputValue) {
if(lastQuery !== null && inputValue.startsWith(lastQuery)) return
if (lastQuery !== null && inputValue.startsWith(lastQuery)) return
setLastQuery(inputValue);
api
@ -363,13 +362,13 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
return (
return (
<div ref={containerRef}>
<form
ref={formRef}
onSubmit={handleRecipientAdd}
id="add-recipient-step-one"
className="w-full bg-white border border-gray-200 rounded-lg shadow-sm px-3 sm:px-4 lg:px-6 py-3 sm:py-4 space-y-3 sm:space-y-4 relative"
className="w-full bg-white border border-gray-300 rounded-lg px-3 sm:px-4 lg:px-6 py-3 sm:py-4 space-y-3 sm:space-y-4 relative"
>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="space-y-2 sm:space-y-3">
@ -482,7 +481,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
value={recipient.email}
placeholder="Recipient Email"
className={`w-full px-3 sm:px-4 py-2 border rounded-md text-sm focus:outline-none transition
${errors.email || errors.emailFormat ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
${errors.email || errors.emailFormat ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => {
setRecipients({ ...recipient, email: e.target.value });
setErrors((prev) => ({ ...prev, email: false }));
@ -555,7 +554,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
name="recipient-type"
value={option.value}
checked={recipient.type === option.value}
onClick={(e) => setRecipients({ ...recipient, type: e.target.value })}
onChange={(e) => setRecipients({ ...recipient, type: e.target.value })}
className="accent-[var(--primary)] cursor-pointer w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">{option.label}</span>
@ -567,11 +566,11 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
{!showCard && (
<button
type="submit"
className="inline-flex items-center justify-center gap-2 bg-[var(--primary)] hover:bg-[var(--primaryLight2)] text-white text-xs sm:text-sm font-semibold px-4 sm:px-5 py-2 rounded-md transition w-full sm:w-auto min-w-fit"
className="inline-flex items-center justify-center gap-2 bg-[var(--mainPrimaryBtn)] text-white text-sm font-semibold px-8 py-2 rounded-md transition w-full sm:w-auto min-w-fit cursor-pointer"
>
<FaPlus size={12} className="sm:w-3.5 sm:h-3.5" />
{/* <FaPlus size={12} className="sm:w-3.5 sm:h-3.5" /> */}
<span className="whitespace-nowrap">
{editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
{editCase === "Edit" ? "Edit" : "Add"}
</span>
</button>
)}

View File

@ -6,6 +6,9 @@ import {
TRANSACTION_TEMPLATE_TOGGLE,
updateRecipient,
recipientType,
setSelectedRecipients,
reorderAllRecipientAscOrder,
updateRecipientByIndxAndRo,
} from "../../../store/slices/selectedRecipientsSlice";
import { AppDispatch, RootState } from "../../../store/store";
import { ImBin } from "react-icons/im";
@ -13,11 +16,24 @@ import { useState } from "react";
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
import { IoCloseOutline } from "react-icons/io5";
import { useParams } from "react-router-dom";
import { FiEdit } from "react-icons/fi";
import { FaUsersLine } from "react-icons/fa6";
import { IoMdClose } from "react-icons/io";
import { BiSolidHelpCircle } from "react-icons/bi";
import {
DndContext,
closestCenter,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import SortableRecipient from "./SortableRecipient";
const getInitials = (name: string) => {
if (!name) return "";
@ -30,7 +46,7 @@ const getInitials = (name: string) => {
type CardsData = {
userEmail: string;
getMinRoutingOrder: () => number;
// getMinRoutingOrder: () => number;
editRecipient: any;
setEditRecipient: (recipient: any) => void;
setEditCase: (string: any) => void;
@ -218,8 +234,127 @@ const RecipientCards = ({
setEditingField(null);
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
console.log(over, "Over")
console.log(active, "Active")
const activeIndex = recipients.findIndex((r) => (r.id || r.uuid) === active.id);
const overIndex = recipients.findIndex((r) => (r.id || r.uuid) === over.id);
const activeRec = recipients[activeIndex]
const overRec = recipients[overIndex]
const activeRecRo = Number(activeRec.routingOrder) === 0 || !activeRec.routingOrder ? 1 : activeRec.routingOrder
const overRecRo = Number(overRec.routingOrder) === 0 || !overRec.routingOrder ? 1 : overRec.routingOrder
let newActiveRecRo = 1
let newOverRecRo = 1
// if (activeRec.routingOrder >= overRecRo && activeIndex > overIndex) {
// newActiveRecRo = activeRecRo > 1 ? activeRecRo - 1 : 1
// newOverRecRo = activeRecRo > 1 ? activeRecRo + 1 : 2
// }
// if (activeRec.routingOrder >= overRecRo && activeIndex < overIndex) {
// newActiveRecRo = overRecRo > 1 ? overRecRo + 1 : 2
// newOverRecRo = overRecRo > 1 ? overRecRo - 1 : 1
// }
// if (activeRec.routingOrder <= overRecRo && activeIndex > overIndex) {
// newActiveRecRo = activeRecRo > 1 ? activeRecRo - 1 : 1
// newOverRecRo = activeRecRo > 1 ? activeRecRo + 1 : 2
// }
// if (activeRec.routingOrder <= overRecRo && activeIndex < overIndex) {
// newActiveRecRo = overRecRo > 1 ? overRecRo + 1 : 2
// newOverRecRo = overRecRo > 1 ? overRecRo - 1 : 1
// }
// if (activeRec.routingOrder > overRecRo && activeIndex < overIndex) {
// console.log(activeIndex, overIndex, "283 2")
// newActiveRecRo = overRecRo > 1 ? overRecRo : 2
// newOverRecRo = activeRecRo > 1 ? activeRecRo : 1
// }
// if (activeRec.routingOrder < overRecRo && activeIndex > overIndex) {
// console.log(activeIndex, overIndex, "283 3")
// newActiveRecRo = overRecRo > 1 ? overRecRo : 1
// newOverRecRo = activeRecRo > 1 ? activeRecRo : 2
// }
if (activeRecRo > overRecRo && activeIndex > overIndex) {
console.log(activeIndex, overIndex, "283 1")
newActiveRecRo = activeRecRo > 1 ? overRecRo : 1
newOverRecRo = overRecRo > 1 ? activeRecRo : 2
}
if (activeRecRo > overRecRo && activeIndex < overIndex) {
console.log(activeIndex, overIndex, "283 kam")
newActiveRecRo = activeRecRo > 1 ? activeRecRo : 2
newOverRecRo = overRecRo > 1 ? overRecRo : 1
}
if (activeRecRo < overRecRo && activeIndex < overIndex) {
console.log(activeIndex, overIndex, "283 4")
newActiveRecRo = overRecRo > 1 ? overRecRo : 2
newOverRecRo = activeRecRo > 1 ? activeRecRo : 1
}
if (activeRecRo < overRecRo && activeIndex > overIndex) {
console.log(activeIndex, overIndex, "283 mak")
newActiveRecRo = activeRecRo > 1 ? activeRecRo : 1
newOverRecRo = overRecRo > 1 ? overRecRo : 1
}
if (activeRecRo === activeRecRo && activeIndex < overIndex) {
newActiveRecRo = activeRecRo > 1 ? activeRecRo + 1 : 2
newOverRecRo = overRecRo > 1 ? overRecRo : 1
}
if (activeRecRo === overRecRo && activeIndex > overIndex) {
newActiveRecRo = activeRecRo > 1 ? activeRecRo : 1
newOverRecRo = overRecRo > 1 ? overRecRo + 1 : 2
}
// let reordered = recipients.map((r, idx) => {
// if (idx === activeIndex) {
// return (
// {
// ...r, routingOrder: newActiveRecRo
// }
// )
// }
// if (idx === overRecRo) {
// return (
// {
// ...r, routingOrder: newOverRecRo
// }
// )
// }
// });
// dispatch(updateRecipientOrder({ id: activeRec.id || activeRec.uuid, routingOrder: newActiveRecRo }));
// dispatch(updateRecipientOrder({ id: overRec.id || overRec.uuid, routingOrder: newOverRecRo }));
dispatch(updateRecipientByIndxAndRo({ indx1: activeIndex, ro1: newActiveRecRo, indx2: overIndex, ro2: newOverRecRo, }));
// dispatch(reorderAllRecipientAscOrder());
};
return (
<div className="w-full bg-white rounded-md shadow-sm border border-gray-200">
<div className="w-full rounded-md bg-slate-50">
{recipients.length > 0 ? (
<>
{recipients.length > 1 && (
@ -237,227 +372,247 @@ const RecipientCards = ({
</div>
)}
<div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
{recipients.map((recipient, idx) => (
<div
key={recipient.email + idx}
className={`relative flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 transition-colors gap-3 lg:gap-0 ${idx % 2 === 0 ? 'bg-gray-50' : 'bg-white'} ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail
? "bg-neutral-100 opacity-40"
: ""
}`}
>
{/* Main Content Section */}
<div className="flex items-start sm:items-center gap-2 sm:gap-3 min-w-0 flex-1">
{/* Order Number */}
{recipients.length > 1 ? (
<div className="relative flex-shrink-0">
<input
type="number"
value={recipient.routingOrder}
// min={1}
onChange={(e) =>
handleOrderChange(
recipient.id || recipient.uuid,
Number(e.target.value)
)
}
className="w-8 sm:w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400"
/>
{idx === 0 && recipients.length > 1 && <p className="group">
<BiSolidHelpCircle className="absolute top-[-12px] right-[-10px] text-gray-400 cursor-pointer" />
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext
items={recipients.map((r) => r.id || r.uuid)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
{recipients.map((recipient, idx) => (
<SortableRecipient
key={recipient.id || recipient.uuid}
recipient={recipient}
idx={idx}
editRecipientCurrentEmail={editRecipientCurrentEmail}
>
<div
key={recipient.email + idx}
className={`relative flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 transition-colors gap-3 lg:gap-0 ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail
? "bg-neutral-100 opacity-40"
: ""
}`}
>
{/* Main Content Section */}
<div className="flex items-start sm:items-center gap-2 sm:gap-3 min-w-0 flex-1">
{/* Order Number */}
{recipients.length > 1 ? (
<div className="relative flex-shrink-0">
<input
type="text"
value={recipient.routingOrder === 0 ? "" : recipient.routingOrder}
onChange={(e) => {
const value = e.target.value;
if (/^\d*$/.test(value)) {
const num = Number(value);
handleOrderChange(recipient.id || recipient.uuid, num);
}
}}
onKeyDown={(e) => {
if (!/[0-9]/.test(e.key) && e.key !== "Backspace" && e.key !== "Tab") {
e.preventDefault();
}
}}
className="w-8 sm:w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400"
/>
{idx === 0 && recipients.length > 1 && <p className="group">
<BiSolidHelpCircle className="absolute top-[-12px] right-[-10px] text-gray-400 cursor-pointer" />
{/* Tooltip */}
<span className="absolute top-[2px] left-[48px] hidden group-hover:block w-56 p-2 rounded-lg text-sm bg-white text-slate-600 shadow-lg z-10">
Signing order: Recipients receive emails one after another.
Same order means they all receive at once.
</span>
</p>}
</div>
) : (
<input
type="number"
disabled
value={recipient.routingOrder}
min={1}
onChange={(e) =>
handleOrderChange(
recipient.id || recipient.uuid,
Number(e.target.value)
)
}
className="w-6 sm:w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)] flex-shrink-0"
/>
)}
{/* Tooltip */}
<span className="absolute top-[2px] left-[48px] hidden group-hover:block w-56 p-2 rounded-lg text-sm bg-white text-slate-600 shadow-lg z-10">
Signing order: Recipients receive emails one after another.
Same order means they all receive at once.
</span>
</p>}
</div>
) : (
<input
type="number"
disabled
value={recipient.routingOrder}
min={1}
onChange={(e) =>
handleOrderChange(
recipient.id || recipient.uuid,
Number(e.target.value)
)
}
className="w-6 sm:w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)] flex-shrink-0"
/>
)}
{/* Avatar */}
{/* <div className="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
{/* Avatar */}
{/* <div className="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
</div> */}
{/* Form Fields Section */}
<div className="min-w-0 flex-1">
{/* Role Name and Permissions Row */}
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <div className="flex-1 lg:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "roleName", "Role name")}
</div>
</div>}
{/* Form Fields Section */}
<div className="min-w-0 flex-1">
{/* Role Name and Permissions Row */}
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <div className="flex-1 lg:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "roleName", "Role name")}
</div>
</div>}
{/* Permissions - Mobile: Below role, Desktop: Right side */}
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="flex ps-2.5 sm:flex-row items-start sm:items-center gap-2 sm:gap-4">
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
{/* Permissions - Mobile: Below role, Desktop: Right side */}
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="flex ps-2.5 sm:flex-row items-start sm:items-center gap-2 sm:gap-4">
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToEdit: !recipient.allowSenderToEdit,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-blue-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Edit</span>
</label>
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToDelete: !recipient.allowSenderToDelete,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Delete</span>
</label>
</div>
)}
</div>
</div>
{/* Name and Email Fields */}
<div className="mb-2 w-[92%]">
{/* Mobile: Stack vertically, Tablet+: Side by side */}
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2 sm:flex-1">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "firstName", "First Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "lastName", "Last Name")}
</div>
</div>
</div>
<div className="w-full sm:flex-1">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
</div>
</div>
</div>
</div>
{/* Host Fields (for In-Person Signers) */}
{recipient.type === recipientType.INPERSONSIGNER && (
<div className="mb-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostName", "Host Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
</div>
</div>
</div>
</div>
)}
{/* Recipient Type Radio Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 my-1.5 mx-1 sm:mx-2">
<label className="flex items-center gap-1">
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToEdit: !recipient.allowSenderToEdit,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-blue-500 w-3 h-3 sm:w-4 sm:h-4"
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.SIGNER}
checked={recipient.type === recipientType.SIGNER}
onChange={() => handleTypeChange(recipient, recipientType.SIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="whitespace-nowrap">Allow Edit</span>
<span className="text-xs sm:text-sm">Signer</span>
</label>
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<label className="flex items-center gap-1">
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToDelete: !recipient.allowSenderToDelete,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.INPERSONSIGNER}
checked={recipient.type === recipientType.INPERSONSIGNER}
onChange={() => handleTypeChange(recipient, recipientType.INPERSONSIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="whitespace-nowrap">Allow Delete</span>
<span className="text-xs sm:text-sm whitespace-nowrap">In Person Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.CC}
checked={recipient.type === recipientType.CC}
onChange={() => handleTypeChange(recipient, recipientType.CC)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">CC</span>
</label>
</div>
)}
</div>
</div>
{/* Name and Email Fields */}
<div className="mb-2 w-[92%]">
{/* Mobile: Stack vertically, Tablet+: Side by side */}
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2 sm:flex-1">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "firstName", "First Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "lastName", "Last Name")}
</div>
</div>
</div>
<div className="w-full sm:flex-1">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
</div>
</div>
</div>
</div>
{/* Host Fields (for In-Person Signers) */}
{recipient.type === recipientType.INPERSONSIGNER && (
<div className="mb-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostName", "Host Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
</div>
</div>
{/* Action Buttons */}
{signType !== "self-sign" && (
<div className="lg:ml-2 absolute top-[-8px] right-[-5px] px-1.5 py-1.5">
<button
type="button"
title="Remove recipient"
aria-label={`Remove recipient ${recipient.firstName}`}
onClick={() => handleRemoveRecipient(recipient)}
disabled={
transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
? false
: !recipient.newOne && !recipient.allowSenderToDelete
}
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50 p-1"
>
<ImBin size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
</div>
)}
{/* Recipient Type Radio Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 my-1.5 mx-1 sm:mx-2">
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.SIGNER}
checked={recipient.type === recipientType.SIGNER}
onChange={() => handleTypeChange(recipient, recipientType.SIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.INPERSONSIGNER}
checked={recipient.type === recipientType.INPERSONSIGNER}
onChange={() => handleTypeChange(recipient, recipientType.INPERSONSIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm whitespace-nowrap">In Person Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.CC}
checked={recipient.type === recipientType.CC}
onChange={() => handleTypeChange(recipient, recipientType.CC)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">CC</span>
</label>
)}
</div>
</div>
</div>
{/* Action Buttons */}
{signType !== "self-sign" && (
<div className="lg:ml-2 absolute top-0 right-0 px-1.5 py-1.5 rounded-bl-sm border-l-1 border-b-1 border-slate-300 border-dashed">
<button
type="button"
title="Remove recipient"
aria-label={`Remove recipient ${recipient.firstName}`}
onClick={() => handleRemoveRecipient(recipient)}
disabled={
transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
? false
: !recipient.newOne && !recipient.allowSenderToDelete
}
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50 p-1"
>
<ImBin size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
)}
</SortableRecipient>
))}
</div>
))}
</div>
</SortableContext>
</ DndContext>
</>
) : (
<div className="p-4 sm:p-6 text-center text-gray-500 font-medium text-sm">
@ -488,4 +643,85 @@ const RecipientCards = ({
);
};
export default RecipientCards;
export default RecipientCards;
// <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
// <SortableContext
// items={recipients.map((r) => r.id || r.uuid)}
// strategy={verticalListSortingStrategy}
// >
// <div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
// {recipients.map((recipient, idx) => (
// <SortableRecipient
// key={recipient.id || recipient.uuid}
// recipient={recipient}
// idx={idx}
// editRecipientCurrentEmail={editRecipientCurrentEmail}
// >
// {/* 🔹 your existing recipient card content goes here */}
// </SortableRecipient>
// ))}
// </div>
// </SortableContext>
// </DndContext>

View File

@ -0,0 +1,179 @@
import { useRef, useState } from "react";
import RecipientAddCard from "./RecipientAddCard"
import RecipientCards from "./RecipientCards"
import { useParams } from "react-router-dom";
import { SignTypeEnum } from "../../../utlity/types";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../../store/store";
import { IoMdClose } from "react-icons/io";
import getMinRoutingOrder from "../../../utlity/commonUtils";
import { addRecipientAsync, recipientType, removeAllRecipients, removeRecipientAsync } from "../../../store/slices/selectedRecipientsSlice";
import { UserState } from "../../../store/slices/userSlice";
import api from "../../../config/api";
import { FormSubmitInt } from "./ProcessStepOne";
interface RecipientsConIntProps {
transactionTemplateToggle: string;
accountId: string | undefined;
transactionId: string | undefined;
userData: UserState;
isModal: boolean;
submitError?: FormSubmitInt;
setEditRecipientsModal?: React.Dispatch<boolean>
}
const RecipientsCon: React.FC<RecipientsConIntProps> = ({ transactionTemplateToggle, accountId, transactionId, userData, submitError, isModal = true, setEditRecipientsModal }) => {
const dispatch = useDispatch<AppDispatch>()
const topRef = useRef<HTMLDivElement | null>(null);
const signType = useParams<{ signType: string }>().signType;
const [showSelfSignWarnModal, setShowSelfSignWarnModal] = useState(false)
const [editRecipient, setEditRecipient] = useState({})
const [editRecipientCurrentEmail, setEditRecipientCurrentEmail] = useState('')
const [editCase, setEditCase] = useState<string>("")
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const globalLoader = useSelector((state: RootState) => state.loader)
// Handle Recipient Process - START
const handleRecipientAdd = async () => {
let minRoutingOrder = getMinRoutingOrder({ recipients: selectedRecipients });
const recipientsData = selectedRecipients.map((recipient, index) => ({
id: "id" in recipient ? recipient.id : null,
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
provideOwnTags: userData.email === recipient.email && (recipient.routingOrder ?? 1) === minRoutingOrder ? true : false,
type: recipient.type,
firstName: recipient.firstName,
lastName: recipient.lastName,
authType: "NONE",
deliveryMethod: "EMAIL",
email: recipient.email,
hostName: recipient.hostName,
hostEmail: recipient.hostEmail,
company: "",
title: "",
website: "",
phone: "",
accessCode: "",
accessCodeDelivery: true,
language: "en",
name: `${recipient.firstName} ${recipient.lastName}`,
roleName: recipient.roleName,
allowSenderToEdit: recipient.allowSenderToEdit,
allowSenderToDelete: recipient.allowSenderToDelete,
}));
let recipientsDataTemp = recipientsData;
let recipientId = null;
if (signType === SignTypeEnum.selfSign) {
dispatch(removeAllRecipients());
await Promise.all(
selectedRecipients.map((recipient) => dispatch(removeRecipientAsync({
recipientId: recipient.id,
accountId,
transactionId,
email: recipient.email,
})))
);
recipientsDataTemp = [{
id: null,
routingOrder: 1,
provideOwnTags: true,
type: recipientType.SIGNER,
firstName: userData.firstName,
lastName: userData.lastName,
authType: "NONE",
deliveryMethod: "EMAIL",
email: userData.email,
hostName: "",
hostEmail: "",
company: "",
title: "",
website: "",
phone: "",
accessCode: "",
accessCodeDelivery: true,
language: userData.language ? userData.language : "en",
name: `${userData.firstName} ${userData.lastName}`,
roleName: "",
allowSenderToEdit: true,
allowSenderToDelete: true,
}]
try {
const basePath = `/api/accounts/${accountId}/transactions/${transactionId}/recipients`;
const res = await api.post(basePath, recipientsDataTemp)
recipientId = res.data[0].id;
} catch (error) {
console.log("Error Creating Self Sign Recipient", error.message)
}
} else {
if (!accountId || !transactionId) return;
dispatch(addRecipientAsync({ recipientsData: recipientsDataTemp, accountId, transactionId }));
}
}
// Handle Recipient Process - END
return (
<>
{(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) && <>
<div className="flex flex-col gap-4 align-start">
{isModal && <div className="flex justify-between items-center">
<p className="font-medium">Recipients</p>
<IoMdClose
onClick={() => {
handleRecipientAdd()
setEditRecipientsModal(false)
}}
className="text-xl text-slate-400 cursor-pointer" />
</div>}
<div id="recipient_cards_con"
className={`${submitError && submitError.showError && (submitError.duplicateEmailSameRoutingOrder || submitError.invalidRecipientLastName || submitError.invalidRecipientEmail) ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
<RecipientCards
userEmail={userData.email}
transactionTemplateToggle={transactionTemplateToggle}
setEditRecipient={setEditRecipient}
setEditCase={setEditCase}
editRecipient={editRecipient}
setEditRecipientCurrentEmail={setEditRecipientCurrentEmail}
editRecipientCurrentEmail={editRecipientCurrentEmail}
/>
{submitError && submitError.showError && <p
className="text-xs text-right px-2 pt-2 text-red-500">
{submitError.duplicateEmailSameRoutingOrder && `Each Signing order must have unique emails, ${submitError && submitError.email}`}
{!submitError.recipients && !submitError.duplicateEmailSameRoutingOrder && submitError.invalidRecipientEmail && `Invalid email address, ${submitError && submitError.email}`}
{!submitError.recipients && !submitError.duplicateEmailSameRoutingOrder && submitError.invalidRecipientLastName && `Last name cannot be empty.`}
</p>}
</div>
<div
ref={topRef}
className={`relative mt-4 mb-2 w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none' : ''} ${submitError && submitError.showError && submitError.recipients ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}
>
<RecipientAddCard
transactionTemplateToggle={transactionTemplateToggle}
editRecipient={editRecipient}
editCase={editCase}
setEditRecipient={setEditRecipient}
setEditCase={setEditCase}
topRef={topRef}
setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
{submitError && submitError.showError && <p
className="text-xs text-right px-2 pt-2 text-red-500">
{submitError.recipients && "At least one recipient is required."}
</p>}
</div>
</div>
</>}
</>
)
}
export default RecipientsCon

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@ const SelfSignWarnModal = ({ setShowSelfSignWarnModal }: PropsType) => {
<div className="bg-white rounded-lg shadow-xl p-6 w-100">
<h3 className="text-lg font-medium text-gray-800 mb-4">Confirm Self Sign</h3>
<p className="text-gray-600 mb-6">
Are you sure you want to proceed with self sign? This will remove all the recipient that you added.
Proceeding with self sign will make you the only signer. Other recipients will not be part of the signing process.
</p>
<div className="flex justify-end gap-3">
<button
@ -22,7 +22,7 @@ const SelfSignWarnModal = ({ setShowSelfSignWarnModal }: PropsType) => {
navigate("./../");
}}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
className="px-4 py-2 cursor-pointer text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
No
</button>
@ -31,7 +31,7 @@ const SelfSignWarnModal = ({ setShowSelfSignWarnModal }: PropsType) => {
// onConfirm();
setShowSelfSignWarnModal(false);
}}
className="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded-md transition-colors"
className="px-4 py-2 cursor-pointer bg-[var(--mainPrimaryBtn)] text-white rounded-md transition-colors"
>
Yes
</button>

View File

@ -0,0 +1,49 @@
import { CSS } from "@dnd-kit/utilities";
import { useSortable } from "@dnd-kit/sortable";
import { MdDragIndicator } from "react-icons/md";
import { useSelector } from "react-redux";
import { RootState } from "../../../store/store";
const SortableRecipient = ({ recipient, idx, editRecipientCurrentEmail, children }: any) => {
const selectedRecipientsLen = useSelector((state: RootState) => state.selectedRecipients.recipients.length);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: recipient.id || recipient.uuid });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
};
return (
<div
ref={setNodeRef}
style={style}
className={`relative flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 transition-colors gap-3 lg:gap-0 ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail ? "bg-neutral-100 opacity-40" : ""}
${isDragging ? "bg-blue-50 shadow-lg" : ""}
`}
>
{/* Drag handle only on left side */}
{selectedRecipientsLen && <div
{...attributes}
{...listeners}
className="cursor-grab text-gray-500 flex items-center pr-2 absolute top-0 left-0 z-50"
>
<MdDragIndicator className="absolute top-[10px] left-[10px] text-xl text-gray-300" />
</div>}
{/* Your card content */}
<div className="flex-1">{children}</div>
</div>
);
}
export default SortableRecipient;

View File

@ -47,6 +47,8 @@ interface DocumentContainerProps {
let copiedTags: DroppedItem[] = []
let selectedDoc: { [key: string]: string | null } = { key: null }
const fetchedTags: string[] = []
const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null }) => {
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch();
@ -87,24 +89,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
hotKey?: string | null;
}
// const hotKeysObjInitial: HotKeysObj = {
// "CtrlA": {
// enabled: false,
// key: null
// },
// "Delete": {
// enabled: false,
// key: null
// },
// "CtrlC": {
// enabled: false,
// key: null,
// tagId: null
// }
// }
const hotKeysObjInitial: HotKeysObj = {
isActive: false,
key: null,
@ -112,11 +96,8 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
hotKey: null
}
const [hotKeysObj, setHotKeysObj] = useState<HotKeysObj>(hotKeysObjInitial);
useEffect(() => {
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
@ -125,12 +106,21 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
const fetchTags = async () => {
try {
const requests = selectedRecipients.map((recipient) =>
api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipient.id}/tags`)
);
const requests = selectedRecipients.filter((recipient) => {
// Adding new rec doesn't have id till we click on IoMdClose or save so removing them and adding rec after click then fetching tags
if (!recipient.id) return false;
// also checking if have id but tags already fetched so to avoid same req
if (fetchedTags.includes(recipient.id)) return false;
fetchedTags.push(recipient.id)
return true
}).map((recipient) => api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipient.id}/tags`));
if (!requests.length) return
const response = await Promise.all(requests);
let recTags = {}
for (const res of response) {
if (!res && !res.data) return
for (const tag of res.data) {
const key = `${tag.documentId}_${tag.pageIndex}`;
if (recTags[key]) {
@ -188,8 +178,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
// Filter Tags that needs to update
const updatedData = {}
// debugger
for (const [recipientId, tags] of Object.entries(tagsData)) {
// console.log(recipientId, tags, "line 133")
const updateTags = []
@ -206,13 +194,20 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
updatedData[recipientId] = updateTags
}
const requests = Object.entries(updatedData).filter(([recipientId, tags]) => {
if (tags.length === 0) return false;
return api.put(
`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`,
tags
);
});
const requests = Object.entries(updatedData)
.filter(([_, tags]) => tags.length > 0) // filter out empty ones
.map(([recipientId, tags]) => {
const updatedTagsWithNavOrder = tags.map((tag, indx) => ({
...tag,
navigationOrder: indx + 1,
}));
return api.put(
`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`,
updatedTagsWithNavOrder
);
});
try {
const responses = await Promise.all(requests);
@ -350,14 +345,11 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
setTimeout(() => {
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
console.log(tagIndex, "line 307", key);
setSelectedTag({ key, tagIndex })
setSelectedTag({ key, index: tagIndex })
}, 0)
};
// HANDLE DROP - END
console.log(selectedTag, "Line 359")
useEffect(() => {
if (!hotKeysObj.isActive) return
@ -466,6 +458,13 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
// return () => targetCon?.removeEventListener("keydown");
}
const handleLazyLoad = (docId: string, pageId: string) => {
api.get(`http://10.10.1.99:8083/api/${accountId}/transactions/${transactionId}/documents/${docId}/pages/${pageId}`).then(res => {
console.log(res)
})
}
return (
<>
{/* Center Doc Container - START */}
@ -514,6 +513,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
draggable="false"
width={page.width}
height={page.height}
// src={`http://10.10.1.99:8083/api/accounts/${accountId}/transactions/${transactionId}/documents/${doc.id}/pages/${page.id}`}
src={page.src}
loading='lazy'
/>
@ -551,6 +551,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
selTagKey={selectedTag.key}
selTagIndx={selectedTag.index}
signClient={false}
emptySelectedTag={() => setSelectedTag(null)}
/>
}

View File

@ -48,17 +48,15 @@ const DroppedItemsTagsCon = forwardRef(({
const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 });
const handleTagClick = (key: string, index: number) => {
console.log(index, "line 307", key);
setSelectedTag({ key, index })
}
const handleRemoveTag = (key: string, targetIndex: number) => {
if(key === undefined || targetIndex === undefined) return
if (key === undefined || targetIndex === undefined) return
const tag = droppedItems[key][targetIndex];
const recipientId = tag.recipientId;
(async () => {
if ("id" in tag) {
try {
@ -70,7 +68,7 @@ const DroppedItemsTagsCon = forwardRef(({
}
}
setSelectedTag(null);
@ -570,9 +568,9 @@ const DroppedItemsTagsCon = forwardRef(({
<p className='w-2.5 h-2.5 rounded'
style={{ background: selectedRecipient[0]?.color }}
></p>
<p
{/* <p
className=''>{IconComponent}
</p>
</p> */}
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
<IoMdClose onClick={(e) => {
e.stopPropagation();
@ -599,10 +597,11 @@ const DroppedItemsTagsCon = forwardRef(({
style={{ background: selectedRecipient[0]?.color }}
>
<p
className={`flex justify-center items-center w-full text-xl text-white h-full`}
className={`flex justify-center items-center w-full text-md capitalize text-white h-full`}
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
>
{IconComponent}
{/* {item.label} */}
</p>
</div>
}

View File

@ -19,17 +19,62 @@ const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string
height = innerIconConRect.height + 10
}
// Handling Initia Width and height of Some tags - START
if (label === TagsTypes.STAMP) {
width = 160
height = 160
} else if (label === TagsTypes.SIGNATURE) {
width = 200
height = 75
} else if (label === TagsTypes.INITIALS) {
width = 85
height = 75
switch (label) {
case TagsTypes.STAMP: {
width = 160
height = 160
break;
}
case TagsTypes.SIGNATURE: {
width = 200
height = 75
break;
}
case TagsTypes.INITIALS: {
width = 85
height = 75
break;
}
case TagsTypes.COMPANY: {
width = 185
height = 35
break;
}
case TagsTypes.EMAIL: {
width = 185
height = 35
break;
}
case TagsTypes.NAME: {
width = 185
height = 35
break;
}
case TagsTypes.NUMBER: {
width = 185
height = 35
break;
}
case TagsTypes.SELECT: {
width = 185
height = 35
break;
}
case TagsTypes.TEMPORAL: {
width = 185
height = 35
break;
}
case TagsTypes.TEXT: {
width = 185
height = 35
break;
}
}
// Handling Initia Width and height of Some tags - END
const offsetX = event.clientX - dragElemRect.left;
@ -84,7 +129,7 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP'];
const data = res.data.filter((obj, indx) => {
// if (obj.type === "STAMP" || obj.type === "RADIO" || obj.type === "CHECKBOX" || obj.type === "ATTACHMENT") return false
if (obj.type === "STAMP" || obj.type === "RADIO" || obj.type === "CHECKBOX" || obj.type === "ATTACHMENT") return false
return true
});
@ -110,8 +155,41 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
});
}, []);
// return (
// <div>
// {tagsData.map((field, index) => {
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
// // Optional: skip unknown types
// if (!IconComponent) return null;
// return (
// // <div key={index} draggable={true} onDragStart={(e) => handleDragStart(e, field.label, recipientId)} className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300">
// <div
// draggable={true}
// key={index}
// onPointerDown={(e) => handlePointerDown(e, field.type.toLowerCase(), recipientId, recipientIndx, field)}
// className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300">
// <div
// className={`p-1 text-sm rounded-sm text-white`}
// style={{ background: color }}>
// {IconComponent}
// </div>
// <p
// className="capitalize text-xs">
// {field.type.toLowerCase()}
// </p>
// </div>
// )
// }
// )}
// </div>
// );
return (
<div>
<div className='flex flex-wrap gap-2 justify-start items-start max-w-[170px] sm:max-w-max'>
{tagsData.map((field, index) => {
const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
@ -124,10 +202,14 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
draggable={true}
key={index}
onPointerDown={(e) => handlePointerDown(e, field.type.toLowerCase(), recipientId, recipientIndx, field)}
className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300">
className={`border-[1px] flex flex-col justify-center bg-white p-2 w-[75px] h-[75px] items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300`}
style={{ borderColor: color }}
>
<div
className={`p-1 text-sm rounded-sm text-white`}
style={{ background: color }}>
className={`p-1 rounded-sm text-white`}
// style={{ background: color }}
>
{IconComponent}
</div>
<p

View File

@ -10,14 +10,20 @@ import { MdKeyboardArrowDown, MdOutlineNavigateNext } from 'react-icons/md'
import { useParams } from 'react-router-dom';
import { fetchDocuments } from '../../../store/slices/documentSlice';
import { GrFormPrevious } from 'react-icons/gr';
import { LuTableOfContents } from 'react-icons/lu';
import { IoIosDocument } from 'react-icons/io';
interface LeftSideBarProps {
isFields: boolean;
isPages: boolean;
selectedRecipientIndx: number;
setSelectedRecipient: React.Dispatch<React.SetStateAction<number>>
handleSetIsFields: (setTo: boolean) => void;
handleSetIsPages:(setTo: boolean) => void;
}
const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, selectedRecipientIndx, setSelectedRecipient }) => {
const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRecipientIndx, setSelectedRecipient, handleSetIsFields, handleSetIsPages }) => {
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch();
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
@ -64,12 +70,46 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, selectedRecipientIn
}
}, [])
useEffect(() => {
if (selectedRecipient && selectedRecipient.type === recipientType.CC) {
for (const [indx, recip] of selectedRecipients.entries()) {
if (recip.type !== recipientType.CC) {
setSelectedRecipient(indx);
break;
}
}
};
}, [selectedRecipient])
if (selectedRecipient && selectedRecipient.type === recipientType.CC) {
return <div className='col-span-2 bg-white h-full p-4 overflow-y-auto'></div>;
}
return (
<>
{/* Left Side Bar - START */}
{
selectedRecipients.length > 0 && <div className='col-span-2 bg-white h-full p-4 overflow-y-auto'>
{isFields && <>
<div className={`${!isFields && !isPages ? "hidden" : ""} p-4 absolute top-[48px] sm:top-[0] rounded-tr-md z-50 sm:relative sm:col-span-2 bg-white border-t-1 border-r-1 border-slate-300 h-[calc(100vh-40px)] overflow-y-auto`}>
<div className='flex flex-col md:flex-row justify-between items-center bg-slate-100 p-1.5 rounded-2xl border-1 border-gray-200 mb-3'>
<div
onClick={() => handleSetIsFields(!isFields)}
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isFields ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300`}>
<LuTableOfContents />
<p className='tracking-wide'>Tags</p>
</div>
<div
onClick={() => handleSetIsPages(!isPages)}
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isPages ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300`}>
<IoIosDocument />
<p className='tracking-wide'>Pages</p>
</div>
</div>
{selectedRecipients.length > 0 && isFields && <>
{/* Fields List and Recipeint - Start */}
{/* Side Bar showing Recipients */}
<div className='relative'
@ -136,9 +176,7 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, selectedRecipientIn
{/* Fields List and Recipeint - End */}
</>}
{!isFields && <>
{isPages && <>
{/* Pages - START */}
<div className='flex flex-col gap-2'>
{selectedDocuments.length && selectedDocuments.map((document, indx) => {
@ -190,7 +228,7 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, selectedRecipientIn
onChange={(e) => {
const newPage = Number(e.target.value);
setCurrentPage(newPage);
window.location.hash = document.id +""+ newPage;
window.location.hash = document.id + "" + newPage;
}}
>
{document.pages != undefined && document.pages.map((_, pageIndex) => (
@ -226,18 +264,7 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, selectedRecipientIn
</div>
{/* Pages - END */}
</>}
</div >
</div>
}
{/* Left Side Bar - END */}

View File

@ -10,15 +10,19 @@ import { MdKeyboardArrowDown } from 'react-icons/md'
import { RiResetRightLine } from 'react-icons/ri'
import { fetchRecipients, recipientType, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'
import { fetchDocuments } from '../../../store/slices/documentSlice'
import { useNavigate, useParams } from 'react-router-dom'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { Bounce, toast } from 'react-toastify'
import { FiChevronDown } from 'react-icons/fi'
import RecipientsCon from '../processStepOne/RecipientsCon'
import MessageCon from '../processStepOne/MessageCon'
interface NavTwoProps {
transactionSaving?: boolean;
// setTransactionSaving: React.Dispatch<React.SetStateAction<boolean>>
isFields?: boolean;
setIsFields?: React.Dispatch<React.SetStateAction<boolean>>
isPages?: boolean;
setIsPages?: React.Dispatch<React.SetStateAction<boolean>>
isSend?: boolean | null;
setIsSend?: React.Dispatch<React.SetStateAction<boolean | null>>
isPreview?: boolean
@ -51,9 +55,27 @@ const fitObj = [
},
]
const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, setIsFields, isSend, setIsSend, isPreview = false, aId = null, tId = null }) => {
const NavTwo: React.FC<NavTwoProps> = ({
transactionSaving = false,
isFields,
setIsFields,
isPages,
setIsPages,
isSend,
setIsSend,
isPreview = false,
aId = null,
tId = null
}) => {
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch();
const [showActionsDropDown, setShowActionsDropDown] = useState<boolean>(false);
const [editRecipientsModal, setEditRecipientsModal] = useState<boolean>(false);
const [messageModal, setMessageModal] = useState<boolean>(false);
console.log(editRecipientsModal)
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
let accountId = params.accountId;
let transactionId = params.transactionId
@ -159,9 +181,23 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
}, 1000)
}
const handleSetIsFields = (setTo: boolean) => {
if (setTo !== isFields) setIsFields(setTo);
}
// const handleSetIsFields = (setTo: boolean) => {
// if (setTo !== undefined && setTo !== isFields) {
// setIsFields(setTo)
// if(setTo === true) {
// setIsPages(false)
// }
// };
// }
// const handleSetIsPages = (setTo: boolean) => {
// if (setTo !== undefined && setTo !== isPages) {
// setIsPages(setTo)
// if(setTo === true) {
// setIsFields(false)
// }
// };
// }
const zoom = (percentage: number, contentClass: string) => {
const content = document.querySelector(`.${contentClass}`);
@ -199,32 +235,28 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
{/* Left Side */}
<div className='flex justify-start items-center gap-2'>
<div
onClick={() => handleSetIsFields(true)}
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${isFields ? 'bg-slate-100' : ''}`}>
<LuTableOfContents />
<p className='tracking-wide'>Fields</p>
<div>
<Link to={"/"}>
<img width={120} src="/sme-colored.png" alt="Sign Made Easy" />
</Link>
</div>
<div
onClick={() => handleSetIsFields(false)}
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${!isFields ? 'bg-slate-100' : ''}`}>
<IoIosDocument />
<p className='tracking-wide'>Pages</p>
</div>
<div
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
className='hidden sm:flex md:flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>Fit</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute bg-white rounded-sm z-50'>
{isFitDd && <div className='absolute bg-white rounded-sm z-[1000] shadow'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
@ -243,12 +275,12 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
</div>
{!isPreview && <div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
{/* {!isPreview && <div className='hidden sm:flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<RiResetRightLine
className={`${transactionSaving ? "animate-spin" : ""} transition duration-300`}
/>
<p className="tracking-wide transition-all">{transactionSaving ? `Saving ${dots}` : "Save"}</p>
</div>}
</div>} */}
</div>
@ -256,34 +288,58 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
{/* Right Side - STARTS */}
<div className='flex justify-center items-center'>
<div className="relative inline-block text-left">
<div
className="relative inline-block text-left">
{/* Trigger */}
<button className="peer flex justify-center items-center bg-white px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 ">
<button
onClick={() => setShowActionsDropDown((prev) => !prev)}
className="flex justify-center items-center bg-white px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 ">
<p className='tracking-wide mr-1'>Actions</p>
<MdKeyboardArrowDown />
</button>
{/* Menu */}
<div className="absolute right-0 mt-2 w-48 hidden peer-focus:block rounded-md bg-white shadow-sm ring-1 ring-black/5 z-10">
<div className="py-1">
{showActionsDropDown && <div
className="absolute right-0 mt-2 w-48 rounded-md bg-white shadow-sm ring-1 ring-black/5 z-10">
<div
className="py-1">
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Save & Close
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
<button
onClick={() => {
setMessageModal(true)
setShowActionsDropDown(false)
}}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Edit Message
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
<button
onClick={() => {
setEditRecipientsModal(true)
setShowActionsDropDown(false)
}}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Edit Recipients
</button>
<button
onClick={() => {
setEditRecipientsModal(true)
setShowActionsDropDown(false)
}}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Edit Documents
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Advanced Options
</button>
</div>
</div>
</div>}
</div>
{/* Documnt Nav */}
<div tabIndex={1} onBlur={() => setNav2DocToggle(false)}>
{/* Document Nav */}
<div className='hidden sm:block'
tabIndex={1} onBlur={() => setNav2DocToggle(false)}>
<div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>{selectedDocuments.length} Documents</p>
<MdKeyboardArrowDown className={`${nav2DocToggle ? 'rotate-180' : ''} transition-all`} />
@ -305,7 +361,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
</div>
{/* Recipients Nav */}
<div className='relative' tabIndex={1} onBlur={() => setNav2RecToggle(false)}>
<div className='hidden sm:block relative' tabIndex={1} onBlur={() => setNav2RecToggle(false)}>
<div onClick={() => setNav2RecToggle(!nav2RecToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>{selectedRecipients.length} Recipients</p>
<MdKeyboardArrowDown className={`${nav2RecToggle ? 'rotate-180' : ''} transition-all`} />
@ -338,7 +394,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
</p>
<FaSignature className='text-white text-md' />
</div>
{transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign && <div
{/* {transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign && <div
id='send-and-sign-dash-btn'
onClick={() => handleSend(selfUserSign.sign)}
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
@ -346,10 +402,39 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
Sign & Send
</p>
<FaSignature className='text-white text-md' />
</div>}
</div>} */}
</>}
{editRecipientsModal && accountId && transactionId && <>
<div className="w-full h-full absolute top-0 left-0 z-[1000] flex justify-center items-center">
<div className="w-full h-full bg-slate-100 opacity-85 blur-2xl absolute top-0 left-0"></div>
<div className="z-50 bg-white p-4 rounded-sm shadow-md">
<RecipientsCon
transactionTemplateToggle={transactionTemplateToggle}
accountId={accountId}
transactionId={transactionId}
userData={userDetails}
setEditRecipientsModal={setEditRecipientsModal}
/>
</div>
</div>
</>}
{messageModal && accountId && transactionId && <>
<div className="w-full h-full absolute top-0 left-0 z-[1000] flex justify-center items-center">
<div className="w-full h-full bg-slate-100 opacity-85 blur-2xl absolute top-0 left-0"></div>
<div className="z-50 bg-white p-4 rounded-sm shadow-md">
<MessageCon
transactionTemplateToggle={transactionTemplateToggle}
accountId={accountId}
transactionId={transactionId}
messageModal={messageModal}
setMessageModal={setMessageModal}
/>
</div>
</div>
</>}
</div>
{/* Right Side - ENDS */}

View File

@ -17,8 +17,11 @@ export interface IsSendInterface {
}
const ProcessStepTwo = () => {
const windowWidth = window.innerWidth;
const [isFields, setIsFields] = useState(windowWidth > 638 ? true : false);
const [isPages, setIsPages] = useState(false);
const [isFields, setIsFields] = useState(true);
const [selectedRecipient, setSelectedRecipient] = useState(0);
const [transactionSaving, setTransactionSaving] = useState<boolean>(false);
@ -34,12 +37,31 @@ const ProcessStepTwo = () => {
}, 800)
}, [])
const handleSetIsFields = (setTo: boolean) => {
if (setTo !== undefined && setTo !== isFields) {
setIsFields(setTo)
if (setTo === true) {
setIsPages(false)
}
};
}
const handleSetIsPages = (setTo: boolean) => {
if (setTo !== undefined && setTo !== isPages) {
setIsPages(setTo)
if(setTo === true) {
setIsFields(false)
}
};
}
return (
<div className={`min-w-full relative h-screen bg-slate-100 overflow-hidden ${(globalLoader.isLoading) ? 'pointer-events-none' : ''}`}>
<NavTwo transactionSaving={transactionSaving} isFields={isFields} setIsFields={setIsFields} isSend={isSend} setIsSend={setIsSend} />
<NavTwo transactionSaving={transactionSaving} isFields={isFields} isPages={isPages} setIsPages={setIsPages} setIsFields={setIsFields} isSend={isSend} setIsSend={setIsSend} />
<div className='grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-46px)] overflow-hidden'>
<LeftSideBar isFields={isFields} selectedRecipientIndx={selectedRecipient} setSelectedRecipient={setSelectedRecipient} />
<div className='sm:grid sm:grid-cols-12 sm:gap-1 mt-1 h-[calc(100vh-46px)] overflow-hidden'>
<LeftSideBar isFields={isFields} isPages={isPages} selectedRecipientIndx={selectedRecipient} setSelectedRecipient={setSelectedRecipient} handleSetIsFields={handleSetIsFields} handleSetIsPages={handleSetIsPages} />
<DocumentContainer transactionSaving={transactionSaving} setTransactionSaving={setTransactionSaving} isSend={isSend} setIsSend={setIsSend} />
</div>

View File

@ -2,7 +2,7 @@ import { DroppedItemsObj } from "../DocumentContainer";
import React, { useEffect, useState } from "react";
import { DroppedItem, StylesInterface } from "../../../../utlity/types";
import { IoIosArrowDown } from "react-icons/io";
import { IoIosArrowDown, IoMdClose } from "react-icons/io";
import { TagsTypes } from "../../../../utlity/types";
@ -26,6 +26,7 @@ export interface DroppedItemProps {
selTagKey: string,
selTagIndx: number,
signClient: boolean,
emptySelectedTag?: () => void
}
export enum TEXT {
@ -92,7 +93,16 @@ const defaultStyles: StylesInterface = {
const RightSideBar: React.FC<DroppedItemProps> = ({ droppedItems, selectedTag, setDroppedItems, selTagKey, selTagIndx, signClient = false }) => {
const RightSideBar: React.FC<DroppedItemProps> = (
{
droppedItems,
selectedTag,
setDroppedItems,
selTagKey,
selTagIndx,
signClient = false,
emptySelectedTag
}) => {
const selectedType = selectedTag.type.toLowerCase() in enumMap && enumMap[selectedTag.type.toLowerCase() as keyof typeof enumMap];
const [styles, setStyles] = useState(defaultStyles);
@ -283,8 +293,13 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ droppedItems, selectedTag, s
return (
<>
{/* Right Side Bar Start */}
<div className='col-span-2 w-full h-full bg-white text-sm overflow-y-auto'>
<p className="p-2 font-semibold text-md"><span className="capitalize">{selectedTag.type.toLowerCase()}</span> Attributes:</p>
<div className='absolute border-l-1 border-t-1 border-slate-300 rounded-tl-lg right-0 top-[48px] sm:rounded-none sm:top-0 sm:border-0 sm:relative sm:col-span-2 sm:w-full h-full bg-white text-sm overflow-y-auto'>
<div className="flex flex-row-reverse justify-end items-center">
<p className="p-2 font-semibold text-md"><span className="capitalize">{selectedTag.type.toLowerCase()}</span></p>
<p className="ml-2.5 cursor-pointer" >
<IoMdClose onClick={emptySelectedTag} />
</p>
</div>
<hr className="text-slate-200" />
<div className="p-3 text-sm text-slate-600 flex flex-col gap-2">

View File

@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { FaArrowUp, FaArrowDown, FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import { MdOutlineKeyboardArrowDown } from 'react-icons/md';
interface DirectionalControlsProps {
onMove: (direction: 'up' | 'down' | 'left' | 'right', stepSize?: number) => void;
@ -103,28 +104,39 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
`;
const inputClass = `
w-14 text-center text-[12px] rounded border px-1 py-0.5 bg-white
w-14 text-left pl-2 text-[12px] rounded py-0.5 bg-white
${disabled
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'border-gray-300 text-gray-700 focus:outline-none focus:ring focus:border-blue-400'}
: 'border-gray-300 text-gray-700 focus:outline-none focus:border-blue-400'}
`;
return (
<div className={`inline-flex flex-col items-center space-y-2 text-[12px] ${className}`}>
<div className={`${className}`}>
{/* Top */}
<button type="button" onClick={() => handleMove('up')} disabled={disabled} className={buttonClass} title="Move Up">
{/* <button type="button" onClick={() => handleMove('up')} disabled={disabled} className={buttonClass} title="Move Up">
<FaArrowUp />
</button>
</button> */}
<div className='flex items-center justify-between'>
<label className='font-medium text-slate-600'>Position</label>
<MdOutlineKeyboardArrowDown className='mr-2' />
</div>
{/* Middle: Left / Inputs / Right */}
<div className="flex items-center space-x-3">
<button type="button" onClick={() => handleMove('left')} disabled={disabled} className={buttonClass} title="Move Left">
{/* <div className="flex w-full items-center justify-center space-x-3"> */}
{/* <button type="button" onClick={() => handleMove('left')} disabled={disabled} className={buttonClass} title="Move Left">
<FaArrowLeft />
</button>
</button> */}
{/* Input fields */}
<div className="flex flex-col items-center space-y-1">
{/* Input fields */}
<div className="py-1 flex items-center gap-1">
<div className='flex items-center font-semibold border border-gray-300 px-2 py-1 rounded'>
<label
htmlFor="fromleft"
className='text-xs'>
X
</label>
<input
id="fromleft"
type="number"
value={leftValue}
onChange={(e) => { e.preventDefault; handlePositionInputChange('left', e.target.value) }}
@ -134,7 +146,16 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
className={inputClass}
placeholder="Left"
/>
</div>
<div className='flex items-center font-semibold border border-gray-300 px-2 py-1 rounded'>
<label
htmlFor="fromtop"
className='text-xs'>
Y
</label>
<input
id="fromtop"
type="number"
value={topValue}
onChange={(e) => { e.preventDefault; handlePositionInputChange('top', e.target.value) }}
@ -145,19 +166,20 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
placeholder="Top"
/>
</div>
<button type="button" onClick={() => handleMove('right')} disabled={disabled} className={buttonClass} title="Move Right">
<FaArrowRight />
</button>
</div>
<button type="button" onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move Down">
{/* <button type="button" onClick={() => handleMove('right')} disabled={disabled} className={buttonClass} title="Move Right">
<FaArrowRight />
</button> */}
{/* </div> */}
{/* <button type="button" onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move Down">
<FaArrowDown />
</button>
<div className="flex items-center space-x-1">
</button> */}
{/* <div className="flex items-center space-x-1">
<div className="relative flex items-center justify-center bg-white rounded-lg px-1 py-2 w-24 ">
</div>
</div>
</div> */}
</div>
);
};

View File

@ -1,13 +1,14 @@
import { JSX, useLayoutEffect } from 'react';
import { useDispatch } from "react-redux";
import { fetchUserDetails } from '../../store/slices/userSlice';
import { AppDispatch } from '../../store/store';
const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const dispatch = useDispatch();
const dispatch = useDispatch<AppDispatch>();
useLayoutEffect(() => {
// useLayoutEffect(() => {
dispatch(fetchUserDetails());
}, []);
// }, []);
return children;
};

View File

@ -1,7 +1,5 @@
interface SubSection {
id: string;
label: string;
}
import { SubSection } from "./Setting";
interface AsideSectionProps {
activeSubSection: string;
@ -23,12 +21,7 @@ const AsideSection = ({
return (
<div className="">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-800">{mainTabTitle}</h3>
<p className="text-sm text-gray-500 mt-1">Navigate through sections</p>
</div>
<nav className="space-y-2 max-h-96 overflow-y-auto">
<nav className="space-y-1 max-h-96 overflow-y-auto">
{subSections.map((subSection) => (
<a
key={subSection.id}
@ -37,12 +30,13 @@ const AsideSection = ({
e.preventDefault(); // Prevent default anchor behavior
onSubSectionClick(subSection.id);
}}
className={`block w-full text-left px-3 py-2.5 rounded-md text-sm font-medium transition-all duration-200 hover:bg-gray-50 cursor-pointer no-underline ${
className={`flex items-center gap-2 w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 hover:bg-gray-50 cursor-pointer no-underline ${
activeSubSection === subSection.id
? "bg-blue-50 text-blue-600 border-l-4 border-blue-600 shadow-sm"
? "bg-[#F4F9FF] text-[var(--primary)]"
: "text-gray-600 hover:text-gray-800 hover:bg-gray-50"
}`}
>
{subSection.component}
<span className="block">{subSection.label}</span>
</a>
))}

View File

@ -1,5 +1,4 @@
import { useEffect, useState, useRef } from "react";
import SettingNav from "./SettingNav";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import api from "../../config/api";
import UserProfile from './UserProfile';
@ -8,8 +7,12 @@ import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
import AccountNav from "../accounts/AccountNav";
import ContactForm, { ContactData } from "./ContactForm";
import { useSelector } from "react-redux";
import { RootState } from "../../store/store";
import { LuUserRound } from "react-icons/lu";
import { AiFillSignature, AiOutlineSignature } from "react-icons/ai";
import { FaRegUser } from "react-icons/fa";
// Define the shape of state object
interface Account {
@ -26,7 +29,7 @@ interface UserData {
account: Account;
}
interface SubSection {
export interface SubSection {
id: string;
label: string;
component?: React.ReactNode;
@ -50,6 +53,8 @@ const Setting = () => {
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const userFetchData = useSelector((state: RootState) => state.user)
// Refs for subsections within each main tab
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -79,8 +84,8 @@ const Setting = () => {
switch (tabId) {
case "userProfile":
return [
{ id: "profile-info", label: "Profile Information" },
{ id: "signature", label: "Signature" },
{ id: "profile-info", label: "Personal Information", component: <FaRegUser /> },
{ id: "signature", label: "Signature", component: <AiOutlineSignature className="text-lg" /> },
];
default:
return [];
@ -241,9 +246,9 @@ const Setting = () => {
const tabs = [
{ id: "userProfile", label: "User Profile", type: "settings" },
{ id: "create and invite", label: "Create and invite", type: "settings" },
// { id: "create and invite", label: "Create and invite", type: "settings" },
{ id: "contacts", label: "Contacts", type: "settings" },
{ id: "language", label: "Language", type: "settings" },
// { id: "language", label: "Language", type: "settings" },
{ id: "contactAdd", label: "Add Contact", type: "contact" },
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
{ id: "contactShare", label: "Share Contact", type: "contact" },
@ -407,19 +412,19 @@ const Setting = () => {
<>
{/* <SettingNav /> */}
{/* <AccountNav/> */}
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
<div className="bg-white h-[calc(90vh-20px)] flex flex-col mx-6 my-6 rounded-lg overflow-hidden">
{/* Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-4 md:px-6 lg:px-8 pt-4">
<div className="flex flex-wrap gap-4 md:gap-6 ">
<div className="flex flex-wrap md:gap-4 ">
{tabs.map((tab) =>
tab.type === page1 && (
<button
key={tab.id}
onClick={() => handleMainTabClick(tab.id)}
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
className={`cursor-pointer relative px-4 pb-3 text-sm font-medium transition-all duration-200
${activeTab === tab.id
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
: "text-gray-600 hover:text-blue-500"
? "text-[#34BEFF] after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-[#34BEFF] bg-gradient-to-t from-[#E5F6FF] to-transparent"
: "text-gray-600 hover:text-[#34BEFF]"
}`}
>
{tab.label}
@ -430,12 +435,26 @@ const Setting = () => {
</div>
{/* Responsive Layout */}
<div className="flex-1 min-h-0 flex flex-col lg:flex-row px-4 md:px-6 lg:px-7 pt-2 lg:gap-6 ">
<div className="flex-1 min-h-0 flex flex-col lg:flex-row">
{/* Sidebar (hidden on small, shown on lg) */}
{activeTab === 'userProfile' && (
<section className="hidden lg:block flex-shrink-0 w-56">
<div className="sticky top-6 z-20">
<section className="hidden lg:block flex-shrink-0 w-70 border-r-1 border-slate-200">
<div className="border-b-1 border-slate-200 py-4 px-6">
<div className="flex justify-start items-center gap-2">
<div>
<p className="bg-[#FAF0FF] text-[#A915FF] font-semibold max-w-max px-3 py-2 rounded-full ">PK</p>
</div>
<div>
<p className="font-semibold">{user.name}</p>
<p className="text-sm text-slate-600 w-[24ch] overflow-hidden text-ellipsis whitespace-nowrap">
{user.email}
</p>
</div>
</div>
</div>
<div className="sticky top-6 z-20 px-6 pt-2">
<AsideSection
activeSubSection={activeSubSection}
onSubSectionClick={handleSubSectionClick}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import React, { useRef, useState, useEffect } from 'react';
import { IoMdClose } from 'react-icons/io';
type Props = {
onSignatureChange: (data: { fullSignature: string | null; initials: string | null }) => void;
@ -9,6 +10,7 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
const canvasRef1 = useRef<HTMLCanvasElement>(null);
const canvasRef2 = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [isDrawingType, setIsDrawingType] = useState<{ signatureDrawn: boolean, initialDrawn: boolean }>({ signatureDrawn: false, initialDrawn: false });
const [paths1, setPaths1] = useState<Array<Array<{ x: number; y: number }>>>([]);
const [paths2, setPaths2] = useState<Array<Array<{ x: number; y: number }>>>([]);
const [currentPath, setCurrentPath] = useState<Array<{ x: number; y: number }>>([]);
@ -21,29 +23,33 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
// Setup canvas dimensions when component mounts
useEffect(() => {
const setupCanvas = () => {
if (canvasRef1.current) {
const rect1 = canvasRef1.current.getBoundingClientRect();
canvasRef1.current.width = rect1.width;
canvasRef1.current.height = rect1.height;
}
if (canvasRef2.current) {
const rect2 = canvasRef2.current.getBoundingClientRect();
canvasRef2.current.width = rect2.width;
canvasRef2.current.height = rect2.height;
const resizeCanvas = (canvas: HTMLCanvasElement | null) => {
if (canvas) {
const rect = canvas.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
canvas.width = rect.width;
canvas.height = rect.height;
}
}
};
setupCanvas();
const observer1 = new ResizeObserver(() => resizeCanvas(canvasRef1.current));
const observer2 = new ResizeObserver(() => resizeCanvas(canvasRef2.current));
// Load initial signatures after canvas setup
if (initialSignatures && !initialLoaded) {
setTimeout(() => {
loadInitialSignatures();
}, 100);
}
if (canvasRef1.current) observer1.observe(canvasRef1.current);
if (canvasRef2.current) observer2.observe(canvasRef2.current);
// initial setup
resizeCanvas(canvasRef1.current);
resizeCanvas(canvasRef2.current);
return () => {
observer1.disconnect();
observer2.disconnect();
};
}, []);
const loadInitialSignatures = () => {
if (!initialSignatures) return;
@ -100,8 +106,11 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
setActiveCanvas(e.currentTarget === canvasRef1.current ? 'full' : 'initials');
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>, type: "SIGNATURE" | "INITIAL") => {
if (!isDrawing) return;
setIsDrawingType((prev) => {
return {...prev, signatureDrawn: type === "SIGNATURE" ? true : prev.signatureDrawn, initialDrawn: type === "INITIAL" ? true : prev.initialDrawn }
})
const rect = e.currentTarget.getBoundingClientRect();
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
const newPath = [...currentPath, point];
@ -138,6 +147,7 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
const handleMouseUp = () => {
setIsDrawing(false);
if (currentPath.length === 0 || !activeCanvas) return;
if (activeCanvas === 'full') {
@ -153,7 +163,8 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
// Export both canvases and notify parent
const fullSignature = exportCanvasAsImage(canvasRef1.current);
const initials = exportCanvasAsImage(canvasRef2.current);
onSignatureChange({ fullSignature, initials });
onSignatureChange({ fullSignature: (isDrawingType.signatureDrawn) ? fullSignature : null, initials: (isDrawingType.initialDrawn) ? initials : null });
// onSignatureChange({ fullSignature, initials });
};
const redrawCanvas = (canvas: HTMLCanvasElement, paths: Array<Array<{ x: number; y: number }>>, initialImage: HTMLImageElement | null) => {
@ -223,63 +234,70 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
};
return (
<div className="h-full flex flex-col">
<div className="mb-2 lg:mb-6">
<h2 className="text-lg sm:text-xl lg:text-2xl font-semibold text-gray-900 mb-1 sm:mb-2 ">Draw Signature</h2>
<p className="text-xs sm:text-sm text-gray-600 hidden sm:block">Create your signature by drawing directly on the canvas</p>
<div className="px-4 lg:px-6 py-4 flex-shrink-0">
<div className='flex justify-between'>
{/* <h2 className="text-lg sm:text-xl lg:text-2xl font-semibold text-gray-900 mb-1 sm:mb-2 ">Draw Signature</h2> */}
<h2 className="mt-4 mb-5 text-sm">Draw your signature. <span className='text-red-600'>*</span> Are required fields.</h2>
{/* Controls */}
<div className="flex gap-2 mt-3">
<div className='flex justify-center items-center gap-1 font-medium'>
<IoMdClose className='text-red-500' />
<button
className="text-xs text-red-500"
onClick={clearCanvas}
>
Clear Signature
</button>
</div>
{/* <button
className="text-xs"
onClick={undoLastPath}
disabled={!activeCanvas || (activeCanvas === 'full' ? paths1.length === 0 : paths2.length === 0)}
>
Undo Last
</button> */}
</div>
</div>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6">
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 lg:gap-6">
{/* Full Signature Canvas */}
<div className="lg:col-span-8">
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">Full Signature</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors">
<div className="md:col-span-8">
<label className="block text-sm font-medium mb-2">Full Name
<span className='text-red-600'> *</span>
</label>
<div className="border-1 border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors">
<canvas
ref={canvasRef1}
style={{ width: "100%", height: "200px" }}
className="w-full h-51 cursor-crosshair"
onPointerDown={handleMouseDown}
onPointerMove={handleMouseMove}
onPointerUp={handleMouseUp}
onPointerLeave={handleMouseUp}
onPointerMove={(e) => handleMouseMove(e, "SIGNATURE")}
onPointerUp={() => handleMouseUp()}
onPointerLeave={() => handleMouseUp()}
/>
</div>
</div>
{/* Initials Canvas */}
<div className="lg:col-span-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors">
<div className="md:col-span-4">
<label className="block text-sm font-medium mb-2">Initials
<span className='text-red-600'> *</span>
</label>
<div className="border-1 border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors">
<canvas
ref={canvasRef2}
className="w-full h-51 cursor-crosshair"
onPointerDown={handleMouseDown}
onPointerMove={handleMouseMove}
onPointerUp={handleMouseUp}
onPointerLeave={handleMouseUp}
onPointerMove={(e) => handleMouseMove(e, "INITIAL")}
onPointerUp={() => handleMouseUp()}
onPointerLeave={() => handleMouseUp()}
/>
</div>
</div>
</div>
{/* Controls */}
<div className="flex gap-4 mt-3">
<button
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
onClick={clearCanvas}
>
Clear All
</button>
<button
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
onClick={undoLastPath}
disabled={!activeCanvas || (activeCanvas === 'full' ? paths1.length === 0 : paths2.length === 0)}
>
Undo Last
</button>
</div>
</div>
</div>
);

View File

@ -71,18 +71,17 @@ const SignatureManager: React.FC = () => {
setLoading(true)
try {
let res = await api.delete(`/api/user/signatures/${id}`)
toast.success("Signature Delete Successfull", {
position: "top-center",
autoClose: 500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// toast.success("Signature Delete Successfull", {
// position: "top-center",
// autoClose: 500,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
await getAllSignatures();
} catch (error) {
console.log("delete signature", error)
@ -116,7 +115,7 @@ const SignatureManager: React.FC = () => {
if (mode === 'initials') {
const initials = `${(firstName?.[0] ?? '')}${(lastName?.[0] ?? '')}`.toUpperCase();
return (
<div className={`text-center text-[24px]`} style={{fontFamily:selectedFont?.style}}>
<div className={`text-center text-[24px]`} style={{ fontFamily: selectedFont?.style }}>
{initials}
</div>
);
@ -124,7 +123,7 @@ const SignatureManager: React.FC = () => {
const fullName = `${firstName ?? ''} ${lastName ?? ''}`.trim();
return (
<div className={`text-center text-[20px]`}style={{fontFamily:selectedFont?.style}}>
<div className={`text-center text-[20px]`} style={{ fontFamily: selectedFont?.style }}>
<div>{fullName}</div>
</div>
);
@ -146,32 +145,33 @@ const SignatureManager: React.FC = () => {
<div className="max-w-[84vw] mx-auto ">
<>
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div>
<h1 className="text-[17px] font-medium text-gray-500 mb-2">Signatures</h1>
<p className="text-gray-300 text-sm">Add or update your name and signature styles.</p>
<div className="flex items-end justify-between mb-3">
<div className="flex flex-col">
<p className="text-lg font-semibold">Signatures</p>
<p className="text-xs text-slate-400 mt-1">Add or update your name and signature styles.</p>
</div>
<button className="bg-gradient-to-br from-[var(--primary)] to-blue-600 hover:bg-[var(--modalSubmitBGHover)] text-white px-3 gap-1 py-1.5 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={handleAddNew}>
<FiPlus className="" />
ADD
<button className="bg-[var(--mainPrimaryBtn)] text-white px-3.5 gap-1 py-2 rounded-lg text-sm font-semibold flex items-center cursor-pointer transition-all duration-200 transform hover:-translate-y-0.5"
onClick={handleAddNew}>
<FiPlus className="text-lg font-semibold" />
Add Signature
</button>
</div>
{/* Table Card */}
<div className="overflow-x-auto">
<div className="bg-white rounded-md min-w-[800px] md:min-w-full overflow-hidden border border-gray-100">
<div className="overflow-x-auto mt-6">
<div className="bg-white min-w-[800px] md:min-w-full overflow-hidden">
{/* Table Header */}
<div className="bg-gray-50 border-b border-gray-200 ">
<div className="grid grid-cols-12 gap-4 px-6 py-4">
<div className="col-span-5">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Signature</h3>
<div className="bg-gray-50 rounded-lg">
<div className="grid grid-cols-12 gap-4 px-6 py-2">
<div className="col-span-6">
<h3 className="text-sm font-semibold">Signature</h3>
</div>
<div className="col-span-5">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Initial</h3>
<div className="col-span-4">
<h3 className="text-sm font-semibold">Initial</h3>
</div>
<div className="col-span-2">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Actions</h3>
<h3 className="text-sm font-semibold ml-4">Action</h3>
</div>
</div>
</div>
@ -179,17 +179,17 @@ const SignatureManager: React.FC = () => {
{/* Table Body */}
<div className="divide-y divide-gray-100">
{signatures.map((signature, index) => (
<div key={signature.id} className="grid grid-cols-12 gap-4 px-6 py-9 hover:bg-gray-50 transition-colors duration-150">
<div key={signature.id} className="grid grid-cols-12 gap-4 py-4 transition-colors duration-150">
{/* Signature Column */}
<div className="col-span-5">
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-blue-600 rounded-bl-2xl rounded-tl-2xl">
<div className="col-span-6">
<div className="flex flex-col space-y-2 relative ps-6 bg-slate-50 p-4 rounded-lg">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
Signed by:
<div className="text-xs text-gray-500 absolute top-[8px] left-[15px]">
Signed by
</div>
{/* Signature Image or Name/Initials */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center">
<div className="rounded-lg p-1 my-4 flex items-center justify-center">
{signature?.signatureType !== 'SELECT' ? (
<img
src={`data:${signature?.signatureMimeType};base64,${signature?.signatureBlob}`}
@ -202,7 +202,7 @@ const SignatureManager: React.FC = () => {
</div>
{/* Signature ID */}
<div className="text-xs text-gray-500 truncate">
<div className="text-xs text-gray-500 truncate absolute bottom-[8px] left-[15px] flex-nowrap max-w-[30ch] md:max-w-[30ch] lg:max-w-[50ch] overflow-hidden text-ellipsis">
{signature.signatureHash}
</div>
</div>
@ -210,15 +210,15 @@ const SignatureManager: React.FC = () => {
</div>
{/* Initial Column */}
<div className="col-span-5">
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
<div className="col-span-4">
<div className="flex flex-col space-y-2 relative ps-6 bg-slate-50 p-4 rounded-lg">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
Initials
<div className="text-xs text-gray-500 absolute top-[8px] left-[15px]">
Initial
</div>
{/* Signature Initials: Image or Generated Text */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center">
<div className="rounded-lg p-1 my-4 flex items-center justify-center">
{signature?.signatureType !== 'SELECT' ? (
<img
src={`data:${signature.initialsMimeType};base64,${signature.initialsBlob}`}
@ -231,43 +231,28 @@ const SignatureManager: React.FC = () => {
</div>
{/* Initial ID */}
<div className="text-xs text-gray-500 truncate">
{signature.initialsHash}
<div className="text-xs text-gray-500 truncate absolute bottom-[8px] left-[15px] flex-nowrap sm:max-w-[10ch] md:max-w-[20ch] lg:max-w-[30ch] overflow-hidden text-ellipsis">
{signature.signatureHash}
</div>
</div>
</div>
{/* Actions Column */}
<div className="col-span-2 flex justify-between items-start pt-9 ps-8 ">
<div className="relative ">
<div className="col-span-2">
<div className="relative flex justify-start gap-4">
<button
onClick={() => toggleDropdown(signature.id)}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-1.5 py-1.5 text-[13px] rounded-lg font-semibold flex items-center transition-all duration-200 border border-gray-300 "
onClick={() => handleEdit(signature.id)}
className="max-w-max text-gray-500 text-lg"
>
ACTIONS
<FiMoreVertical />
<FiEdit3 />
</button>
<button
onClick={() => handleDelete(signature.id)}
className="max-w-max text-red-500 text-lg"
>
<FiTrash2 />
</button>
{/* Dropdown Menu */}
{activeDropdown === signature.id && (
<div className={`absolute right-0 mt-0.5 -top-5 w-28 bg-white rounded-lg border border-gray-200 z-10 py-1`}>
<button
onClick={() => handleEdit(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-blue-50 hover:text-blue-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiEdit3 />
Edit
</button>
<button
onClick={() => handleDelete(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-red-50 hover:text-red-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiTrash2 />
Delete
</button>
</div>
)}
</div>
</div>
</div>

View File

@ -20,36 +20,36 @@ interface UserProfileProps {
const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
return (
<div className="bg-white rounded-md shadow-sm flex flex-col sm:flex-row items-center sm:items-start w-full p-6 lg:gap-6 ">
{/* Profile Picture */}
<div className="flex-shrink-0 text-center">
<div className="w-20 h-20 bg-gradient-to-br from-[var(--primary)] to-blue-600 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
{userData.name.slice(0, 2).toUpperCase()}
<div className="bg-white rounded-md flex flex-col items-center sm:items-start w-full p-6 lg:gap-4 ">
<div className="flex flex-col">
<p className="text-lg font-semibold">Personal Information</p>
<p className="text-xs text-slate-400 mt-1">Basic info, like your name and email address.</p>
</div>
<div className="bg-slate-50 w-full px-3 py-2 rounded-lg">
<p className="text-sm font-semibold">Information</p>
</div>
<div className="w-full border-gray-200 rounded-md bg-white font-sans">
<div className="sm:flex sm:flex-col md:grid md:grid-cols-[200px_1fr] lg:grid-cols-[300px_1fr] border-b border-gray-100 px-4 py-4">
<span className="text-gray-600">Full Name :</span>
<span className="font-medium text-black">{userData.name}</span>
</div>
<div className="sm:flex sm:flex-col md:grid md:grid-cols-[200px_1fr] lg:grid-cols-[300px_1fr] border-b border-gray-100 px-4 py-4">
<span className="text-gray-600">Email Address :</span>
<span className="font-medium text-black">{userData.email}</span>
</div>
<div className="sm:flex sm:flex-col md:grid md:grid-cols-[200px_1fr] lg:grid-cols-[300px_1fr] px-4 py-4">
<span className="text-gray-600">Account Name :</span>
<span className="font-medium text-black">{userData.account.name}</span>
</div>
</div>
{/* Info Fields */}
<div className="flex flex-col justify-center flex-1 w-full">
{/* Row: Name */}
<div className="flex flex-col sm:flex-row py-1">
<div className="text-gray-500 font-medium text-[13px] sm:w-32">Name</div>
<div className="text-gray-400 font-medium text-[13px]">{userData.name}</div>
</div>
{/* Row: Email */}
<div className="flex flex-col sm:flex-row py-1">
<div className="text-gray-500 font-medium text-[13px] sm:w-32">Email</div>
<div className="text-gray-400 font-medium text-[13px]">{userData.email}</div>
</div>
{/* Row: Account Name */}
<div className="flex flex-col sm:flex-row py-1">
<div className="text-gray-500 font-medium text-[13px] sm:w-32">Account name</div>
<div className="text-gray-400 font-medium text-[13px]">
{userData.account.name}
</div>
</div>
</div>
</div>
);
};

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ interface SignatureConProps {
const SignatureCon: React.FC<SignatureConProps> = ({ recipientId, openSign, setOpenSign, savedSignature, setSavedSignature }) => {
const handleSaveSignature = (signatureDataUrl: string) => {
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/signature?recipientId=${recipientId}`,
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/${recipientId}/signatures`,
{
signature: signatureDataUrl,
initials: signatureDataUrl,

View File

@ -0,0 +1,43 @@
import { Dispatch } from "@reduxjs/toolkit";
import { useNavigate } from "react-router-dom";
type PropsType = {
setShowTemplateDeleteWarnModal: React.Dispatch<{show: boolean, templateId: string}>;
onConfirm: () => void;
};
const TemplateDeleteWarnModal = ({ setShowTemplateDeleteWarnModal, onConfirm }: PropsType) => {
const navigate = useNavigate()
return (
<div className="fixed inset-0 backdrop-blur-sm bg-black/30 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-100">
<h3 className="text-lg font-medium text-gray-800 mb-4">Confirm Self Sign</h3>
<p className="text-gray-600 mb-6">
Are you sure you want to proceed with self sign? This will remove all the recipient that you added.
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setShowTemplateDeleteWarnModal({show: false, templateId: ""});
}}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
No
</button>
<button
onClick={() => {
onConfirm();
}}
className="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded-md transition-colors"
>
Yes
</button>
</div>
</div>
</div>
);
};
export default TemplateDeleteWarnModal;

View File

@ -6,6 +6,7 @@ import LoaderUi from '../../helper/LoaderUi';
import api from '../../config/api';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { TemplateInterface } from '../../utlity/types';
import TemplateDeleteWarnModal from './TemplateDeleteWarnModal';
const TemplateTable = () => {
const [loading, setLoading] = useState(false)
@ -13,6 +14,7 @@ const TemplateTable = () => {
const accountDetails = useSelector((state: any) => state.user.account);
const accountId = accountDetails.id || "";
const [showMenu, setShowMenu] = useState(null);
const [showTemplateDeleteWarnModal, setShowTemplateDeleteWarnModal] = useState<{ show: boolean, templateId: string }>({ show: false, templateId: "" });
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
@ -99,6 +101,16 @@ const TemplateTable = () => {
setShowMenu(null);
navigate(`/process-step-one/${accountId}/template/${templateId}`);
}
const handleDeleteClick = async (templateId: string) => {
setShowMenu(null);
try {
await api.delete(`api/accounts/${activeAccountId}/templates/${templateId}`)
setTemplates((prev) => {
return prev.filter((temp) => temp.id !== Number(templateId));
});
setShowTemplateDeleteWarnModal({ show: false, templateId: "" })
} catch (e) { }
}
function formatTimestamp(isoString, options = { timeZone: 'local' }) {
// Parse the ISO string to a Date object
@ -199,6 +211,7 @@ const TemplateTable = () => {
}
}}
>
<button
className="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
onClick={() =>
@ -217,14 +230,14 @@ const TemplateTable = () => {
Use
</button>
<button
onMouseDown={() => handleEditClick(item.id)}
onClick={() => handleEditClick(item.id)}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-yellow-50 hover:text-yellow-600 transition-colors"
>
Edit
</button>
<button
onMouseDown={() => {
setShowMenu(null);
onClick={() => {
setShowTemplateDeleteWarnModal({ show: true, templateId: item.id+"" })
}}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-red-50 hover:text-red-600 transition-colors"
>
@ -256,6 +269,13 @@ const TemplateTable = () => {
)}
</table>
</div>
{
showTemplateDeleteWarnModal.show && <TemplateDeleteWarnModal
setShowTemplateDeleteWarnModal={setShowTemplateDeleteWarnModal}
onConfirm={() => handleDeleteClick(showTemplateDeleteWarnModal.templateId)}
/>
}
</div>
</>
)

View File

@ -28,7 +28,7 @@ const Templates = () => {
const filters = ["All", "My Templates", "Shared"];
const handleAddTemplate = async () => {
try {
@ -48,7 +48,7 @@ const Templates = () => {
return (
<div className="p-3 text-sm">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3 px-4">
{/* Filter as pill buttons */}
<div className="flex flex-wrap gap-2">
{filters.map((filter) => (
@ -80,8 +80,10 @@ const Templates = () => {
{/* Table */}
<section className="my-4 mx-3"></section>
<TemplateTable />
{/* <section className="my-4 mx-3"></section> */}
<div className="px-4">
<TemplateTable />
</div>
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useLayoutEffect, useState } from "react";
import {
FiX,
FiCheckCircle,
@ -47,7 +47,7 @@ const AuditModal: React.FC<AuditModalProps> = ({ closeModal }) => {
let [auditStatus, setAuditStatus] = useState([])
let [loading, setLoading] = useState(false)
const getAuditDetails = async () => {
setLoading(true)
// setLoading(true)
try {
let res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/audit?lang=${userDetails.language || ''}`)
// let res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/audit?lang=hi`)
@ -63,8 +63,9 @@ const AuditModal: React.FC<AuditModalProps> = ({ closeModal }) => {
}
}
useEffect(() => {
useLayoutEffect(() => {
if (accountId && transactionId) {
setLoading(true)
getAuditDetails()
}
}, [accountId, transactionId])
@ -272,13 +273,23 @@ const AuditModal: React.FC<AuditModalProps> = ({ closeModal }) => {
))}
</div>
) : (
<div className="flex justify-center items-center h-full">
<img
src={logoRecord}
alt="Record Not Found"
className="w-64 h-64 opacity-70 object-contain"
/>
</div>
<>
{loading
? <>
<div className="flex justify-center items-center h-full">
F e t c h i n g . . .
</div>
</> :
<>
<div className="flex justify-center items-center h-full">
<img
src={logoRecord}
alt="Record Not Found"
className="w-64 h-64 opacity-70 object-contain"
/>
</div>
</>}
</>
)}
</div>

View File

@ -12,6 +12,9 @@ import { RootState } from '../../store/store';
import { SignTypeEnum } from '../../utlity/types';
import AuditModal from './AuditModal';
import { FaFile } from 'react-icons/fa6';
import { IoMdDownload } from 'react-icons/io';
import downloadDocumentUtils from '../../utlity/downloadDocumentsUtils';
import signSessionRedirect from '../../utlity/signSessionRedirect';
// Sample recipient data — you can replace this with actual API data later
const recipients = [
@ -111,7 +114,7 @@ const TransactionView = () => {
const handleNavigateToSign = (id: number) => {
window.open(`./../../../../sign/${id}/${SignTypeEnum.selfSign}`)
signSessionRedirect(accountId, transactionId, id+"" as string)
}
const getStatusBadge = (status: string) => {
@ -159,9 +162,9 @@ const TransactionView = () => {
More
</button>
<Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-one/${accountId}/${transactionId}`}>
{transactionDetails.transaction.status === "CREATED" && <Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-one/${accountId}/${transactionId}`}>
Edit
</Link>
</Link>}
<button className='cursor-pointer border text-[var(--primary)] border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' onClick={handleOpenAuditModal}>
Audit
@ -193,7 +196,7 @@ const TransactionView = () => {
<div className="flex flex-col sm:flex-row gap-4 sm:items-center">
<div>
<p className="text-slate-600 font-semibold text-xs sm:xs">{recipient.name}</p>
<p className="text-slate-600 text-xs sm:text-xs">{recipient.email}</p>
<p className="text-slate-600 text-xs sm:text-xs">{recipient.email} | {recipient.type}</p>
{recipient.hostEmail && (
<p className="text-xs text-gray-500 italic">
<strong>Host:</strong> {recipient.hostEmail}
@ -271,7 +274,12 @@ const TransactionView = () => {
<div className='bg-slate-100 p-2 absolute w-full bottom-[0px]'>
<p className='text-[0.7rem] text-slate-500 flex justify-end items-center'>
{/* <span> 1 of {docsD?.pages?.length}</span> */}
<span className='cursor-pointer '><Link to={`/process-step-two/${accountId}/${transactionId}`}><MdEditDocument size={16} /></Link></span>
<IoMdDownload
className='cursor-pointer'
onClick={() => downloadDocumentUtils(accountId, transactionId, docsD.id)}
size={18} />
</p>
</div>

View File

@ -9,11 +9,38 @@ const api = axios.create({
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "69420",
"Access-Control-Allow-Origin": "*"
},
validateStatus: (status) => status >= 200 && status < 400,
});
// Token storage utilities
export const TokenStorage = {
getAccessToken: () => localStorage.getItem('access_token'),
getRefreshToken: () => localStorage.getItem('refresh_token'),
setTokens: (accessToken: string, refreshToken: string) => {
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
},
clearTokens: () => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
};
// Request interceptor - Add access token to headers
api.interceptors.request.use(
(config) => {
const accessToken = TokenStorage.getAccessToken();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Flag to prevent infinite loop
let isRefreshing = false;
@ -26,11 +53,22 @@ api.interceptors.response.use(
return response;
},
async error => {
const originalRequest = error.config;
// const res = error.response;
// 🔹 Handle 301/302 manually
// if (res && (res.status === 301 || res.status === 302)) {
// const redirectUrl = res.headers["location"];
// if (redirectUrl) {
// window.location.href = redirectUrl;
// }
// }
// Prevent multiple refresh attempts
// if (error.response?.status === 401 || error.response?.status === 500) {
if (error.response?.status === 401) {
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
if (!isRefreshing) {

View File

@ -4,7 +4,7 @@ import { RootState } from "../store/store";
/ Create an Axios instance
// Create an Axios instance
const useAxios = () => {
const user = useSelector((state: RootState) => state.user);
const instance = axios.create({

View File

@ -11,7 +11,7 @@ const LoaderUi = ({ classStyle = '' }: StyleProps) => {
return (
<div className={`absolute inset-0 flex items-center justify-center z-50 ${classStyle}`}>
{/* Overlay */}
<div className="absolute inset-0 bg-black/30 opacity-60"></div>
<div className="absolute inset-0 opacity-60"></div>
{/* Loader box */}
<div className="w-60 relative z-10 bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg">

View File

@ -10,22 +10,134 @@ import { RiDropdownList } from 'react-icons/ri';
import { MdEmail, MdBusiness } from 'react-icons/md';
import { FaUser } from 'react-icons/fa';
// const typeToIconMap = {
// TEXT: <RxText />,
// SIGNATURE: <AiFillSignature />,
// INITIALS: <TbSignature />,
// TEMPORAL: <CiCalendarDate />,
// NUMBER: <MdConfirmationNumber />,
// IMAGE: <FaRegImage />,
// STAMP: <FaStamp />,
// CHECKBOX: <IoMdCheckboxOutline />,
// SELECT: <RiDropdownList />,
// FILE: <FaFileAlt />,
// ATTACHMENT: <FaFileAlt />,
// RADIO: <MdRadioButtonChecked />,
// EMAIL: <MdEmail />,
// NAME: <FaUser />,
// COMPANY: <MdBusiness />,
// };
const typeToIconMap = {
TEXT: <RxText />,
SIGNATURE: <AiFillSignature />,
INITIALS: <TbSignature />,
TEMPORAL: <CiCalendarDate />,
NUMBER: <MdConfirmationNumber />,
TEXT:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36.6673 18.8333H25.0007C24.5586 18.8333 24.1347 19.0089 23.8221 19.3215C23.5096 19.634 23.334 20.058 23.334 20.5V22.8333C23.334 23.2753 23.5096 23.6993 23.8221 24.0118C24.1347 24.3244 24.5586 24.5 25.0007 24.5C25.4427 24.5 25.8666 24.3244 26.1792 24.0118C26.4917 23.6993 26.6673 23.2753 26.6673 22.8333V22.1666H29.1673V31.6666H27.334C26.892 31.6666 26.468 31.8422 26.1555 32.1548C25.8429 32.4674 25.6673 32.8913 25.6673 33.3333C25.6673 33.7753 25.8429 34.1993 26.1555 34.5118C26.468 34.8244 26.892 35 27.334 35H34.334C34.776 35 35.1999 34.8244 35.5125 34.5118C35.8251 34.1993 36.0007 33.7753 36.0007 33.3333C36.0007 32.8913 35.8251 32.4674 35.5125 32.1548C35.1999 31.8422 34.776 31.6666 34.334 31.6666H32.5007V22.1666H35.0007V22.8333C35.0007 23.2753 35.1762 23.6993 35.4888 24.0118C35.8014 24.3244 36.2253 24.5 36.6673 24.5C37.1093 24.5 37.5333 24.3244 37.8458 24.0118C38.1584 23.6993 38.334 23.2753 38.334 22.8333V20.5C38.334 20.058 38.1584 19.634 37.8458 19.3215C37.5333 19.0089 37.1093 18.8333 36.6673 18.8333Z" fill="#FBC249" />
<path d="M22.5927 31.6667H18.3327V8.33333H28.3327V9.62833C28.3327 10.0704 28.5083 10.4943 28.8208 10.8068C29.1334 11.1194 29.5573 11.295 29.9993 11.295C30.4414 11.295 30.8653 11.1194 31.1779 10.8068C31.4904 10.4943 31.666 10.0704 31.666 9.62833V6.66667C31.666 6.22464 31.4904 5.80072 31.1779 5.48816C30.8653 5.17559 30.4414 5 29.9993 5H3.33268C2.89065 5 2.46673 5.17559 2.15417 5.48816C1.84161 5.80072 1.66602 6.22464 1.66602 6.66667V9.62833C1.66602 10.0704 1.84161 10.4943 2.15417 10.8068C2.46673 11.1194 2.89065 11.295 3.33268 11.295C3.77471 11.295 4.19863 11.1194 4.51119 10.8068C4.82375 10.4943 4.99935 10.0704 4.99935 9.62833V8.33333H14.9993V31.6667H10.7393C10.2973 31.6667 9.8734 31.8423 9.56084 32.1548C9.24828 32.4674 9.07268 32.8913 9.07268 33.3333C9.07268 33.7754 9.24828 34.1993 9.56084 34.5118C9.8734 34.8244 10.2973 35 10.7393 35H22.5927C23.0347 35 23.4586 34.8244 23.7712 34.5118C24.0838 34.1993 24.2593 33.7754 24.2593 33.3333C24.2593 32.8913 24.0838 32.4674 23.7712 32.1548C23.4586 31.8423 23.0347 31.6667 22.5927 31.6667Z" fill="#0D6EFD" />
</svg>
,
SIGNATURE:
<svg width="40" height="28" viewBox="0 0 40 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.0801 17.5987C21.5418 19.6325 18.4018 20.647 14.7059 20.647C13.0573 20.647 11.6623 20.2612 10.5509 19.6122C9.23383 20.2583 7.82453 20.647 6.47062 20.647C2.39344 20.647 0 18.453 0 14.7647C0 14.1149 0.52672 13.5882 1.17649 13.5882C1.82625 13.5882 2.35297 14.1149 2.35297 14.7647C2.35297 17.1057 3.64945 18.2941 6.47062 18.2941C7.1664 18.2941 7.89992 18.1431 8.62586 17.8792C7.57188 16.4205 7.22195 14.5556 7.68218 12.7146C8.42484 9.74382 10.361 7.70585 12.9412 7.70585C16.0278 7.70585 17.2912 11.6365 15.7581 14.7026C15.151 15.9167 14.1543 17.0938 12.9456 18.0758C13.4687 18.2168 14.0559 18.2941 14.7059 18.2941C17.8277 18.2941 20.4095 17.4789 22.4945 15.8528L24.1591 9.74928C24.2129 9.55178 24.3174 9.3717 24.4622 9.22694L33.2857 0.403428C33.7451 -0.0560254 34.49 -0.0560254 34.9495 0.403428C35.4089 0.862881 35.4089 1.6078 34.9495 2.06718L27.3366 9.68007C27.98 10.045 28.5577 10.4822 29.0671 10.9917C29.5766 11.5012 30.0138 12.0789 30.3787 12.7222L37.9916 5.10936C38.451 4.64991 39.1959 4.64991 39.6554 5.10936C40.1148 5.56874 40.1148 6.31366 39.6554 6.77311L30.8319 15.5966C30.6871 15.7414 30.507 15.8458 30.3095 15.8997L24.0801 17.5987ZM26.0741 11.6702L25.2062 14.8526L28.3886 13.9848C28.1148 13.4805 27.7864 13.0384 27.4034 12.6554C27.0204 12.2725 26.5784 11.9441 26.0741 11.6702ZM13.6536 13.6503C14.5119 11.9338 13.9092 10.0588 12.9412 10.0588C11.6096 10.0588 10.4722 11.2561 9.96492 13.2854C9.64672 14.5583 9.92117 15.831 10.7534 16.7749C12.0156 15.9148 13.0857 14.7862 13.6536 13.6503Z" fill="#0D6EFD" />
<path d="M0 26.1765C0 26.8262 0.52672 27.353 1.17649 27.353H21.1765C21.8263 27.353 22.353 26.8262 22.353 26.1765C22.353 25.5267 21.8263 25 21.1765 25H1.17649C0.52672 25 0 25.5267 0 26.1765Z" fill="#FBC249" />
</svg>
,
INITIALS: <svg width="39" height="36" viewBox="0 0 39 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.91844 27.3C4.31844 27.3 3.10844 26.85 2.28844 25.95C1.46844 25.03 1.05844 23.72 1.05844 22.02V20.82H4.17844V22.26C4.17844 23.62 4.74844 24.3 5.88844 24.3C6.44844 24.3 6.86844 24.14 7.14844 23.82C7.44844 23.48 7.59844 22.94 7.59844 22.2C7.59844 21.32 7.39844 20.55 6.99844 19.89C6.59844 19.21 5.85844 18.4 4.77844 17.46C3.41844 16.26 2.46844 15.18 1.92844 14.22C1.38844 13.24 1.11844 12.14 1.11844 10.92C1.11844 9.26 1.53844 7.98 2.37844 7.08C3.21844 6.16 4.43844 5.7 6.03844 5.7C7.61844 5.7 8.80844 6.16 9.60844 7.08C10.4284 7.98 10.8384 9.28 10.8384 10.98V11.85H7.71844V10.77C7.71844 10.05 7.57844 9.53 7.29844 9.21C7.01844 8.87 6.60844 8.7 6.06844 8.7C4.96844 8.7 4.41844 9.37 4.41844 10.71C4.41844 11.47 4.61844 12.18 5.01844 12.84C5.43844 13.5 6.18844 14.3 7.26844 15.24C8.64844 16.44 9.59844 17.53 10.1184 18.51C10.6384 19.49 10.8984 20.64 10.8984 21.96C10.8984 23.68 10.4684 25 9.60844 25.92C8.76844 26.84 7.53844 27.3 5.91844 27.3ZM12.7905 6H17.5005L19.6005 21.03H19.6605L21.7605 6H26.4705V27H23.3505V11.1H23.2905L20.8905 27H18.1305L15.7305 11.1H15.6705V27H12.7905V6ZM28.9331 6H37.9331V9H32.2331V14.55H36.7631V17.55H32.2331V24H37.9331V27H28.9331V6Z" fill="#0D6EFD" />
<path d="M0.398438 29.4H38.5931V31.8H0.398438V29.4Z" fill="#0D6EFD" />
<rect y="29" width="39" height="3" fill="#FBC249" />
</svg>,
TEMPORAL:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3333 3.33334V8.33334M26.6667 3.33334V8.33334M5.83333 15.15H34.1667M35 14.1667V28.3333C35 33.3333 32.5 36.6667 26.6667 36.6667H13.3333C7.5 36.6667 5 33.3333 5 28.3333V14.1667C5 9.16668 7.5 5.83334 13.3333 5.83334H26.6667C32.5 5.83334 35 9.16668 35 14.1667Z" stroke="#0D6EFD" stroke-width="2.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
<path d="M26.1583 22.8333H26.1733M26.1583 27.8333H26.1733M19.9916 22.8333H20.0083M19.9916 27.8333H20.0083M13.8233 22.8333H13.84M13.8233 27.8333H13.84" stroke="#FBC249" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
,
NUMBER:
<svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.6157 2L21.8867 34.5769" stroke="#FBC249" stroke-width="3.38462" stroke-linecap="round" />
<path d="M15.6528 2L6.92383 34.5769" stroke="#FBC249" stroke-width="3.38462" stroke-linecap="round" />
<path d="M34.9991 13H3.69141" stroke="#0D6EFD" stroke-width="3.38462" stroke-linecap="round" />
<path d="M33.3077 24.8462H2" stroke="#0D6EFD" stroke-width="3.38462" stroke-linecap="round" />
</svg>
,
IMAGE: <FaRegImage />,
STAMP: <FaStamp />,
CHECKBOX: <IoMdCheckboxOutline />,
SELECT: <RiDropdownList />,
STAMP: <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_575_574)">
<mask id="mask0_575_574" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40">
<path d="M0 3.8147e-06H40V40H0V3.8147e-06Z" fill="white" />
</mask>
<g mask="url(#mask0_575_574)">
<path d="M32.8125 29.7656H7.1875C7.1875 26.3138 9.9857 23.5156 13.4375 23.5156H26.5625C30.0143 23.5156 32.8125 26.3138 32.8125 29.7656Z" stroke="#0D6EFD" stroke-width="2.34375" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
<path d="M37.2656 38.8281H2.73437C1.87141 38.8281 1.17188 38.1285 1.17188 37.2656V31.3281C1.17188 30.4651 1.87141 29.7656 2.73437 29.7656H37.2656C38.1286 29.7656 38.8281 30.4651 38.8281 31.3281V37.2656C38.8281 38.1285 38.1286 38.8281 37.2656 38.8281Z" stroke="#FBC249" stroke-width="2.34375" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
<path d="M26.8428 8.924C27.3573 4.80775 24.1476 1.17182 19.9993 1.17182C15.8509 1.17182 12.6413 4.80775 13.1558 8.924L14.9797 23.5156H25.0188L26.8428 8.924Z" stroke="#0D6EFD" stroke-width="2.34375" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
</g>
</g>
<defs>
<clipPath id="clip0_575_574">
<rect width="40" height="40" fill="white" />
</clipPath>
</defs>
</svg>
,
CHECKBOX:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3341 26.25C18.1699 26.2504 18.0072 26.2182 17.8556 26.1553C17.704 26.0923 17.5664 25.9999 17.4507 25.8833L12.4507 20.8833C12.2299 20.6464 12.1097 20.333 12.1154 20.0091C12.1211 19.6853 12.2523 19.3763 12.4814 19.1473C12.7104 18.9183 13.0193 18.7871 13.3432 18.7814C13.667 18.7757 13.9804 18.8959 14.2174 19.1167L18.3341 23.2317L27.4507 14.1167C27.6877 13.8959 28.0011 13.7757 28.3249 13.7814C28.6488 13.7871 28.9577 13.9183 29.1868 14.1473C29.4158 14.3763 29.547 14.6853 29.5527 15.0091C29.5584 15.333 29.4382 15.6464 29.2174 15.8833L19.2174 25.8833C19.1018 25.9999 18.9641 26.0923 18.8125 26.1553C18.6609 26.2182 18.4982 26.2504 18.3341 26.25Z" fill="#FBC249" />
<path d="M30.0007 37.9167H10.0007C7.90183 37.914 5.88973 37.0791 4.40564 35.595C2.92156 34.1109 2.08663 32.0988 2.08398 30V10C2.08663 7.90119 2.92156 5.88909 4.40564 4.405C5.88973 2.92091 7.90183 2.08599 10.0007 2.08334H30.0007C32.0995 2.08599 34.1116 2.92091 35.5957 4.405C37.0798 5.88909 37.9147 7.90119 37.9173 10V30C37.9147 32.0988 37.0798 34.1109 35.5957 35.595C34.1116 37.0791 32.0995 37.914 30.0007 37.9167ZM10.0007 4.58334C8.5646 4.58511 7.18788 5.15636 6.17244 6.1718C5.157 7.18724 4.58575 8.56396 4.58398 10V30C4.58575 31.4361 5.157 32.8128 6.17244 33.8282C7.18788 34.8437 8.5646 35.4149 10.0007 35.4167H30.0007C31.4367 35.4149 32.8134 34.8437 33.8289 33.8282C34.8443 32.8128 35.4156 31.4361 35.4173 30V10C35.4156 8.56396 34.8443 7.18724 33.8289 6.1718C32.8134 5.15636 31.4367 4.58511 30.0007 4.58334H10.0007Z" fill="#0D6EFD" />
</svg>
,
SELECT:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 34.1666C19.6667 34.1666 19.3333 34 19 33.8333L5.66666 23.8333C4.99999 23.3333 4.83332 22.1666 5.33332 21.5C5.83332 20.8333 6.99999 20.6666 7.66666 21.1666L20 30.3333L32.3333 21.1666C33 20.6666 34.1667 20.8333 34.6667 21.5C35.1667 22.1666 35 23.3333 34.3333 23.8333L21 33.8333C20.6667 34 20.3333 34.1666 20 34.1666Z" fill="#0D6EFD" />
<path d="M33.3333 9.16665H6.66667C5.66667 9.16665 5 8.49998 5 7.49998C5 6.49998 5.66667 5.83331 6.66667 5.83331H33.3333C34.3333 5.83331 35 6.49998 35 7.49998C35 8.49998 34.3333 9.16665 33.3333 9.16665Z" fill="#FBC249" />
<path d="M33.3333 15.8333H6.66667C5.66667 15.8333 5 15.1667 5 14.1667C5 13.1667 5.66667 12.5 6.66667 12.5H33.3333C34.3333 12.5 35 13.1667 35 14.1667C35 15.1667 34.3333 15.8333 33.3333 15.8333Z" fill="#FBC249" />
</svg>
,
FILE: <FaFileAlt />,
ATTACHMENT: <FaFileAlt />,
RADIO: <MdRadioButtonChecked />,
EMAIL: <MdEmail />,
NAME: <FaUser />,
COMPANY: <MdBusiness />,
ATTACHMENT:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.3329 19.6667L17.9829 22.0167C16.6829 23.3167 16.6829 25.4333 17.9829 26.7333C19.2829 28.0333 21.3996 28.0333 22.6996 26.7333L26.3996 23.0333C27.6482 21.7811 28.3493 20.085 28.3493 18.3167C28.3493 16.5484 27.6482 14.8522 26.3996 13.6C25.1474 12.3514 23.4513 11.6503 21.6829 11.6503C19.9146 11.6503 18.2185 12.3514 16.9663 13.6L12.9329 17.6333C12.4019 18.1639 11.9806 18.794 11.6932 19.4875C11.4058 20.181 11.2578 20.9243 11.2578 21.675C11.2578 22.4257 11.4058 23.169 11.6932 23.8625C11.9806 24.556 12.4019 25.1861 12.9329 25.7167" stroke="#FBC249" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M15.0007 36.6667H25.0007C33.334 36.6667 36.6673 33.3333 36.6673 25V15C36.6673 6.66668 33.334 3.33334 25.0007 3.33334H15.0007C6.66732 3.33334 3.33398 6.66668 3.33398 15V25C3.33398 33.3333 6.66732 36.6667 15.0007 36.6667Z" stroke="#0D6EFD" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
,
RADIO:
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 1.125C27.3139 1.125 34.875 8.68612 34.875 18C34.875 27.3139 27.3139 34.875 18 34.875C8.68612 34.875 1.125 27.3139 1.125 18C1.125 8.68612 8.68612 1.125 18 1.125ZM18 3.375C9.92812 3.375 3.375 9.92812 3.375 18C3.375 26.0719 9.92812 32.625 18 32.625C26.0719 32.625 32.625 26.0719 32.625 18C32.625 9.92812 26.0719 3.375 18 3.375Z" fill="#0D6EFD" />
<path d="M27 18C27 13.0331 22.9669 9 18 9C13.0331 9 9 13.0331 9 18C9 22.9669 13.0331 27 18 27C22.9669 27 27 22.9669 27 18Z" fill="#FBC249" />
</svg>
,
EMAIL:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8903 4.5H26.1098C28.845 4.49996 31.0496 4.49992 32.7836 4.73304C34.5838 4.97508 36.0994 5.49286 37.3034 6.6967C38.5072 7.90054 39.025 9.41628 39.267 11.2165C39.5 12.9504 39.5 15.1551 39.5 17.8903V22.1098C39.5 24.845 39.5 27.0496 39.267 28.7836C39.025 30.5838 38.5072 32.0994 37.3034 33.3034C36.0994 34.5072 34.5838 35.025 32.7836 35.267C31.0496 35.5 28.845 35.5 26.1098 35.5H13.8903C11.1551 35.5 8.95044 35.5 7.2165 35.267C5.41628 35.025 3.90054 34.5072 2.6967 33.3034C1.49287 32.0994 0.975079 30.5838 0.733047 28.7836C0.499925 27.0496 0.499959 24.845 0.500001 22.1098V17.8903C0.499959 15.1551 0.499925 12.9504 0.733047 11.2165C0.975079 9.41628 1.49287 7.90054 2.6967 6.6967C3.90054 5.49286 5.41628 4.97508 7.2165 4.73304C8.95044 4.49992 11.1551 4.49996 13.8903 4.5ZM7.61624 7.7063C6.1487 7.9036 5.37154 8.2645 4.81802 8.81802C4.2645 9.37154 3.9036 10.1487 3.7063 11.6162C3.50318 13.1269 3.5 15.1292 3.5 18V22C3.5 24.8708 3.50318 26.873 3.7063 28.3838C3.9036 29.8514 4.2645 30.6284 4.81802 31.182C5.37154 31.7356 6.1487 32.0964 7.61624 32.2938C9.12694 32.4968 11.1292 32.5 14 32.5H26C28.8708 32.5 30.873 32.4968 32.3838 32.2938C33.8514 32.0964 34.6284 31.7356 35.182 31.182C35.7356 30.6284 36.0964 29.8514 36.2938 28.3838C36.4968 26.873 36.5 24.8708 36.5 22V18C36.5 15.1292 36.4968 13.1269 36.2938 11.6162C36.0964 10.1487 35.7356 9.37154 35.182 8.81802C34.6284 8.2645 33.8514 7.9036 32.3838 7.7063C30.873 7.50318 28.8708 7.5 26 7.5H14C11.1292 7.5 9.12694 7.50318 7.61624 7.7063Z" fill="#0D6EFD" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75183 13.168C7.21135 12.4787 8.14267 12.2924 8.83195 12.7519L14.1756 16.3143C15.816 17.4079 16.9479 18.1601 17.8927 18.6516C18.8049 19.1261 19.4187 19.2889 19.9999 19.2889C20.5811 19.2889 21.1949 19.1261 22.1071 18.6516C23.0519 18.1601 24.1839 17.4079 25.8243 16.3143L31.1679 12.7519C31.8571 12.2924 32.7885 12.4787 33.2479 13.168C33.7075 13.8572 33.5213 14.7886 32.8319 15.2481L27.4191 18.8567C25.8647 19.893 24.5979 20.7374 23.4917 21.313C22.3365 21.914 21.2353 22.289 19.9999 22.289C18.7645 22.289 17.6634 21.914 16.5082 21.313C15.4019 20.7374 14.1351 19.893 12.5807 18.8566L7.16785 15.2481C6.47857 14.7886 6.29231 13.8572 6.75183 13.168Z" fill="#FBC249" />
</svg>
,
NAME:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.3169 36.6667C34.3169 30.2167 27.9003 25 20.0003 25C12.1003 25 5.68359 30.2167 5.68359 36.6667" stroke="#0D6EFD" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M25.8919 17.5592C24.3291 19.122 22.2095 20 19.9993 20C17.7892 20 15.6696 19.122 14.1068 17.5592C12.544 15.9964 11.666 13.8768 11.666 11.6666C11.666 9.45651 12.544 7.33689 14.1068 5.77409C15.6696 4.21129 17.7892 3.33331 19.9993 3.33331C22.2095 3.33331 24.3291 4.21129 25.8919 5.77409C27.4547 7.33689 28.3327 9.45651 28.3327 11.6666C28.3327 13.8768 27.4547 15.9964 25.8919 17.5592Z" stroke="#FBC249" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
,
COMPANY:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.5 36.25H36.25V5C36.25 2.9375 34.5625 1.25 32.5 1.25H17.5C15.4375 1.25 13.75 2.9375 13.75 5V7.5H7.5C5.4375 7.5 3.75 9.1875 3.75 11.25V36.25H2.5C1.8125 36.25 1.25 36.8125 1.25 37.5C1.25 38.1875 1.8125 38.75 2.5 38.75H37.5C38.1875 38.75 38.75 38.1875 38.75 37.5C38.75 36.8125 38.1875 36.25 37.5 36.25ZM26.25 33.75H33.75V36.25H26.25V33.75ZM16.25 5C16.25 4.3125 16.8125 3.75 17.5 3.75H32.5C33.1875 3.75 33.75 4.3125 33.75 5V31.25H26.25V11.25C26.25 9.1875 24.5625 7.5 22.5 7.5H16.25V5ZM6.25 11.25C6.25 10.5625 6.8125 10 7.5 10H22.5C23.1875 10 23.75 10.5625 23.75 11.25V36.25H20V29.375C20.6875 29.375 21.25 28.8125 21.25 28.125C21.25 27.4375 20.6875 26.875 20 26.875H10C9.3125 26.875 8.75 27.4375 8.75 28.125C8.75 28.8125 9.3125 29.375 10 29.375V36.25H6.25V11.25ZM17.5 29.375V36.25H12.5V29.375H17.5Z" fill="#0D6EFD" />
<path d="M9.375 14.375H10.0375C10.725 14.375 11.2875 13.8125 11.2875 13.125C11.2875 12.4375 10.725 11.875 10.0375 11.875H9.375C8.6875 11.875 8.125 12.4375 8.125 13.125C8.125 13.8125 8.6875 14.375 9.375 14.375Z" fill="#FBC249" />
<path d="M14.6758 14.375H15.3383C16.0258 14.375 16.5883 13.8125 16.5883 13.125C16.5883 12.4375 16.0258 11.875 15.3383 11.875H14.6758C13.9883 11.875 13.4258 12.4375 13.4258 13.125C13.4258 13.8125 13.9883 14.375 14.6758 14.375Z" fill="#FBC249" />
<path d="M19.9629 14.375H20.6254C21.3129 14.375 21.8754 13.8125 21.8754 13.125C21.8754 12.4375 21.3129 11.875 20.6254 11.875H19.9629C19.2754 11.875 18.7129 12.4375 18.7129 13.125C18.7129 13.8125 19.2754 14.375 19.9629 14.375Z" fill="#FBC249" />
<path d="M9.375 19.375H10.0375C10.725 19.375 11.2875 18.8125 11.2875 18.125C11.2875 17.4375 10.725 16.875 10.0375 16.875H9.375C8.6875 16.875 8.125 17.4375 8.125 18.125C8.125 18.8125 8.6875 19.375 9.375 19.375Z" fill="#FBC249" />
<path d="M14.6758 19.375H15.3383C16.0258 19.375 16.5883 18.8125 16.5883 18.125C16.5883 17.4375 16.0258 16.875 15.3383 16.875H14.6758C13.9883 16.875 13.4258 17.4375 13.4258 18.125C13.4258 18.8125 13.9883 19.375 14.6758 19.375Z" fill="#FBC249" />
<path d="M19.9629 19.375H20.6254C21.3129 19.375 21.8754 18.8125 21.8754 18.125C21.8754 17.4375 21.3129 16.875 20.6254 16.875H19.9629C19.2754 16.875 18.7129 17.4375 18.7129 18.125C18.7129 18.8125 19.2754 19.375 19.9629 19.375Z" fill="#FBC249" />
<path d="M10.0375 24.375C10.725 24.375 11.2875 23.8125 11.2875 23.125C11.2875 22.4375 10.725 21.875 10.0375 21.875H9.375C8.6875 21.875 8.125 22.4375 8.125 23.125C8.125 23.8125 8.6875 24.375 9.375 24.375H10.0375Z" fill="#FBC249" />
<path d="M14.6758 24.375H15.3383C16.0258 24.375 16.5883 23.8125 16.5883 23.125C16.5883 22.4375 16.0258 21.875 15.3383 21.875H14.6758C13.9883 21.875 13.4258 22.4375 13.4258 23.125C13.4258 23.8125 13.9883 24.375 14.6758 24.375Z" fill="#FBC249" />
<path d="M19.9629 24.375H20.6254C21.3129 24.375 21.8754 23.8125 21.8754 23.125C21.8754 22.4375 21.3129 21.875 20.6254 21.875H19.9629C19.2754 21.875 18.7129 22.4375 18.7129 23.125C18.7129 23.8125 19.2754 24.375 19.9629 24.375Z" fill="#FBC249" />
<path d="M29.375 13.75H30.625C31.3125 13.75 31.875 13.1875 31.875 12.5C31.875 11.8125 31.3125 11.25 30.625 11.25H29.375C28.6875 11.25 28.125 11.8125 28.125 12.5C28.125 13.1875 28.6875 13.75 29.375 13.75Z" fill="#FBC249" />
<path d="M29.375 18.75H30.625C31.3125 18.75 31.875 18.1875 31.875 17.5C31.875 16.8125 31.3125 16.25 30.625 16.25H29.375C28.6875 16.25 28.125 16.8125 28.125 17.5C28.125 18.1875 28.6875 18.75 29.375 18.75Z" fill="#FBC249" />
<path d="M29.375 23.75H30.625C31.3125 23.75 31.875 23.1875 31.875 22.5C31.875 21.8125 31.3125 21.25 30.625 21.25H29.375C28.6875 21.25 28.125 21.8125 28.125 22.5C28.125 23.1875 28.6875 23.75 29.375 23.75Z" fill="#FBC249" />
<path d="M29.375 28.75H30.625C31.3125 28.75 31.875 28.1875 31.875 27.5C31.875 26.8125 31.3125 26.25 30.625 26.25H29.375C28.6875 26.25 28.125 26.8125 28.125 27.5C28.125 28.1875 28.6875 28.75 29.375 28.75Z" fill="#FBC249" />
</svg>
,
};
export default typeToIconMap;

View File

@ -52,7 +52,8 @@
--sidebarDropDownBg : #003C78;
--sidebarMenuCol : #FFFFFF;
--documentUploadBg :#FFFFFF;
--mainPrimaryBtn : #077DEC
--mainPrimaryBtn : #077DEC;
--dashBoardStartNowBtn : #077DEC;

View File

@ -2,6 +2,7 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { toast, Bounce } from 'react-toastify';
import api from '../../config/api';
import { hideLoader, showLoader } from './loaderSlice';
import { AxiosError } from 'axios';
// import { showLoader, hideLoader } from '../loader/loaderSlice';
// -----------------------
@ -19,18 +20,35 @@ export interface Document {
// -----------------------
// Initial State
// -----------------------
export enum DocumentErrorEnum {
UNSUPPORTED_DOC_TYPE = "UNSUPPORTED_DOC_TYPE",
FILE_SIZE_EXCEEDED = "FILE_SIZE_EXCEEDED"
}
export interface DocumentErrorInt {
[DocumentErrorEnum.UNSUPPORTED_DOC_TYPE]: string[];
[DocumentErrorEnum.FILE_SIZE_EXCEEDED]: string[];
}
interface DocumentState {
documents: Document[];
loading: boolean;
error: string | null;
error: DocumentErrorInt;
}
const initialState: DocumentState = {
documents: [],
loading: false,
error: null,
error: {
[DocumentErrorEnum.UNSUPPORTED_DOC_TYPE]: [],
[DocumentErrorEnum.FILE_SIZE_EXCEEDED]: []
},
};
// -----------------------
// Thunk: Fetch Documents
// -----------------------
@ -47,7 +65,7 @@ export const fetchDocuments = createAsyncThunk<
const toggle = state.selectedRecipients.transactionTemplateToggle;
const response = await api.get(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents?include_images=true`
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents?include_pages=true`
);
const data = response.data as Document[];
@ -67,6 +85,8 @@ export const fetchDocuments = createAsyncThunk<
// Thunk: Upload Documents
// -----------------------
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const uploadDocuments = createAsyncThunk<
Document[],
{ formData: FormData; accountId: string; transactionId: string }
@ -79,26 +99,54 @@ export const uploadDocuments = createAsyncThunk<
const state = getState() as {
selectedRecipients: { transactionTemplateToggle: string };
};
const toggle = state.selectedRecipients.transactionTemplateToggle;
let response = [];
// debugger
for (let i = 0; i < formData.getAll("file").length; i++) {
const currentFile = formData.getAll("file")[i] as File;
// ---- Client-side validation ----
if (currentFile.type !== "application/pdf") {
response.push({
error: true,
docName: currentFile.name,
reason: DocumentErrorEnum.UNSUPPORTED_DOC_TYPE,
});
continue; // skip upload
}
if (currentFile.size > 10 * 1024 * 1024) {
response.push({
error: true,
docName: currentFile.name,
reason: DocumentErrorEnum.FILE_SIZE_EXCEEDED,
});
continue; // skip upload
}
const file = new FormData();
file.append("file", formData.getAll("file")[i]);
file.append("file", currentFile);
const res = await api.post(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`,
file,
{
headers: {
'Content-Type': 'multipart/form-data',
},
try {
const res = await api.post(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`,
file,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
response.push(res.data);
} catch (e: unknown) {
if (e instanceof AxiosError && (e.response?.status === 415)) {
response.push({ error: true, docName: (formData.getAll("file")[i] as File).name, reason: DocumentErrorEnum.UNSUPPORTED_DOC_TYPE });
} else if (e instanceof AxiosError && (e.response?.status === 413)) {
response.push({ error: true, docName: (formData.getAll("file")[i] as File).name, reason: DocumentErrorEnum.FILE_SIZE_EXCEEDED });
}
);
console.log(res.data);
response.push(res.data);
}
}
// await sleep(5000);
@ -107,18 +155,92 @@ export const uploadDocuments = createAsyncThunk<
} catch (error: any) {
dispatch(hideLoader());
toast.error('Error Uploading Document', {
position: 'bottom-right',
autoClose: 3000,
theme: 'light',
transition: Bounce,
});
// toast.error('Error Uploading Document', {
// position: 'bottom-right',
// autoClose: 3000,
// theme: 'light',
// transition: Bounce,
// });
return rejectWithValue('Failed Uploading Documents');
}
}
);
export const handleMultipleTransactionThunk = createAsyncThunk<
Document[],
{ formData: FormData; accountId: string; transactionId: string }
>(
'documents/handleMultipleTransactionThunk',
async ({ formData, accountId }, { dispatch, rejectWithValue, getState }) => {
try {
const state = getState() as {
selectedRecipients: { transactionTemplateToggle: string };
};
const toggle = state.selectedRecipients.transactionTemplateToggle;
let response = [];
for (let i = 0; i < formData.getAll("file").length; i++) {
const currentFile = formData.getAll("file")[i] as File;
dispatch(showLoader(`Creating Transaction ${i + 1} ...`));
const transactionRes = await api.post(`/api/accounts/${accountId}/transactions`, { "subject": currentFile.name });
const transactionId = transactionRes.data.id
dispatch(showLoader('Uploading documents...'));
// ---- Client-side validation ----
if (currentFile.type !== "application/pdf") {
response.push({
error: true,
docName: currentFile.name,
reason: DocumentErrorEnum.UNSUPPORTED_DOC_TYPE,
});
continue; // skip upload
}
if (currentFile.size > 10 * 1024 * 1024) {
response.push({
error: true,
docName: currentFile.name,
reason: DocumentErrorEnum.FILE_SIZE_EXCEEDED,
});
continue; // skip upload
}
const file = new FormData();
file.append("file", currentFile);
try {
const res = await api.post(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`,
file,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
response.push(res.data);
} catch (e: unknown) {
if (e instanceof AxiosError && (e.response?.status === 415)) {
response.push({ error: true, docName: (formData.getAll("file")[i] as File).name, reason: DocumentErrorEnum.UNSUPPORTED_DOC_TYPE });
} else if (e instanceof AxiosError && (e.response?.status === 413)) {
response.push({ error: true, docName: (formData.getAll("file")[i] as File).name, reason: DocumentErrorEnum.FILE_SIZE_EXCEEDED });
}
}
}
// await sleep(5000);
dispatch(hideLoader());
return [...response];
} catch (error: any) {
dispatch(hideLoader());
return rejectWithValue('Failed Uploading Documents');
}
}
);
// -----------------------
// Slice
// -----------------------
@ -129,6 +251,9 @@ const documentsSlice = createSlice({
setDocuments: (state, action: PayloadAction<Document[]>) => {
state.documents = action.payload;
},
clearDocumentUploadErrors: (state) => {
state.error = initialState.error;
},
setDocProgComplete: (state, action: PayloadAction<string>) => {
const docIndex = state.documents.findIndex(doc => doc.id === action.payload);
if (docIndex !== -1) {
@ -147,7 +272,7 @@ const documentsSlice = createSlice({
// fetchDocuments
.addCase(fetchDocuments.pending, (state) => {
state.loading = true;
state.error = null;
state.error = initialState.error;
})
.addCase(fetchDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
state.loading = false;
@ -155,35 +280,42 @@ const documentsSlice = createSlice({
})
.addCase(fetchDocuments.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
toast.error('Failed to fetch documents', {
position: 'bottom-right',
autoClose: 3000,
theme: 'light',
transition: Bounce,
});
state.error = initialState.error;
// toast.error('Failed to fetch documents', {
// position: 'bottom-right',
// autoClose: 3000,
// theme: 'light',
// transition: Bounce,
// });
})
// uploadDocuments
.addCase(uploadDocuments.pending, (state) => {
state.loading = true;
state.error = null;
state.error = initialState.error;
})
.addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
state.loading = false;
action.payload.forEach((doc, indx) => {
state.documents.push(doc);
if ("error" in doc && doc.error) {
if (doc.reason in state.error) {
(state.error[doc.reason as DocumentErrorEnum.UNSUPPORTED_DOC_TYPE | DocumentErrorEnum.FILE_SIZE_EXCEEDED]).push(doc.docName)
}
} else {
state.documents.push(doc);
}
});
})
.addCase(uploadDocuments.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
state.error = initialState.error;
})
},
});
// -----------------------
// Exports
// -----------------------
export const { deleteDocument, resetDocuments, setDocuments, setDocProgComplete } = documentsSlice.actions;
export const { deleteDocument, resetDocuments, setDocuments, setDocProgComplete, clearDocumentUploadErrors } = documentsSlice.actions;
export default documentsSlice.reducer;

View File

@ -136,9 +136,9 @@ export const fetchRecipients = createAsyncThunk<
// return recipient.type !== "CC";
// })
if (data.length === 0 && window.location.pathname.includes('/process-step-two')) {
window.location.href = window.location.href.replace("process-step-two", "process-step-one") // ✅ navigate using window
}
// if (data.length === 0 && window.location.pathname.includes('/process-step-two')) {
// window.location.href = window.location.href.replace("process-step-two", "process-step-one") // ✅ navigate using window
// }
return data as Recipient[];
} catch (err: any) {
@ -205,6 +205,19 @@ const selectedRecipientsSlice = createSlice({
recipient.routingOrder = routingOrder;
}
},
reorderAllRecipientAscOrder: (state) => {
state.recipients = state.recipients.sort((a, b) => a.routingOrder - b.routingOrder);
},
updateRecipientByIndxAndRo: (state, action: PayloadAction<{ indx1: number; ro1: number, indx2: number; ro2: number }>) => {
const indx1 = action.payload.indx1
const indx2 = action.payload.indx2
const ro1 = action.payload.ro1
const ro2 = action.payload.ro2
const temp = { ...state.recipients[indx1], routingOrder: ro1 };
state.recipients[indx1] = { ...state.recipients[indx2], routingOrder: ro2 };
state.recipients[indx2] = temp;
},
updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => {
for (const recObj of state.recipients) {
for (const recObj2 of action.payload) {
@ -305,7 +318,9 @@ export const {
resetSelRecipient,
updateRecipientOrder,
updateRecipient,
resetRecipients
resetRecipients,
reorderAllRecipientAscOrder,
updateRecipientByIndxAndRo
} = selectedRecipientsSlice.actions;
export default selectedRecipientsSlice.reducer;

12
src/utlity/commonUtils.ts Normal file
View File

@ -0,0 +1,12 @@
import { Recipient } from "../store/slices/selectedRecipientsSlice";
interface GetMinRoutingOrderIntProps {
recipients: Recipient[]
}
const getMinRoutingOrder = ({ recipients }: GetMinRoutingOrderIntProps): number => {
return recipients.reduce((min, recipient) => {
return Math.min(min, recipient.routingOrder ?? Infinity);
}, Infinity);
};
export default getMinRoutingOrder

View File

@ -0,0 +1,64 @@
import api from "../config/api";
// Accepts: type = 'archived' | 'combined' | 'certificate' | documentId (number or string)
const downloadDocumentUtils = async (accountId: string, transactionId: string, type: string, includeCertificate: string | null = null) => {
let documentId = type;
let url = `/api/accounts/${accountId}/transactions/${transactionId}/documents/${documentId}`;
if (includeCertificate !== null) {
url = url + includeCertificate;
}
try {
const response = await api.get(url, {
responseType: 'blob', // in case it's a file
});
// Create a blob URL and trigger download
const blob = new Blob([response.data]);
// Try to extract filename from headers, fallback to default
let filename = "document";
// content-disposition
const disposition = response.headers?.["content-disposition"];
if (disposition && disposition.includes("filename=")) {
const match = disposition.match(/filename="?([^"]+)"?/);
if (match?.[1]) {
filename = match[1];
}
} else {
// Fall back to content-type → extension
const contentType = response.headers?.["content-type"];
let extension = "bin";
if (contentType) {
const mimeMap: Record<string, string> = {
"application/pdf": "pdf",
"application/msword": "doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
"application/vnd.ms-excel": "xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
"image/png": "png",
"image/jpeg": "jpg",
"text/plain": "txt",
"application/zip": "zip",
"text/html": "html",
};
extension = mimeMap[contentType] || "bin";
}
filename = `${documentId || "document"}.${extension}`;
}
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
setTimeout(() => {
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
}, 100);
} catch (error) {
console.error('Error downloading document:', error);
}
}
export default downloadDocumentUtils;

View File

@ -0,0 +1,9 @@
import api from "../config/api";
const signSessionRedirect = async (accountId: string, transactionId: string, recipientId: string) => {
const session = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients/${recipientId}/create_session`)
window.location.href = session.data.url;
return;
}
export default signSessionRedirect

View File

@ -8,16 +8,19 @@ export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
allowedHosts: [
'quality-friendly-goldfish.ngrok-free.app',
'officially-powerful-goat.ngrok-free.app',
'89f0-49-36-189-120.ngrok-free.app',
'c8ee-49-36-191-130.ngrok-free.app',
'c76f1251c173.ngrok-free.app',
'enabled-upright-boxer.ngrok-free.app'
],
'quality-friendly-goldfish.ngrok-free.app',
'officially-powerful-goat.ngrok-free.app',
'89f0-49-36-189-120.ngrok-free.app',
'c8ee-49-36-191-130.ngrok-free.app',
'c76f1251c173.ngrok-free.app',
'enabled-upright-boxer.ngrok-free.app',
'basically-polished-yeti.ngrok-free.app',
'10.10.1.248:8083'
],
},
build: {
chunkSizeWarningLimit: 600,
}
},
})