972 lines
43 KiB
TypeScript
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;
|