Working on deleted code part - Recipient add tag vanish bug + Meeting changes

This commit is contained in:
Rohit Singh 2026-06-29 19:23:20 +05:30
parent ad297b763a
commit c8bba1d34f
8 changed files with 400 additions and 140 deletions

View File

@ -29,13 +29,30 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
window.location.hash = documentId + "0"; window.location.hash = documentId + "0";
}, [selectedDocuments]) }, [selectedDocuments])
const [openDocs, setOpenDocs] = useState<Record<string, boolean>>(
() =>
Object.fromEntries(
(selectedDocuments ?? []).map(d => [d.documentId, true])
)
);
const toggleDoc = (indx: number, documentId: string) => { const toggleDoc = (indx: number, documentId: string) => {
const key = 'pageDropDown' + indx; // const key = 'pageDropDown' + indx;
setDropDownTogglePage(dropDownTogglePage === key ? '' : key); // setDropDownTogglePage(dropDownTogglePage === key ? '' : key);
setActivePage(`${documentId}_0`); // setActivePage(`${documentId}_0`);
window.location.hash = documentId + "0"; // window.location.hash = documentId + "0";
setOpenDocs(prev => ({ ...prev, [documentId]: !prev[documentId] }))
} }
useEffect(() => {
if (selectedDocuments && selectedDocuments.length) {
setOpenDocs(() =>
Object.fromEntries(
(selectedDocuments ?? []).map(d => [d.documentId, true])
))
}
}, [selectedDocuments])
const goToPage = (documentId: string, pageIndex: number) => { const goToPage = (documentId: string, pageIndex: number) => {
const key = `${documentId}_${pageIndex}`; const key = `${documentId}_${pageIndex}`;
setActivePage(key); setActivePage(key);
@ -67,7 +84,7 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
return ( return (
<div className="flex flex-col w-full px-2 gap-2 pb-[6rem] bg-slate-100 border-r-1 border-slate-200 h-full"> <div className="flex flex-col w-full px-2 gap-2 pb-[6rem] bg-slate-100 border-r-1 border-slate-200 h-full">
{selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((document, indx) => { {selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((document, indx) => {
const isOpen = dropDownTogglePage === 'pageDropDown' + indx; // const isOpen = dropDownTogglePage === 'pageDropDown' + indx;
return ( return (
<div key={indx} className="border-b border-slate-200"> <div key={indx} className="border-b border-slate-200">
@ -87,64 +104,64 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
</div> </div>
<MdKeyboardArrowUp <MdKeyboardArrowUp
size={20} size={20}
className={`text-slate-400 mt-0.5 flex-shrink-0 transition-transform duration-200 ${isOpen ? '' : 'rotate-180'}`} className={`text-slate-400 mt-0.5 flex-shrink-0 transition-transform duration-200 ${openDocs[document.documentId] ? '' : 'rotate-180'}`}
/> />
</div> </div>
{/* Page Thumbnails - DocuSign style */} {/* Page Thumbnails - DocuSign style */}
{openDocs[document.documentId] && (
<div className={`flex flex-col px-2`}>
{document.pages?.map((page, pageIndex) => {
const key = `${document.documentId}_${pageIndex}`;
const isActive = activePage === key;
<div className={`flex flex-col px-2 ${isOpen ? 'block' : 'hidden'}`}> return (
{document.pages?.map((page, pageIndex) => { <div
const key = `${document.documentId}_${pageIndex}`; key={pageIndex}
const isActive = activePage === key; onClick={() => goToPage(document.documentId, pageIndex)}
className="flex flex-col items-center py-2 px-1 rounded-md cursor-pointer hover:bg-slate-50 transition-colors"
return (
<div
key={pageIndex}
onClick={() => goToPage(document.documentId, pageIndex)}
className="flex flex-col items-center py-2 px-1 rounded-md cursor-pointer hover:bg-slate-50 transition-colors"
>
{/* Big Thumbnail */}
<div className={`relative sm:p-2 lg:p-4 bg-slate-100 rounded border flex items-center justify-center overflow-hidden
${isActive ? 'border-blue-500 border-2' : 'border-slate-200'}`}
> >
<img {/* Big Thumbnail */}
src={page.src} <div className={`relative sm:p-2 lg:p-4 bg-slate-100 rounded border flex items-center justify-center overflow-hidden
alt={`Page ${pageIndex + 1}`} ${isActive ? 'border-blue-500 border-2' : 'border-slate-200'}`}
className="w-[140px] h-[180px] h-full object-cover" >
draggable="false" <img
/> src={page.src}
alt={`Page ${pageIndex + 1}`}
className="w-[140px] h-[180px] h-full object-cover"
draggable="false"
/>
{/* Page number + dots */} {/* Page number + dots */}
<div className="absolute bottom-0 bg-slate-800 flex items-center justify-between w-full pl-4 pr-2 py-3 bg-white"> <div className="absolute bottom-0 bg-slate-800 flex items-center justify-between w-full pl-4 pr-2 py-3 bg-white">
<span className="text-[13px] text-slate-500">{pageIndex + 1}</span> <span className="text-[13px] text-slate-500">{pageIndex + 1}</span>
{ {
// <> // <>
// <button // <button
// onClick={(e) => { e.stopPropagation(); /* open context menu */ }} // onClick={(e) => { e.stopPropagation(); /* open context menu */ }}
// className="text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded p-0.5" // className="text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded p-0.5"
// > // >
// <MdMoreVert size={18} /> // <MdMoreVert size={18} />
// </button> // </button>
// </> // </>
} }
</div> </div>
{documentPagesTagsMapping[document.documentId]?.[pageIndex] && ( {documentPagesTagsMapping[document.documentId]?.[pageIndex] && (
<div className='absolute top-4 right-2 rotate-180'> <div className='absolute top-4 left-2'>
{/* <span className='absolute text-white text-[10px] font-medium text-slate-500 top-[50%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 pointer-events-none select-none rotate-180'> {/* <span className='absolute text-white text-[10px] font-medium text-slate-500 top-[50%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 pointer-events-none select-none rotate-180'>
{documentPagesTagsMapping[document.documentId][pageIndex][0]} {documentPagesTagsMapping[document.documentId][pageIndex][0]}
</span> */} </span> */}
<PiTagSimpleFill style={{ fontSize: '1.8rem', color: documentPagesTagsMapping[document.documentId][pageIndex][1] }} /> <PiTagSimpleFill style={{ fontSize: '1.8rem', color: documentPagesTagsMapping[document.documentId][pageIndex][1] }} />
</div> </div>
)} )}
</div>
</div> </div>
</div> )
) })}
})} </div>
</div> )}
</div> </div>
) )

View File

@ -79,7 +79,7 @@ export default function SignButton({
<div <div
tabIndex={0} tabIndex={0}
onBlur={() => setMenuOpen(false)} // close on blur onBlur={() => setMenuOpen(false)} // close on blur
className="absolute right-0 top-10 w-56 bg-white border border-gray-200 rounded-lg shadow-lg z-10" className="absolute right-0 top-10 w-56 bg-white border border-gray-200 rounded-lg shadow-lg z-50"
> >
<ul className="py-1 text-sm text-gray-700"> <ul className="py-1 text-sm text-gray-700">
{/* <li> {/* <li>

View File

@ -219,7 +219,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>(windowWidth > 638 ? 3 : 0); const [selFitIndx, setSelFitIndx] = useState<number>(windowWidth > 638 ? 2 : 0);
const globalLoader = useSelector((state: RootState) => state.loader) const globalLoader = useSelector((state: RootState) => state.loader)
@ -235,7 +235,7 @@ const SignDocument = () => {
const [provideTags, setProvideTags] = useState(false) const [provideTags, setProvideTags] = useState(false)
const [showCompletionScreen, setShowCompletionScreen] = useState(false) const [showCompletionScreen, setShowCompletionScreen] = useState(false)
const [zoomScale, setZoomScale] = useState<number>(windowWidth > 638 ? 150 : 100) const [zoomScale, setZoomScale] = useState<number>(windowWidth > 638 ? 130 : 100)
const scaleFactor: number = (zoomScale ?? 100) / 100; const scaleFactor: number = (zoomScale ?? 100) / 100;
// Handle Confirm Sign Modal - Start // Handle Confirm Sign Modal - Start
@ -977,7 +977,7 @@ const SignDocument = () => {
</div> </div>
<div className="flex justify-center items-center text-shite gap-2"> <div className="flex justify-center items-center text-white gap-2">
{/* DOWNLOAD AND PRINT OPTION - START */} {/* DOWNLOAD AND PRINT OPTION - START */}
{/* <div className='flex'> {/* <div className='flex'>
@ -1107,6 +1107,11 @@ const SignDocument = () => {
transactionId={"1"} transactionId={"1"}
/> />
</div> </div>
{/* Right Side - ENDS */} {/* Right Side - ENDS */}
</div> </div>
@ -1137,60 +1142,27 @@ const SignDocument = () => {
{/* 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 {provideTags && selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag &&
id="tags-list-client-side" <div className={`col-span-2 ${provideTags ? "" : "order-first"} h-full ${!isFields ? "bg-white" : isFields && provideTags ? "bg-white" : ""} overflow-y-auto`}>
className={`${isPages ? "block" : "hidden"} bg-slate-100 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 overflow-y-auto ${recipient?.type != "CC" ? "bg-slate-100" : ""} ${!provideTags && isFields ? "hidden" : ""}`}> {/* Right Side Attributes Section if providedTags - START */}
<TagProperties
droppedItems={droppedItems}
<div selectedTag={droppedItems[selectedTag.index]}
onClick={() => { setDroppedItems={setDroppedItems}
setIsPages(false) selTagKey={selectedTag.key}
setIsFields(true) selTagIndx={selectedTag.index}
}} signClient={true}
className='absolute sm:hidden top-[5px] right-[10px] bg-white rounded-full text-2xl z-50'> />
<IoMdClose /> {/* Right Side Attributes Section if providedTags - END */}
</div> </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
onClick={() => handleSetIsFields(true)}
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isFields ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300`}
>
<IoIosDocument />
<p className='tracking-wide'>Tags</p>
</div>}
<div
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${!isFields ? 'bg-[var(--primary)] text-white' : ''} ${!provideTags ? "sm:hidden" : ""} transition-all duration-300`}
onClick={() => handleSetIsFields(provideTags ? false : !isFields)}
>
<IoIosDocument />
<p className='tracking-wide'>Pages</p>
</div>
</div>
}
</>
{!transparentTags && isFields && recipientId && provideTags && recipient?.type != "CC" && <FieldList color={"#8f87f1"} recipientId={recipientId} recipientIndx={0} acntId={"1"} />}
<DocumentPages
selectedDocuments={selectedDocuments}
setDropDownTogglePage={setDropDownTogglePage}
dropDownTogglePage={dropDownTogglePage}
droppedItems={droppedItems}
/>
</div>
{/* Pages - END */} {/* Pages - END */}
{/* Main container */} {/* Main container */}
<div <div
id="doc-con" id="doc-con"
className={`col-span-12 zoom-con relative ${provideTags ? (isFields && selectedTag != null ? "sm:col-span-8" : "sm:col-span-10") : (isFields ? "sm:col-span-12" : "sm:col-span-10")} sm:px-4 bg-slate-100 h-full w-full ${zoomScale > 150 ? "overflow-x-auto" : "overflow-x-hidden"} scroll-smooth transition-all duration-200 pb-[4rem]`} className={`col-span-12 zoom-con relative ${provideTags ? (isFields && selectedTag != null ? "sm:col-span-8" : "sm:col-span-10") : (isFields ? "sm:col-span-12" : "sm:col-span-10")} sm:px-4 bg-slate-100 h-full w-full ${zoomScale > 130 ? "overflow-x-auto" : "overflow-x-hidden"} scroll-smooth transition-all duration-200 pb-[4rem]`}
> >
@ -1308,16 +1280,17 @@ const SignDocument = () => {
{!transparentTags && focusElement != null && focusElement && Object.keys(focusElement).length > 1 && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index && {!transparentTags && focusElement != null && focusElement && Object.keys(focusElement).length > 1 && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index &&
<div <div
id="tag-navigation-btn" id="tag-navigation-btn"
className={`hidden fixed right-0 translate-x-[100%] sm:flex justify-center gap-0 items-center z-50 cursor-pointer transition-all duration-300`} className={`hidden fixed left-0 translate-x-[-50%] 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")}
> >
<svg width="100" height="27" viewBox="0 0 112 45" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="100" height="27" viewBox="0 0 112 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.87927 20.8542L15.0554 2.53357C15.7668 1.54438 16.9108 0.958008 18.1292 0.958008H108.049C110.14 0.958008 111.835 2.65317 111.835 4.74426V41.0235C111.835 43.1146 110.14 44.8098 108.049 44.8098H18.1084C16.9016 44.8098 15.7671 44.2345 15.054 43.261L1.89871 25.3024C0.930133 23.9802 0.92229 22.1849 1.87927 20.8542Z" fill="#005AB5" /> <path
{/* <path d="M53.2649 16.7474V28.3838H51.3899L45.907 20.4577H45.8104V28.3838H43.7024V16.7474H45.5888L51.0661 24.6792H51.1683V16.7474H53.2649ZM58.8545 28.5542C57.9795 28.5542 57.2238 28.3724 56.5875 28.0088C55.9549 27.6414 55.4682 27.1224 55.1272 26.452C54.7863 25.7777 54.6159 24.9842 54.6159 24.0713C54.6159 23.1736 54.7863 22.3857 55.1272 21.7077C55.4719 21.0258 55.953 20.4955 56.5704 20.1167C57.1879 19.7342 57.9132 19.5429 58.7466 19.5429C59.2844 19.5429 59.792 19.63 60.2693 19.8042C60.7504 19.9747 61.1746 20.2398 61.542 20.5997C61.9132 20.9595 62.2049 21.4179 62.417 21.9747C62.6291 22.5277 62.7352 23.1868 62.7352 23.952V24.5827H55.5818V23.1963H60.7636C60.7598 22.8023 60.6746 22.452 60.5079 22.1452C60.3413 21.8345 60.1083 21.5902 59.8091 21.4122C59.5136 21.2342 59.1689 21.1452 58.775 21.1452C58.3545 21.1452 57.9852 21.2474 57.667 21.452C57.3488 21.6527 57.1007 21.9179 56.9227 22.2474C56.7485 22.5732 56.6594 22.9311 56.6557 23.3213V24.5315C56.6557 25.0391 56.7485 25.4747 56.9341 25.8383C57.1197 26.1982 57.3791 26.4747 57.7125 26.6679C58.0458 26.8573 58.436 26.952 58.8829 26.952C59.1822 26.952 59.453 26.9103 59.6954 26.827C59.9379 26.7398 60.1481 26.613 60.3261 26.4463C60.5041 26.2796 60.6386 26.0732 60.7295 25.827L62.65 26.0429C62.5288 26.5505 62.2977 26.9936 61.9568 27.3724C61.6197 27.7474 61.1879 28.0391 60.6613 28.2474C60.1348 28.452 59.5325 28.5542 58.8545 28.5542ZM65.4896 19.6565L67.2509 22.8781L69.0407 19.6565H71.2168L68.5862 24.0202L71.2623 28.3838H69.0975L67.2509 25.2417L65.4214 28.3838H63.2396L65.8987 24.0202L63.3078 19.6565H65.4896ZM76.6857 19.6565V21.2474H71.6687V19.6565H76.6857ZM72.9073 17.5656H74.9641V25.7588C74.9641 26.0353 75.0058 26.2474 75.0891 26.3952C75.1762 26.5391 75.2899 26.6376 75.43 26.6906C75.5702 26.7436 75.7255 26.7702 75.8959 26.7702C76.0247 26.7702 76.1421 26.7607 76.2482 26.7417C76.3581 26.7228 76.4414 26.7058 76.4982 26.6906L76.8448 28.2986C76.735 28.3364 76.5778 28.3781 76.3732 28.4236C76.1725 28.469 75.9262 28.4955 75.6346 28.5031C75.1194 28.5183 74.6554 28.4406 74.2425 28.2702C73.8296 28.0959 73.502 27.827 73.2596 27.4633C73.0209 27.0997 72.9035 26.6452 72.9073 26.0997V17.5656Z" fill="white" /> */} d="M110.121 20.8542L96.9446 2.53357C96.2332 1.54438 95.0892 0.958008 93.8708 0.958008H3.951C1.8599 0.958008 0.1647 2.65317 0.1647 4.74426V41.0235C0.1647 43.1146 1.8599 44.8098 3.951 44.8098H93.8916C95.0984 44.8098 96.2329 44.2345 96.946 43.261L110.101 25.3024C111.07 23.9802 111.078 22.1849 110.121 20.8542Z"
fill="#005AB5"
<text className="font-semibold capitalize" x="55%" y="54%" fill="white" font-size="14" font-family="Arial, sans-serif" dominant-baseline="middle" text-anchor="middle"> />
<text className="font-semibold capitalize" x="45%" y="54%" fill="white" font-size="14" font-family="Arial, sans-serif" dominant-baseline="middle" text-anchor="middle">
{requiredFilled && focusElement.required == true ? "Submit" : focusElement.type.toLowerCase()} {requiredFilled && focusElement.required == true ? "Submit" : focusElement.type.toLowerCase()}
</text> </text>
</svg> </svg>
@ -1354,13 +1327,13 @@ 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={`hidden sm:block cursor-pointer fixed ${navigationTagPos == NavigationTagPosEnum.ABOVE ? "top-[55px]" : requiredFilled ? "bottom-[82px]" : "bottom-[55px]"} right-[8%] flex flex-col items-center`}> className={`hidden sm:block cursor-pointer fixed ${navigationTagPos == NavigationTagPosEnum.ABOVE ? "top-[55px]" : requiredFilled ? "bottom-[82px]" : "bottom-[55px]"} left-[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>} */}
{/* Label box */} {/* Label box */}
<div <div
className="bg-[var(--primary)] text-white text-sm px-6 py-3 rounded-md shadow-md flex items-center gap-1 font-medium"> className="bg-[var(--primary)] text-white text-xs px-5 py-2 rounded-md shadow-md flex items-center gap-1 font-medium">
{/* <span className="capitalize">{focusElement?.type.toLowerCase()}</span> */} {/* <span className="capitalize">{focusElement?.type.toLowerCase()}</span> */}
<span className="capitalize">Next</span> <span className="capitalize">Next</span>
</div> </div>
@ -1368,21 +1341,57 @@ const SignDocument = () => {
</div>} </div>}
</div> </div>
{/* Right Side Pages - PART - START */}
<div
id="tags-list-client-side"
className={`${isPages ? "block" : "hidden"} bg-slate-100 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 overflow-y-auto ${recipient?.type != "CC" ? "bg-slate-100" : ""} ${!provideTags && isFields ? "hidden" : ""}`}>
{provideTags && selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag &&
<div className={`col-span-2 ${provideTags ? "" : "order-first"} h-full ${!isFields ? "bg-white" : isFields && provideTags ? "bg-white" : ""} overflow-y-auto`}> <div
{/* Right Side Attributes Section if providedTags - START */} onClick={() => {
<TagProperties setIsPages(false)
droppedItems={droppedItems} setIsFields(true)
selectedTag={droppedItems[selectedTag.index]} }}
setDroppedItems={setDroppedItems} className='absolute sm:hidden top-[5px] right-[10px] bg-white rounded-full text-2xl z-50'>
selTagKey={selectedTag.key} <IoMdClose />
selTagIndx={selectedTag.index}
signClient={true}
/>
{/* Right Side Attributes Section if providedTags - END */}
</div> </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
onClick={() => handleSetIsFields(true)}
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isFields ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300`}
>
<IoIosDocument />
<p className='tracking-wide'>Tags</p>
</div>}
<div
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${!isFields ? 'bg-[var(--primary)] text-white' : ''} ${!provideTags ? "sm:hidden" : ""} transition-all duration-300`}
onClick={() => handleSetIsFields(provideTags ? false : !isFields)}
>
<IoIosDocument />
<p className='tracking-wide'>Pages</p>
</div>
</div>
}
</>
{!transparentTags && isFields && recipientId && provideTags && recipient?.type != "CC" && <FieldList color={"#8f87f1"} recipientId={recipientId} recipientIndx={0} acntId={"1"} />}
<DocumentPages
selectedDocuments={selectedDocuments}
setDropDownTogglePage={setDropDownTogglePage}
dropDownTogglePage={dropDownTogglePage}
droppedItems={droppedItems}
/>
</div>
{/* Right Side Pages - PART - END */}
</div> </div>
{/* Document Container End */} {/* Document Container End */}

View File

@ -23,6 +23,7 @@ import {
clearTransactionTags, clearTransactionTags,
incrementCounter, incrementCounter,
} from '../../../store/slices/documentTagsSlice'; } from '../../../store/slices/documentTagsSlice';
import { serializeGroupedFields } from '../../../helper/handleGroupedFields';
export interface DroppedItemsObj { export interface DroppedItemsObj {
[key: string]: DroppedItem[] [key: string]: DroppedItem[]
@ -103,6 +104,10 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
// const tagUndoRef = useRef<DroppedItem[] | []>([]); // const tagUndoRef = useRef<DroppedItem[] | []>([]);
const imageRefs = useRef<{ [key: string]: HTMLImageElement | null }>({}); const imageRefs = useRef<{ [key: string]: HTMLImageElement | null }>({});
const pageRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const [activePage, setActivePage] = useState<string | null>(null); // NEW
const isProgrammaticScrollRef = useRef(false); // NEW — guards against feedback loop
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
@ -110,6 +115,58 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
const selectedDocuments = useSelector((state: RootState) => state.documents.documents) const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
// Sync activePage with scroll position via IntersectionObserver
useEffect(() => {
const scrollRoot = document.getElementById('doc-con');
if (!scrollRoot) return;
// Track intersection ratio per page so we can pick the most-visible one
// when multiple pages are partially visible at once.
const ratios: Record<string, number> = {};
const observer = new IntersectionObserver(
(entries) => {
// Ignore intersection changes caused by our own programmatic
// scroll (e.g. clicking a thumbnail) to avoid feedback loops.
if (isProgrammaticScrollRef.current) return;
entries.forEach((entry) => {
const key = entry.target.getAttribute('data-page-key');
if (!key) return;
ratios[key] = entry.isIntersecting ? entry.intersectionRatio : 0;
});
// Pick the key with the highest intersection ratio
let bestKey: string | null = null;
let bestRatio = 0;
for (const [key, ratio] of Object.entries(ratios)) {
if (ratio > bestRatio) {
bestRatio = ratio;
bestKey = key;
}
}
if (bestKey && bestRatio > 0) {
setActivePage((prev) => (prev === bestKey ? prev : bestKey)); // dedupe, avoid re-render
}
},
{
root: scrollRoot,
// Multiple thresholds so ratio updates fire smoothly while
// scrolling, without needing scroll-event listeners at all.
threshold: [0, 0.25, 0.5, 0.75, 1],
rootMargin: '0px',
}
);
Object.values(pageRefs.current).forEach((el) => {
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, [selectedDocuments]);
// handle Tag - Document Pos and size for Responsiveness - START // handle Tag - Document Pos and size for Responsiveness - START
const [imageSizes, setImageSizes] = useState<Record<string, { width: number; height: number }>>({}); const [imageSizes, setImageSizes] = useState<Record<string, { width: number; height: number }>>({});
@ -184,6 +241,8 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
const fetchTags = async () => { const fetchTags = async () => {
try { try {
if (Object.keys(droppedItems).length) return
const requests = selectedRecipients.filter((recipient) => { const requests = selectedRecipients.filter((recipient) => {
// Adding new rec doesn't have id till we click on IoMdClose or save so removing them and adding rec after click then fetching tags // Adding new rec doesn't have id till we click on IoMdClose or save so removing them and adding rec after click then fetching tags
if (!recipient.id) return false; if (!recipient.id) return false;
@ -394,6 +453,8 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
navigationOrder: indx + 1, navigationOrder: indx + 1,
})); }));
// const tagsData: any[] = serializeGroupedFields(updatedTagsWithNavOrder)
return api.put( return api.put(
`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`, `/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`,
updatedTagsWithNavOrder updatedTagsWithNavOrder
@ -768,6 +829,8 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
onFocus={() => handleDocumentFocus(key)} onFocus={() => handleDocumentFocus(key)}
key={indx} key={indx}
id={`${doc.id}${indx}`} id={`${doc.id}${indx}`}
data-page-key={key}
ref={(el) => { pageRefs.current[key] = el; }}
onDrop={(e) => handleDrop(e, doc.id, indxDoc, page.index, page.width, page.height)} onDrop={(e) => handleDrop(e, doc.id, indxDoc, page.index, page.width, page.height)}
onDragOver={handleDragOver} onDragOver={handleDragOver}
className='drop-page-in shadow-md relative' className='drop-page-in shadow-md relative'
@ -881,10 +944,25 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
} }
</> </>
{selectedDocuments && selectedDocuments.length > 0 && <DocumentPages {/* {selectedDocuments && selectedDocuments.length > 0 && <DocumentPages
isPages={isPages} isPages={isPages}
setIsPages={setIsPages} setIsPages={setIsPages}
droppedItems={droppedItems} droppedItems={droppedItems}
/>} */}
{!isPreview && selectedDocuments && selectedDocuments.length > 0 && <DocumentPages
isPages={isPages}
setIsPages={setIsPages}
droppedItems={droppedItems}
activePage={activePage} // NEW
onPageSelect={(key: string) => { // NEW — replaces hash-based nav
isProgrammaticScrollRef.current = true;
setActivePage(key);
pageRefs.current[key]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Release the guard after the smooth scroll has had time to settle,
// so the IntersectionObserver resumes driving activePage afterward.
window.setTimeout(() => { isProgrammaticScrollRef.current = false; }, 600);
}}
/>} />}
{/* Center Doc Container - END */} {/* Center Doc Container - END */}

View File

@ -1,16 +1,24 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { RootState } from '../../../../store/store'; import { RootState } from '../../../../store/store';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { DroppedItemsObj } from '../DocumentContainer'; import { DroppedItemsObj } from '../DocumentContainer';
import { MdKeyboardArrowUp, MdMoreVert } from 'react-icons/md'; import { MdKeyboardArrowUp, MdMoreVert } from 'react-icons/md';
import { PiTagSimpleFill } from 'react-icons/pi'; import { PiTagSimpleFill } from 'react-icons/pi';
// const DocumentPages: React.FC<{
// isPages: boolean;
// droppedItems: DroppedItemsObj;
// setIsPages: React.Dispatch<React.SetStateAction<boolean>>;
// }> = ({ isPages, droppedItems, setIsPages }) => {
const DocumentPages: React.FC<{ const DocumentPages: React.FC<{
isPages: boolean; isPages: boolean;
droppedItems: DroppedItemsObj; droppedItems: DroppedItemsObj;
setIsPages: React.Dispatch<React.SetStateAction<boolean>>; setIsPages: React.Dispatch<React.SetStateAction<boolean>>;
}> = ({ isPages, droppedItems, setIsPages }) => { activePage: string | null; // NEW
onPageSelect: (key: string) => void; // NEW
}> = ({ isPages, droppedItems, setIsPages, activePage, onPageSelect }) => {
type RecipientPageTagInfo = { type RecipientPageTagInfo = {
color: string; color: string;
@ -30,21 +38,35 @@ const DocumentPages: React.FC<{
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients); const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
const [openDocs, setOpenDocs] = useState<Record<string, boolean>>( const [openDocs, setOpenDocs] = useState<Record<string, boolean>>(
() => Object.fromEntries(selectedDocuments.map((d, i) => [d.id, i === 0])) () => Object.fromEntries(selectedDocuments.map((d, i) => [d.id, true]))
)
const [activePage, setActivePage] = useState<string | null>(
selectedDocuments[0] ? `${selectedDocuments[0].id}_0` : null
) )
// const [activePage, setActivePage] = useState<string | null>(
// selectedDocuments[0] ? `${selectedDocuments[0].id}_0` : null
// )
const toggleDoc = (docId: string) => { const toggleDoc = (docId: string) => {
setOpenDocs(prev => ({ ...prev, [docId]: !prev[docId] })) setOpenDocs(prev => ({ ...prev, [docId]: !prev[docId] }))
} }
// const goToPage = (docId: string, pageIndex: number) => {
// const key = `${docId}_${pageIndex}`
// setActivePage(key)
// window.location.hash = docId + "" + pageIndex
// }
const goToPage = (docId: string, pageIndex: number) => { const goToPage = (docId: string, pageIndex: number) => {
const key = `${docId}_${pageIndex}` const key = `${docId}_${pageIndex}`;
setActivePage(key) onPageSelect(key);
window.location.hash = docId + "" + pageIndex };
}
const thumbnailRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); // NEW
useEffect(() => { // NEW
if (!activePage) return;
const el = thumbnailRefs.current[activePage];
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, [activePage]);
// useEffect(() => { // useEffect(() => {
// setTimeout(() => { // setTimeout(() => {
@ -189,6 +211,7 @@ const DocumentPages: React.FC<{
return ( return (
<div <div
key={pageIndex} key={pageIndex}
ref={(el) => { thumbnailRefs.current[key] = el; }} // NEW
onClick={() => goToPage(document.id, pageIndex)} onClick={() => goToPage(document.id, pageIndex)}
className="flex flex-col items-center py-2 px-1 rounded-md cursor-pointer hover:bg-slate-50 transition-colors" className="flex flex-col items-center py-2 px-1 rounded-md cursor-pointer hover:bg-slate-50 transition-colors"
> >

View File

@ -10,11 +10,14 @@ 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"; import { showLoader } from "../../store/slices/loaderSlice";
import { FaEdit } from "react-icons/fa";
import { MdDelete } from "react-icons/md";
interface Template { interface Template {
id: number; id: number;
name: string | null; name: string | null;
description: string | null; description: string | null;
message: string | null;
createdAt: string; createdAt: string;
lastModifiedAt: string; lastModifiedAt: string;
createdBy?: { name: string }; createdBy?: { name: string };
@ -87,7 +90,7 @@ const TemplateTable = () => {
const payload = { const payload = {
name: item.name ?? "", name: item.name ?? "",
subject: item.description, subject: item.description,
message: "", message: item.message,
expireAfter: item.expireAfter, expireAfter: item.expireAfter,
templateId: item.id, templateId: item.id,
}; };
@ -164,7 +167,9 @@ const TemplateTable = () => {
className={`${idx % 2 != 0 ? "bg-gray-50" : "bg-white" className={`${idx % 2 != 0 ? "bg-gray-50" : "bg-white"
} border-b border-gray-300 hover:bg-gray-100 transition`} } border-b border-gray-300 hover:bg-gray-100 transition`}
> >
<td className="px-6 py-3 text-gray-800 font-medium truncate max-w-xs"> <td
onClick={() => handleUseClick(template)}
className="underline px-6 py-3 text-[var(--primary)] font-medium truncate max-w-xs cursor-pointer">
{template.name || "Untitled"} {template.name || "Untitled"}
</td> </td>
<td className="px-6 py-3 text-gray-700"> <td className="px-6 py-3 text-gray-700">
@ -175,8 +180,8 @@ const TemplateTable = () => {
{formatDate(template.lastModifiedAt)} {formatDate(template.lastModifiedAt)}
</td> </td>
<td className="px-6 py-3 text-gray-700"> <td className="px-6 py-3 text-gray-700">
<div className="relative inline-block text-left"> <div className="flex">
<button {/* <button
onClick={() => onClick={() =>
setShowMenu({ setShowMenu({
open: open:
@ -215,7 +220,31 @@ const TemplateTable = () => {
Delete Delete
</button> </button>
</div> </div>
)} )} */}
{/* <button
onClick={() => handleUseClick(template)}
className="w-full text-left px-4 py-2 text-sm hover:bg-blue-50 hover:text-blue-600 cursor-pointer"
>
Use
</button> */}
<button
onClick={() => handleEditClick(template.id + "")}
className="px-4 py-2 text-md hover:bg-yellow-50 hover:text-yellow-600 cursor-pointer"
>
<FaEdit />
</button>
<button
onClick={() =>
setShowTemplateDeleteWarnModal({
show: true,
templateId: template.id + "",
})
}
className="px-4 py-2 text-md hover:bg-red-50 hover:text-red-600 cursor-pointer"
>
<MdDelete />
</button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,104 @@
import { v4 as uuid } from "uuid";
export function serializeGroupedFields(tags: any[]) {
const result: any[] = [];
const grouped = new Map<string, any[]>();
// Separate grouped and non-grouped tags
for (const tag of tags) {
if (
(tag.type.toUpperCase() === "RADIO" || tag.type.toUpperCase() === "CHECKBOX") &&
tag.group
) {
if (!grouped.has(tag.group)) grouped.set(tag.group, []);
grouped.get(tag.group)!.push(tag);
} else {
result.push(tag);
}
}
grouped.forEach(groupTags => {
const first = groupTags[0];
// Sort by navigation order
groupTags.sort(
(a, b) => (a.navigationOrder ?? 0) - (b.navigationOrder ?? 0)
);
const parent = {
...first,
options: [],
};
delete parent.group;
delete parent.isGroupExist;
if (parent.type.toUpperCase() === "RADIO") {
parent.required = groupTags.some(t => t.required);
}
if (parent.type.toUpperCase() === "CHECKBOX") {
// group level required (leave false for now)
parent.required = false;
}
parent.options = groupTags.map(tag => ({
label: tag.label,
value: tag.value ?? null,
required:
parent.type.toUpperCase() === "CHECKBOX"
? tag.required
: undefined,
selected: tag.selected ?? false,
readOnly: tag.readOnly ?? false,
pageIndex: tag.pageIndex,
dimensions: tag.dimensions,
}));
result.push(parent);
});
return result;
}
export function deserializeGroupedFields(tags: any[]) {
const result: any[] = [];
for (const tag of tags) {
if (
(tag.type.toUpperCase() !== "RADIO" && tag.type.toUpperCase() !== "CHECKBOX") ||
!tag.options?.length
) {
result.push(tag);
continue;
}
const groupId = uuid();
tag.options.forEach((option: any, index: number) => {
result.push({
...tag,
group: groupId,
isGroupExist: index !== 0,
dimensions: option.dimensions,
label: option.label,
value: option.value,
required:
tag.type.toUpperCase() === "RADIO"
? tag.required
: option.required,
options: [],
});
});
}
return result;
}

View File

@ -84,7 +84,7 @@ export const addRecipientAsync = createAsyncThunk<
? await api.post(basePath, createRecipientsData) ? await api.post(basePath, createRecipientsData)
: { data: [] }; : { data: [] };
return [...resCreate.data, ...resUpdate.data] as Recipient[]; return [...resUpdate.data, ...resCreate.data] as Recipient[];
} catch (err: any) { } catch (err: any) {
return rejectWithValue(err.response?.data || "Failed to add recipient"); return rejectWithValue(err.response?.data || "Failed to add recipient");
} }