1214 lines
52 KiB
TypeScript
1214 lines
52 KiB
TypeScript
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 } 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';
|
|
|
|
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<React.SetStateAction<boolean | null>>;
|
|
}
|
|
|
|
const DocumentContainer: React.FC<DocumentContainerProps> = ({ 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)
|
|
|
|
console.log("selectedDocuments",selectedDocuments)
|
|
|
|
const [droppedItems, setDroppedItems] = useState<DroppedItemsObj>({});
|
|
console.log("droppedItems", droppedItems)
|
|
|
|
// const [uniqGroupIds, setUniqGroupIds] = useState<string[]>([]);
|
|
|
|
|
|
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<number>(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<number, any[]> = {};
|
|
|
|
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<number, any[]>,
|
|
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("Creating Transaction"))
|
|
|
|
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 : true,
|
|
"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")
|
|
})()
|
|
}, [isSend])
|
|
// Save Tags - END
|
|
|
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
|
event.preventDefault(); // Allows the drop event to work
|
|
};
|
|
|
|
// HANDLE DROP - START
|
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>, 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] || <FaQuestionCircle />;
|
|
|
|
let prepData = {
|
|
properties: data.properties,
|
|
icon: typeToIconMap[field.label.toUpperCase()] || <FaQuestionCircle />,
|
|
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,
|
|
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<HTMLDivElement>, 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<HTMLElement> | 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<HTMLDivElement>, 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<HTMLElement> = 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<HTMLElement>, 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 = '<IoAdd />'; // 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
|
|
) || [];
|
|
|
|
// 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<string>();
|
|
|
|
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 handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => {
|
|
// // const dataToSet: DroppedItemsObj = {
|
|
// // ...droppedItems,
|
|
// // [key]: [...(droppedItems[key] || []),
|
|
// // {
|
|
// // ...prepData,
|
|
// // radioGroups: dragElemData.label === "radio" ? [{ ...prepData }] : []
|
|
// // }],
|
|
// // }
|
|
|
|
// // const lastRadioElemRect = e.currentTarget?.parentElement.previousSibling.getBoundingClientRect();
|
|
// // console.log(lastRadioElemRect, "lastRadioElemRect")
|
|
|
|
// const setNewData = droppedItems;
|
|
|
|
// if (Array.isArray(setNewData[key][index].radioGroups) && setNewData[key][index].radioGroups?.length > 0) {
|
|
// setNewData[key][index].radioGroups?.push(setNewData[key][index].radioGroups[0])
|
|
// }
|
|
|
|
// setDroppedItems(setNewData);
|
|
// };
|
|
|
|
|
|
|
|
// const handleRadioTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number, radioIndex: number) => {
|
|
|
|
// event.preventDefault();
|
|
|
|
// let targetContainer = event?.currentTarget?.parentElement;
|
|
// let targetElement = event?.currentTarget;
|
|
|
|
// 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 handleMouseMove = (event: PointerEvent) => {
|
|
// // Get new X and Y positions relative to the container
|
|
// // let x = offsetX;
|
|
// // let y = offsetY;
|
|
|
|
// let x = event.clientX - elementRect.left;
|
|
// let y = event.clientY - elementRect.top;
|
|
|
|
// const newData = droppedItems;
|
|
|
|
// if (newData[key][index].radioGroups && newData[key][index].radioGroups.length > 0) {
|
|
// newData[key][index].radioGroups[radioIndex].dimensions.left = x;
|
|
// newData[key][index].radioGroups[radioIndex].dimensions.top = y;
|
|
|
|
// let topMin = Number.POSITIVE_INFINITY;
|
|
// let topMax = Number.NEGATIVE_INFINITY;
|
|
// let leftMin = Number.POSITIVE_INFINITY;
|
|
// let leftMax = Number.NEGATIVE_INFINITY;
|
|
|
|
// for (const radio of newData[key][index].radioGroups) {
|
|
// if (radio.dimensions.left < leftMin) {
|
|
// leftMin = radio.dimensions.left;
|
|
// }
|
|
// if (radio.dimensions.left > leftMax) {
|
|
// leftMax = radio.dimensions.left;
|
|
// }
|
|
// if (radio.dimensions.top < topMin) {
|
|
// topMin = radio.dimensions.top;
|
|
// }
|
|
// if (radio.dimensions.top > topMax) {
|
|
// topMax = radio.dimensions.top;
|
|
// }
|
|
// }
|
|
|
|
// newData[key][index].radioPosition = {
|
|
// topMin: topMin,
|
|
// topMax: topMax,
|
|
// leftMin: leftMin,
|
|
// leftMax: leftMax,
|
|
// width: leftMax - leftMin,
|
|
// height: topMax - topMin
|
|
// }
|
|
|
|
// newData[key][index].dimensions.left = x;
|
|
// newData[key][index].dimensions.top = y;
|
|
// }
|
|
|
|
// setDroppedItems({ ...newData });
|
|
// };
|
|
|
|
// const handleMouseUp = () => {
|
|
// document.removeEventListener("pointermove", handleMouseMove);
|
|
// document.removeEventListener("pointerup", handleMouseUp);
|
|
// };
|
|
|
|
// // Add event listeners
|
|
// document.addEventListener("pointermove", handleMouseMove);
|
|
// document.addEventListener("pointerup", handleMouseUp);
|
|
// }
|
|
|
|
|
|
// NEW DIRECTIONAL MOVEMENT HANDLER - START
|
|
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
|
|
)
|
|
: [],
|
|
});
|
|
};
|
|
|
|
// Create a wrapper that matches DirectionalControls' expected signature
|
|
const directionalControlsHandler = (direction: 'up' | 'down' | 'left' | 'right') => {
|
|
handleDirectionalMove(direction, 5); // Use default stepSize of 5
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Center Doc Container - START */}
|
|
<div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'>
|
|
|
|
{
|
|
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
|
|
return (
|
|
<div key={indxDoc} className='flex flex-col'>
|
|
<div className='py-2 text-sm'>
|
|
<p className='text-center'>{doc.name}</p>
|
|
</div>
|
|
|
|
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'>
|
|
{
|
|
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
|
|
const key = `${doc.id}_${page.index}`;
|
|
return (
|
|
<div
|
|
key={indx}
|
|
|
|
id={`${doc.id}${indx}`}
|
|
onDrop={(e) => 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] || <FaQuestionCircle />;
|
|
const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id === item.recipientId);
|
|
return (
|
|
<>
|
|
<div
|
|
key={index}
|
|
draggable="false"
|
|
className="absolute rounded dropped-item"
|
|
onClick={() => 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 &&
|
|
<div className='absolute flex justify-between items-center w-full max-w-max bg-white top-[-22px] shadow z-20 text-sm rounded-t-sm' >
|
|
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
|
|
<p className='w-2.5 h-2.5 rounded'
|
|
style={{ background: selectedRecipient[0]?.color }}
|
|
></p>
|
|
<p
|
|
className=''>{IconComponent}
|
|
</p>
|
|
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
|
|
<IoMdClose onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleRemoveTag(key, index)
|
|
}} className='cursor-pointer' />
|
|
</div>
|
|
</div>}
|
|
{/* If Selected Tag Recipient Info- END */}
|
|
|
|
|
|
|
|
|
|
{/* Expand Circle - STARTS */}
|
|
{item.type !== "radio" && <div
|
|
onPointerDown={(e) => 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'>
|
|
</div>}
|
|
{/* Expand Circle - ENDS */}
|
|
|
|
{/* Tag - START */}
|
|
{
|
|
<div
|
|
{...("group" in item && item.group ? { "group": item.group } : {})}
|
|
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
|
|
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
|
|
style={{ background: selectedRecipient[0]?.color }}
|
|
>
|
|
<p
|
|
className={`flex justify-center items-center w-full text-xl text-white h-full`}
|
|
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
|
|
>
|
|
{IconComponent}
|
|
</p>
|
|
</div>
|
|
}
|
|
{/* Tag - END */}
|
|
|
|
</div>
|
|
|
|
{
|
|
"group" in item && item.group && "isGroupExist" in item && !item.isGroupExist &&
|
|
<>
|
|
|
|
|
|
<div className="group-border" border-group={item.group} border-pos="top"></div>
|
|
<div className="group-border" border-group={item.group} border-pos="right"></div>
|
|
<div className="group-border bottom-border" border-group={item.group} border-pos="bottom">
|
|
<div onClick={() => handleAddGroupItemClicked(key, item)}><IoAdd /></div>
|
|
</div>
|
|
<div className="group-border" border-group={item.group} border-pos="left"></div>
|
|
</>
|
|
|
|
}
|
|
|
|
</>
|
|
)
|
|
}
|
|
|
|
)}
|
|
{/* Render only the dropped items for this specific docId and page - ENDS */}
|
|
|
|
<img
|
|
style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
|
|
draggable="false"
|
|
width={page.width}
|
|
height={page.height}
|
|
src={page.src}
|
|
loading='lazy'
|
|
/>
|
|
|
|
|
|
|
|
{/* Side details of documents Pages and document Name - START */}
|
|
<p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-60px] text-slate-600'>
|
|
page {indx + 1} of {doc.pages && doc.pages.length}
|
|
</p>
|
|
{/* <p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs right-[-140px] text-slate-600'>
|
|
Document {indxDoc + 1} {doc.name}
|
|
</p> */}
|
|
{/* Side details of documents Pages and document Name - END */}
|
|
|
|
|
|
</div>
|
|
)
|
|
})
|
|
}
|
|
</div>
|
|
|
|
|
|
</div>
|
|
)
|
|
}) : <Loader />
|
|
}
|
|
|
|
|
|
</div >
|
|
|
|
{selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} DirectionalControlsProps={{
|
|
onMove: directionalControlsHandler,
|
|
disabled: false,
|
|
stepSize: 5,
|
|
className: ''
|
|
}} />}
|
|
{/* Center Doc Container - END */}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default DocumentContainer;
|