import React, { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { RootState } from '../../../store/store' import api from '../../../config/api'; import RightSideBar, { MASKTYPE, TEXT } from './rightSideBar/RightSideBar'; import { DroppedItem, TagsTypes } from '../../../utlity/types'; import { useNavigate, useParams } from 'react-router-dom'; 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 Loader from '../../dashboard/Loader'; import { hideLoader, showLoader } from '../../../store/slices/loaderSlice'; import DroppedItemsTagsCon from './DroppedItemsTagsCon'; 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 { transactionSaving?: boolean; setTransactionSaving?: React.Dispatch>; isSend?: boolean | null; setIsSend?: React.Dispatch>; isPreview?: boolean; aId?: string; tId?: string; } let copiedTags: DroppedItem[] = [] let selectedDoc: { [key: string]: string | null } = { key: null } const fetchedTags: string[] = [] // 👇 Create an interface for the exposed methods export interface DocumentContainerRef { handleTransactionProcess: (isSave?: boolean, isNavigate?: boolean, navigateTo?: string) => void; } const DocumentContainer = forwardRef( ({ transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null }, ref) => { let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; const dispatch = useDispatch(); const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>(); let 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; } // If is preview is enable if (isPreview && aId != null && tId != null) { accountId = aId; transactionId = tId; } // const transactionId = useSelector((state: RootState) => state.transaction.transactionId) const navigate = useNavigate(); const childRef = useRef(null); const tagUndoRef = useRef([]); const imageRefs = useRef<{ [key: string]: HTMLImageElement | null }>({}); 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({}); interface HotKeysObj { isActive: boolean; key: string | null; tagId?: string | null; hotKey?: string | null; } const hotKeysObjInitial: HotKeysObj = { isActive: false, key: null, tagId: null, hotKey: null } const [hotKeysObj, setHotKeysObj] = useState(hotKeysObjInitial); useEffect(() => { selectedRecipients.length == 0 && dispatch(fetchRecipients({ accountId, transactionId })); selectedDocuments.length == 0 && dispatch(fetchDocuments({ accountId, transactionId })); }, []) const fetchTags = async () => { try { 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; // also checking if have id but tags already fetched so to avoid same req if (fetchedTags.includes(recipient.id)) return false; fetchedTags.push(recipient.id) return true }).map((recipient) => api.get(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipient.id}/tags`)); if (!requests.length) return const response = await Promise.all(requests); let recTags = {} for (const res of response) { if (!res && !res.data) return 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 } } } setDroppedItems(recTags) // Handle your response here } catch (err) { console.error(err); } }; // FETCHING TAG - START useEffect(() => { if (selectedRecipients.length == 0) return; fetchTags(); }, [selectedRecipients, isPreview]); // 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 = {} 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 delete updateTags[updateTags.length - 1].icon } } updatedData[recipientId] = updateTags.filter((tag, indx) => !tag.isDeleted); } const requests = Object.entries(updatedData) .filter(([_, tags]) => tags.length > 0) // filter out empty ones .map(([recipientId, tags]) => { const updatedTagsWithNavOrder = tags.map((tag, indx) => ({ ...tag, navigationOrder: indx + 1, })); return api.put( `/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`, updatedTagsWithNavOrder ); }); 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); } }; const handleTransactionProcess = async (save: boolean = false, isNavigate: boolean = false, navigateTo: string = "/") => { // if (TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES === transactionTemplateToggle) save = true; dispatch(showLoader("Saving Tags ...")) if (Object.keys(droppedItems).length == 0) { if (isNavigate) navigate(navigateTo); dispatch(hideLoader()); }; // if (!save) return; let updatedTags: any = []; for (const dropPagesId of Object.keys(droppedItems)) { console.log("Dropped Item:", droppedItems[dropPagesId]); updatedTags.push(...droppedItems[dropPagesId]); } let tagsData = groupByRecipientId(updatedTags); await sendTagsToRecipients(tagsData, accountId, transactionId); // return // SENDING Transaction - START if (!save) { dispatch(showLoader("Sending ...")) const res = await api.put(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" }) if (res.status >= 200 && res.status < 300) { navigate(`/transaction-sent/${accountId}/${transactionId}`, { replace: true }) // window.location.href = `${import.meta.env.VITE_BASE_URI}/transaction-sent/${accountId}/${transactionId}` } dispatch(hideLoader()) } if (isNavigate) { navigate(navigateTo); } dispatch(hideLoader()); // SENDING Transaction - END } // Save Tags - START // useEffect(() => { // handleTransactionProcess() // }, [isSend]) useImperativeHandle(ref, () => ({ handleTransactionProcess, })); // Save Tags - END useEffect(() => { if (transactionSaving) { setTimeout(() => { setTransactionSaving(false) }, 1000) } }, [transactionSaving]) 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, originalWidth: number, originalHeight: number) => { event.preventDefault(); const imageElement = document.getElementById(`${docId}-${pageIndex}`) as HTMLImageElement; const currentWidth = imageElement.offsetWidth; const currentHeight = imageElement.offsetHeight; const scaleX = currentWidth / originalWidth; // 350 / 700 = 0.5 const scaleY = currentHeight / originalHeight; // 225 / 450 = 0.5 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(); let clientX, clientY; if (event.type.startsWith("touch") || event.nativeEvent?.changedTouches) { const touch = event.nativeEvent?.changedTouches?.[0] || event.changedTouches?.[0]; clientX = touch.pageX; clientY = touch.pageY; } else { clientX = event.clientX; clientY = event.clientY; } let x = clientX - dropZone.left - (dragElemData.width / 2); let y = clientY - dropZone.top - (dragElemData.height / 2); // Handling Drop Over Right // if (dropZone.width < x + dragElemData.width) { // x -= (x + dragElemData.width) - dropZone.width; // } // Handling Drop Over Bottom // if (dropZone.height < y + dragElemData.height) { // y -= (y + dragElemData.height) - dropZone.height; // } // Handling Drop Over Left // if (x < 0) { // x = 0; // } // Handling Drop Over Top // if (y < 0) { // y = 0; // } x = Math.max(0, Math.min(x, dropZone.width - dragElemData.width)); y = Math.max(0, Math.min(y, dropZone.height - dragElemData.height)); const key = `${docId}_${pageIndex}`; // x = x * scaleX // y = y * scaleY // const IconComponent = typeToIconMap[field.type] || ; let prepData = { ...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": data.fontSize } : 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 (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], } setDroppedItems(dataToSet); setDroppedItemsCounter((prev) => prev + 1); setTimeout(() => { const tagIndex = key in droppedItems ? droppedItems[key].length : 0; setSelectedTag({ key, index: tagIndex }) }, 1000) }; // HANDLE DROP - END useEffect(() => { if (!hotKeysObj.isActive) return const docCon = document.getElementById("doc-con"); switch (hotKeysObj.hotKey) { case "Ctrl+c": const tempCopy = { ...droppedItems[selectedTag?.key][selectedTag?.index] } delete tempCopy.id; copiedTags = [{ ...tempCopy, dimensions: { ...tempCopy.dimensions, top: tempCopy.dimensions.top + 10, left: tempCopy.dimensions.left + 10 } }] console.log("from Copy copiedTags", copiedTags) break; case "Ctrl+v": console.log(selectedDoc.key, "keyss") let updatedKey = droppedItems[selectedDoc.key] updatedKey.push(...copiedTags) console.log("copiedTags", copiedTags) console.log("updatedKey", updatedKey) setDroppedItems(prevState => ({ ...prevState, [selectedDoc.key]: updatedKey })); // Handle Ctrl+V break; case "Ctrl+x": // Handle Ctrl+V break; case "Ctrl+z": // Handle Ctrl+V break; case "Ctrl+y": // Handle Ctrl+V break; case "Ctrl+a": docCon?.querySelectorAll("[data-tag-id]").forEach(tag => { tag.classList.add("border-2", "border-sky-700") tag.setAttribute("all-select-group", "true") docCon.addEventListener("click", () => { tag.classList.remove("border-2", "border-sky-800") tag.removeAttribute("all-select-group") }) // docCon.addEventListener("focusout", () => { // console.log("focus out", tag) // tag.classList.remove("border-2", "border-slate-800") // tag.removeAttribute("all-select-group") // }) }); break; case "delete": const allselectedTags = docCon?.querySelectorAll("[all-select-group]") ?? [] allselectedTags.forEach((tag) => { const tagIndex = tag.getAttribute("data-tag-id") ?? ""; const key = tag.getAttribute("data-drop-key") ?? ""; childRef.current.handleRemoveTag(key, tagIndex) }) if (allselectedTags.length == 0) childRef.current.handleRemoveTag(selectedTag?.key, selectedTag?.index) break; default: break; } }, [hotKeysObj]) const handleDocumentFocus = (key: string) => { // const id = key.replace("_", ""); const targetCon = document.getElementById('doc-con'); selectedDoc.key = key; targetCon?.addEventListener("keydown", (event) => { event.preventDefault(); const ctrl = event.ctrlKey; const alt = event.altKey; const metaKey = event.metaKey; const shift = event.shiftKey; const pressedKey = event.key.toLowerCase(); let hotKey = pressedKey if (ctrl) { hotKey = "Ctrl+" + pressedKey; } else if (alt) { hotKey = "Alt" + pressedKey; } else if (metaKey) { hotKey = "Meta+" + pressedKey; } else if (shift) { hotKey = "Shift+" + pressedKey; } setHotKeysObj({ isActive: true, key: key, tagId: null, hotKey: hotKey }); }); // return () => targetCon?.removeEventListener("keydown"); } 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 (
handleDocumentFocus(key)} key={indx} id={`${doc.id}${indx}`} onDrop={(e) => handleDrop(e, doc.id, indxDoc, page.index, page.width, page.height)} onDragOver={handleDragOver} className='drop-page-in shadow-md relative' style={{ aspectRatio: `${page.width}/${page.height}` }} > (imageRefs.current[page.index] = el)} draggable="false" width={page.width} height={page.height} // src={`http://10.10.1.99:8083/api/v1/accounts/${accountId}/transactions/${transactionId}/documents/${doc.id}/pages/${page.id}`} src={page.src} loading='lazy' /> {/* 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 && selectedTag != null && "key" in selectedTag && "index" in selectedTag && setSelectedTag(null)} /> } {/* Center Doc Container - END */} ) }) export default DocumentContainer;