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

640 lines
27 KiB
TypeScript

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<React.SetStateAction<boolean>>;
isSend?: boolean | null;
setIsSend?: React.Dispatch<React.SetStateAction<boolean | null>>;
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<DocumentContainerRef, DocumentContainerProps>(
({ 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<any>(null);
const tagUndoRef = useRef<DroppedItem[] | []>([]);
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<DroppedItemsObj>({});
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 {
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<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 = {}
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<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 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] || <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,
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 */}
<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 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}`}
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) => (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'
/>
<DroppedItemsTagsCon
pageWidth={page.width}
pageHeight={page.height}
renderedWidth={imageRefs.current[page.index]?.offsetWidth || 0}
renderedHeight={imageRefs.current[page.index]?.offsetHeight || 0}
ref={childRef}
droppedItems={droppedItems}
pageIdkey={key}
selectedTag={selectedTag}
selectedRecipients={selectedRecipients}
setSelectedTag={setSelectedTag}
setDroppedItems={setDroppedItems}
accountId={accountId}
transactionId={transactionId}
transactionTemplateToggle={transactionTemplateToggle}
tagUndoRef={tagUndoRef}
/>
{/* 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 && selectedTag != null && "key" in selectedTag && "index" in selectedTag && <RightSideBar
droppedItems={droppedItems}
selectedTag={droppedItems[selectedTag.key][selectedTag.index]}
setDroppedItems={setDroppedItems}
selTagKey={selectedTag.key}
selTagIndx={selectedTag.index}
signClient={false}
emptySelectedTag={() => setSelectedTag(null)}
/>
}
{/* Center Doc Container - END */}
</>
)
})
export default DocumentContainer;