From c8bba1d34fb6273e91375d43f8bbae70f99d8af3 Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Mon, 29 Jun 2026 19:23:20 +0530 Subject: [PATCH] Working on deleted code part - Recipient add tag vanish bug + Meeting changes --- src/components/client/DocumentPages.tsx | 119 ++++++++------ src/components/client/SignButton.tsx | 2 +- src/components/client/SignDocument.tsx | 149 ++++++++++-------- .../processStepTwo/DocumentContainer.tsx | 80 +++++++++- .../documentPages/DocumentPages.tsx | 43 +++-- src/components/templates/TemplateTable.tsx | 39 ++++- src/helper/handleGroupedFields.ts | 104 ++++++++++++ src/store/slices/selectedRecipientsSlice.tsx | 4 +- 8 files changed, 400 insertions(+), 140 deletions(-) create mode 100644 src/helper/handleGroupedFields.ts diff --git a/src/components/client/DocumentPages.tsx b/src/components/client/DocumentPages.tsx index b25ff7b..e920456 100644 --- a/src/components/client/DocumentPages.tsx +++ b/src/components/client/DocumentPages.tsx @@ -29,13 +29,30 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl window.location.hash = documentId + "0"; }, [selectedDocuments]) + const [openDocs, setOpenDocs] = useState>( + () => + Object.fromEntries( + (selectedDocuments ?? []).map(d => [d.documentId, true]) + ) + ); + const toggleDoc = (indx: number, documentId: string) => { - const key = 'pageDropDown' + indx; - setDropDownTogglePage(dropDownTogglePage === key ? '' : key); - setActivePage(`${documentId}_0`); - window.location.hash = documentId + "0"; + // const key = 'pageDropDown' + indx; + // setDropDownTogglePage(dropDownTogglePage === key ? '' : key); + // setActivePage(`${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 key = `${documentId}_${pageIndex}`; setActivePage(key); @@ -67,7 +84,7 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl return (
{selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((document, indx) => { - const isOpen = dropDownTogglePage === 'pageDropDown' + indx; + // const isOpen = dropDownTogglePage === 'pageDropDown' + indx; return (
@@ -87,64 +104,64 @@ const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownToggl
{/* Page Thumbnails - DocuSign style */} + {openDocs[document.documentId] && ( +
+ {document.pages?.map((page, pageIndex) => { + const key = `${document.documentId}_${pageIndex}`; + const isActive = activePage === key; -
- {document.pages?.map((page, pageIndex) => { - const key = `${document.documentId}_${pageIndex}`; - const isActive = activePage === key; - - return ( -
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 */} -
goToPage(document.documentId, pageIndex)} + className="flex flex-col items-center py-2 px-1 rounded-md cursor-pointer hover:bg-slate-50 transition-colors" > - {`Page + {/* Big Thumbnail */} +
+ {`Page - {/* Page number + dots */} -
- {pageIndex + 1} - { - // <> - // - // - } -
+ {/* Page number + dots */} +
+ {pageIndex + 1} + { + // <> + // + // + } +
- {documentPagesTagsMapping[document.documentId]?.[pageIndex] && ( -
- {/* + {documentPagesTagsMapping[document.documentId]?.[pageIndex] && ( +
+ {/* {documentPagesTagsMapping[document.documentId][pageIndex][0]} */} - -
- )} + +
+ )} +
-
- ) - })} -
- + ) + })} +
+ )} ) diff --git a/src/components/client/SignButton.tsx b/src/components/client/SignButton.tsx index 1002abe..4323c57 100644 --- a/src/components/client/SignButton.tsx +++ b/src/components/client/SignButton.tsx @@ -79,7 +79,7 @@ export default function SignButton({
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" >
    {/*
  • diff --git a/src/components/client/SignDocument.tsx b/src/components/client/SignDocument.tsx index 2c5c7f2..628c633 100644 --- a/src/components/client/SignDocument.tsx +++ b/src/components/client/SignDocument.tsx @@ -219,7 +219,7 @@ const SignDocument = () => { const [isFields, setIsFields] = useState(true); const [isFitDd, setIsFitDd] = useState(false) - const [selFitIndx, setSelFitIndx] = useState(windowWidth > 638 ? 3 : 0); + const [selFitIndx, setSelFitIndx] = useState(windowWidth > 638 ? 2 : 0); const globalLoader = useSelector((state: RootState) => state.loader) @@ -235,7 +235,7 @@ const SignDocument = () => { const [provideTags, setProvideTags] = useState(false) const [showCompletionScreen, setShowCompletionScreen] = useState(false) - const [zoomScale, setZoomScale] = useState(windowWidth > 638 ? 150 : 100) + const [zoomScale, setZoomScale] = useState(windowWidth > 638 ? 130 : 100) const scaleFactor: number = (zoomScale ?? 100) / 100; // Handle Confirm Sign Modal - Start @@ -977,7 +977,7 @@ const SignDocument = () => {
-
+
{/* DOWNLOAD AND PRINT OPTION - START */} {/*
@@ -1107,6 +1107,11 @@ const SignDocument = () => { transactionId={"1"} /> + + + + +
{/* Right Side - ENDS */}
@@ -1137,60 +1142,27 @@ const SignDocument = () => { {/* Left Side pages start */} {/*
*/} -
- - -
{ - setIsPages(false) - setIsFields(true) - }} - className='absolute sm:hidden top-[5px] right-[10px] bg-white rounded-full text-2xl z-50'> - + {provideTags && selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag && +
+ {/* Right Side Attributes Section if providedTags - START */} + + {/* Right Side Attributes Section if providedTags - END */}
+ } - - <> - {provideTags && !transparentTags &&
- {provideTags && recipient?.type != "CC" &&
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`} - > - -

Tags

-
} - -
handleSetIsFields(provideTags ? false : !isFields)} - > - -

Pages

-
- -
- } - - - - {!transparentTags && isFields && recipientId && provideTags && recipient?.type != "CC" && } - - - -
{/* Pages - END */} {/* Main container */}
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 &&
handleTagNavClick("N")} > - - {/* */} - - + + {requiredFilled && focusElement.required == true ? "Submit" : focusElement.type.toLowerCase()} @@ -1354,13 +1327,13 @@ const SignDocument = () => { {(navigationTagPos == NavigationTagPosEnum.ABOVE || navigationTagPos == NavigationTagPosEnum.BELOW) &&
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 */} {/* {navigationTagPos == NavigationTagPosEnum.ABOVE &&
} */} {/* Label box */}
+ className="bg-[var(--primary)] text-white text-xs px-5 py-2 rounded-md shadow-md flex items-center gap-1 font-medium"> {/* {focusElement?.type.toLowerCase()} */} Next
@@ -1368,21 +1341,57 @@ const SignDocument = () => {
}
+ {/* Right Side Pages - PART - START */} +
- {provideTags && selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag && -
- {/* Right Side Attributes Section if providedTags - START */} - - {/* Right Side Attributes Section if providedTags - END */} + +
{ + setIsPages(false) + setIsFields(true) + }} + className='absolute sm:hidden top-[5px] right-[10px] bg-white rounded-full text-2xl z-50'> +
- } + + + <> + {provideTags && !transparentTags &&
+ {provideTags && recipient?.type != "CC" &&
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`} + > + +

Tags

+
} + +
handleSetIsFields(provideTags ? false : !isFields)} + > + +

Pages

+
+ +
+ } + + + + {!transparentTags && isFields && recipientId && provideTags && recipient?.type != "CC" && } + + + +
+ {/* Right Side Pages - PART - END */} +
{/* Document Container End */} diff --git a/src/components/process/processStepTwo/DocumentContainer.tsx b/src/components/process/processStepTwo/DocumentContainer.tsx index c527c0e..7fab608 100644 --- a/src/components/process/processStepTwo/DocumentContainer.tsx +++ b/src/components/process/processStepTwo/DocumentContainer.tsx @@ -23,6 +23,7 @@ import { clearTransactionTags, incrementCounter, } from '../../../store/slices/documentTagsSlice'; +import { serializeGroupedFields } from '../../../helper/handleGroupedFields'; export interface DroppedItemsObj { [key: string]: DroppedItem[] @@ -103,6 +104,10 @@ const DocumentContainer = forwardRef([]); const imageRefs = useRef<{ [key: string]: HTMLImageElement | null }>({}); + const pageRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); + + const [activePage, setActivePage] = useState(null); // NEW + const isProgrammaticScrollRef = useRef(false); // NEW — guards against feedback loop const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); @@ -110,6 +115,58 @@ const DocumentContainer = forwardRef 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 = {}; + + 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 const [imageSizes, setImageSizes] = useState>({}); @@ -184,6 +241,8 @@ const DocumentContainer = forwardRef { try { + if (Object.keys(droppedItems).length) return + const requests = selectedRecipients.filter((recipient) => { // Adding new rec doesn't have id till we click on IoMdClose or save so removing them and adding rec after click then fetching tags if (!recipient.id) return false; @@ -394,6 +453,8 @@ const DocumentContainer = forwardRef handleDocumentFocus(key)} key={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)} onDragOver={handleDragOver} className='drop-page-in shadow-md relative' @@ -881,10 +944,25 @@ const DocumentContainer = forwardRef - {selectedDocuments && selectedDocuments.length > 0 && 0 && } */} + + {!isPreview && selectedDocuments && selectedDocuments.length > 0 && { // 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 */} diff --git a/src/components/process/processStepTwo/documentPages/DocumentPages.tsx b/src/components/process/processStepTwo/documentPages/DocumentPages.tsx index e8f0ca8..7738b47 100644 --- a/src/components/process/processStepTwo/documentPages/DocumentPages.tsx +++ b/src/components/process/processStepTwo/documentPages/DocumentPages.tsx @@ -1,16 +1,24 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { RootState } from '../../../../store/store'; import { useSelector } from 'react-redux'; import { DroppedItemsObj } from '../DocumentContainer'; import { MdKeyboardArrowUp, MdMoreVert } from 'react-icons/md'; import { PiTagSimpleFill } from 'react-icons/pi'; +// const DocumentPages: React.FC<{ +// isPages: boolean; +// droppedItems: DroppedItemsObj; +// setIsPages: React.Dispatch>; +// }> = ({ isPages, droppedItems, setIsPages }) => { + const DocumentPages: React.FC<{ isPages: boolean; droppedItems: DroppedItemsObj; setIsPages: React.Dispatch>; -}> = ({ isPages, droppedItems, setIsPages }) => { + activePage: string | null; // NEW + onPageSelect: (key: string) => void; // NEW +}> = ({ isPages, droppedItems, setIsPages, activePage, onPageSelect }) => { type RecipientPageTagInfo = { color: string; @@ -30,21 +38,35 @@ const DocumentPages: React.FC<{ const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients); const [openDocs, setOpenDocs] = useState>( - () => Object.fromEntries(selectedDocuments.map((d, i) => [d.id, i === 0])) - ) - const [activePage, setActivePage] = useState( - selectedDocuments[0] ? `${selectedDocuments[0].id}_0` : null + () => Object.fromEntries(selectedDocuments.map((d, i) => [d.id, true])) ) + // const [activePage, setActivePage] = useState( + // selectedDocuments[0] ? `${selectedDocuments[0].id}_0` : null + // ) + const toggleDoc = (docId: string) => { 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 key = `${docId}_${pageIndex}` - setActivePage(key) - window.location.hash = docId + "" + pageIndex - } + const key = `${docId}_${pageIndex}`; + onPageSelect(key); + }; + + 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(() => { // setTimeout(() => { @@ -189,6 +211,7 @@ const DocumentPages: React.FC<{ return (
{ thumbnailRefs.current[key] = el; }} // NEW 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" > diff --git a/src/components/templates/TemplateTable.tsx b/src/components/templates/TemplateTable.tsx index 8e53042..6236f00 100644 --- a/src/components/templates/TemplateTable.tsx +++ b/src/components/templates/TemplateTable.tsx @@ -10,11 +10,14 @@ import { resetDocuments } from "../../store/slices/documentSlice"; import { AppDispatch } from "../../store/store"; import { useDispatch } from "react-redux"; import { showLoader } from "../../store/slices/loaderSlice"; +import { FaEdit } from "react-icons/fa"; +import { MdDelete } from "react-icons/md"; interface Template { id: number; name: string | null; description: string | null; + message: string | null; createdAt: string; lastModifiedAt: string; createdBy?: { name: string }; @@ -87,7 +90,7 @@ const TemplateTable = () => { const payload = { name: item.name ?? "", subject: item.description, - message: "", + message: item.message, expireAfter: item.expireAfter, templateId: item.id, }; @@ -164,7 +167,9 @@ const TemplateTable = () => { className={`${idx % 2 != 0 ? "bg-gray-50" : "bg-white" } border-b border-gray-300 hover:bg-gray-100 transition`} > - + handleUseClick(template)} + className="underline px-6 py-3 text-[var(--primary)] font-medium truncate max-w-xs cursor-pointer"> {template.name || "Untitled"} @@ -175,8 +180,8 @@ const TemplateTable = () => { {formatDate(template.lastModifiedAt)} -
-
- )} + )} */} + + {/* */} + +
diff --git a/src/helper/handleGroupedFields.ts b/src/helper/handleGroupedFields.ts new file mode 100644 index 0000000..cd4f423 --- /dev/null +++ b/src/helper/handleGroupedFields.ts @@ -0,0 +1,104 @@ +import { v4 as uuid } from "uuid"; + +export function serializeGroupedFields(tags: any[]) { + const result: any[] = []; + const grouped = new Map(); + + // 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; +} \ No newline at end of file diff --git a/src/store/slices/selectedRecipientsSlice.tsx b/src/store/slices/selectedRecipientsSlice.tsx index 521ca3d..f94e39c 100644 --- a/src/store/slices/selectedRecipientsSlice.tsx +++ b/src/store/slices/selectedRecipientsSlice.tsx @@ -25,7 +25,7 @@ export interface Recipient { provideOwnTags?: boolean; uuid?: string; newOne?: boolean; - completed?: boolean; + completed?: boolean; } export interface SelectedRecipientInt { @@ -84,7 +84,7 @@ export const addRecipientAsync = createAsyncThunk< ? await api.post(basePath, createRecipientsData) : { data: [] }; - return [...resCreate.data, ...resUpdate.data] as Recipient[]; + return [...resUpdate.data, ...resCreate.data] as Recipient[]; } catch (err: any) { return rejectWithValue(err.response?.data || "Failed to add recipient"); }