import React, { useEffect, useLayoutEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { RootState } from '../../../store/store' import api from '../../../config/api'; import RightSideBar from './rightSideBar/RightSideBar'; import { DroppedItem, TagsTypes } from '../../../utlity/types'; import { useNavigate, useParams } from 'react-router-dom'; import { IoMdClose } from 'react-icons/io'; import { fetchRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'; import { fetchDocuments } from '../../../store/slices/documentSlice'; import { FaQuestionCircle } from 'react-icons/fa'; import typeToIconMap from '../../../helper/iconMap'; import { IoAdd } from 'react-icons/io5'; import Loader from '../../dashboard/Loader'; // import { TRANSACTION_TEMPLATE_TOGGLE } from "../../../store/slices/selectedRecipientsSlice"; import { handleGroupBorderPosition } from '../../../utlity/tagGroupConUtils'; import { hideLoader, showLoader } from '../../../store/slices/loaderSlice'; export interface DroppedItemsObj { [key: string]: DroppedItem[] } interface DroppedTagsDefaultFieldType { [key: string]: string } export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = { text: "paragraph", temporal: "date", number: "integer", // image: "STAMP", select: "single select", checkbox: "TOGGLE" } interface DocumentContainerProps { isSend: boolean | null; setIsSend: React.Dispatch>; } const DocumentContainer: React.FC = ({ isSend }) => { let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; const dispatch = useDispatch(); const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>(); const accountId = params.accountId; let transactionId = params.transactionId if (!transactionId) { transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; transactionId = params.templateId; dispatch(toggleToTemplates()); } if (!accountId || !transactionId) { window.location.href = "/"; return null; } // const transactionId = useSelector((state: RootState) => state.transaction.transactionId) const navigate = useNavigate(); const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) const selectedDocuments = useSelector((state: RootState) => state.documents.documents) const [droppedItems, setDroppedItems] = useState({}); useEffect(() => { selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId })); selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId })); }, []) // FETCHING TAG - START useEffect(() => { if (selectedRecipients.length === 0) return; const fetchTags = async () => { try { const requests = selectedRecipients.map((recipient) => api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipient.id}/tags`) ); const response = await Promise.all(requests); let recTags = {} for (const res of response) { for (const tag of res.data) { const key = `${tag.documentId}_${tag.pageIndex}`; if (recTags[key]) { recTags[key].push(tag); } else { recTags[key] = [tag]; // Initialize with an array } } } // console.log(recTags, "Line 83"); setDroppedItems(recTags) // Handle your response here } catch (err) { console.error(err); } }; fetchTags(); }, [selectedRecipients]); // FETCHING TAG - END const [droppedItemsCounter, setDroppedItemsCounter] = useState(1); const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null); // NAVIGATING to Dashboard if Transaction is not created - START useLayoutEffect(() => { if (transactionId) return; navigate("/"); }, [transactionId]) // NAVIGATING to Dashboard if Transaction is not created - END const groupByRecipientId = (tagsArray: any[]) => { const tagsData: Record = {}; for (const tag of tagsArray) { const recipientId = tag.recipientId; if (!tagsData[recipientId]) { tagsData[recipientId] = []; } tagsData[recipientId].push(tag); } return tagsData; }; const sendTagsToRecipients = async ( tagsData: Record, accountId: string, transactionId: string ) => { // tagsData = tagsData.filter((tag, indx) => (("update" in tag && tag.update) || !("id" in tag)) ) // Filter Tags that needs to update const updatedData = {} // debugger for (const [recipientId, tags] of Object.entries(tagsData)) { // console.log(recipientId, tags, "line 133") const updateTags = [] for (const tag of tags) { if ("update" in tag && tag.update) { updateTags.push(tag) } if (updateTags.length && "update" in updateTags[updateTags.length - 1]) { delete updateTags[updateTags.length - 1].update } } updatedData[recipientId] = updateTags } const requests = Object.entries(updatedData).map(([recipientId, tags]) => { return api.put( `/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`, tags ); }); try { const responses = await Promise.all(requests); const droppedItemsUpdateFalse = Object.fromEntries( Object.entries(droppedItems).map(([recipientId, tags]) => { return [recipientId, tags.map(tag => ({ ...tag, update: false }))]; }) ) setDroppedItems(droppedItemsUpdateFalse); } catch (error) { console.error("Error submitting recipient tags:", error); } }; // Save Tags - START useEffect(() => { if (isSend === null) return; dispatch(showLoader("Loading ...")) let updatedTags = { ...droppedItems } // const sendTags = { // "mod": [], // "remove": [] // } let tagsData = [] // const modified = [] // const remove = [] // console.log(updatedTags, "updatedTags"); for (const dropPageIds of Object.keys(updatedTags)) { for (const dropTag of updatedTags[dropPageIds]) { let mask = null // console.log(dropTag, "line 132") // if (dropTag.subType?.toLowerCase() === "password") { // console.log(dropTag) mask = { "type": dropTag.maskType ? `${dropTag.maskType}`.toUpperCase() : null, "character": dropTag.maskCharacter ? `${dropTag.maskCharacter}`.toUpperCase() : null } // } // console.log(dropTag, "line 204") const isDeleted = "isDeleted" in dropTag ? dropTag.isDeleted : false const updatedTagData = { "id": "id" in dropTag ? dropTag.id : null, "label": dropTag.label, "dimensions": dropTag.dimensions, "type": dropTag.type.toUpperCase(), // "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null), "minLength": "minLength" in dropTag ? dropTag.minLength : 0, "maxLength": "maxLength" in dropTag ? dropTag.maxLength : 0, "error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error", "regex": "regex" in dropTag ? dropTag.regex : [] // } , "documentId": dropTag.documentId, "options": ("options" in dropTag) ? dropTag.options : [], "pageIndex": dropTag.pageIndex, "recipientId": dropTag.recipientId, "styles": "styles" in dropTag ? dropTag.styles : { "font": "", "italic": false, "bold": false, "underline": false, "color": "black", "textSize": 0 }, "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true, // "required": "required" in dropTag ? dropTag.required : , "update": "update" in dropTag ? dropTag.update : false, "readOnly": false, } if ("format" in dropTag) { updatedTagData.format = `${dropTag.subType}`.toUpperCase(); updatedTagData.dateFormat = dropTag.format; updatedTagData.currentTimeStamp = dropTag.currentTimeStamp } if (`${dropTag.type}`.toLowerCase() === "number") { updatedTagData.decimalPlace = dropTag.decimalPlace updatedTagData.isCurrency = dropTag.isCurrency } if (updatedTagData.type === "TEXT") { updatedTagData.multiLine = dropTag.subType === "paragraph" if (dropTag.subType === "password") { updatedTagData.maskCharacter = mask.character updatedTagData.maskType = mask.type } } // if ("required" in updatedTagData && !updatedTagData.required) delete updatedTagData.required; if (updatedTagData.subType === 'SINGLE SELECT') updatedTagData.subType = "SINGLE_SELECT" if (updatedTagData.subType === 'MULTI SELECT') updatedTagData.subType = "MULTI_SELECT" if (updatedTagData.options && !updatedTagData.options.length) delete updatedTagData.options; if (!updatedTagData.id) delete updatedTagData.id; if (!isDeleted) { // sendTags.mod.push(updatedTagData); tagsData.push(updatedTagData) } } } // for(const recipient of selectedRecipients) { // const recrecipient // } tagsData = groupByRecipientId(tagsData); sendTagsToRecipients(tagsData, accountId, transactionId); (async () => { const res = await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" }) // const res = await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, { status: "SENT" }) console.log(res, "Check123") dispatch(hideLoader()) })() }, [isSend]) // Save Tags - END const handleDragOver = (event: React.DragEvent) => { event.preventDefault(); // Allows the drop event to work }; // HANDLE DROP - START const handleDrop = (event: React.DragEvent, docId: string, documentId: number, pageIndex: number) => { console.log("// HANDLE DROP - START") event.preventDefault(); const data = window.dragElemData; if (!data) return; const dragElemData = JSON.parse(data); // Get the dropped label const field = { ...dragElemData } if (!field.label) return; // const field = fields.find((item) => item.label === dragElemData.label); // Find corresponding field // if (!field) return; // If not found, exit const dropZone = event.currentTarget.getBoundingClientRect(); const x = event.clientX - dropZone.left - dragElemData.offsetX; const y = event.clientY - dropZone.top - dragElemData.offsetY; const key = `${docId}_${pageIndex}`; // const IconComponent = typeToIconMap[field.type] || ; // Handling Initia Width and height of Some tags - START if (dragElemData.label === TagsTypes.STAMP) { dragElemData.width = 160 dragElemData.height = 160 } else if (dragElemData.label === TagsTypes.SIGNATURE) { dragElemData.width = 200 dragElemData.height = 75 } else if (dragElemData.label === TagsTypes.INITIALS) { dragElemData.width = 85 dragElemData.height = 75 } // Handling Initia Width and height of Some tags - END let prepData = { properties: data.properties, icon: typeToIconMap[field.label.toUpperCase()] || , type: dragElemData.label, label: dragElemData.label, subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '', dimensions: { left: x, top: y, width: dragElemData.width, height: dragElemData.height, }, styles: dragElemData.label === "text" ? { "font": "Great Vibes", "italic": false, "bold": false, "underline": false, "color": "black", "textSize": 14.0 } : null, documentId: docId, pageIndex: pageIndex, recipientId: dragElemData.recipientId, recipientIndx: dragElemData.recipientIndx, required: (dragElemData.label !== TagsTypes.SIGNATURE && dragElemData.label !== TagsTypes.INITIALS) ? false : true, update: true, isDeleted: false } if ("group" in dragElemData) { prepData.group = dragElemData.group; prepData.isGroupExist = false; // if(uniqGroupIds.includes(dragElemData.group)) { // prepData.isGroupExist = true; // } else { // prepData.isGroupExist = false; // setUniqGroupIds([...uniqGroupIds, dragElemData.group]); // } } if (prepData.type === "temporal") { prepData.currentTimeStamp = true; } if (prepData.type.toLowerCase() === "number") { prepData.decimalPlace = 0; prepData.isCurrency = false; } const dataToSet: DroppedItemsObj = { ...droppedItems, [key]: [...(droppedItems[key] || []), prepData], } // const dataToSet: DroppedItemsObj = { // ...droppedItems, // [key]: [...(droppedItems[key] || []), // { // ...prepData, // radioGroups: dragElemData.label === "radio" ? // [ // { // ...prepData, // dimensions: { // ...prepData.dimensions, top: 8, left: 10 // } // }, // { // ...prepData, dimensions: { // ...prepData.dimensions, // top: 8, // left: 10 // } // } // ] // : // [] // }], // } // if (dragElemData.label === "radio") { // setDroppedItems(dataToSet); // } else { // setDroppedItems(dataToSet); // } console.log("data311", dataToSet) setDroppedItems(dataToSet); setDroppedItemsCounter((prev) => prev + 1); setTimeout(() => { const tagIndex = key in droppedItems ? droppedItems[key].length : 0; handleTagClick(key, tagIndex) }, 0) }; // HANDLE DROP - END // HANDLE TAG MOUSE DOWN - START // Updated handleTagMouseDown function with proper group border positioning const handleTagMouseDown = (event: React.MouseEvent, key: string, index: number) => { event.preventDefault(); let targetContainer = event?.currentTarget?.parentElement?.parentElement; let targetElement = event?.currentTarget?.parentElement; if (!targetContainer || !targetElement) return; const containerRect = targetContainer.getBoundingClientRect(); const elementRect = targetElement.getBoundingClientRect(); // Calculate the initial offset (where the mouse was clicked inside the element) const offsetX = event.clientX - elementRect.left; const offsetY = event.clientY - elementRect.top; const conScrollWidth = targetContainer?.scrollWidth; const conScrollHeight = targetContainer?.scrollHeight; let isGroupAttr = event.currentTarget.hasAttribute("group"); let groupId = null; if (isGroupAttr) { groupId = event.currentTarget.getAttribute("group"); } let groupElements: NodeListOf | null = null; if (isGroupAttr && groupId) { groupElements = document.querySelectorAll(`div[group="${groupId}"]`); // Show initial border position during drag start handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY); } const handleMouseMove = (event: MouseEvent) => { // Get new X and Y positions relative to the container let left = event.clientX - containerRect.left - offsetX; let top = event.clientY - containerRect.top - offsetY; // Restrict movement within container bounds left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width)); top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height)); // Apply the movement to the element first setDroppedItems(prevState => ({ ...prevState, [key]: Array.isArray(prevState[key]) ? prevState[key].map((item, i) => i === index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item ) : [], })); // Update group border position during drag if it's a group element if (groupElements != null && groupElements.length) { // Use a small timeout to ensure DOM is updated before recalculating borders requestAnimationFrame(() => { handleGroupBorderPosition(groupElements!, containerRect, offsetX, offsetY); }); } }; const handleMouseUp = () => { // CRITICAL: Ensure group borders are properly positioned after drag ends if (groupElements != null && groupElements.length && groupId) { // Multiple approaches to ensure the positioning works: // 1. Immediate update setTimeout(() => { handleIGroupBorderPosition(groupId); }, 10); // 2. Secondary update to ensure it's properly positioned setTimeout(() => { handleIGroupBorderPosition(groupId); }, 100); // 3. Final update using requestAnimationFrame for DOM sync requestAnimationFrame(() => { setTimeout(() => { handleIGroupBorderPosition(groupId); }, 50); }); } document.removeEventListener("pointermove", handleMouseMove); document.removeEventListener("pointerup", handleMouseUp); }; // Add event listeners document.addEventListener("pointermove", handleMouseMove); document.addEventListener("pointerup", handleMouseUp); }; // HANDLE TAG MOUSE DOWN - END // HANDLE EXPAND TAG - START const handleExpandTag = (event: React.MouseEvent, key: string, index: number) => { console.log("HANDLE EXPAND TAG - START") event.preventDefault(); // Prevent text selection issues const expandingElemIcon = event.currentTarget; const targetElement = expandingElemIcon.nextElementSibling?.parentElement; const targetContainer = targetElement?.parentElement; if (!targetContainer || !targetElement) return; const elementRect = targetElement.getBoundingClientRect(); const startX = event.clientX; const startY = event.clientY; const startWidth = elementRect.width; const startHeight = elementRect.height; const handleMouseMove = (event: MouseEvent) => { let newWidth = startWidth + (event.clientX - startX); let newHeight = startHeight + (event.clientY - startY); if (newHeight > 200 || newWidth > 400) { if (newHeight > 200) { newHeight = 200 } if (newWidth > 400) { newWidth = 400 } } // Apply the new width & height setDroppedItems({ ...droppedItems, [key]: Array.isArray(droppedItems[key]) // Ensure it's an array ? droppedItems[key].map((item, i) => i === index ? { ...item, dimensions: { ...item.dimensions, width: newWidth, height: newHeight }, update: true } : item ) : [], // Provide a fallback in case it's undefined or not an array }); }; const handleMouseUp = () => { document.removeEventListener("pointermove", handleMouseMove); document.removeEventListener("pointerup", handleMouseUp); }; document.addEventListener("pointermove", handleMouseMove); document.addEventListener("pointerup", handleMouseUp); }; // HANDLE EXPAND TAG - END const handleTagClick = (key: string, index: number) => { setSelectedTag({ key, index }) } // Updated handleRemoveTag function with your existing API logic and group handling const handleRemoveTag = (key: string, targetIndex: number) => { const tag = droppedItems[key][targetIndex]; const recipientId = tag.recipientId; (async () => { if ("id" in tag) { try { const response = await api.delete(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags/${tag.id}`) if (response.status !== 200) throw "Tag Deletion error" } catch (err) { console.error("Tag Deletion error") return; } } setSelectedTag(null); // Check if this item has a group for special group handling if ('group' in tag && tag.group) { const groupId = tag.group; // Find all items in the same group const groupItems = droppedItems[key] .map((item, idx) => ({ item, index: idx })) .filter(({ item }) => 'group' in item && item.group === groupId && !item.isDeleted); // Sort group items by their top position to identify the parent (top-most) groupItems.sort((a, b) => a.item.dimensions.top - b.item.dimensions.top); // Check if the item being removed is the parent (top-most item) const isParentItem = groupItems.length > 0 && groupItems[0].index === targetIndex; if (isParentItem && groupItems.length > 1) { // If removing the parent and there are child items, mark all items in the group as deleted console.log(`Removing parent item and all children for group: ${groupId}`); // Get indices of all group items to mark as deleted const indicesToDelete = groupItems.map(({ index }) => index); setDroppedItems(prevState => ({ ...prevState, [key]: prevState[key].map((item, index) => indicesToDelete.includes(index) ? { ...item, isDeleted: true } : item ) })); // Remove group border elements after state update setTimeout(() => { removeGroupBorders(groupId); }, 100); } else { // If removing a child item or the only item in group, just mark this item as deleted console.log(`Removing individual item from group: ${groupId}`); setDroppedItems(prevState => ({ ...prevState, [key]: prevState[key].map((item, index) => index === targetIndex ? { ...item, isDeleted: true } : item ) })); // Check if this was the last item in the group after state update setTimeout(() => { const remainingGroupItems = droppedItems[key].filter(item => 'group' in item && item.group === groupId && !item.isDeleted ); if (remainingGroupItems.length === 0) { // Remove group borders if no items left removeGroupBorders(groupId); } else { // Update group borders for remaining items handleIGroupBorderPosition(groupId); } }, 100); } } else { // Handle non-group item removal (your existing logic) setDroppedItems(prevState => ({ ...prevState, [key]: prevState[key].map((item, index) => index === targetIndex ? { ...item, isDeleted: true } : item ) })); } })() }; // Helper function to remove group border elements from DOM const removeGroupBorders = (groupId: string) => { console.log(`Removing borders for group: ${groupId}`); const borderPositions = ['top', 'right', 'bottom', 'left']; borderPositions.forEach(position => { const borderElement = document.querySelector(`[border-group='${groupId}'][border-pos='${position}']`); if (borderElement && borderElement.parentNode) { borderElement.parentNode.removeChild(borderElement); } }); }; // const handleAddRadioClick = (key:string) => { // console.log("Handle Radio Clicled", key) // console.log(droppedItems, "line 525") // } const handleIGroupBorderPosition = (groupId: string) => { console.log("handleIGroupBorderPosition for group:", groupId); const groupElements: NodeListOf = document.querySelectorAll(`div[group="${groupId}"]`); if (groupElements.length === 0) { console.log("No group elements found for:", groupId); return; } let targetContainer = groupElements[0].parentElement?.parentElement; if (!targetContainer) { console.log("No target container found"); return; } const containerRect = targetContainer.getBoundingClientRect(); // Use consistent offset values const offsetX = 10; const offsetY = 10; // Ensure DOM is fully updated before positioning requestAnimationFrame(() => { requestAnimationFrame(() => { handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY); }); }); }; // Enhanced handleGroupBorderPosition with better positioning logic const handleGroupBorderPosition = (groupElements: NodeListOf, containerRect: DOMRect, offsetX: number, offsetY: number) => { if (!groupElements || groupElements.length === 0) return; const groupId = groupElements[0].getAttribute('group'); if (!groupId) return; // Calculate the bounding box of all group elements let minLeft = Infinity, minTop = Infinity, maxRight = -Infinity, maxBottom = -Infinity; groupElements.forEach(element => { const parentElement = element.parentElement; if (!parentElement) return; const rect = parentElement.getBoundingClientRect(); const relativeLeft = rect.left - containerRect.left; const relativeTop = rect.top - containerRect.top; const relativeRight = relativeLeft + rect.width; const relativeBottom = relativeTop + rect.height; minLeft = Math.min(minLeft, relativeLeft); minTop = Math.min(minTop, relativeTop); maxRight = Math.max(maxRight, relativeRight); maxBottom = Math.max(maxBottom, relativeBottom); }); // Add some padding around the group const padding = 10; minLeft -= padding; minTop -= padding; maxRight += padding; maxBottom += padding; const width = maxRight - minLeft; const height = maxBottom - minTop; // Update or create border elements const borderPositions = ['top', 'right', 'bottom', 'left']; borderPositions.forEach(position => { let borderElement = document.querySelector(`[border-group='${groupId}'][border-pos='${position}']`) as HTMLElement; if (!borderElement) { // Create border element if it doesn't exist borderElement = document.createElement('div'); borderElement.className = `group-border ${position === 'bottom' ? 'bottom-border' : ''}`; borderElement.setAttribute('border-group', groupId); borderElement.setAttribute('border-pos', position); // Add the + icon for bottom border if (position === 'bottom') { const addIcon = document.createElement('div'); addIcon.innerHTML = ''; // You might need to adjust this based on your icon system addIcon.onclick = () => { // Find the original item to pass to handleAddGroupItemClicked const originalItem = droppedItems[key]?.find(item => 'group' in item && item.group === groupId); if (originalItem) { handleAddGroupItemClicked(key, originalItem); } }; borderElement.appendChild(addIcon); } // Append to container groupElements[0].parentElement?.parentElement?.appendChild(borderElement); } // Position the border element borderElement.style.position = 'absolute'; borderElement.style.pointerEvents = position === 'bottom' ? 'auto' : 'none'; switch (position) { case 'top': borderElement.style.left = `${minLeft}px`; borderElement.style.top = `${minTop}px`; borderElement.style.width = `${width}px`; borderElement.style.height = '2px'; break; case 'right': borderElement.style.left = `${maxRight - 2}px`; borderElement.style.top = `${minTop}px`; borderElement.style.width = '2px'; borderElement.style.height = `${height}px`; break; case 'bottom': borderElement.style.left = `${minLeft}px`; borderElement.style.top = `${maxBottom - 2}px`; borderElement.style.width = `${width}px`; borderElement.style.height = '2px'; break; case 'left': borderElement.style.left = `${minLeft}px`; borderElement.style.top = `${minTop}px`; borderElement.style.width = '2px'; borderElement.style.height = `${height}px`; break; } }); }; const handleAddGroupItemClicked = (key: string, item: any) => { console.log("handleAddGroupItemClicked", item); const groupId = item.group; // Find all existing items in this group to determine the bottom-most position const existingGroupItems = droppedItems[key]?.filter(existingItem => 'group' in existingItem && existingItem.group === groupId ) || []; const existingGroupItem = droppedItems[key]?.filter(existingItem => 'group' in existingItem && existingItem.group === groupId ) || []; existingGroupItem[0].options // Find the bottom-most item in the group let bottomMostItem = item; if (existingGroupItems.length > 0) { bottomMostItem = existingGroupItems.reduce((lowest, current) => { const currentBottom = current.dimensions.top + current.dimensions.height; const lowestBottom = lowest.dimensions.top + lowest.dimensions.height; return currentBottom > lowestBottom ? current : lowest; }); } // Calculate new position: place below the bottom-most item const spacing = 10; // Adjust spacing as needed const newTop = bottomMostItem.dimensions.top + bottomMostItem.dimensions.height + spacing; // Use the same left position as the parent (original) item const newItem = { ...item, dimensions: { ...item.dimensions, left: item.dimensions.left, // Keep same horizontal position as parent top: newTop // Position below the bottom-most item }, isGroupExist: true }; // Update the dropped items setDroppedItems({ ...droppedItems, [key]: [...droppedItems[key], newItem], }); // Update group borders after adding the new item // Use setTimeout to ensure state update is processed setTimeout(() => { handleIGroupBorderPosition(groupId); }, 50); // Slightly longer timeout to ensure DOM updates }; useEffect(() => { // Initialize group borders for all existing groups after component updates const timeoutId = setTimeout(() => { const allGroupElements = document.querySelectorAll('[group]'); const groupIds = new Set(); allGroupElements.forEach((element) => { const groupId = element.getAttribute('group'); if (groupId) { groupIds.add(groupId); } }); groupIds.forEach((groupId) => { handleIGroupBorderPosition(groupId); }); }, 100); return () => clearTimeout(timeoutId); }, [droppedItems]); // Re-run when droppedItems changes const handleDirectionalMove = (direction: 'up' | 'down' | 'left' | 'right', stepSize: number = 5) => { if (!selectedTag) return; const { key, index } = selectedTag; const currentItem = droppedItems[key]?.[index]; if (!currentItem) return; let newLeft = currentItem.dimensions.left; let newTop = currentItem.dimensions.top; switch (direction) { case 'up': newTop = Math.max(0, newTop - stepSize); break; case 'down': newTop = newTop + stepSize; break; case 'left': newLeft = Math.max(0, newLeft - stepSize); break; case 'right': newLeft = newLeft + stepSize; break; } setDroppedItems({ ...droppedItems, [key]: Array.isArray(droppedItems[key]) ? droppedItems[key].map((item, i) => i === index ? { ...item, dimensions: { ...item.dimensions, left: newLeft, top: newTop }, update: true } : item ) : [], }); }; const directionalControlsHandler = (direction: 'up' | 'down' | 'left' | 'right') => { handleDirectionalMove(direction, 5); // Use default stepSize of 5 }; // Add this new handler function to your parent component const handleDirectPositionChange = (newLeft: number, newTop: number) => { console.log("handleDirectPositionChange", newLeft, newTop) if (!selectedTag) return; const { key, index } = selectedTag; const currentItem = droppedItems[key]?.[index]; if (!currentItem) return; // Ensure values are not negative const clampedLeft = Math.max(0, newLeft); const clampedTop = Math.max(0, newTop); setDroppedItems({ ...droppedItems, [key]: Array.isArray(droppedItems[key]) ? droppedItems[key].map((item, i) => i === index ? { ...item, dimensions: { ...item.dimensions, left: clampedLeft, top: clampedTop }, update: true } : item ) : [], }); }; return ( <> {/* Center Doc Container - START */}
{ selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => { return (

{doc.name}

{ doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => { const key = `${doc.id}_${page.index}`; return (
handleDrop(e, doc.id, indxDoc, page.index)} onDragOver={handleDragOver} className='drop-page-in shadow-md relative' > {/* DROPPED ITEMS - Render only the dropped items for this specific docId and page - STARTS */} {droppedItems[key]?.map((item, index) => { if (item.isDeleted) return; const type = item.type.toUpperCase(); const IconComponent = typeToIconMap[type] || ; const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id === item.recipientId); return ( <>
handleTagClick(key, index)} style={{ left: `${item.dimensions.left}px`, top: `${item.dimensions.top}px`, width: item.type === "radio" ? `${item.dimensions.width + 20}px` : `${item.dimensions.width}px`, height: item.type === "radio" ? `max-content` : `${item.dimensions.height}px` }} > {/* If Selected Tag Recipient Info- START */} {selectedTag?.key && selectedTag.key === key && selectedTag.index === index &&

{IconComponent}

{item.label}

{ e.stopPropagation(); handleRemoveTag(key, index) }} className='cursor-pointer' />
} {/* If Selected Tag Recipient Info- END */} {/* Expand Circle - STARTS */} {item.type !== "radio" && item.type !== "checkbox" &&
handleExpandTag(e, key, index)} className='w-2.5 h-2.5 absolute bg-red-400 rounded-full bottom-[-8px] right-[-8px] cursor-nw-resize hover:bg-blue-400'>
} {/* Expand Circle - ENDS */} {/* Tag - START */} {
handleTagMouseDown(e, key, index)} className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full' style={{ background: selectedRecipient[0]?.color }} >

{IconComponent}

} {/* Tag - END */}
{ "group" in item && item.group && "isGroupExist" in item && !item.isGroupExist && <>
handleAddGroupItemClicked(key, item)}>
} ) } )} {/* Render only the dropped items for this specific docId and page - ENDS */} {/* Side details of documents Pages and document Name - START */}

page {indx + 1} of {doc.pages && doc.pages.length}

{/*

Document {indxDoc + 1} {doc.name}

*/} {/* Side details of documents Pages and document Name - END */}
) }) }
) }) : }
{selectedTag && } {/* Center Doc Container - END */} ) } export default DocumentContainer;