From aa9bd93bcef041bb3dec63dcb07523952335a41c Mon Sep 17 00:00:00 2001 From: =bhagwanSingh <=bhagwan.r@hicglobalsolutions.com> Date: Fri, 5 Sep 2025 12:16:39 +0530 Subject: [PATCH] merge 05 Sep --- .env | 4 +- src/components/CommonNav.tsx | 53 ++-- src/components/accounts/Accounts.tsx | 10 - src/components/auth/LoginForm.tsx | 128 +------- src/components/auth/RegisterForm.tsx | 176 +++++------ src/components/client/ConsentScreen.tsx | 77 +++++ src/components/client/DeclineModal.tsx | 49 ++++ src/components/client/ReassignModal.tsx | 108 +++++++ src/components/client/SignButton.tsx | 112 +++++++ src/components/client/SignDocument.tsx | 273 ++++++++++++++++-- src/components/client/TagsCon.tsx | 8 +- src/components/dashboard/Dashboard.tsx | 25 -- src/components/dashboard/SendForSign.tsx | 4 - .../process/CompletionScreen/SignerList.tsx | 5 +- .../processStepOne/AdvancedOptions.tsx | 114 ++++++++ .../process/processStepOne/ProcessStepOne.tsx | 121 +++++--- .../processStepOne/RecipientAddCard.tsx | 5 +- .../process/processStepOne/RecipientCards.tsx | 69 +++-- .../processStepTwo/DocumentContainer.tsx | 12 +- .../processStepTwo/DroppedItemsTagsCon.tsx | 9 +- .../process/processStepTwo/NavTwo.tsx | 32 +- src/components/templates/TemplateTable.tsx | 36 ++- .../transactions/TransactionView.tsx | 13 +- src/config/api.tsx | 2 +- src/store/slices/selectedRecipientsSlice.tsx | 2 +- src/utlity/types.tsx | 45 ++- 26 files changed, 1090 insertions(+), 402 deletions(-) create mode 100644 src/components/client/ConsentScreen.tsx create mode 100644 src/components/client/DeclineModal.tsx create mode 100644 src/components/client/ReassignModal.tsx create mode 100644 src/components/client/SignButton.tsx create mode 100644 src/components/process/processStepOne/AdvancedOptions.tsx diff --git a/.env b/.env index cdb5098..2b5b434 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ -VITE_BASE_URI="https://wired-terminally-muskrat.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="http://192.168.2.176:8083" \ No newline at end of file diff --git a/src/components/CommonNav.tsx b/src/components/CommonNav.tsx index 4053dcb..41f0b44 100644 --- a/src/components/CommonNav.tsx +++ b/src/components/CommonNav.tsx @@ -3,6 +3,8 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'; import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa"; import api from '../config/api'; import { IoSettings, IoSettingsOutline } from 'react-icons/io5'; +import { useSelector } from 'react-redux'; +import { RootState } from '../store/store'; interface AccountNavProps { accountName?: string | null; @@ -21,6 +23,8 @@ interface Account { interface UserData { id: number; name: string; + firstName: string; + lastName: string; email: string; language: string; account: Account; @@ -51,6 +55,8 @@ const CommonNav: React.FC = ({ const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null'); const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}'); + const userDetailsAsync = useSelector((state: RootState) => state.user) + const [open, setOpen] = useState(false); const [accountsOpen, setAccountsOpen] = useState(false); const [loadingAccounts, setLoadingAccounts] = useState(false); @@ -58,9 +64,13 @@ const CommonNav: React.FC = ({ const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false); const dropdownRef = useRef(null); + const [initial, setInitial] = useState(""); + const [user, setUser] = useState({ id: 0, name: "", + firstName: "", + lastName: "", email: "", language: "", account: { @@ -75,6 +85,14 @@ const CommonNav: React.FC = ({ navigate("/s/settings#profile-info"); } + useEffect(() => { + if(initial !== "") return + console.log(userDetailsAsync, 'line 89') + let initialN = (userDetailsAsync.firstName && userDetailsAsync.firstName[0]) + initialN = initialN+""+(userDetailsAsync.lastName && userDetailsAsync.lastName[0]) + setInitial(initialN) + }, [userDetailsAsync]) + const getAccountDetails = async () => { try { let res = await api.get('/api/user/accounts'); @@ -123,6 +141,8 @@ const CommonNav: React.FC = ({ setUser({ id: account.userId, name: account.name, + firstName: account.firstName, + lastName: account.lastName, email: account.userEmail, language: account.language, // set it if available account: { @@ -132,6 +152,12 @@ const CommonNav: React.FC = ({ }, }); + if (account.firstName && account.lastName) { + setInitial((account?.firstName?.slice(0, 1)).toUpperCase() + "" + (account?.lastName?.slice(0, 1)).toUpperCase()) + } else { + setInitial((account?.name?.slice(0, 1)).toUpperCase()) + } + sessionStorage.setItem('userDetails', JSON.stringify({ id: account.userId, name: account.name, @@ -159,6 +185,8 @@ const CommonNav: React.FC = ({ const userData = { id: item?.id, name: item?.firstName + " " + item?.lastName, + firstName: item?.firstName, + lastName: item?.lastName, email: item?.email, language: item?.language, account: { @@ -169,6 +197,12 @@ const CommonNav: React.FC = ({ }; setUser(userData); + if (userData.firstName && userData.lastName) { + setInitial((userData?.firstName?.slice(0, 1)).toUpperCase() + "" + (userData?.lastName?.slice(0, 1)).toUpperCase()) + } else { + setInitial((userData?.name?.slice(0, 1)).toUpperCase()) + } + sessionStorage.setItem('userDetails', JSON.stringify(userData)); sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id)); @@ -340,7 +374,7 @@ const CommonNav: React.FC = ({
{user ? ( - {(user?.name?.slice(0, 2)).toUpperCase()} + {initial} ) : ( @@ -355,7 +389,7 @@ const CommonNav: React.FC = ({
{user ? ( - {(user?.name?.slice(0, 2)).toUpperCase()} + {initial} ) : ( @@ -455,20 +489,5 @@ const CommonNav: React.FC = ({
); - - // const ShimmerRow = () => ( - //
- //
- //
- //
- //
- //
- //
- //
- //
- //
- // ); - // }; - } export default CommonNav; \ No newline at end of file diff --git a/src/components/accounts/Accounts.tsx b/src/components/accounts/Accounts.tsx index 3537389..768aa8d 100644 --- a/src/components/accounts/Accounts.tsx +++ b/src/components/accounts/Accounts.tsx @@ -1,15 +1,11 @@ import { useEffect, useState } from "react"; -import { FaCheck } from "react-icons/fa"; import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi"; import { FaDatabase } from "react-icons/fa"; -import AccountNav from "./AccountNav"; import api from "../../config/api"; import { AccountData, AccountStatus, UserRole } from "../../utlity/types"; import { useNavigate } from "react-router-dom"; -import Cards from "../home/Cards"; import AddAccountModal from "./AddAccountModal"; import { Bounce, toast } from "react-toastify"; -import Loader from "../../helper/Loader"; // import { UserRole, AccountStatus, AccountData } from "../../types/account"; @@ -36,15 +32,9 @@ const getStatusLabel = ( const Accounts = () => { const [allAccounts, setAllAccounts] = useState([]); - const [selectedAccounts, setSelectedAccounts] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); - const [accounts, setAccounts] = useState([]); const [openMenuId, setOpenMenuId] = useState(null); - const handleOpenModal = () => { - setIsModalOpen(true); - }; - const navigate = useNavigate(); const fetchAccounts = async () => { diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 730d857..3177b27 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -1,14 +1,11 @@ // import OAuthGoogle from "./OAuthGoogle"; import axios from "axios"; -import { useState } from "react"; +import { useRef, useState } from "react"; // import { FcGoogle } from "react-icons/fc"; import { Link, useNavigate } from "react-router-dom"; import OAuthGoogle from "./OAuthGoogle"; -import { Bounce, toast } from "react-toastify"; // import Cookies from "js-cookie"; -import { useDispatch } from "react-redux"; -import { AppDispatch } from "../../store/store"; import { BsEye } from "react-icons/bs"; import { FiEyeOff } from "react-icons/fi"; // import api from "../../config/api"; @@ -27,6 +24,9 @@ const LoginForm = ({ onSwitchToRegister }) => { const [showPassword, setShowPassword] = useState(false); const [errors, setErrors] = useState({ email: "", password: "" }); const [isLoading, setIsLoading] = useState(false); + + const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false }); + const navigate = useNavigate() const validateEmail = (email) => { @@ -66,15 +66,6 @@ const LoginForm = ({ onSwitchToRegister }) => { if (!password) { return "Password is required"; } - - // if (password.length < 6) { - // return "Password must be at least 6 characters long"; - // } - - // if (password.length > 128) { - // return "Password is too long"; - // } - return ""; }; @@ -98,49 +89,6 @@ const LoginForm = ({ onSwitchToRegister }) => { } }; - const handleNextClick = async () => { - const emailError = validateEmail(formData.email); - - if (emailError) { - setErrors({ ...errors, email: emailError }); - return; - } - - setIsLoading(true); - - try { - const res = await axios.get( - `${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`, - { - headers: { - "ngrok-skip-browser-warning": "69420", - } - } - ); - - if (res.status === 200) { - toast.dismiss(); - setCacheKey(res.data); - setNextClick(true); - } - } catch (err: any) { - toast.dismiss(); - toast.error("User Does Not Exist", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - theme: "light", - transition: Bounce, - }); - } finally { - setIsLoading(false); - } - }; - - const handleFormLoginClick = async () => { const emailError = validateEmail(formData.email); const passwordError = validatePassword(formData.password); @@ -167,57 +115,16 @@ const LoginForm = ({ onSwitchToRegister }) => { ); if (res.status === 200) { - toast.dismiss(); - - // Example if you want cookies / redux: - // Cookies.set("authorization", res.headers.authorization, { - // path: "/", - // secure: true, - // sameSite: "Lax", - // }); - // dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization })); - - navigate("/"); // Redirect after 1 second - - toast.success("Login Successful", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); + navigate("/"); + formErrorRef.current = { message: "Login Successfully", error: false } } } catch (err) { - toast.dismiss(); - if (err.response?.status === 400 || err.response?.status === 401) { - toast.error("Credentials Not Match", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); + formErrorRef.current = { message: "The email or password you entered is invalid.", error: true } + } else if(err.response?.status === 404) { + formErrorRef.current = { message: "Email not found. Please sign up to continue.", error: true } } else { - toast.error("Some Error Occurred", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); + formErrorRef.current = { message: "Something went wrong. Please refresh and try again.", error: true } } } finally { setIsLoading(false); @@ -305,18 +212,9 @@ const LoginForm = ({ onSwitchToRegister }) => {
{/* )} */} - {/* {!nextClick ? ( - - ) : ( */} +

{formErrorRef.current.message}

+ + + + {/* {onDecline && ( + + )} */} + + + + + + ); +} diff --git a/src/components/client/DeclineModal.tsx b/src/components/client/DeclineModal.tsx new file mode 100644 index 0000000..bd0def1 --- /dev/null +++ b/src/components/client/DeclineModal.tsx @@ -0,0 +1,49 @@ +import { FaTimesCircle } from "react-icons/fa"; + +interface DeclineModalProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export default function DeclineModal({ open, onClose, onConfirm }: DeclineModalProps) { + if (!open) return null; + + return ( +
+
+ {/* Header */} +
+ +

+ Confirm Decline +

+
+ + {/* Message */} +

+ Are you sure you want to decline this document? This action cannot be undone. +

+ + {/* Buttons */} +
+ + +
+
+
+ ); +} diff --git a/src/components/client/ReassignModal.tsx b/src/components/client/ReassignModal.tsx new file mode 100644 index 0000000..46dda30 --- /dev/null +++ b/src/components/client/ReassignModal.tsx @@ -0,0 +1,108 @@ +import { useState } from "react"; +import { FaUserEdit } from "react-icons/fa"; + +interface ReassignModalProps { + open: boolean; + onClose: () => void; + onConfirm: (data: { firstName: string; lastName: string; email: string }) => void; +} + +export default function ReassignModal({ open, onClose, onConfirm }: ReassignModalProps) { + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + + const [errors, setErrors] = useState<{ lastName?: string; email?: string }>({}); + + if (!open) return null; + + const validate = () => { + const newErrors: { lastName?: string; email?: string } = {}; + if (!lastName.trim()) newErrors.lastName = "Last name is required."; + if (!email.trim()) newErrors.email = "Email is required."; + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) newErrors.email = "Enter a valid email."; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = () => { + if (!validate()) return; + onConfirm({ firstName, lastName, email }); + onClose(); + }; + + return ( +
+
+ {/* Header */} +
+ +

Reassign Document

+
+ + {/* Message */} +

+ Please enter the recipient details to reassign this document. +

+ + {/* Form Fields */} +
+
+
+ setFirstName(e.target.value)} + 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" + /> +
+
+ setLastName(e.target.value)} + className={`w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 ${ + errors.lastName ? "border-red-500 focus:ring-red-400" : "focus:ring-blue-400" + }`} + /> + {errors.lastName && ( +

{errors.lastName}

+ )} +
+
+ +
+ setEmail(e.target.value)} + className={`w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 ${ + errors.email ? "border-red-500 focus:ring-red-400" : "focus:ring-blue-400" + }`} + /> + {errors.email &&

{errors.email}

} +
+
+ + {/* Buttons */} +
+ + +
+
+
+ ); +} diff --git a/src/components/client/SignButton.tsx b/src/components/client/SignButton.tsx new file mode 100644 index 0000000..6d9fcd3 --- /dev/null +++ b/src/components/client/SignButton.tsx @@ -0,0 +1,112 @@ +import { useState, useRef, useEffect } from "react"; +import { FaSignature, FaDownload, FaUserEdit, FaTimesCircle } from "react-icons/fa"; +import { BsThreeDotsVertical } from "react-icons/bs"; + +interface Props { + transparentTags?: boolean; + hideTags?: boolean; + handleFormSubmit: () => void; + onDownloadClick?: () => void; + onDeclineClick?: () => void; + onReassignClick?: () => void; +} + +export default function SignButton({ + transparentTags, + hideTags, + handleFormSubmit, + onDownloadClick, + onDeclineClick, + onReassignClick, +}: Props) { + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + + // Close menu on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setMenuOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ {(!transparentTags && !hideTags) ? ( +
+ {/* Sign Button */} + + + {/* Three Dots Dropdown Trigger */} + + + {/* Dropdown Menu */} + {menuOpen && ( +
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" + > +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ )} +
+ ) : ( +

Signed Successfully

+ )} +
+ ); +} diff --git a/src/components/client/SignDocument.tsx b/src/components/client/SignDocument.tsx index b386590..5744d98 100644 --- a/src/components/client/SignDocument.tsx +++ b/src/components/client/SignDocument.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import axios from "axios" import { RootState } from "../../store/store"; -import { useNavigate, useParams } from "react-router-dom" +import { Link, useNavigate, useParams } from "react-router-dom" import { DroppedItem, TagsTypes } from "../../utlity/types"; import Loader from "../dashboard/Loader"; @@ -12,8 +12,9 @@ import { useDispatch, useSelector } from "react-redux"; import { hideLoader, showLoader } from "../../store/slices/loaderSlice"; import { FaSignature, FaCheck, FaQuestionCircle } from 'react-icons/fa' -import { IoIosDocument } from 'react-icons/io' +import { IoIosDocument, IoMdHome } from 'react-icons/io' import { MdKeyboardArrowDown } from 'react-icons/md' +import confetti from 'canvas-confetti' import dayjs from "dayjs"; @@ -25,6 +26,10 @@ import SignCompletionScreen from "./SignCompletionScreen"; import RightSideBar from "../process/processStepTwo/rightSideBar/RightSideBar"; import TagsCon from "./TagsCon"; +import ConsentScreen from "./ConsentScreen"; +import SignButton from "./SignButton"; +import DeclineModal from "./DeclineModal"; +import ReassignModal from "./ReassignModal"; @@ -150,11 +155,18 @@ const SignDocument = () => { const [recSignStatus, setRecSignStatus] = useState(null); const [droppedItems, setDroppedItems] = useState([]); const [signTagId, setSignTagId] = useState(0) + const [showConfetti, setShowConfetti] = useState(false) const [stamp, setStamp] = useState({ open: false, blob: "" }); - const [rightSideNavToggle, setRightSideNavToggle] = useState(RightSideNav.PAGES); + const [signerConsent, setSignerConsent] = useState<{ consent: boolean; decline: boolean }>({ + consent: false, + decline: false + }); + const [declineOpen, setDeclineOpen] = useState(false); + const [reassignOpen, setReassignOpen] = useState(false); + const [rightSideNavToggle, setRightSideNavToggle] = useState(RightSideNav.PAGES); // Show is fields - Showing all fields There const [isFields, setIsFields] = useState(true); @@ -226,7 +238,7 @@ const SignDocument = () => { 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 @@ -237,6 +249,35 @@ const SignDocument = () => { } }, [droppedItems]) + useEffect(() => { + + if (showConfetti) { + const duration = 3000; + const end = Date.now() + duration; + + const interval = setInterval(() => { + if (Date.now() > end) { + clearInterval(interval); + return; + } + + confetti({ + particleCount: 5, // fewer particles per burst for smoothness + angle: 90, // falling straight down + spread: 30, // slight spread + startVelocity: 8, // gentle falling speed + origin: { + x: Math.random(), // random horizontal position across the top + y: 0, + }, + scalar: 1, + zIndex: 9999, + }); + }, 50); // fire every 50ms for continuous effect + + return () => clearInterval(interval); + } + }, [showConfetti]); useEffect(() => { if (recSignStatus === "COMPLETED") { @@ -371,24 +412,7 @@ const SignDocument = () => { .then((res) => { if (res.status === 200) { setTransparentTags(true) - setTimeout(() => { - dispatch(hideLoader()) - toast.dismiss(); - toast.info("Signed Successfully!", { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - // Completion Sign Screen Modal and download file - setShowCompletionScreen(true) - }, 800) - + setShowConfetti(true) } }) .catch((err) => { @@ -584,11 +608,18 @@ const SignDocument = () => { if (setTo !== isFields) setIsFields(setTo); } + + const handleOnConsent = () => { + setSignerConsent((prev) => { + return { ...prev, consent: true } + }); + } + return (
- <> + {!showCompletionScreen && <> {/* */} - {showCompletionScreen && } + {/* {showCompletionScreen && } */}
) => handleFormSubmit(e)} > @@ -688,7 +719,7 @@ const SignDocument = () => { {/* Send */} - {(!transparentTags && !hideTags) ? :

Signed Successfully

- } + } */} + + setDeclineOpen(true)} + onReassignClick={() => setReassignOpen(true)} + /> +
{/* Right Side - ENDS */}
@@ -827,8 +867,169 @@ const SignDocument = () => { {/* Document Container End */} - + } + {signerConsent && <> +
+ +
+ + {/* */} + SME + +
+

🎉 The document has been signed successfully!

+
+ +
+ +
+
+
+ +
+
+

You have successfully signed the document.

+ {/*

All signers will be notified via email

*/} +
+
+ +
+
+
+
+

QuickPreview

+
+ {/* */} +
+ {/* SIGN Documen Container - START */} +
+ { + selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => { + return ( +
+
+

{doc.name}

+
+ +
+ { + 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 ( +
handleDrop(e, doc.documentId, doc.documentId, page.index)} + onDragOver={(e) => handleDragOver(e)} + className='drop-page-in shadow-md relative' + > + + {!hideTags && <> + + } + + + + + {/* 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} +

+ {/* Side details of documents Pages and document Name - END */} + + +
+ ) + }) + } +
+ + +
+ ) + }) + } + +
+ {/* SIGN Documen Container - END */} +
+
+
+ +
+ Download Document + Visit Sign Made Easy +
+ + {/* Right Side */} + {/*
*/} + + {/*
+
+ +
+
*/} + + {/*
+

Document Send Successfully!

+
*/} + {/*
*/} + {/* */} + {/*
*/} + + {/*
+ {selfUserSign.sign && Sign Now} + Home +
*/} + + {/*
*/} + {/*
+ +
*/} + {/*
*/} + + {/*
*/} +
+ +
+
+ +
+ }
{recipient && @@ -848,6 +1049,24 @@ const SignDocument = () => { */} {globalLoader.isLoading && } + + {!showCompletionScreen && !signerConsent.consent && handleOnConsent()} + // onDecline={() => console.log("User declined")} + />} + + setDeclineOpen(false)} + onConfirm={() => setSignerConsent((prev) => ({ ...prev, decline: false }))} + /> + + setReassignOpen(false)} + onConfirm={(email) => console.log("Reassigned to:", email)} + /> +
) } diff --git a/src/components/client/TagsCon.tsx b/src/components/client/TagsCon.tsx index 025ee43..3334247 100644 --- a/src/components/client/TagsCon.tsx +++ b/src/components/client/TagsCon.tsx @@ -312,18 +312,18 @@ const TagsCon: React.FC = ({ {selectedTag?.key && selectedTag.key === pageIdkey && selectedTag.index === index &&
-

-

+

*/} {/*

{IconComponent}

*/}

{item.label}

- { + {provideTags && { e.stopPropagation(); handleRemoveTag(pageIdkey, index) - }} className='cursor-pointer' /> + }} className='cursor-pointer' />}
} diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index d9ccf92..db30aa0 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -11,11 +11,9 @@ import { clearTransaction, TRANSACTION_STATUS_ENUMS } from '../../store/slices/t import { resetSelDoc } from '../../store/slices/selectedDocsSlice' import { resetSelRecipient } from '../../store/slices/selectedRecipientsSlice' import { hideLoader, showLoader } from '../../store/slices/loaderSlice' -import { Bounce, toast } from 'react-toastify' import SignModal from '../modal/SignModal' import confetti from 'canvas-confetti' -import { useWindowSize } from 'react-use' import { FaCheckCircle } from 'react-icons/fa' import { IoClose } from 'react-icons/io5' import CommonNav from '../CommonNav' @@ -39,13 +37,6 @@ function Dashboard() { // Check if cookie already exists - Redirect to Dashboard useLayoutEffect(() => { - // if (!user.authorized) { - // window.location.href = '/login'; - // return; - // } - // console.log("OUT MIC CHECK", transactionStatus, TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS, TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED); - - // Clear all if transaction success if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_FAILURE) { dispatch(resetSelDoc()) @@ -54,22 +45,6 @@ function Dashboard() { if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS) { setShowConfetti(true) - // setTimeout(() => { - // setShowConfetti(false) - // dispatch(hideLoader()) - // toast.dismiss(); - // toast.info("Transaction Send Successfully!", { - // position: "bottom-right", - // autoClose: 5000, - // hideProgressBar: false, - // closeOnClick: false, - // pauseOnHover: true, - // draggable: true, - // progress: undefined, - // theme: "light", - // transition: Bounce, - // }); - // }, 1000) } return diff --git a/src/components/dashboard/SendForSign.tsx b/src/components/dashboard/SendForSign.tsx index 3c66878..3d3301a 100644 --- a/src/components/dashboard/SendForSign.tsx +++ b/src/components/dashboard/SendForSign.tsx @@ -1,13 +1,9 @@ import React, { useEffect, useRef } from 'react' -import { IoMdArrowDropdown } from "react-icons/io"; import { IoIosSend } from "react-icons/io"; import { useDispatch, useSelector } from 'react-redux'; -import { setSignType, SignType } from '../../store/slices/selectedDocsSlice'; import { RootState } from '../../store/store'; import { useNavigate } from 'react-router-dom'; -import { Bounce, toast } from 'react-toastify'; import api from '../../config/api'; -import { StartCard } from '../home/StartSection'; import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice'; import { SignTypeEnum } from '../../utlity/types'; // import { IoMdArrowDropright } from "react-icons/io"; diff --git a/src/components/process/CompletionScreen/SignerList.tsx b/src/components/process/CompletionScreen/SignerList.tsx index 67032c1..05e8fe8 100644 --- a/src/components/process/CompletionScreen/SignerList.tsx +++ b/src/components/process/CompletionScreen/SignerList.tsx @@ -19,7 +19,10 @@ export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => { {signers.map((signer) => (
- {signer.name.length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.email.split("").map(n => n[0].toUpperCase()).join("")} + {/* {signer.name.split(" ").length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.name[0].toUpperCase()} */} + + {signer.firstName && signer.firstName[0].toUpperCase()} + {signer.lastName && signer.lastName[0].toUpperCase()}
{signer.name}
diff --git a/src/components/process/processStepOne/AdvancedOptions.tsx b/src/components/process/processStepOne/AdvancedOptions.tsx new file mode 100644 index 0000000..1ccb25b --- /dev/null +++ b/src/components/process/processStepOne/AdvancedOptions.tsx @@ -0,0 +1,114 @@ +import { useState } from "react"; +import { FiLock } from "react-icons/fi"; + +export default function AdvancedOptions({setShowAdvancedOptions}) { + const [autoReminders, setAutoReminders] = useState(false); + const [expireDays, setExpireDays] = useState(120); + const [alertDays, setAlertDays] = useState(0); + + return ( +
+ {/* Header */} +
+

Advanced Options

+ +
+ + {/* Content */} +
+ {/* Sidebar */} +
+
Recipient Privileges
+
Reminders
+
Expiration
+
+ + {/* Main Content */} +
+ {/* Recipient Privileges */} +
+

Recipient Privileges

+

Give recipients options for how they sign.

+
+ {/* */} + +
+ +
+ + {/* Reminders */} +
+

Reminders

+

+ Follow up with automatic reminders. Signers will receive emails until they sign or decline the envelope. +

+
+ + Turn on auto reminders +
+
+ + {/* Expiration */} +
+

Expiration

+

+ By default, envelopes expire after 120 days. Recipients can no longer view or sign an envelope after it expires. +

+ +
+ + setExpireDays(Number(e.target.value))} + className="border rounded px-3 py-2 text-sm w-20" + /> +
+ +

+ Envelopes will expire on: 1/3/2026 +

+ +
+ + setAlertDays(Number(e.target.value))} + className="border rounded px-3 py-2 text-sm w-20" + /> +
+
+
+
+ + {/* Footer */} +
+ +
+
+ ); +} diff --git a/src/components/process/processStepOne/ProcessStepOne.tsx b/src/components/process/processStepOne/ProcessStepOne.tsx index 3a2bbbb..8e61937 100644 --- a/src/components/process/processStepOne/ProcessStepOne.tsx +++ b/src/components/process/processStepOne/ProcessStepOne.tsx @@ -18,6 +18,7 @@ import { fetchDocuments, resetDocuments, setDocuments, uploadDocuments } from ". import { SignTypeEnum } from "../../../utlity/types"; import { RiPencilFill } from "react-icons/ri"; import SelfSignWarnModal from "./SelfSignWarnModal"; +import AdvancedOptions from "./AdvancedOptions"; const ProcessStepOne = () => { interface EmailDtls { @@ -25,10 +26,29 @@ const ProcessStepOne = () => { 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 + } + let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; const signType = useParams<{ signType: string }>().signType; const templateId = useParams<{ templateId: string }>().templateId; + const [submitError, setSubmitError] = useState({ ...FormSubmitIntitialState }) + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) + let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); const [searchParams] = useSearchParams(); @@ -102,6 +122,12 @@ const ProcessStepOne = () => { dispatch(hideLoader()); }, []) + useEffect(() => { + if (submitError.duplicateEmailSameRoutingOrder) setSubmitError(prev => ({ ...prev, duplicateEmailSameRoutingOrder: false })) + if (submitError.documents) setSubmitError(prev => ({ ...prev, documents: false })) + if (submitError.recipients) setSubmitError(prev => ({ ...prev, recipients: false })) + }, [selectedRecipients]) + const getMinRoutingOrder = () => { return selectedRecipients.reduce((min, recipient) => { @@ -112,11 +138,20 @@ const ProcessStepOne = () => { const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const handleNextClick = async (saveAndClose: boolean = false) => { - let formError = { - documents: false, - recipients: false, - subject: false, - recipientEmailAndLastName: false + 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; + break; + } + + if (recipientsData[recipient.routingOrder]) { + recipientsData[recipient.routingOrder] = [...recipientsData[recipient.routingOrder], recipient.email] + } else { + recipientsData[recipient.routingOrder] = [recipient.email] + } } if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) { @@ -143,29 +178,12 @@ const ProcessStepOne = () => { if (SignTypeEnum.selfSign === signType) formError.recipients = false; - if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName) { - toast.dismiss(); - toast.error(`${formError.documents ? "Add Document" : ""} - ${(formError.documents && formError.recipients) ? " | " : ""} - ${formError.recipients ? "Add Recipient" : ""} - ${formError.recipientEmailAndLastName ? "All recipients must have both Email and Last Name." : ""} - ${(formError.documents || formError.recipients) ? " | " : ""} - ${formError.subject ? "Add Subject" : ""}`, { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); + if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName || formError.duplicateEmailSameRoutingOrder) { + setSubmitError(formError) return; } } - try { dispatch(showLoader("Processing Documents...")); @@ -188,11 +206,6 @@ const ProcessStepOne = () => { dispatch(showLoader("Processing Recipients...")); let minRoutingOrder = getMinRoutingOrder(); - // selectedRecipients.forEach((recipient) => { - // if (recipient.routingOrder ?? 1 < minRoutingOrder) { - // minRoutingOrder = recipient.routingOrder ?? 1; - // } - // }); const recipientsData = selectedRecipients.map((recipient, index) => ({ id: "id" in recipient ? recipient.id : null, @@ -303,8 +316,6 @@ const ProcessStepOne = () => { navigate(`/process-step-two/${accountId}/${transactionId}`); } } - // }, 1000); - // } } catch (error) { dispatch(hideLoader()); @@ -340,6 +351,14 @@ const ProcessStepOne = () => { const globalLoader = useSelector((state: RootState) => state.loader) + + if (showAdvancedOptions) { + return ( + + ) + } + + return ( <>
@@ -388,6 +407,15 @@ const ProcessStepOne = () => { )} + + + +
diff --git a/src/components/process/processStepOne/RecipientCards.tsx b/src/components/process/processStepOne/RecipientCards.tsx index aa0626b..1670e38 100644 --- a/src/components/process/processStepOne/RecipientCards.tsx +++ b/src/components/process/processStepOne/RecipientCards.tsx @@ -15,6 +15,9 @@ 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"; + const getInitials = (name: string) => { if (!name) return ""; @@ -121,7 +124,7 @@ const RecipientCards = ({ return currentValue && currentValue.toString().trim() !== ""; }; - const renderEditableField = ( + const renderEditableField = ( recipient: any, fieldName: string, placeholder: string, @@ -215,7 +218,7 @@ const RecipientCards = ({ setEditingField(null); }; - return ( + return (
{recipients.length > 0 ? ( <> @@ -238,7 +241,7 @@ const RecipientCards = ({ {recipients.map((recipient, idx) => (
handleOrderChange( - recipient.id || recipient.email, + 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 bg-slate-100" + 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 &&

+ + + {/* Tooltip */} + + Signing order: Recipients receive emails one after another. + Same order means they all receive at once. + +

}
) : ( handleOrderChange( - recipient.id || recipient.email, + 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 */} -
+ {/*
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)} -
+
*/} {/* Form Fields Section */}
{/* Role Name and Permissions Row */}
-
-

+ {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES &&

+
{renderEditableField(recipient, "roleName", "Role name")} -

-
- +
+
} + {/* Permissions - Mobile: Below role, Desktop: Right side */}
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && ( @@ -337,25 +350,25 @@ const RecipientCards = ({
{/* Name and Email Fields */} -
+
{/* Mobile: Stack vertically, Tablet+: Side by side */}
-

+

{renderEditableField(recipient, "firstName", "First Name")} -

+
-

+

{renderEditableField(recipient, "lastName", "Last Name")} -

+
-

+

{renderEditableField(recipient, "email", "Recipient Email", "", "email")} -

+
@@ -365,14 +378,14 @@ const RecipientCards = ({
-

+

{renderEditableField(recipient, "hostName", "Host Name")} -

+
-

+

{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")} -

+
@@ -425,7 +438,7 @@ const RecipientCards = ({ {/* Action Buttons */} {signType !== "self-sign" && ( -
+
+ + {/* Menu */} +
+
+ + + + +
+
+
+ {/* Documnt Nav */}
setNav2DocToggle(false)}>
setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'> diff --git a/src/components/templates/TemplateTable.tsx b/src/components/templates/TemplateTable.tsx index e2b381d..1d561c1 100644 --- a/src/components/templates/TemplateTable.tsx +++ b/src/components/templates/TemplateTable.tsx @@ -1,16 +1,15 @@ -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useSelector } from 'react-redux'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import logoRecord from '../../assets/record.svg'; import LoaderUi from '../../helper/LoaderUi'; import api from '../../config/api'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; +import { TemplateInterface } from '../../utlity/types'; const TemplateTable = () => { - const loaction = useLocation() - const [loading, setLoading] = useState(false) - const [templates, setTemplates] = useState([]) + const [templates, setTemplates] = useState([]) const accountDetails = useSelector((state: any) => state.user.account); const accountId = accountDetails.id || ""; const [showMenu, setShowMenu] = useState(null); @@ -88,7 +87,7 @@ const TemplateTable = () => { templateId: item.id, }; let res = await api.post(`/api/accounts/${activeAccountId}/transactions`, payload) - + // /process-step-one/:accountId/:transactionId navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`); } catch (error) { @@ -139,7 +138,7 @@ const TemplateTable = () => { Last Updated )} - Use + {/* Use */} Action @@ -181,14 +180,14 @@ const TemplateTable = () => { )} - + {/* - + */}
{ }} > {showMenu === index && ( -
+
+ @@ -221,12 +226,13 @@ const TemplateTable = () => { onMouseDown={() => { setShowMenu(null); }} - className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600" + 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" > Delete
)} +
diff --git a/src/components/transactions/TransactionView.tsx b/src/components/transactions/TransactionView.tsx index 9df1096..2b4217e 100644 --- a/src/components/transactions/TransactionView.tsx +++ b/src/components/transactions/TransactionView.tsx @@ -87,6 +87,10 @@ const TransactionView = () => { } } + + console.log(transactionDetails, "transactionDetails") + + const fetchDocuments = async () => { try { const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents`) @@ -155,13 +159,16 @@ const TransactionView = () => { More - {/* + Edit - */} + + + +

@@ -203,7 +210,7 @@ const TransactionView = () => {

{recipient.status}

- {recipient.email === user.email && recipient.status !== "COMPLETED" && ( + {transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status !== "COMPLETED" && (