This commit is contained in:
=bhagwanSingh 2025-10-27 12:25:48 +05:30
parent 950601171c
commit ca1a4da954
21 changed files with 757 additions and 368 deletions

View File

@ -6,6 +6,7 @@ import { IoSettings, IoSettingsOutline } from 'react-icons/io5';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../store/store'; import { RootState } from '../store/store';
import { GoBell } from 'react-icons/go'; import { GoBell } from 'react-icons/go';
import getInitialsUtils from '../utlity/getInitialUtils';
interface AccountNavProps { interface AccountNavProps {
accountName?: string | null; accountName?: string | null;
@ -65,7 +66,7 @@ const CommonNav: React.FC<AccountNavProps> = ({
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false); const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const [initial, setInitial] = useState<string>(""); // const [initial, setInitial] = useState<string>("");
const [user, setUser] = useState<UserData>({ const [user, setUser] = useState<UserData>({
id: 0, id: 0,
@ -86,13 +87,13 @@ const CommonNav: React.FC<AccountNavProps> = ({
navigate("/s/settings#profile-info"); navigate("/s/settings#profile-info");
} }
useEffect(() => { // useEffect(() => {
if (initial != "") return // if (initial != "") return
console.log(userDetailsAsync, 'line 89') // console.log(userDetailsAsync, 'line 89')
let initialN = (userDetailsAsync.firstName && userDetailsAsync.firstName[0]) // let initialN = (userDetailsAsync.firstName && userDetailsAsync.firstName[0])
initialN = initialN + "" + (userDetailsAsync.lastName && userDetailsAsync.lastName[0]) // initialN = initialN + "" + (userDetailsAsync.lastName && userDetailsAsync.lastName[0])
setInitial(initialN) // setInitial(initialN)
}, [userDetailsAsync]) // }, [userDetailsAsync])
const getAccountDetails = async () => { const getAccountDetails = async () => {
try { try {
@ -153,11 +154,11 @@ const CommonNav: React.FC<AccountNavProps> = ({
}, },
}); });
if (account.firstName && account.lastName) { // if (account.firstName && account.lastName) {
setInitial((account?.firstName?.slice(0, 1)).toUpperCase() + "" + (account?.lastName?.slice(0, 1)).toUpperCase()) // setInitial((account?.firstName?.slice(0, 1)).toUpperCase() + "" + (account?.lastName?.slice(0, 1)).toUpperCase())
} else { // } else {
setInitial((account?.name?.slice(0, 1)).toUpperCase()) // setInitial((account?.name?.slice(0, 1)).toUpperCase())
} // }
sessionStorage.setItem('userDetails', JSON.stringify({ sessionStorage.setItem('userDetails', JSON.stringify({
id: account.userId, id: account.userId,
@ -198,11 +199,12 @@ const CommonNav: React.FC<AccountNavProps> = ({
}; };
setUser(userData); setUser(userData);
if (userData.firstName && userData.lastName) {
setInitial((userData?.firstName?.slice(0, 1)).toUpperCase() + "" + (userData?.lastName?.slice(0, 1)).toUpperCase()) // if (userData.firstName && userData.lastName) {
} else { // setInitial((userData?.firstName?.slice(0, 1)).toUpperCase() + "" + (userData?.lastName?.slice(0, 1)).toUpperCase())
setInitial((userData?.name?.slice(0, 1)).toUpperCase()) // } else {
} // setInitial((userData?.name?.slice(0, 1)).toUpperCase())
// }
sessionStorage.setItem('userDetails', JSON.stringify(userData)); sessionStorage.setItem('userDetails', JSON.stringify(userData));
sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id)); sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id));
@ -405,8 +407,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
> >
<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"> <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 ? ( {user ? (
<span className="w-full h-full object-cover p-1.5 font-bold"> <span className="w-full h-full object-cover p-1.5 font-bold capitalize">
{initial} {getInitialsUtils((user.firstName ?? '') + (user.firstName && user.lastName ? ' ' : '') + (user.lastName ?? ''))}
</span> </span>
) : ( ) : (
<FaUser className="text-lg" /> <FaUser className="text-lg" />
@ -420,8 +422,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold shadow-md"> <div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold shadow-md">
{user ? ( {user ? (
<span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold"> <span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold capitalize">
{initial} {getInitialsUtils((user.firstName ?? '') + (user.firstName && user.lastName ? ' ' : '') + (user.lastName ?? ''))}
</span> </span>
) : ( ) : (
<FaUser className="text-lg" /> <FaUser className="text-lg" />

View File

@ -0,0 +1,45 @@
type PropsType = {
setShowConfirmationModal: React.Dispatch<{ show: boolean, id: string }>;
onConfirm: () => void;
message: string;
title: string;
textColor: string;
hoverTextColor: string;
rejectBtnText: string;
confirmBtnText: string;
};
const ConfirmationModal = ({ setShowConfirmationModal, onConfirm, message, title, textColor, hoverTextColor, rejectBtnText, confirmBtnText }: PropsType) => {
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">{title}</h3>
<p className="text-gray-600 mb-6">
{message}
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setShowConfirmationModal({ show: false, id: "" });
}}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors cursor-pointer"
>
{rejectBtnText}
</button>
<button
onClick={() => {
onConfirm();
}}
className={`px-4 py-2 ${textColor} text-white hover:${hoverTextColor} rounded-md transition-colors cursor-pointer`}
>
{confirmBtnText}
</button>
</div>
</div>
</div>
);
};
export default ConfirmationModal;

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa"; import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
import api from '../../config/api'; import api from '../../config/api';
import getInitialsUtils from '../../utlity/getInitialUtils';
interface AccountNavProps { interface AccountNavProps {
accountName?: string | null; accountName?: string | null;
@ -294,7 +295,7 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md"> <div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
{user ? ( {user ? (
<span className="w-full h-full object-cover p-1.5 font-bold"> <span className="w-full h-full object-cover p-1.5 font-bold">
{(user?.name?.slice(0, 2)).toUpperCase()} {(user?.name?.slice(0, 2)).toUpperCase()}
</span> </span>
) : ( ) : (
<FaUser className="text-lg" /> <FaUser className="text-lg" />

View File

@ -20,6 +20,7 @@ const DownloadDocument: React.FC<DownloadDocumentInt> = ({ showMenu, setShowMenu
const includeCertificate = formData.get('download-doc-type-include-cert'); const includeCertificate = formData.get('download-doc-type-include-cert');
downloadDocumentUtils(accountId, showMenu.transactionId as string, docType as string, includeCertificate as string | null) downloadDocumentUtils(accountId, showMenu.transactionId as string, docType as string, includeCertificate as string | null)
setShowMenu({ open: false, transactionId: null });
} }

View File

@ -158,7 +158,7 @@ const TransactionDataTable = ({ activeAccountId }) => {
<td className="px-6 py-3 text-[var(--mainPrimaryBtn)] font-medium"> <td className="px-6 py-3 text-[var(--mainPrimaryBtn)] font-medium">
<button <button
onClick={() => setShowMenu({ open: true, transactionId: tx.id })} onClick={() => setShowMenu({ open: true, transactionId: tx.id })}
className="flex items-center gap-2 text-sm hover:underline" className="flex items-center gap-2 text-sm hover:underline cursor-pointer"
> >
<MdOutlineFileDownload className="text-lg" /> Download <MdOutlineFileDownload className="text-lg" /> Download
</button> </button>

View File

@ -26,6 +26,7 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
<div <div
onClick={() => { onClick={() => {
setDropDownTogglePage(`${(dropDownTogglePage == 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`); setDropDownTogglePage(`${(dropDownTogglePage == 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`);
setCurrentPage(0);
window.location.hash = document.documentId + "" + 0; window.location.hash = document.documentId + "" + 0;
} }
} }

View File

@ -11,8 +11,8 @@ import { Bounce, toast } from "react-toastify";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { hideLoader, showLoader } from "../../store/slices/loaderSlice"; import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
import { FaCheck, FaQuestionCircle, FaDownload } from 'react-icons/fa' import { FaCheck, FaQuestionCircle, FaDownload, FaAngleUp } from 'react-icons/fa'
import { IoIosDocument } from 'react-icons/io' import { IoIosDocument, IoMdClose } from 'react-icons/io'
import { MdKeyboardArrowDown } from 'react-icons/md' import { MdKeyboardArrowDown } from 'react-icons/md'
import confetti from 'canvas-confetti' import confetti from 'canvas-confetti'
@ -134,6 +134,10 @@ const fitObj = [
label: "50%", label: "50%",
value: "50" value: "50"
}, },
{
label: "130%",
value: "130"
},
{ {
label: "150%", label: "150%",
value: "150" value: "150"
@ -159,6 +163,7 @@ const SignDocument = () => {
// Signer data - START // Signer data - START
const [selectedDocuments, setSelectedDocuments] = useState<DocumentType[] | null>(null); const [selectedDocuments, setSelectedDocuments] = useState<DocumentType[] | null>(null);
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null); const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
const [isPages, setIsPages] = useState<boolean>(true);
const [recipient, setRecipient] = useState<RecipientType | null>(null); const [recipient, setRecipient] = useState<RecipientType | null>(null);
const [tagJson, setTagJson] = useState<DroppedItem[] | []>([]); const [tagJson, setTagJson] = useState<DroppedItem[] | []>([]);
@ -190,7 +195,7 @@ const SignDocument = () => {
const [isFields, setIsFields] = useState<boolean>(true); const [isFields, setIsFields] = useState<boolean>(true);
const [isFitDd, setIsFitDd] = useState<boolean>(false) const [isFitDd, setIsFitDd] = useState<boolean>(false)
const [selFitIndx, setSelFitIndx] = useState<number>(0); const [selFitIndx, setSelFitIndx] = useState<number>(2);
const globalLoader = useSelector((state: RootState) => state.loader) const globalLoader = useSelector((state: RootState) => state.loader)
@ -833,7 +838,7 @@ const SignDocument = () => {
} else { } else {
setNavigationTagPos(NavigationTagPosEnum.VISIBLE) setNavigationTagPos(NavigationTagPosEnum.VISIBLE)
} }
}, 150); }, 10);
// Initial check // Initial check
checkPosition(); checkPosition();
@ -871,8 +876,42 @@ const SignDocument = () => {
{/* Nav Two - Options - START */} {/* Nav Two - Options - START */}
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className='flex justify-between items-center px-4 py-1 bg-[var(--primary)]'> <div className='flex justify-between items-center px-4 py-1 bg-[var(--primary)] mt-0'>
<img className="w-[150px] max-w-max py-1 pr-3.5" src="/sme-white.png" alt="" /> <div className="flex items-center">
<img className="w-[150px] max-w-max py-1 pr-3.5" src="/sme-white.png" alt="" />
{/* FIT - START */}
<div
className="hidden sm:block relative"
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='text-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-[30px] left-[20px] 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>
{/* Right Side - STARTS */} {/* Right Side - STARTS */}
<div className='flex justify-center items-center'> <div className='flex justify-center items-center'>
@ -950,7 +989,7 @@ const SignDocument = () => {
<div <div
className={`absolute z-50 left-0 bottom-0 bg-[var(--primary)] w-full flex justify-around items-center className={`absolute z-50 left-0 bottom-0 bg-[var(--primary)] w-full flex justify-around items-center
px-2.5 transition-all duration-500 ease-in-out px-2.5 transition-all duration-500 ease-in-out
${requiredFilled ? "translate-y-0 opacity-100" : "translate-y-full opacity-0"}`} ${requiredFilled ? "translate-y-0" : "translate-y-full hidden"}`}
> >
<div className="py-6 text-white"> <div className="py-6 text-white">
<p className="text-2xl font-semibold">All Set to Wrap This Up?</p> <p className="text-2xl font-semibold">All Set to Wrap This Up?</p>
@ -995,16 +1034,26 @@ const SignDocument = () => {
{/* Document Container Start */} {/* Document Container Start */}
<div className="grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-86px)] overflow-hidden"> <div className={`grid grid-cols-12 gap-1 mt-1 ${requiredFilled ? "h-[calc(100vh-100px)]" : "h-[calc(100vh-86px)]"} overflow-hidden`}>
{/* Left Side pages start */} {/* Left Side pages start */}
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */} {/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
<div <div
id="tags-list-client-side" id="tags-list-client-side"
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-full p-4 overflow-y-auto ${recipient?.type != "CC" ? "bg-white" : ""} ${!provideTags && isFields ? "hidden" : ""}`}> className={`${isPages ? "block" : "hidden"} sm:block 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-full p-4 overflow-y-auto ${recipient?.type != "CC" ? "bg-white" : ""} ${!provideTags && isFields ? "hidden" : ""}`}>
{!transparentTags && <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={() => {
setIsPages(false)
setIsFields(true)
}}
className='absolute sm:hidden top-[5px] right-[10px] bg-white rounded-full text-2xl z-50'>
<IoMdClose />
</div>
{provideTags && !transparentTags && <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 {provideTags && recipient?.type != "CC" && <div
onClick={() => handleSetIsFields(true)} 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'}`} 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'}`}
@ -1026,10 +1075,10 @@ const SignDocument = () => {
{!transparentTags && isFields && recipientId && provideTags && recipient?.type != "CC" && <FieldList color={"#8f87f1"} recipientId={recipientId} recipientIndx={0} acntId={"1"} />} {!transparentTags && isFields && recipientId && provideTags && recipient?.type != "CC" && <FieldList color={"#8f87f1"} recipientId={recipientId} recipientIndx={0} acntId={"1"} />}
{!isFields && <DocumentPages <DocumentPages
selectedDocuments={selectedDocuments} selectedDocuments={selectedDocuments}
setDropDownTogglePage={setDropDownTogglePage} setDropDownTogglePage={setDropDownTogglePage}
dropDownTogglePage={dropDownTogglePage} />} dropDownTogglePage={dropDownTogglePage} />
</div> </div>
{/* Pages - END */} {/* Pages - END */}
@ -1084,6 +1133,7 @@ const SignDocument = () => {
width={page.width} width={page.width}
height={page.height} height={page.height}
src={page.src} src={page.src}
loading='lazy'
/> />
{!hideTags && <> {!hideTags && <>
@ -1127,7 +1177,7 @@ const SignDocument = () => {
{!transparentTags && focusElement != null && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index && {!transparentTags && focusElement != null && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index &&
<div <div
id="tag-navigation-btn" id="tag-navigation-btn"
className={`absolute right-0 translate-x-[100%] flex justify-center gap-0 items-center z-50 cursor-pointer transition-all duration-300`} className={`hidden absolute right-0 translate-x-[100%] sm:flex justify-center gap-0 items-center z-50 cursor-pointer transition-all duration-300`}
style={{ top: (focusElement.dimensions.top) + "px" }} style={{ top: (focusElement.dimensions.top) + "px" }}
onClick={() => handleTagNavClick("N")} onClick={() => handleTagNavClick("N")}
> >
@ -1159,7 +1209,7 @@ const SignDocument = () => {
{(navigationTagPos == NavigationTagPosEnum.ABOVE || navigationTagPos == NavigationTagPosEnum.BELOW) && {(navigationTagPos == NavigationTagPosEnum.ABOVE || navigationTagPos == NavigationTagPosEnum.BELOW) &&
<div <div
onClick={() => scrollToElement(focusElement?.navigationOrder)} onClick={() => scrollToElement(focusElement?.navigationOrder)}
className={`cursor-pointer fixed ${navigationTagPos == NavigationTagPosEnum.ABOVE ? "top-[45px]" : "bottom-[45px]"} right-[8%] flex flex-col items-center`}> className={`hidden sm:block cursor-pointer fixed ${navigationTagPos == NavigationTagPosEnum.ABOVE ? "top-[45px]" : requiredFilled ? "bottom-[82px]" : "bottom-[35px]"} right-[8%] flex flex-col items-center`}>
{/* Arrow pointing up */} {/* Arrow pointing up */}
{navigationTagPos == NavigationTagPosEnum.ABOVE && <div className="w-0 h-0 border-l-6 border-r-6 border-b-6 border-l-transparent border-r-transparent border-b-[var(--primary)]"></div>} {navigationTagPos == NavigationTagPosEnum.ABOVE && <div className="w-0 h-0 border-l-6 border-r-6 border-b-6 border-l-transparent border-r-transparent border-b-[var(--primary)]"></div>}
@ -1214,6 +1264,25 @@ const SignDocument = () => {
{globalLoader.isLoading && <Loader />} {globalLoader.isLoading && <Loader />}
<div
onClick={() => {
setIsPages(true)
setIsFields(false)
}}
className='sm:hidden fixed top-[8%] left-0 bg-[var(--primary)] flex items-center text-white rounded-tr-md rounded-br-md cursor-pointer opacity-50 hover:opacity-100 transition-all duration-300'>
<div className='py-2 pl-2 pr-1'>
<FaAngleUp className='rotate-90' />
<p className='flex flex-col justify-center items-center text-xs tracking-widest mt-1 pt-1 border-t-1 border-white'>
<span>P</span>
<span>A</span>
<span>G</span>
<span>E</span>
<span>S</span>
</p>
</div>
</div>
{ {
!transparentTags && selectedDocuments != null && !showCompletionScreen && !signerConsent.consent && recipient?.type != "CC" && !transparentTags && selectedDocuments != null && !showCompletionScreen && !signerConsent.consent && recipient?.type != "CC" &&
<ConsentScreen <ConsentScreen
@ -1244,8 +1313,8 @@ const SignDocument = () => {
{!transparentTags && recipient?.type != "CC" && <> {!transparentTags && recipient?.type != "CC" && <>
<div className={`fixed bottom-0 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`}> <div className={`fixed bottom-0 w-full shadow-[0_20px_35px_rgba(0,0,0,0.25)] 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> <p className='text-xs sm:text-sm text-slate-600'>By Signing I agree Sign Made Easy <a href="" className="block py-1 sm:py-0 sm:inline 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'> {/* <div className='flex items-center gap-8 cursor-pointer text-xs px-2 py-1 rounded-sm outline-none'>
<FaAngleLeft <FaAngleLeft
onClick={() => handleTagNavClick("P")} onClick={() => handleTagNavClick("P")}
@ -1260,44 +1329,25 @@ const SignDocument = () => {
onClick={() => handleTagNavClick("N")} onClick={() => handleTagNavClick("N")}
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" /> className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
</div> */} </div> */}
<div></div> <div>
{!transparentTags && focusElement != null &&
<div
id="tag-navigation-btn-mb"
className={`sm:hidden flex justify-center gap-0 items-center z-50 cursor-pointer transition-all duration-300`}
style={{ top: (focusElement.dimensions.top) + "px" }}
onClick={() => handleTagNavClick("N")}
>
<p className="bg-[var(--mainPrimaryBtn)] p-2 rounded-md text-white capitalize">{requiredFilled && focusElement.required == true ? "Submit" : focusElement.type.toLowerCase()}</p>
{/* NEXT Button - END */}
</div>}
</div>
{/* <p className='cursor-pointer text-sm text-white bg-[var(--mainPrimaryBtn)] px-4 py-2 rounded' {/* <p className='cursor-pointer text-sm text-white bg-[var(--mainPrimaryBtn)] px-4 py-2 rounded'
onClick={handleFormSubmit} onClick={handleFormSubmit}
>Sign</p> */} >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> </div>
</>} </>}

View File

@ -7,6 +7,7 @@ import { IoMdAdd } from 'react-icons/io';
import LoaderUi from '../../helper/LoaderUi'; import LoaderUi from '../../helper/LoaderUi';
import logoRecord from '../../assets/record.svg' import logoRecord from '../../assets/record.svg'
import Loader from '../dashboard/Loader'; import Loader from '../dashboard/Loader';
import ConfirmationModal from '../ConfirmationModal';
type ContactSchema = [{ type ContactSchema = [{
id: number, id: number,
@ -27,6 +28,12 @@ const Contacts = () => {
const [editJsonData, setEditJsonData] = useState({}) const [editJsonData, setEditJsonData] = useState({})
const [actionStatus, setActionStatus] = useState("") const [actionStatus, setActionStatus] = useState("")
// Delete modal
const [showDeleteModal, setShowDeleteModal] = useState<{
show: boolean;
id: string;
}>({ show: false, id: "" });
const fetchContacts = async () => { const fetchContacts = async () => {
setLoading(true) setLoading(true)
try { try {
@ -65,6 +72,7 @@ const Contacts = () => {
// }); // });
fetchContacts() fetchContacts()
setOpenMenuId(null); setOpenMenuId(null);
setShowDeleteModal({ show: false, id: "" });
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@ -88,9 +96,9 @@ const Contacts = () => {
let res; let res;
setCreateAndInviteLoader(true); setCreateAndInviteLoader(true);
if (type == 'Edit') { if (type == 'Edit') {
res = await api.put(`/api/v1/accounts/${activeAccountId}/contacts`, {contacts: [data], empty: true}) res = await api.put(`/api/v1/accounts/${activeAccountId}/contacts`, { contacts: [data], empty: true })
} else { } else {
res = await api.post(`/api/v1/accounts/${activeAccountId}/contacts`, {contacts: [data], empty: true}); res = await api.post(`/api/v1/accounts/${activeAccountId}/contacts`, { contacts: [data], empty: true });
} }
if (res.status == 200) { if (res.status == 200) {
// toast.success('Contact created successfully!', { // toast.success('Contact created successfully!', {
@ -177,7 +185,7 @@ const Contacts = () => {
onBlur={handleBlur} onBlur={handleBlur}
> >
<button <button
className="text-gray-400 hover:text-gray-600 p-1 focus:outline-none" className="text-gray-400 hover:text-gray-600 p-1 focus:outline-none cursor-pointer"
onClick={() => toggleMenu(contact.id)} onClick={() => toggleMenu(contact.id)}
> >
<FaEllipsisV /> <FaEllipsisV />
@ -186,19 +194,22 @@ const Contacts = () => {
{openMenuId == contact.id && ( {openMenuId == contact.id && (
<div <div
className={`absolute mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-20 min-w-[100px] ${contacts.length - 1 == index className={`absolute mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-20 min-w-[100px] ${contacts.length - 1 == index
? "bottom-1 right-4" ? "bottom-1 right-4"
: "bottom-0 right-5" : "bottom-0 right-5"
}`} }`}
> >
<button <button
onMouseDown={() => handleEdit(contact.id)} onMouseDown={() => handleEdit(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600" className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600 cursor-pointer"
> >
Edit Edit
</button> </button>
<button <button
onMouseDown={() => handleDelete(contact.id)} onMouseDown={() => setShowDeleteModal({
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600" show: true,
id: contact.id + "",
})}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600 cursor-pointer"
> >
Delete Delete
</button> </button>
@ -224,6 +235,19 @@ const Contacts = () => {
</div> </div>
</div> </div>
{showDeleteModal.show && <ConfirmationModal
setShowConfirmationModal={setShowDeleteModal}
onConfirm={() => handleDelete(showDeleteModal.id)}
message={"Are you sure you want to delete this contact? This action cannot be undone."}
title={"Delete Confirmation."}
textColor={"bg-red-600"}
hoverTextColor={"bg-red-700"}
rejectBtnText={"Cancel"}
confirmBtnText={"Yes, Delete it!"}
/>}
</> </>
); );
}; };

View File

@ -1,15 +1,11 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { FiUpload } from 'react-icons/fi'
import { handleMultipleTransactionThunk, resetDocuments, uploadDocuments } from '../../store/slices/documentSlice'; import { handleMultipleTransactionThunk, resetDocuments, uploadDocuments } from '../../store/slices/documentSlice';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { toast } from 'react-toastify';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { RootState } from '../../store/store'; import { RootState } from '../../store/store';
import api from '../../config/api'; import api from '../../config/api';
import TagsCon from '../client/TagsCon';
import TransactionChoiceModal from './TransactionChoiceModal'; import TransactionChoiceModal from './TransactionChoiceModal';
import { resetRecipients } from '../../store/slices/selectedRecipientsSlice'; import { resetRecipients } from '../../store/slices/selectedRecipientsSlice';
import { AiOutlineConsoleSql } from 'react-icons/ai';
const StartNow = () => { const StartNow = () => {

View File

@ -228,6 +228,15 @@ const ProcessStepOne = () => {
order: index, order: index,
})); }));
// ---- Neeed to add documents check tha tif the processed completed only then allow for next step ----
// for (let i = 0; i < documentsData.length; i++) {
// const res = await api.get(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents/${documentsData[i].documentId}`);
// console.log(res)
// }
await api.patch(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents`, { "documents": documentsData }); await api.patch(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents`, { "documents": documentsData });
await api.get(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents?include_pages=true`).then(res => { await api.get(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents?include_pages=true`).then(res => {

View File

@ -419,6 +419,7 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
useEffect(() => { useEffect(() => {
if (!hotKeysObj.isActive) return if (!hotKeysObj.isActive) return
if((!selectedTag?.key || `${selectedTag?.key}` in droppedItems && droppedItems[selectedTag?.key].length == 0)) return;
const docCon = document.getElementById("doc-con"); const docCon = document.getElementById("doc-con");

View File

@ -203,7 +203,11 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
// onBlur={() => setDropDownTogglePage('')} // onBlur={() => setDropDownTogglePage('')}
> >
<div <div
onClick={() => setDropDownTogglePage(`${(dropDownTogglePage == 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`)} onClick={() => {
setDropDownTogglePage(`${(dropDownTogglePage == 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`);
setCurrentPage(0);
window.location.hash = document.id + "" + 0;
}}
className={`p-2 ${(dropDownTogglePage == 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}> className={`p-2 ${(dropDownTogglePage == 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
{ {
document.pages != undefined && <img document.pages != undefined && <img

View File

@ -13,6 +13,7 @@ import { RootState } from "../../store/store";
import { LuUserRound } from "react-icons/lu"; import { LuUserRound } from "react-icons/lu";
import { AiFillSignature, AiOutlineSignature } from "react-icons/ai"; import { AiFillSignature, AiOutlineSignature } from "react-icons/ai";
import { FaRegUser } from "react-icons/fa"; import { FaRegUser } from "react-icons/fa";
import getInitialsUtils from "../../utlity/getInitialUtils";
// Define the shape of state object // Define the shape of state object
interface Account { interface Account {
@ -247,7 +248,7 @@ const Setting = () => {
const tabs = [ const tabs = [
{ id: "userProfile", label: "User Profile", type: "settings" }, { 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: "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: "contactAdd", label: "Add Contact", type: "contact" },
{ id: "contactRemove", label: "Remove Contact", type: "contact" }, { id: "contactRemove", label: "Remove Contact", type: "contact" },
@ -443,7 +444,7 @@ const Setting = () => {
<div className="border-b-1 border-slate-200 py-4 px-6"> <div className="border-b-1 border-slate-200 py-4 px-6">
<div className="flex justify-start items-center gap-2"> <div className="flex justify-start items-center gap-2">
<div> <div>
<p className="bg-[#FAF0FF] text-[#A915FF] font-semibold max-w-max px-3 py-2 rounded-full ">PK</p> <p className="bg-[#FAF0FF] text-[#A915FF] font-semibold max-w-max px-3 py-2 rounded-full capitalize">{getInitialsUtils(user.name)}</p>
</div> </div>
<div> <div>
<p className="font-semibold">{user.name}</p> <p className="font-semibold">{user.name}</p>
@ -468,12 +469,12 @@ const Setting = () => {
{/* Sidebar as horizontal nav (only on mobile) */} {/* Sidebar as horizontal nav (only on mobile) */}
{activeTab == 'userProfile' && ( {activeTab == 'userProfile' && (
<section className="lg:hidden mb-2"> <section className="lg:hidden mb-2">
<div className="flex overflow-x-auto no-scrollbar gap-4 border-b border-gray-200 pb-2"> <div className="flex overflow-x-auto no-scrollbar gap-2 border-b border-gray-200 p-2">
{getSubSections(activeTab).map((sub) => ( {getSubSections(activeTab).map((sub) => (
<button <button
key={sub.id} key={sub.id}
onClick={() => handleSubSectionClick(sub.id)} onClick={() => handleSubSectionClick(sub.id)}
className={`whitespace-nowrap text-sm font-medium px-2 py-1 rounded-md className={`whitespace-nowrap text-sm font-medium px-2 py-2 rounded-md cursor-pointer
${activeSubSection == sub.id ? "bg-blue-100 text-blue-600" : "text-gray-600"} ${activeSubSection == sub.id ? "bg-blue-100 text-blue-600" : "text-gray-600"}
`} `}
> >

View File

@ -8,6 +8,7 @@ import { GrCheckboxSelected } from "react-icons/gr";
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
import { FiCheck } from 'react-icons/fi'; import { FiCheck } from 'react-icons/fi';
import { IoMdClose } from 'react-icons/io'; import { IoMdClose } from 'react-icons/io';
import getInitialsUtils from '../../../utlity/getInitialUtils';
interface Signature { interface Signature {
id: string; id: string;
@ -125,7 +126,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
useEffect(() => { useEffect(() => {
if (editingSignature) { if (editingSignature) {
setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`); setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
setInitials((`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`).toUpperCase()); setInitials(getInitialsUtils((editingSignature.firstName ?? '') + (editingSignature.firstName && editingSignature.lastName ? ' ' : '') + (editingSignature.lastName ?? '')))
setActiveTab(editingSignature.signatureType as 'SELECT' | 'DRAW' | 'UPLOAD'); setActiveTab(editingSignature.signatureType as 'SELECT' | 'DRAW' | 'UPLOAD');
// Set font for SELECT tab // Set font for SELECT tab
@ -222,11 +223,12 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
try { try {
// Base payload // Base payload
const nameParts = fullName.trim().split(/\s+/);
const payload = { const payload = {
signatureType: activeTab, signatureType: activeTab,
initials: activeTab == "SELECT" ? initials : "", initials: activeTab == "SELECT" ? initials : "",
firstName: fullName.split(' ').length > 1 ? fullName.split(' ')[0] : "", firstName: nameParts[0] || '',
lastName: fullName.split(' ').length > 1 ? fullName.split(' ')[1] : fullName.split(' ')[0], lastName: nameParts.length > 1 ? nameParts.slice(1).join(' ') : nameParts[0] || '',
font: activeTab == "SELECT" ? selectedSignature.id : "", font: activeTab == "SELECT" ? selectedSignature.id : "",
default: checked default: checked
}; };
@ -317,7 +319,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
value={fullName} value={fullName}
onChange={(e) => { onChange={(e) => {
setFullName(e.target.value) setFullName(e.target.value)
setInitials(e.target.value.trim().split(' ').length > 1 ? e.target.value.split(' ')[0][0] + "" + e.target.value.split(' ')[1][0] : e.target.value.split(' ')[0][0]) setInitials(getInitialsUtils(e.target.value))
}} }}
placeholder="Enter full name" placeholder="Enter full name"
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-md outline-none" className="w-full px-3 py-2 text-sm border border-gray-200 rounded-md outline-none"
@ -470,7 +472,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
// Only set default values if not in edit mode // Only set default values if not in edit mode
if (!isEditMode) { if (!isEditMode) {
setFullName(currUser.firstName + " " + currUser.lastName); setFullName(currUser.firstName + " " + currUser.lastName);
setInitials(currUser.firstName.slice(0, 1) + currUser.lastName.slice(0, 1)); setInitials(getInitialsUtils(currUser.firstName + " " + currUser.lastName));
} }
} catch (error) { } catch (error) {
console.error("Error fetching user details:", error); console.error("Error fetching user details:", error);
@ -519,7 +521,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
<button <button
key={item.id} key={item.id}
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')} onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
className={`flex items-center justify-start space-x-1 px-2 sm:space-x-2 sm:px-3 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap ${activeTab == item.id className={`flex items-center justify-start space-x-1 px-2 sm:space-x-2 sm:px-3 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap cursor-pointer ${activeTab == item.id
? ' text-[var(--primary)]' ? ' text-[var(--primary)]'
: 'text-gray-400' : 'text-gray-400'
}`} }`}
@ -569,14 +571,14 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
<div className='flex sm:flex-row justify-end gap-2 md:gap-3 flex-shrink-0 px-2 mt-4 mb-2'> <div className='flex sm:flex-row justify-end gap-2 md:gap-3 flex-shrink-0 px-2 mt-4 mb-2'>
<button <button
className="px-4 py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200" className="px-4 py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200 cursor-pointer"
onClick={() => setShowAddSignature?.(false)} onClick={() => setShowAddSignature?.(false)}
disabled={isSubmitting} disabled={isSubmitting}
> >
CANCEL CANCEL
</button> </button>
<button <button
className="px-4 py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg bg-[var(--mainPrimaryBtn)] text-white hover:opacity-90 hover:shadow-lg flex items-center justify-center gap-1 md:gap-2 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200" className="px-4 py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg bg-[var(--mainPrimaryBtn)] text-white hover:opacity-90 hover:shadow-lg flex items-center justify-center gap-1 md:gap-2 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200 cursor-pointer"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} disabled={isSubmitting}
> >

View File

@ -1,3 +1,318 @@
// import React, { useRef, useState, useEffect } from 'react';
// import { IoMdClose } from 'react-icons/io';
// type Props = {
// onSignatureChange: (data: { fullSignature: string | null; initials: string | null }) => void;
// initialSignatures?: { fullSignature: string | null; initials: string | null };
// };
// const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignatures }) => {
// 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 }>>([]);
// const [activeCanvas, setActiveCanvas] = useState<'full' | 'initials' | null>(null);
// const [initialLoaded, setInitialLoaded] = useState(false);
// // Store initial signature images to avoid reloading
// const [initialImage1, setInitialImage1] = useState<HTMLImageElement | null>(null);
// const [initialImage2, setInitialImage2] = useState<HTMLImageElement | null>(null);
// // Setup canvas dimensions when component mounts
// useEffect(() => {
// 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;
// }
// }
// };
// const observer1 = new ResizeObserver(() => resizeCanvas(canvasRef1.current));
// const observer2 = new ResizeObserver(() => resizeCanvas(canvasRef2.current));
// 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;
// let loadedCount = 0;
// const totalToLoad = (initialSignatures.fullSignature ? 1 : 0) + (initialSignatures.initials ? 1 : 0);
// const checkComplete = () => {
// loadedCount++;
// if (loadedCount == totalToLoad) {
// setInitialLoaded(true);
// }
// };
// // Load full signature
// if (initialSignatures.fullSignature && canvasRef1.current) {
// const img1 = new Image();
// img1.onload = () => {
// setInitialImage1(img1);
// const ctx1 = canvasRef1.current?.getContext('2d');
// if (ctx1) {
// ctx1.clearRect(0, 0, canvasRef1.current!.width, canvasRef1.current!.height);
// ctx1.drawImage(img1, 0, 0);
// }
// checkComplete();
// };
// img1.src = initialSignatures.fullSignature;
// }
// // Load initials
// if (initialSignatures.initials && canvasRef2.current) {
// const img2 = new Image();
// img2.onload = () => {
// setInitialImage2(img2);
// const ctx2 = canvasRef2.current?.getContext('2d');
// if (ctx2) {
// ctx2.clearRect(0, 0, canvasRef2.current!.width, canvasRef2.current!.height);
// ctx2.drawImage(img2, 0, 0);
// }
// checkComplete();
// };
// img2.src = initialSignatures.initials;
// }
// if (totalToLoad == 0) {
// setInitialLoaded(true);
// }
// };
// const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
// setIsDrawing(true);
// const rect = e.currentTarget.getBoundingClientRect();
// const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
// setCurrentPath([point]);
// setActiveCanvas(e.currentTarget == canvasRef1.current ? 'full' : 'initials');
// };
// 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];
// setCurrentPath(newPath);
// // Draw immediately for smooth experience
// drawCurrentPath(e.currentTarget as HTMLCanvasElement, newPath);
// };
// const drawCurrentPath = (canvas: HTMLCanvasElement, path: Array<{ x: number; y: number }>) => {
// if (!canvas || path.length < 2) return;
// const ctx = canvas.getContext('2d');
// if (!ctx) return;
// // Only draw the current path segment without clearing the canvas
// ctx.strokeStyle = '#000';
// ctx.lineWidth = 2;
// ctx.lineJoin = 'round';
// ctx.lineCap = 'round';
// const lastPoint = path[path.length - 2];
// const currentPoint = path[path.length - 1];
// ctx.beginPath();
// ctx.moveTo(lastPoint.x, lastPoint.y);
// ctx.lineTo(currentPoint.x, currentPoint.y);
// ctx.stroke();
// };
// const exportCanvasAsImage = (canvas: HTMLCanvasElement | null): string | null => {
// if (!canvas) return null;
// return canvas.toDataURL('image/png');
// };
// const handleMouseUp = () => {
// setIsDrawing(false);
// if (currentPath.length == 0 || !activeCanvas) return;
// if (activeCanvas == 'full') {
// const newPaths = [...paths1, currentPath];
// setPaths1(newPaths);
// } else {
// const newPaths = [...paths2, currentPath];
// setPaths2(newPaths);
// }
// setCurrentPath([]);
// // Export both canvases and notify parent
// const fullSignature = exportCanvasAsImage(canvasRef1.current);
// const initials = exportCanvasAsImage(canvasRef2.current);
// 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) => {
// const ctx = canvas.getContext('2d');
// if (!ctx) return;
// // Clear canvas
// ctx.clearRect(0, 0, canvas.width, canvas.height);
// // Draw initial signature if exists
// if (initialImage) {
// ctx.drawImage(initialImage, 0, 0);
// }
// // Draw all paths
// ctx.strokeStyle = '#000';
// ctx.lineWidth = 2;
// ctx.lineJoin = 'round';
// ctx.lineCap = 'round';
// paths.forEach((path) => {
// if (path.length < 2) return;
// ctx.beginPath();
// ctx.moveTo(path[0].x, path[0].y);
// for (let i = 1; i < path.length; i++) {
// ctx.lineTo(path[i].x, path[i].y);
// }
// ctx.stroke();
// });
// };
// const clearCanvas = () => {
// if (canvasRef1.current) {
// const ctx1 = canvasRef1.current.getContext('2d');
// ctx1?.clearRect(0, 0, canvasRef1.current.width, canvasRef1.current.height);
// }
// if (canvasRef2.current) {
// const ctx2 = canvasRef2.current.getContext('2d');
// ctx2?.clearRect(0, 0, canvasRef2.current.width, canvasRef2.current.height);
// }
// setIsDrawingType({ signatureDrawn: false, initialDrawn: false })
// setPaths1([]);
// setPaths2([]);
// setInitialImage1(null);
// setInitialImage2(null);
// setInitialLoaded(false);
// onSignatureChange({ fullSignature: null, initials: null });
// };
// const undoLastPath = () => {
// if (activeCanvas == 'full' && paths1.length > 0) {
// const newPaths = paths1.slice(0, -1);
// setPaths1(newPaths);
// redrawCanvas(canvasRef1.current!, newPaths, initialImage1);
// } else if (activeCanvas == 'initials' && paths2.length > 0) {
// const newPaths = paths2.slice(0, -1);
// setPaths2(newPaths);
// redrawCanvas(canvasRef2.current!, newPaths, initialImage2);
// }
// // Update parent with new signature data
// setTimeout(() => {
// const fullSignature = exportCanvasAsImage(canvasRef1.current);
// const initials = exportCanvasAsImage(canvasRef2.current);
// onSignatureChange({ fullSignature, initials });
// }, 10);
// };
// return (
// <div className="px-6 py-2 sm: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
// onClick={clearCanvas}
// className='flex justify-center items-center gap-1 font-medium'>
// <IoMdClose className='text-red-500' />
// <button
// className="text-xs text-red-500 text-nowrap"
// >
// 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="sm:grid sm:grid-cols-12 gap-4 lg:gap-6 overflow-x-auto">
// {/* Full Signature Canvas */}
// <div className="sm: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}
// className="w-full h-36 sm:h-51 cursor-crosshair"
// onPointerDown={handleMouseDown}
// onPointerMove={(e) => handleMouseMove(e, "SIGNATURE")}
// onPointerUp={() => handleMouseUp()}
// onPointerLeave={() => handleMouseUp()}
// />
// </div>
// </div>
// {/* Initials Canvas */}
// <div className="mt-2 sm:mt-0 sm: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-36 sm:h-51 cursor-crosshair"
// onPointerDown={handleMouseDown}
// onPointerMove={(e) => handleMouseMove(e, "INITIAL")}
// onPointerUp={() => handleMouseUp()}
// onPointerLeave={() => handleMouseUp()}
// />
// </div>
// </div>
// </div>
// </div>
// </div>
// );
// };
// export default DrawSignatureCanvas;
import React, { useRef, useState, useEffect } from 'react'; import React, { useRef, useState, useEffect } from 'react';
import { IoMdClose } from 'react-icons/io'; import { IoMdClose } from 'react-icons/io';
@ -9,26 +324,36 @@ type Props = {
const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignatures }) => { const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignatures }) => {
const canvasRef1 = useRef<HTMLCanvasElement>(null); const canvasRef1 = useRef<HTMLCanvasElement>(null);
const canvasRef2 = 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 [isDrawingType, setIsDrawingType] = useState({
signatureDrawn: false,
initialDrawn: false,
});
const [activeCanvas, setActiveCanvas] = useState<'full' | 'initials' | null>(null);
const [paths1, setPaths1] = useState<Array<Array<{ x: number; y: number }>>>([]); const [paths1, setPaths1] = useState<Array<Array<{ x: number; y: number }>>>([]);
const [paths2, setPaths2] = 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 }>>([]); const [currentPath, setCurrentPath] = useState<Array<{ x: number; y: number }>>([]);
const [activeCanvas, setActiveCanvas] = useState<'full' | 'initials' | null>(null); const [pointerDown, setPointerDown] = useState(false);
const [initialLoaded, setInitialLoaded] = useState(false); const [pointerId, setPointerId] = useState<number | null>(null);
// Store initial signature images to avoid reloading // 📏 Resize observer to match canvas with container size
const [initialImage1, setInitialImage1] = useState<HTMLImageElement | null>(null);
const [initialImage2, setInitialImage2] = useState<HTMLImageElement | null>(null);
// Setup canvas dimensions when component mounts
useEffect(() => { useEffect(() => {
const resizeCanvas = (canvas: HTMLCanvasElement | null) => { const resizeCanvas = (canvas: HTMLCanvasElement | null) => {
if (canvas) { if (canvas) {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) { if (rect.width > 0 && rect.height > 0) {
const imgData = canvas.toDataURL(); // save before resize
canvas.width = rect.width; canvas.width = rect.width;
canvas.height = rect.height; canvas.height = rect.height;
if (imgData) {
const img = new Image();
img.onload = () => {
canvas.getContext('2d')?.drawImage(img, 0, 0);
};
img.src = imgData;
}
} }
} }
}; };
@ -39,7 +364,6 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
if (canvasRef1.current) observer1.observe(canvasRef1.current); if (canvasRef1.current) observer1.observe(canvasRef1.current);
if (canvasRef2.current) observer2.observe(canvasRef2.current); if (canvasRef2.current) observer2.observe(canvasRef2.current);
// initial setup
resizeCanvas(canvasRef1.current); resizeCanvas(canvasRef1.current);
resizeCanvas(canvasRef2.current); resizeCanvas(canvasRef2.current);
@ -49,94 +373,31 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
}; };
}, []); }, []);
const getPoint = (e: React.PointerEvent<HTMLCanvasElement>) => {
const loadInitialSignatures = () => {
if (!initialSignatures) return;
let loadedCount = 0;
const totalToLoad = (initialSignatures.fullSignature ? 1 : 0) + (initialSignatures.initials ? 1 : 0);
const checkComplete = () => {
loadedCount++;
if (loadedCount == totalToLoad) {
setInitialLoaded(true);
}
};
// Load full signature
if (initialSignatures.fullSignature && canvasRef1.current) {
const img1 = new Image();
img1.onload = () => {
setInitialImage1(img1);
const ctx1 = canvasRef1.current?.getContext('2d');
if (ctx1) {
ctx1.clearRect(0, 0, canvasRef1.current!.width, canvasRef1.current!.height);
ctx1.drawImage(img1, 0, 0);
}
checkComplete();
};
img1.src = initialSignatures.fullSignature;
}
// Load initials
if (initialSignatures.initials && canvasRef2.current) {
const img2 = new Image();
img2.onload = () => {
setInitialImage2(img2);
const ctx2 = canvasRef2.current?.getContext('2d');
if (ctx2) {
ctx2.clearRect(0, 0, canvasRef2.current!.width, canvasRef2.current!.height);
ctx2.drawImage(img2, 0, 0);
}
checkComplete();
};
img2.src = initialSignatures.initials;
}
if (totalToLoad == 0) {
setInitialLoaded(true);
}
};
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
setIsDrawing(true);
const rect = e.currentTarget.getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top }; return { x: e.clientX - rect.left, y: e.clientY - rect.top };
setCurrentPath([point]);
setActiveCanvas(e.currentTarget == canvasRef1.current ? 'full' : 'initials');
}; };
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>, type: "SIGNATURE" | "INITIAL") => { const drawSmoothLine = (
if (!isDrawing) return; canvas: HTMLCanvasElement,
setIsDrawingType((prev) => { path: Array<{ x: number; y: number }>
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];
setCurrentPath(newPath);
// Draw immediately for smooth experience
drawCurrentPath(e.currentTarget as HTMLCanvasElement, newPath);
};
const drawCurrentPath = (canvas: HTMLCanvasElement, path: Array<{ x: number; y: number }>) => {
if (!canvas || path.length < 2) return;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx || path.length < 2) return;
// Only draw the current path segment without clearing the canvas
ctx.strokeStyle = '#000'; ctx.strokeStyle = '#000';
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
ctx.lineCap = 'round'; ctx.lineCap = 'round';
const lastPoint = path[path.length - 2];
const currentPoint = path[path.length - 1];
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(lastPoint.x, lastPoint.y); ctx.moveTo(path[0].x, path[0].y);
ctx.lineTo(currentPoint.x, currentPoint.y); for (let i = 1; i < path.length - 1; i++) {
const midX = (path[i].x + path[i + 1].x) / 2;
const midY = (path[i].y + path[i + 1].y) / 2;
ctx.quadraticCurveTo(path[i].x, path[i].y, midX, midY);
}
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
ctx.stroke(); ctx.stroke();
}; };
@ -145,158 +406,105 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
return canvas.toDataURL('image/png'); return canvas.toDataURL('image/png');
}; };
const handleMouseUp = () => { const handlePointerDown = (e: React.PointerEvent<HTMLCanvasElement>) => {
setIsDrawing(false); e.currentTarget.setPointerCapture(e.pointerId);
setPointerDown(true);
if (currentPath.length == 0 || !activeCanvas) return; setPointerId(e.pointerId);
setCurrentPath([getPoint(e)]);
if (activeCanvas == 'full') { setActiveCanvas(e.currentTarget === canvasRef1.current ? 'full' : 'initials');
const newPaths = [...paths1, currentPath];
setPaths1(newPaths);
} else {
const newPaths = [...paths2, currentPath];
setPaths2(newPaths);
}
setCurrentPath([]);
// Export both canvases and notify parent
const fullSignature = exportCanvasAsImage(canvasRef1.current);
const initials = exportCanvasAsImage(canvasRef2.current);
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) => { const handlePointerMove = (e: React.PointerEvent<HTMLCanvasElement>, type: 'SIGNATURE' | 'INITIAL') => {
const ctx = canvas.getContext('2d'); if (!pointerDown || pointerId !== e.pointerId) return;
if (!ctx) return;
// Clear canvas const newPath = [...currentPath, getPoint(e)];
ctx.clearRect(0, 0, canvas.width, canvas.height); setCurrentPath(newPath);
// Draw initial signature if exists setIsDrawingType(prev => ({
if (initialImage) { ...prev,
ctx.drawImage(initialImage, 0, 0); signatureDrawn: type === 'SIGNATURE' ? true : prev.signatureDrawn,
} initialDrawn: type === 'INITIAL' ? true : prev.initialDrawn,
}));
// Draw all paths drawSmoothLine(e.currentTarget, newPath);
ctx.strokeStyle = '#000'; };
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
paths.forEach((path) => { const handlePointerUp = (e: React.PointerEvent<HTMLCanvasElement>) => {
if (path.length < 2) return; if (!pointerDown || pointerId !== e.pointerId) return;
ctx.beginPath(); setPointerDown(false);
ctx.moveTo(path[0].x, path[0].y); setPointerId(null);
for (let i = 1; i < path.length; i++) {
ctx.lineTo(path[i].x, path[i].y); if (currentPath.length > 0 && activeCanvas) {
if (activeCanvas === 'full') {
setPaths1(prev => [...prev, currentPath]);
} else {
setPaths2(prev => [...prev, currentPath]);
} }
ctx.stroke(); }
setCurrentPath([]);
const fullSignature = exportCanvasAsImage(canvasRef1.current);
const initials = exportCanvasAsImage(canvasRef2.current);
onSignatureChange({
fullSignature: isDrawingType.signatureDrawn ? fullSignature : null,
initials: isDrawingType.initialDrawn ? initials : null,
}); });
}; };
const clearCanvas = () => { const clearCanvas = () => {
if (canvasRef1.current) { const ctx1 = canvasRef1.current?.getContext('2d');
const ctx1 = canvasRef1.current.getContext('2d'); const ctx2 = canvasRef2.current?.getContext('2d');
ctx1?.clearRect(0, 0, canvasRef1.current.width, canvasRef1.current.height); if (ctx1 && canvasRef1.current) ctx1.clearRect(0, 0, canvasRef1.current.width, canvasRef1.current.height);
} if (ctx2 && canvasRef2.current) ctx2.clearRect(0, 0, canvasRef2.current.width, canvasRef2.current.height);
if (canvasRef2.current) {
const ctx2 = canvasRef2.current.getContext('2d');
ctx2?.clearRect(0, 0, canvasRef2.current.width, canvasRef2.current.height);
}
setIsDrawingType({ signatureDrawn: false, initialDrawn: false })
setPaths1([]); setPaths1([]);
setPaths2([]); setPaths2([]);
setInitialImage1(null); setIsDrawingType({ signatureDrawn: false, initialDrawn: false });
setInitialImage2(null);
setInitialLoaded(false);
onSignatureChange({ fullSignature: null, initials: null }); onSignatureChange({ fullSignature: null, initials: null });
}; };
const undoLastPath = () => {
if (activeCanvas == 'full' && paths1.length > 0) {
const newPaths = paths1.slice(0, -1);
setPaths1(newPaths);
redrawCanvas(canvasRef1.current!, newPaths, initialImage1);
} else if (activeCanvas == 'initials' && paths2.length > 0) {
const newPaths = paths2.slice(0, -1);
setPaths2(newPaths);
redrawCanvas(canvasRef2.current!, newPaths, initialImage2);
}
// Update parent with new signature data
setTimeout(() => {
const fullSignature = exportCanvasAsImage(canvasRef1.current);
const initials = exportCanvasAsImage(canvasRef2.current);
onSignatureChange({ fullSignature, initials });
}, 10);
};
return ( return (
<div className="lg:px-6 py-4 flex-shrink-0"> <div className="px-6 py-2 sm:py-4 flex-shrink-0">
<div className='flex justify-between'> <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">
<h2 className="mt-4 mb-5 text-sm">Draw your signature. <span className='text-red-600'>*</span> Are required fields.</h2> Draw your signature. <span className="text-red-600">*</span> Required fields.
</h2>
{/* Controls */} <div className="flex gap-2 mt-3 items-center" onClick={clearCanvas}>
<div className="flex gap-2 mt-3"> <IoMdClose className="text-red-500 cursor-pointer" />
<div <button className="text-xs text-red-500 text-nowrap cursor-pointer">Clear Signature</button>
onClick={clearCanvas}
className='flex justify-center items-center gap-1 font-medium'>
<IoMdClose className='text-red-500' />
<button
className="text-xs text-red-500"
>
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> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-12 gap-4 lg:gap-6 overflow-x-auto"> <div className="sm:grid sm:grid-cols-12 gap-4 lg:gap-6 overflow-x-auto">
{/* Full Signature Canvas */} {/* Full Signature */}
<div className="col-span-8"> <div className="sm:col-span-8">
<label className="block text-sm font-medium mb-2">Full Name <label className="block text-sm font-medium mb-2">
<span className='text-red-600'> *</span> Full Name <span className="text-red-600">*</span>
</label> </label>
<div className="border-1 border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors"> <div className="border border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors">
<canvas <canvas
ref={canvasRef1} ref={canvasRef1}
style={{ width: "100%", height: "200px" }} className="w-full h-36 sm:h-51 cursor-crosshair"
className="w-full h-51 cursor-crosshair" onPointerDown={handlePointerDown}
onPointerDown={handleMouseDown} onPointerMove={e => handlePointerMove(e, 'SIGNATURE')}
onPointerMove={(e) => handleMouseMove(e, "SIGNATURE")} onPointerUp={handlePointerUp}
onPointerUp={() => handleMouseUp()}
onPointerLeave={() => handleMouseUp()}
/> />
</div> </div>
</div> </div>
{/* Initials Canvas */} {/* Initials */}
<div className="col-span-4"> <div className="mt-2 sm:mt-0 sm:col-span-4">
<label className="block text-sm font-medium mb-2">Initials <label className="block text-sm font-medium mb-2">
<span className='text-red-600'> *</span> Initials <span className="text-red-600">*</span>
</label> </label>
<div className="border-1 border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors"> <div className="border border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors">
<canvas <canvas
ref={canvasRef2} ref={canvasRef2}
className="w-full h-51 cursor-crosshair" className="w-full h-36 sm:h-51 cursor-crosshair"
onPointerDown={handleMouseDown} onPointerDown={handlePointerDown}
onPointerMove={(e) => handleMouseMove(e, "INITIAL")} onPointerMove={e => handlePointerMove(e, 'INITIAL')}
onPointerUp={() => handleMouseUp()} onPointerUp={handlePointerUp}
onPointerLeave={() => handleMouseUp()}
/> />
</div> </div>
</div> </div>

View File

@ -4,6 +4,9 @@ import AddSignatureModal from './AddSignatureModal';
import api from '../../../config/api'; import api from '../../../config/api';
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
import { FaSpinner } from 'react-icons/fa'; import { FaSpinner } from 'react-icons/fa';
import getInitialsUtils from '../../../utlity/getInitialUtils';
import DeleteModal from '../../ConfirmationModal';
import ConfirmationModal from '../../ConfirmationModal';
interface Signature { interface Signature {
id: string; id: string;
@ -30,6 +33,12 @@ const SignatureManager: React.FC = () => {
const [showAddSignature, setShowAddSignature] = useState<boolean>(false); const [showAddSignature, setShowAddSignature] = useState<boolean>(false);
const [editingSignature, setEditingSignature] = useState<Signature | null>(null); const [editingSignature, setEditingSignature] = useState<Signature | null>(null);
// Delete modal
const [showDeleteModal, setShowDeleteModal] = useState<{
show: boolean;
id: string;
}>({ show: false, id: "" });
const signatureOptions = [ const signatureOptions = [
{ id: 1, name: 'print', initials: 'LA', style: 'print' }, { id: 1, name: 'print', initials: 'LA', style: 'print' },
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' }, { id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
@ -71,17 +80,8 @@ const SignatureManager: React.FC = () => {
setLoading(true) setLoading(true)
try { try {
let res = await api.delete(`/api/v1/user/signatures/${id}`) let res = await api.delete(`/api/v1/user/signatures/${id}`)
// toast.success("Signature Delete Successfull", {
// position: "top-center", setShowDeleteModal({ show: false, id: "" });
// autoClose: 500,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
await getAllSignatures(); await getAllSignatures();
} catch (error) { } catch (error) {
console.log("delete signature", error) console.log("delete signature", error)
@ -113,7 +113,7 @@ const SignatureManager: React.FC = () => {
const fontClass = `${selectedFont?.style || 'print'}`; const fontClass = `${selectedFont?.style || 'print'}`;
if (mode == 'initials') { if (mode == 'initials') {
const initials = `${(firstName?.[0] ?? '')}${(lastName?.[0] ?? '')}`.toUpperCase(); const initials = getInitialsUtils((firstName ?? '') + (firstName && lastName ? ' ' : '') + (lastName ?? ''));
return ( return (
<div className={`text-center text-[24px]`} style={{ fontFamily: selectedFont?.style }}> <div className={`text-center text-[24px]`} style={{ fontFamily: selectedFont?.style }}>
{initials} {initials}
@ -243,13 +243,18 @@ const SignatureManager: React.FC = () => {
<div className="relative flex justify-start gap-4"> <div className="relative flex justify-start gap-4">
<button <button
onClick={() => handleEdit(signature.id)} onClick={() => handleEdit(signature.id)}
className="max-w-max text-gray-500 text-lg" className="max-w-max text-gray-500 text-lg cursor-pointer"
> >
<FiEdit3 /> <FiEdit3 />
</button> </button>
<button <button
onClick={() => handleDelete(signature.id)} onClick={() =>
className="max-w-max text-red-500 text-lg" setShowDeleteModal({
show: true,
id: signature.id + "",
})
}
className="max-w-max text-red-500 text-lg cursor-pointer"
> >
<FiTrash2 /> <FiTrash2 />
</button> </button>
@ -290,7 +295,21 @@ const SignatureManager: React.FC = () => {
/> />
)} )}
</div> </div>
}</> }
{showDeleteModal.show && <ConfirmationModal
setShowConfirmationModal={setShowDeleteModal}
onConfirm={() => handleDelete(showDeleteModal.id)}
message={"Are you sure you want to delete this signature? This action cannot be undone."}
title={"Delete Confirmation."}
textColor={"bg-red-600"}
hoverTextColor={"bg-red-700"}
rejectBtnText={"Cancel"}
confirmBtnText={"Yes, Delete it!"}
/>}
</>
); );
}; };

View File

@ -30,6 +30,7 @@ import { SaveType, SaveTypeEnum } from '../client/SignDocument';
import { CiSaveDown1 } from 'react-icons/ci'; import { CiSaveDown1 } from 'react-icons/ci';
import api from '../../config/api'; import api from '../../config/api';
import { IoMdClose } from 'react-icons/io'; import { IoMdClose } from 'react-icons/io';
import getInitialsUtils from '../../utlity/getInitialUtils';
interface Signature { interface Signature {
id: string; id: string;
@ -244,12 +245,13 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
} }
try { try {
const nameParts = fullName.trim().split(/\s+/);
const payload = { const payload = {
tagId: signTagId, tagId: signTagId,
// signatureId: signatureId, // signatureId: signatureId,
type: activeTab, type: activeTab,
firstName: fullName.split(' ').length > 1 ? fullName.split(' ')[0] : "", firstName: nameParts[0] || '',
lastName: fullName.split(' ').length > 1 ? fullName.split(' ')[1] : fullName.split(' ')[0], lastName: nameParts.length > 1 ? nameParts.slice(1).join(' ') : nameParts[0] || '',
initials: initials, initials: initials,
initialsBase64: signatures.initials, initialsBase64: signatures.initials,
signatureBase64: signatures.fullSignature, signatureBase64: signatures.fullSignature,
@ -356,7 +358,7 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
value={fullName} value={fullName}
onChange={(e) => { onChange={(e) => {
setFullName(e.target.value) setFullName(e.target.value)
setInitials(e.target.value.trim().split(' ').length > 1 ? e.target.value.split(' ')[0][0]+""+e.target.value.split(' ')[1][0] : e.target.value.split(' ')[0][0]) setInitials(getInitialsUtils(e.target.value))
}} }}
placeholder="Enter full name" placeholder="Enter full name"
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-md outline-none" className="w-full px-3 py-2 text-sm border border-gray-200 rounded-md outline-none"
@ -417,9 +419,9 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
case 'UPLOAD': case 'UPLOAD':
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col px-6">
<div className="mt-4 mb-5 text-sm"> <div className="mt-4 mb-5 text-sm">
<h2 className="text-sm font-semibold text-gray-900 mb-1">Upload your Signature and confirm your name * Are required fields.</h2> <h2 className="text-sm text-gray-900 mb-1">Upload your Signature and confirm your name * Are required fields.</h2>
<p className="text-xs md:text-sm text-gray-400 hidden md:block mt-2">For best results, use an image that is 400 x 150 pixels for signature and 150 x 150 for initials</p> <p className="text-xs md:text-sm text-gray-400 hidden md:block mt-2">For best results, use an image that is 400 x 150 pixels for signature and 150 x 150 for initials</p>
<p className="text-xs md:text-sm text-gray-400 hidden md:block">The maximum file size for the image is 300kb. File types supported include .png, .jpg, .jpeg</p> <p className="text-xs md:text-sm text-gray-400 hidden md:block">The maximum file size for the image is 300kb. File types supported include .png, .jpg, .jpeg</p>
</div> </div>
@ -586,7 +588,7 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
// let currUser = res?.data; // let currUser = res?.data;
setUser(recipient); setUser(recipient);
setFullName(recipient.firstName + " " + recipient.lastName); setFullName(recipient.firstName + " " + recipient.lastName);
setInitials((recipient.firstName.slice(0, 1) + recipient.lastName.slice(0, 1)).toUpperCase()); setInitials(getInitialsUtils((recipient.firstName ?? '') + (recipient.firstName && recipient.lastName ? ' ' : '') + (recipient.lastName ?? '')));
} catch (error) { } catch (error) {
console.error("Error fetching user details:", error); console.error("Error fetching user details:", error);
} }
@ -597,8 +599,8 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
}, []); }, []);
return ( return (
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 bg-black/30 flex items-center justify-center z-50 p-2 md:p-4"> <div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 bg-black/30 flex items-center justify-center z-50 p-0 sm:p-2 md:p-4">
<div className="bg-white max-w-[80%] rounded-2xl shadow-xl w-full relative overflow-hidden "> <div className="bg-white sm:max-w-[80%] max-w-[100%] h-[100dvh] sm:h-auto sm:rounded-2xl shadow-xl w-full relative overflow-hidden ">
{/* Main Content Area */} {/* Main Content Area */}
<div className="flex-1 md:flex flex-col min-h-0"> <div className="flex-1 md:flex flex-col min-h-0">
@ -608,9 +610,9 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
{/* Navigation Menu - START */} {/* Navigation Menu - START */}
<div className="px-6 flex-shrink-0 border-b-2 border-gray-200"> <div className="px-1 sm:px-6 flex-shrink-0 border-b-2 border-gray-200">
<div className='pt-6 pb-4 relative '> <div className='px-6 sm:px-0 pt-6 pb-4 relative '>
<div className='flex justify-between'> <div className='flex justify-between'>
<p className='text-2xl font-semibold'>Signature Style Selection</p> <p className='text-2xl font-semibold'>Signature Style Selection</p>
<IoMdClose <IoMdClose
@ -620,27 +622,35 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
<p className='text-gray-500'>Create your digital signatures</p> <p className='text-gray-500'>Create your digital signatures</p>
</div> </div>
<nav className="flex gap-4"> <nav className="flex gap-0 sm:gap-4">
{navigationItems.map((item) => ( {navigationItems.map((item) => {
<> if (item.label === 'SAVED' && preSaveSignatures.length === 0) return null;
<div className='relative'>
<button return (
key={item.id} <>
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')} <div className='relative'>
className={`flex items-center justify-start space-x-2 px-3 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap ${activeTab == item.id <button
? ' text-[var(--primary)]' key={item.id}
: 'text-gray-400' onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
}`} className={`flex items-center justify-start space-x-2 px-2.5 sm:px-3 py-2 sm:py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap cursor-pointer ${activeTab == item.id
> ? ' text-[var(--primary)]'
<span className={activeTab == item.id ? 'text-[var(--mainPrimaryBtn)]' : 'text-gray-400'}> : 'text-gray-400'
{item.icon} }`}
</span> >
<span>{item.label}</span> <span className={activeTab == item.id ? 'text-[var(--mainPrimaryBtn)]' : 'text-gray-400'}>
</button> {item.icon}
{activeTab == item.id && <div className='absolute bottom-[-1px] w-full h-[3px] bg-[var(--mainPrimaryBtn)]'></div>} </span>
</div> <span>{item.label}</span>
</> </button>
))} {activeTab == item.id && <div className='absolute bottom-[-1px] w-full h-[3px] bg-[var(--mainPrimaryBtn)]'></div>}
</div>
</>
)
}
)}
</nav> </nav>
</div> </div>
@ -652,18 +662,18 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
</div> </div>
{/* Footer Buttons - Fixed */} {/* Footer Buttons - Fixed */}
<div className="px-4 py-3 flex sm:flex-row justify-between items-center gap-2 md:gap-3 flex-shrink-0"> <div className="absolute bottom-0 w-full sm:relative px-6 py-3 flex flex-col sm:flex-row justify-center sm:justify-between items-start sm:items-center gap-2 md:gap-3 flex-shrink-0">
<p className='text-sm'>* I understand that I am signing this document.</p> <p className='text-sm'>* I understand that I am signing this document.</p>
<div className='flex sm:flex-row justify-end gap-2 md:gap-3 flex-shrink-0 px-2 mt-4 mb-2'> <div className='flex w-full sm:max-w-max sm:flex-row justify-end gap-2 md:gap-3 flex-shrink-0 sm:px-2 sm:mt-4 mb-2'>
<button <button
className="px-4 py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200" className="px-4 py-3 sm:py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200 cursor-pointer"
onClick={() => setOpenSign?.(false)} onClick={() => setOpenSign?.(false)}
disabled={isSubmitting} disabled={isSubmitting}
> >
CANCEL CANCEL
</button> </button>
<button <button
className="px-4 py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg bg-[var(--mainPrimaryBtn)] text-white hover:opacity-90 hover:shadow-lg flex items-center justify-center gap-1 md:gap-2 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200" className="cursor-pointer px-4 py-3 sm:py-2 w-full sm:w-auto text-xs md:text-sm font-medium rounded-lg bg-[var(--mainPrimaryBtn)] text-white hover:opacity-90 hover:shadow-lg flex items-center justify-center gap-1 md:gap-2 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} disabled={isSubmitting}
> >
@ -673,7 +683,7 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
<span>CREATING...</span> <span>CREATING...</span>
</> </>
) : ( ) : (
<span>CREATE SIGNATURE</span> <span>Sign</span>
)} )}
</button> </button>
</div> </div>

View File

@ -22,7 +22,7 @@ const TemplateDeleteWarnModal = ({ setShowTemplateDeleteWarnModal, onConfirm }:
setShowTemplateDeleteWarnModal({show: false, templateId: ""}); setShowTemplateDeleteWarnModal({show: false, templateId: ""});
}} }}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors" className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors cursor-pointer"
> >
No No
</button> </button>
@ -30,7 +30,7 @@ const TemplateDeleteWarnModal = ({ setShowTemplateDeleteWarnModal, onConfirm }:
onClick={() => { onClick={() => {
onConfirm(); onConfirm();
}} }}
className="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded-md transition-colors" className="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded-md transition-colors cursor-pointer"
> >
Yes Yes
</button> </button>

View File

@ -9,6 +9,7 @@ import { resetRecipients } from "../../store/slices/selectedRecipientsSlice";
import { resetDocuments } from "../../store/slices/documentSlice"; import { resetDocuments } from "../../store/slices/documentSlice";
import { AppDispatch } from "../../store/store"; import { AppDispatch } from "../../store/store";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { showLoader } from "../../store/slices/loaderSlice";
interface Template { interface Template {
id: number; id: number;
@ -80,6 +81,7 @@ const TemplateTable = () => {
const handleUseClick = async (item: Template) => { const handleUseClick = async (item: Template) => {
try { try {
await dispatch(showLoader("Creating Transaction..."))
await dispatch(resetRecipients()) await dispatch(resetRecipients())
await dispatch(resetDocuments()) await dispatch(resetDocuments())
const payload = { const payload = {
@ -97,6 +99,7 @@ const TemplateTable = () => {
}; };
const handleEditClick = (templateId: string) => { const handleEditClick = (templateId: string) => {
dispatch(showLoader("Loading..."))
setShowMenu({ open: false, templateId: null }); setShowMenu({ open: false, templateId: null });
navigate(`/setup-transaction/${activeAccountId}/template/${templateId}`); navigate(`/setup-transaction/${activeAccountId}/template/${templateId}`);
}; };
@ -181,7 +184,7 @@ const TemplateTable = () => {
templateId: template.id, templateId: template.id,
}) })
} }
className="text-gray-500 hover:text-gray-700 p-1 rounded-md hover:bg-gray-100" className="text-gray-500 hover:text-gray-700 p-1 rounded-md hover:bg-gray-100 cursor-pointer"
> >
<PiDotsThreeOutlineVerticalFill className="text-lg" /> <PiDotsThreeOutlineVerticalFill className="text-lg" />
</button> </button>
@ -190,13 +193,13 @@ const TemplateTable = () => {
<div className="absolute right-0 mt-2 w-40 bg-white border border-gray-200 rounded-lg shadow-lg z-20 animate-fade-in"> <div className="absolute right-0 mt-2 w-40 bg-white border border-gray-200 rounded-lg shadow-lg z-20 animate-fade-in">
<button <button
onClick={() => handleUseClick(template)} onClick={() => handleUseClick(template)}
className="w-full text-left px-4 py-2 text-sm hover:bg-blue-50 hover:text-blue-600" className="w-full text-left px-4 py-2 text-sm hover:bg-blue-50 hover:text-blue-600 cursor-pointer"
> >
Use Use
</button> </button>
<button <button
onClick={() => handleEditClick(template.id + "")} onClick={() => handleEditClick(template.id + "")}
className="w-full text-left px-4 py-2 text-sm hover:bg-yellow-50 hover:text-yellow-600" className="w-full text-left px-4 py-2 text-sm hover:bg-yellow-50 hover:text-yellow-600 cursor-pointer"
> >
Edit Edit
</button> </button>
@ -207,7 +210,7 @@ const TemplateTable = () => {
templateId: template.id + "", templateId: template.id + "",
}) })
} }
className="w-full text-left px-4 py-2 text-sm hover:bg-red-50 hover:text-red-600" className="w-full text-left px-4 py-2 text-sm hover:bg-red-50 hover:text-red-600 cursor-pointer"
> >
Delete Delete
</button> </button>

View File

@ -54,7 +54,7 @@ const Templates = () => {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3 px-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3 px-4">
{/* Filter as pill buttons */} {/* Filter as pill buttons */}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{filters.map((filter) => ( {/* {filters.map((filter) => (
<button <button
key={filter} key={filter}
onClick={() => setActiveFilter(filter)} onClick={() => setActiveFilter(filter)}
@ -66,7 +66,7 @@ const Templates = () => {
> >
{filter} {filter}
</button> </button>
))} ))} */}
</div> </div>
{/* Add Template Button */} {/* Add Template Button */}
@ -75,7 +75,7 @@ const Templates = () => {
className="flex items-center justify-center gap-1 bg-gradient-to-br from-[var(--primary)] to-blue-600 px-3 py-2 rounded-sm cursor-pointer hover:opacity-90" className="flex items-center justify-center gap-1 bg-gradient-to-br from-[var(--primary)] to-blue-600 px-3 py-2 rounded-sm cursor-pointer hover:opacity-90"
> >
<IoMdAdd className="text-white text-lg" /> <IoMdAdd className="text-white text-lg" />
<button className="text-white text-sm font-medium"> <button className="text-white text-sm font-medium cursor-pointer">
Add Template Add Template
</button> </button>
</div> </div>

View File

@ -0,0 +1,12 @@
function getInitialsUtils(name) {
if (!name || typeof name !== 'string') return '';
return name
.trim()
.split(/\s+/) // split by spaces (handles multiple spaces)
.filter(word => word.length) // remove empty strings
.map(word => word[0]) // take first letter of each word
.join('');
}
export default getInitialsUtils;