SME-Web/src/components/process/processStepTwo/DocumentContainer.tsx

972 lines
43 KiB
TypeScript

import React, { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../store/store'
import api from '../../../config/api';
import TagProperties, { MASKTYPE, TEXT } from './tagProperties/TagProperties';
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';
import DocumentPages from './documentPages/DocumentPages';
import {
initializeDroppedItems,
setDroppedItems,
clearTransactionTags,
incrementCounter,
} from '../../../store/slices/documentTagsSlice';
import { serializeGroupedFields } from '../../../helper/handleGroupedFields';
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 {
isPages: boolean;
setIsPages: React.Dispatch<React.SetStateAction<boolean>>;
zoomScale: number;
transactionSaving?: boolean;
setTransactionSaving?: React.Dispatch<React.SetStateAction<boolean>>;
isSend?: boolean | null;
setIsSend?: React.Dispatch<React.SetStateAction<boolean | null>>;
isPreview?: boolean;
aId?: string;
tId?: string;
selectedRecipientIndx?: number;
onHistoryChange?: (canUndo: boolean, canRedo: boolean) => void;
}
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;
handleUndo: () => void;
handleRedo: () => void;
handleCopy: () => void;
handlePaste: () => void;
}
const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProps>(
({ isPages, zoomScale, transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null, selectedRecipientIndx = 0, setIsPages, onHistoryChange }, ref) => {
const EMPTY_DROPPED_ITEMS: DroppedItemsObj = {};
const scaleFactor: number = (zoomScale ?? 100) / 100;
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<any>(null);
// const tagUndoRef = useRef<DroppedItem[] | []>([]);
const imageRefs = useRef<{ [key: string]: HTMLImageElement | null }>({});
const pageRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const [activePage, setActivePage] = useState<string | null>(null); // NEW
const isProgrammaticScrollRef = useRef(false); // NEW — guards against feedback loop
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
// Sync activePage with scroll position via IntersectionObserver
useEffect(() => {
const scrollRoot = document.getElementById('doc-con');
if (!scrollRoot) return;
// Track intersection ratio per page so we can pick the most-visible one
// when multiple pages are partially visible at once.
const ratios: Record<string, number> = {};
const observer = new IntersectionObserver(
(entries) => {
// Ignore intersection changes caused by our own programmatic
// scroll (e.g. clicking a thumbnail) to avoid feedback loops.
if (isProgrammaticScrollRef.current) return;
entries.forEach((entry) => {
const key = entry.target.getAttribute('data-page-key');
if (!key) return;
ratios[key] = entry.isIntersecting ? entry.intersectionRatio : 0;
});
// Pick the key with the highest intersection ratio
let bestKey: string | null = null;
let bestRatio = 0;
for (const [key, ratio] of Object.entries(ratios)) {
if (ratio > bestRatio) {
bestRatio = ratio;
bestKey = key;
}
}
if (bestKey && bestRatio > 0) {
setActivePage((prev) => (prev === bestKey ? prev : bestKey)); // dedupe, avoid re-render
}
},
{
root: scrollRoot,
// Multiple thresholds so ratio updates fire smoothly while
// scrolling, without needing scroll-event listeners at all.
threshold: [0, 0.25, 0.5, 0.75, 1],
rootMargin: '0px',
}
);
Object.values(pageRefs.current).forEach((el) => {
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, [selectedDocuments]);
// handle Tag - Document Pos and size for Responsiveness - START
const [imageSizes, setImageSizes] = useState<Record<string, { width: number; height: number }>>({});
useEffect(() => {
const observers: ResizeObserver[] = [];
Object.keys(imageRefs.current).forEach((key) => {
const el = imageRefs.current[key];
if (!el) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
setImageSizes((prev) => ({
...prev,
[key]: { width, height },
}));
}
});
observer.observe(el);
observers.push(observer);
});
return () => observers.forEach((o) => o.disconnect());
}, [selectedDocuments]);
// handle Tag - Document Pos and size for Responsiveness - START
useEffect(() => {
if (selectedDocuments.length > 0 && selectedDocuments[0].pages?.length > 0) {
const firstDoc = selectedDocuments[0];
const firstPage = firstDoc.pages[0];
if (!selectedDoc.key) {
selectedDoc.key = `${firstDoc.id}_${firstPage.index}`;
}
}
}, [selectedDocuments]);
// Redux state for dropped items
const transactionKey = useMemo(
() => `${accountId}_${transactionId}`,
[accountId, transactionId]
);
const droppedItems = useSelector((state: RootState) => state.documentTags.items[transactionKey] ?? EMPTY_DROPPED_ITEMS);
const droppedItemsCounter = useSelector((state: RootState) => state.documentTags.counter[transactionKey] || 0);
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<HotKeysObj>(hotKeysObjInitial);
useEffect(() => {
selectedRecipients.length == 0 && dispatch(fetchRecipients({ accountId, transactionId }));
selectedDocuments.length == 0 && dispatch(fetchDocuments({ accountId, transactionId }));
}, [])
const fetchTags = async () => {
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;
// 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
}
}
}
// Dispatch to Redux instead of local state
dispatch(initializeDroppedItems({
transactionKey,
items: 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 [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 hotKeysObj = useRef<HotKeysObj>(hotKeysObjInitial); // use ref, not state
// const docKeyListenerRef = useRef<((e: KeyboardEvent) => void) | null>(null);
const selectedTagRef = useRef<{ key: string; index: number } | null>(null);
const droppedItemsRef = useRef(droppedItems);
const historyRef = useRef<DroppedItemsObj[]>([]);
const cursorRef = useRef<number>(-1);
const isUndoRedoRef = useRef<boolean>(false); // prevents history push during undo/redo replay
const notifyHistoryChange = () => {
onHistoryChange?.(
cursorRef.current > 0,
cursorRef.current < historyRef.current.length - 1
);
};
// Global keydown listener — works regardless of which element has focus
useEffect(() => {
const handler = (event: KeyboardEvent) => {
// Don't intercept when user is typing in an input/textarea/contenteditable
const target = event.target as HTMLElement;
if (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable
) return;
const ctrl = event.ctrlKey || event.metaKey;
const shift = event.shiftKey;
const pressedKey = event.key.toLowerCase();
// Only intercept known hotkeys to avoid blocking browser shortcuts
const isKnownHotkey =
(ctrl && ['c', 'v', 'a', 'z', 'y'].includes(pressedKey)) ||
['delete', 'backspace'].includes(pressedKey);
if (!isKnownHotkey) return;
event.preventDefault();
let hotKey = pressedKey;
if (ctrl && shift) hotKey = 'Ctrl+Shift+' + pressedKey;
else if (ctrl) hotKey = 'Ctrl+' + pressedKey;
handleHotKey(hotKey, selectedDoc.key ?? '');
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, []); // empty deps — handleHotKey uses refs so it's always fresh
useEffect(() => {
droppedItemsRef.current = droppedItems;
// For Undo Redo
// Skip recording when the change was caused by undo/redo itself
if (isUndoRedoRef.current) {
isUndoRedoRef.current = false;
return;
}
if (Object.keys(droppedItems).length === 0) return;
// Trim any future states beyond cursor (new action wipes redo stack)
historyRef.current = historyRef.current.slice(0, cursorRef.current + 1);
// Deep clone so mutations don't corrupt history
historyRef.current.push(JSON.parse(JSON.stringify(droppedItems)));
cursorRef.current = historyRef.current.length - 1;
notifyHistoryChange();
}, [droppedItems]);
useEffect(() => {
selectedTagRef.current = selectedTag;
}, [selectedTag]);
const handleUndo = () => {
if (cursorRef.current <= 0) return; // nothing to undo
cursorRef.current -= 1;
const snapshot = historyRef.current[cursorRef.current];
isUndoRedoRef.current = true; // flag so the effect above skips pushing
dispatch(setDroppedItems({
transactionKey,
items: JSON.parse(JSON.stringify(snapshot))
}));
notifyHistoryChange();
};
const handleRedo = () => {
if (cursorRef.current >= historyRef.current.length - 1) return; // nothing to redo
cursorRef.current += 1;
const snapshot = historyRef.current[cursorRef.current];
isUndoRedoRef.current = true;
dispatch(setDroppedItems({
transactionKey,
items: JSON.parse(JSON.stringify(snapshot))
}));
notifyHistoryChange();
};
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 = {}
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) {
const lastTag = updateTags[updateTags.length - 1];
if ("update" in lastTag || "icon" in lastTag) {
// Create new object without update and icon properties
const { update, icon, ...cleanTag } = lastTag as any;
updateTags[updateTags.length - 1] = cleanTag;
}
}
}
updatedData[recipientId] = updateTags.filter((tag) => !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,
}));
// const tagsData: any[] = serializeGroupedFields(updatedTagsWithNavOrder)
return api.put(
`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`,
updatedTagsWithNavOrder
);
});
try {
const responses = await Promise.all(requests);
// Dispatch Redux action instead of local state
const droppedItemsUpdateFalse = Object.fromEntries(
Object.entries(droppedItems).map(([recipientId, tags]) => {
return [recipientId, tags.map(tag => ({ ...tag, update: false }))];
})
)
dispatch(setDroppedItems({
transactionKey,
items: droppedItemsUpdateFalse
}));
return responses;
} 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)) {
updatedTags.push(...droppedItems[dropPagesId]);
}
let tagsData = groupByRecipientId(updatedTags);
const responses = await sendTagsToRecipients(tagsData, accountId, transactionId);
if (responses && "status" in responses && responses?.status !== 200) return
// 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,
handleUndo,
handleRedo,
handleCopy,
handlePaste
}));
// Save Tags - END
useEffect(() => {
if (transactionSaving) {
setTimeout(() => {
setTransactionSaving(false)
}, 1000)
}
}, [transactionSaving])
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, originalWidth: number, originalHeight: number) => {
event.preventDefault();
const pageRefKey = `${docId}_${pageIndex}`;
const imageElement = imageRefs.current[pageRefKey] || document.getElementById(`${docId}-${pageIndex}`) as HTMLImageElement;
const currentWidth = imageElement?.offsetWidth || originalWidth;
const currentHeight = imageElement?.offsetHeight || originalHeight;
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);
let x = (clientX - dropZone.left) / scaleFactor - (dragElemData.width / 2);
let y = (clientY - dropZone.top) / scaleFactor - (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));
x = Math.max(0, Math.min(x, originalWidth - dragElemData.width));
y = Math.max(0, Math.min(y, originalHeight - dragElemData.height));
const key = `${docId}_${pageIndex}`;
// x = x * scaleX
// y = y * scaleY
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
let prepData = {
...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 / scaleX,
top: y / scaleY,
width: dragElemData.width / scaleX,
height: dragElemData.height / scaleY,
},
styles: 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() == "text") {
prepData.multiLine = true
}
if (prepData.type.toLowerCase() == "number") {
prepData.decimalPlace = 0;
prepData.isCurrency = false;
}
const dataToSet: DroppedItemsObj = {
...droppedItems,
[key]: [...(droppedItems[key] || []),
prepData],
}
dispatch(setDroppedItems({
transactionKey,
items: dataToSet
}));
dispatch(incrementCounter(transactionKey));
};
// HANDLE DROP - END
// useEffect(() => {
// return () => {
// const targetCon = document.getElementById('doc-con');
// if (docKeyListenerRef.current && targetCon) {
// targetCon.removeEventListener('keydown', docKeyListenerRef.current);
// }
// };
// }, []);
const handleCopy = () => {
const currentSelectedTag = selectedTagRef.current;
const currentDroppedItems = droppedItemsRef.current;
if (!currentSelectedTag) return;
const tag = currentDroppedItems[currentSelectedTag.key]?.[currentSelectedTag.index];
if (!tag) return;
const { id, ...tagWithoutId } = tag as any;
copiedTags = [{
...tagWithoutId,
dimensions: {
...tagWithoutId.dimensions,
top: tagWithoutId.dimensions.top + 10,
left: tagWithoutId.dimensions.left + 10,
}
}];
};
const handlePaste = () => {
if (!copiedTags.length || !selectedDoc.key) return;
const currentDroppedItems = droppedItemsRef.current;
const existingTags = currentDroppedItems[selectedDoc.key] || [];
dispatch(setDroppedItems({
transactionKey,
items: {
...currentDroppedItems,
[selectedDoc.key]: [...existingTags, ...copiedTags.map(t => ({ ...t, update: true }))]
}
}));
dispatch(incrementCounter(transactionKey));
};
// Replace the hotkeys useEffect with a direct handler function
const handleHotKey = (hotKey: string, docKey: string) => {
const currentDroppedItems = droppedItemsRef.current;
const currentSelectedTag = selectedTagRef.current;
const docCon = document.getElementById('doc-con');
switch (hotKey) {
case 'Ctrl+c': {
handleCopy();
break;
}
case 'Ctrl+v': {
handlePaste();
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');
});
// One-time click to deselect all
const deselect = () => {
docCon?.querySelectorAll('[all-select-group]').forEach(tag => {
tag.classList.remove('border-2', 'border-sky-700');
tag.removeAttribute('all-select-group');
});
docCon?.removeEventListener('click', deselect);
};
docCon?.addEventListener('click', deselect);
break;
}
case 'delete':
case 'backspace': {
const allSelected = docCon?.querySelectorAll('[all-select-group]') ?? [];
if (allSelected.length > 0) {
allSelected.forEach(tag => {
const tagIndex = tag.getAttribute('data-tag-id') ?? '';
const key = tag.getAttribute('data-drop-key') ?? '';
childRef.current?.handleRemoveTag(key, tagIndex);
});
} else if (currentSelectedTag) {
childRef.current?.handleRemoveTag(currentSelectedTag.key, currentSelectedTag.index);
}
break;
}
case 'Ctrl+z': {
handleUndo();
break;
}
case 'Ctrl+y': {
handleRedo();
break;
}
case 'Ctrl+Shift+z': {
handleRedo(); // Mac convention
break;
}
default:
break;
}
};
const handleDocumentFocus = (key: string) => {
selectedDoc.key = key;
};
return (
<>
{/* Center Doc Container - START */}
<div id="doc-con"
className={`sm:col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth`}>
<div style={{
width: `${100 * scaleFactor}%`, // ← outer wrapper grows to make room
height: `${100 * scaleFactor}%`,
}}>
<div className="zoom-con"
style={{
transform: `scale(${scaleFactor})`,
transformOrigin: 'top left',
width: `${100 / scaleFactor}%`,
height: scaleFactor < 1
? `${100 / scaleFactor}%` // zoomed out — expand to fill
: 'auto', // zoomed in — let content grow
}}>
{
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
return (
<div key={indxDoc} className='flex flex-col overflow-x-auto'>
<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%] ${isPreview ? "pointer-events-none" : ""}`}>
{
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
const key = `${doc.id}_${page.index}`;
return (
<div
tabIndex={-1}
onFocus={() => 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'
style={{ aspectRatio: `${page.width}/${page.height}` }}
>
<img
// style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
// style={{ "width": page.width, "height": page.height, "maxWidth": "100%", "maxHeight": "100%" }}
// style={{ "width": "100%", "height": "100%", "maxWidth": "100%", "maxHeight": "100%" }}
// className={`w-[${(page.width / 2)}px] h-[${(page.height / 2)}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
id={`${doc.id}-${page.index}`}
className={`w-[${page.width / 2}px] h-[${page.height / 2}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
ref={(el) => {
const pageRefKey = `${doc.id}_${page.index}`;
imageRefs.current[pageRefKey] = el;
}}
onLoad={(event) => {
const pageRefKey = `${doc.id}_${page.index}`;
const el = event.currentTarget;
setImageSizes((prev) => ({
...prev,
[pageRefKey]: {
width: el.offsetWidth,
height: el.offsetHeight,
},
}));
}}
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'
data-doc-img="true"
/>
<DroppedItemsTagsCon
scaleFactor={scaleFactor}
pageWidth={page.width}
pageHeight={page.height}
renderedWidth={imageSizes[key]?.width || imageRefs.current[key]?.offsetWidth || 0}
renderedHeight={imageSizes[key]?.height || imageRefs.current[key]?.offsetHeight || 0}
ref={childRef}
droppedItems={droppedItems}
pageIdkey={key}
selectedTag={selectedTag}
selectedRecipients={selectedRecipients}
setSelectedTag={setSelectedTag}
setDroppedItems={(updateFn: any) => {
const newItems = typeof updateFn === 'function'
? updateFn(droppedItems)
: updateFn;
dispatch(setDroppedItems({
transactionKey,
items: newItems
}));
}}
accountId={accountId}
transactionId={transactionId}
transactionTemplateToggle={transactionTemplateToggle}
// tagUndoRef={tagUndoRef}
transactionKey={transactionKey}
/>
{/* 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>
</div>
</div>
{/* Note Check Here - Work on Tag Propertieres Apppearrrrrrrrrrrrrrrr ------------------------- */}
<>
{selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag && droppedItems[selectedTag.key] && droppedItems[selectedTag.key][selectedTag.index] && <TagProperties
droppedItems={droppedItems}
selectedTag={droppedItems[selectedTag.key][selectedTag.index]}
setDroppedItems={(updateFn: any) => {
const newItems = typeof updateFn === 'function'
? updateFn(droppedItems)
: updateFn;
dispatch(setDroppedItems({
transactionKey,
items: newItems
}));
}}
selTagKey={selectedTag.key}
selTagIndx={selectedTag.index}
signClient={false}
emptySelectedTag={() => setSelectedTag(null)}
/>
}
</>
{/* {selectedDocuments && selectedDocuments.length > 0 && <DocumentPages
isPages={isPages}
setIsPages={setIsPages}
droppedItems={droppedItems}
/>} */}
{!isPreview && selectedDocuments && selectedDocuments.length > 0 && <DocumentPages
isPages={isPages}
setIsPages={setIsPages}
droppedItems={droppedItems}
activePage={activePage} // NEW
onPageSelect={(key: string) => { // NEW — replaces hash-based nav
isProgrammaticScrollRef.current = true;
setActivePage(key);
pageRefs.current[key]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Release the guard after the smooth scroll has had time to settle,
// so the IntersectionObserver resumes driving activePage afterward.
window.setTimeout(() => { isProgrammaticScrollRef.current = false; }, 600);
}}
/>}
{/* Center Doc Container - END */}
</>
)
})
export default DocumentContainer;