SME-Web/src/components/client/SignDocument.tsx

1733 lines
101 KiB
TypeScript

import { useEffect, useLayoutEffect, useState } from "react";
import axios from "axios"
import { RootState } from "../../store/store";
import { useNavigate, useParams } from "react-router-dom"
import { DroppedItem, DroppedItemsObj, TagsTypes } from "../../utlity/types";
import Loader from "../dashboard/Loader";
import SignCon from "../signature/SignCon";
import { Bounce, toast } from "react-toastify";
import { useDispatch, useSelector } from "react-redux";
import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
import { FaRegImage, FaFileAlt, FaSignature, FaStamp, FaCheck, FaQuestionCircle } from 'react-icons/fa'
import { IoIosDocument, IoMdClose } from 'react-icons/io'
import { MdKeyboardArrowDown } from 'react-icons/md'
import { TbChevronsDownLeft, TbSignature } from "react-icons/tb";
import { BsFileArrowDownFill } from "react-icons/bs";
import dayjs from "dayjs";
import maskUtils from "../../utlity/maskUtils";
import StampUpload from "../stamp/StampUpload";
import TermsModal from "./TermsModal";
import DocumentPages from "./DocumentPages";
import FieldList from "../process/processStepTwo/FieldList";
import typeToIconMap from "../../helper/iconMap";
import { droppedTagsDefaultField } from "../process/processStepTwo/DocumentContainer";
import SignCompletionScreen from "./SignCompletionScreen";
import api from "../../config/api";
import RightSideBar from "../process/processStepTwo/rightSideBar/RightSideBar";
import { RiDragMoveFill } from "react-icons/ri";
import { handleGroupBorderPosition } from "../../utlity/tagGroupConUtils";
interface RecipientType {
id: string;
firstName: string | null;
lastName: string;
fullName: string | null;
email: string;
title: string | null;
initials: string | null;
company: string | null;
website: string | null;
phone: string | null;
index: number;
type: string;
recipientStatus: string;
authType: string | null;
language: string | null;
transaction: string | null;
acceptedTerms: boolean;
name: string | null;
}
interface pagesType {
index: number;
type: string;
src: string;
height: number;
width: number;
}
export interface DocumentType {
documentId: string;
pages: pagesType[];
name: string;
}
interface TransactionType {
status: string;
transationId: string;
}
interface TagSubmitData {
id: string;
type?: string;
subType?: string;
signed: boolean;
value: string;
}
interface TagSubmitRecData {
recipientId: string;
}
export enum SaveTypeEnum {
DRAW = "DRAW",
TYPE = "TEXT",
UPLOAD = "IMAGE",
}
export interface SaveType {
signatureId: string | null
saveType: SaveTypeEnum | null;
save: boolean;
signature: string | null;
initial: string | null;
}
interface TagsSubmitDataObj {
tags: TagSubmitData[];
recipient?: TagSubmitRecData
}
interface SignConfType {
accepted: boolean;
index: number;
}
interface SignConfTypeObj {
[key: number]: SignConfType;
}
export interface StampTypeObj {
open: boolean;
blob: string;
}
interface FIT {
label: string,
value: number | null
}
const fitObj = [
{
label: "Fit",
value: "FIT"
},
{
label: "50%",
value: "50"
},
{
label: "150%",
value: "150"
},
{
label: "200%",
value: "200"
},
]
// Functions - LIST
// handleSignClick - Handle when sign || Initial tag got clicked
// fetchData - Fetch document pages and tags data
// handleFormSubmit - handle form submit and send data to server
const SignDocument = () => {
const { recipientId } = useParams();
const dispatch = useDispatch();
// Signer data - START
const [selectedDocuments, setSelectedDocuments] = useState<DocumentType[] | null>(null);
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
const [recipient, setRecipient] = useState<RecipientType | null>(null);
const [tagJson, setTagJson] = useState<DroppedItem[] | []>([]);
const [transaction, setTransaction] = useState<TransactionType | null>(null);
const [recSignStatus, setRecSignStatus] = useState<string | null>(null);
const [droppedItems, setDroppedItems] = useState<DroppedItem[]>([]);
const [signTagId, setSignTagId] = useState<number>(0)
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
// Handling Date Opacity
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
const [isDateOpen, setIsDateOpen] = useState<string | null>(null);
// Signer data - END
// Show is fields - Showing all fields There
const [isFields, setIsFields] = useState<boolean>(true);
const [isFitDd, setIsFitDd] = useState<boolean>(false)
const [selFitIndx, setSelFitIndx] = useState<number>(0);
const globalLoader = useSelector((state: RootState) => state.loader)
const [openSign, setOpenSign] = useState<boolean>(false);
const [focusElement, setFocusElement] = useState<DroppedItem | null>(null);
const [dropDownTogglePage, setDropDownTogglePage] = useState<string>("");
const [nav2DocToggle, setNav2DocToggle] = useState<boolean>(false);
const [hideTags, setHideTags] = useState<boolean>(false);
const [transparentTags, setTransparentTags] = useState<boolean>(false);
const [provideTags, setProvideTags] = useState(false)
const [showCompletionScreen, setShowCompletionScreen] = useState(false)
const [saved, setSaved] = useState<SaveType>({
signatureId: null,
saveType: null,
save: false,
signature: null,
initial: null,
});
const navigate = useNavigate();
const [signConfObj, setSignConfObj] = useState<SignConfTypeObj | {}>({});
const zoom = (percentage: number, contentClass: string) => {
const content = document.querySelector(`.${contentClass}`);
if (content) {
(content as HTMLElement).style.zoom = `${percentage}%`;
}
}
const handleTagClick = (key: string, index: number) => {
setSelectedTag({ key, index })
}
useEffect(() => {
let indx = fitObj[selFitIndx].value
if (indx === "FIT") {
indx = "100";
}
zoom(Number(indx), 'zoom-con')
}, [selFitIndx])
useEffect(() => {
dispatch(showLoader("Loading ..."));
fetchData();
}, [])
useEffect(() => {
if (!focusElement) {
if (droppedItems && droppedItems.length) {
setFocusElement({ ...droppedItems[0], focusElementIndx: 0 });
}
}
}, [droppedItems])
useEffect(() => {
if (recSignStatus === "COMPLETED") {
// setHideTags(true);
setTransparentTags(true);
} else {
setHideTags(false);
}
}, [recSignStatus])
useEffect(() => {
if (saved.save && Object.keys(signConfObj).length) {
const keys = Object.keys(signConfObj)
const index = Number(keys[0])
const newObj = {
accepted: true,
index: index
}
setSignConfObj((prev) => {
return (
{ ...prev, [index]: { ...newObj } }
)
});
}
}, [saved])
const handleSignClick = (index: number, id: number) => {
setSignTagId(id)
if (saved.save) {
if (index in signConfObj) {
setOpenSign(true);
return
};
setSignConfObj({
...signConfObj,
[index]: {
accepted: true,
index: index
}
})
return
}
setOpenSign(true);
const newObj = {
[index]: {
"accepted": false,
index: index
}
}
setSignConfObj({ ...newObj })
}
const handleStampClick = (index: number) => {
setStamp({
open: true,
blob: ""
})
}
const handleSignAcknowledgeClick = (index: number) => {
if (saved.save && signConfObj === null) {
setSignConfObj([...signConfObj, index])
}
}
// Step 2: Prevent browser back when status === 'COMPLETED'
// useEffect(() => {
// if (recSignStatus !== "COMPLETED") return;
// const currentPath = window.location.pathname;
// const handlePopState = (event) => {
// // Show warning
// alert("Back navigation is blocked on this screen.");
// // Push the current URL back to prevent navigating away
// window.history.pushState(null, "", currentPath);
// };
// // Push extra states to build history buffer
// for (let i = 0; i < 50; i++) {
// window.history.pushState(null, "", currentPath);
// }
// // Add popstate listener
// window.addEventListener("popstate", handlePopState);
// // Cleanup on unmount
// return () => {
// window.removeEventListener("popstate", handlePopState);
// };
// }, [recSignStatus]);
const fetchData = (isSign: boolean = false) => {
axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign?recipientId=${recipientId}`,
{
headers:
{ "ngrok-skip-browser-warning": "69420", }
})
.then((res) => {
if (res.status === 200) {
setSelectedDocuments(res?.data?.documents)
setRecipient(res?.data?.details)
setDroppedItems(res?.data?.tagJsons)
setTransaction(res?.data?.transaction)
setRecSignStatus(res?.data?.status)
if ("provideOwnTags" in res?.data?.details && res?.data?.details.provideOwnTags) {
setProvideTags(true)
}
if (res?.data?.status === "COMPLETED") {
setShowCompletionScreen(true)
}
setTimeout(() => {
dispatch(hideLoader());
}, 600);
}
})
.catch((err) => {
console.log(err)
});
}
const handleToastError = (msg: string) => {
toast.dismiss();
toast.error(msg, {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
const handleFormSubmit = () => {
// e.preventDefault();
const submitData: TagsSubmitDataObj = {
tags: []
}
if (!droppedItems) return;
// const form = e.currentTarget;
// const formData = new FormData(form);
// / Convert FormData to an object
// const data: { [key: string]: FormDataEntryValue } = {};
// formData.forEach((value, key) => {
// data[key] = value;
// });
let signInitialsCount = 0;
for (let i = 0; i < droppedItems.length; i++) {
if (droppedItems[i].type === "SIGNATURE" || droppedItems[i].type === "INITIALS") {
signInitialsCount += 1;
}
}
if (signInitialsCount !== Object.keys(signConfObj).length) {
handleToastError("Please ensure all signature and initials are signed")
return;
}
for (const tag of droppedItems) {
if ("required" in tag && tag.required && ((tag.type !== "SIGNATURE" && tag.type !== "INITIALS") && (tag.value === undefined || tag.value === null || tag.value === "" || !("value" in tag)))) {
handleToastError(`${tag.label} is required`)
return;
}
if (tag.type === "SIGNATURE" || tag.type === "INITIALS") {
const tagData: TagSubmitData = {
id: tag.id as string,
type: tag.type,
subType: saved.saveType as string,
signed: true,
value: ""
}
submitData.tags.push(tagData)
} else {
const tagData: TagSubmitData = {
id: tag.id as string,
type: tag.type,
// subType: tag.subType ? tag.subType : "",
signed: true,
value: tag.value ? tag.value as string : ""
}
submitData.tags.push(tagData);
}
}
// return;
dispatch(showLoader("Signing ..."));
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign?recipientId=${recipientId}`, submitData, { headers: { "ngrok-skip-browser-warning": "69420", } })
.then((res) => {
if (res.status === 200) {
setTransparentTags(true)
setTimeout(() => {
dispatch(hideLoader())
toast.dismiss();
toast.info("Signed Successfully!", {
position: "bottom-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// Completion Sign Screen Modal and download file
setShowCompletionScreen(true)
}, 800)
}
})
.catch((err) => {
}
)
};
const handleTagNavClick = (action: "N" | "P") => {
if (!tagJson || !tagJson.length) return
// If FocusElement is Null Setting up 0 th Tag
if (!focusElement) {
setFocusElement({ ...tagJson[0], focusElementIndx: 0 });
return;
}
// If FocusElement is present updating in Based on Next and Prev
const currFocusElemIndx = focusElement.focusElementIndx;
if (currFocusElemIndx === undefined) return;
const updIndx = (action === "N") ? (currFocusElemIndx + 1) : (currFocusElemIndx - 1)
if (updIndx >= tagJson.length || updIndx < 0 || !tagJson[updIndx].id) return
scrollToElement(tagJson[updIndx].id)
setFocusElement({ ...tagJson[updIndx], focusElementIndx: updIndx })
}
const scrollToElement = (elementId: string) => {
const element = document.getElementById(elementId);
if (element) {
element.scrollIntoView({
behavior: "smooth", // You can adjust this to "auto" if you don't want smooth scrolling
block: "center" // Align the element in the center of the viewport
});
}
}
// const handleSelectOptClick = (tagId: string | undefined, optionIndx: number) => {
// if (!tagId) return;
// const selectedTag = tagJson?.filter((val, indx) => val.id === tagId)
// if (selectedTag && selectedTag?.length && "options" in selectedTag[0] && selectedTag[0]["options"]) {
// selectedTag[0].label = selectedTag[0]["options"][optionIndx]
// setTagJson(tagJson)
// }
// }
const fetchSignatureData = async (signatureId: string) => {
try {
const response = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/signatures/${signatureId}?recipientId=${recipientId}`, {
headers: { "ngrok-skip-browser-warning": "69420" }
});
if (response.status === 200) {
return response.data;
}
} catch (error) {
}
return null;
}
// useEffect(() => {
// (async () => {
// if (droppedItems) {
// for (const tag of droppedItems) {
// if (tag.type === "SIGNATURE" && "value" in tag) {
// const signatureData = await fetchSignatureData(tag["value"]);
// }
// }
// }
// })()
// }, [droppedItems])
const handleTagUpdate = (updateIndx: number, updateValue: string) => {
if (droppedItems && droppedItems.length) {
const updatedTag = droppedItems.map((val, indx) => {
if (indx === updateIndx) {
return {
...val,
value: updateValue
}
}
return val;
})
setDroppedItems(updatedTag);
}
// setTagJson((prev) => {
// const newTagJson = [...prev];
// if (newTagJson[updateIndx]) {
// newTagJson[updateIndx].label = updateValue;
// }
// return newTagJson;
// });
}
const handlePrefillValues = () => {
const prefilledTags = ["NAME", "EMAIL", "TEXT", "COMPANY", "TEMPORAL"]
const updatedItems = droppedItems.map((item, index) => {
if (prefilledTags.includes(item.type)) {
let value = null;
let isUpdate = false
switch (item.type) {
case "NAME":
value = recipient?.firstName + " " + recipient?.lastName;
isUpdate = true;
break;
case "EMAIL":
value = recipient?.email;
isUpdate = true;
break;
case "TEXT":
value = item.value;
isUpdate = true;
break;
case "COMPANY":
value = item.value;
isUpdate = true;
break;
case "TEMPORAL":
if (item.currentTimeStamp) isUpdate = true;
if (isUpdate) {
value = dayjs(Date.now()).format(item.format);
}
break;
default:
break;
}
return { ...item, value }
} else {
return item
}
})
setDroppedItems(updatedItems);
}
useEffect(() => {
if (droppedItems.length > 0) {
handlePrefillValues();
}
}, [droppedItems.length])
// HANDLE DROP - START
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
event.preventDefault();
const data = window.dragElemData;
window.dragElemData = null;
if (!data) return;
const dragElemData = JSON.parse(data); // Get the dropped label
const field = { ...dragElemData }
if (!field.label) return;
// const field = fields.find((item) => item.label === dragElemData.label); // Find corresponding field
// if (!field) return; // If not found, exit
const dropZone = event.currentTarget.getBoundingClientRect();
const x = event.clientX - dropZone.left - dragElemData.offsetX;
const y = event.clientY - dropZone.top - dragElemData.offsetY;
const key = `${docId}_${pageIndex}`;
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
// let prepData = {
// properties: data.properties,
// icon: typeToIconMap[field.label.toUpperCase()] || <FaQuestionCircle />,
// type: dragElemData.label.toUpperCase(),
// subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '',
// label: dragElemData.label,
// dimensions: {
// left: x,
// top: y,
// width: dragElemData.width,
// height: dragElemData.height,
// },
// styles: dragElemData.label === "text" ? {
// "font": "Great Vibes",
// "italic": false,
// "bold": false,
// "underline": false,
// "color": "black",
// "textSize": 14.0
// } : null,
// documentId: docId,
// pageIndex: pageIndex,
// recipientId: dragElemData.recipientId,
// recipientIndx: dragElemData.recipientIndx,
// update: true,
// isDeleted: false
// }
let prepData = {
...data.properties,
icon: typeToIconMap[field.label.toUpperCase()] || <FaQuestionCircle />,
type: dragElemData.label.toUpperCase(),
label: dragElemData.label,
// subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '',
dimensions: {
left: x,
top: y,
width: dragElemData.width,
height: dragElemData.height,
},
styles: dragElemData.label === "text" ? {
"font": "Great Vibes",
"italic": false,
"bold": false,
"underline": false,
"color": "black",
"textSize": 14.0
} : null,
documentId: docId,
pageIndex: pageIndex,
recipientId: dragElemData.recipientId,
recipientIndx: dragElemData.recipientIndx,
required: (dragElemData.label !== TagsTypes.SIGNATURE && dragElemData.label !== TagsTypes.INITIALS) ? false : true,
update: true,
isDeleted: false
}
if (prepData.type === "NAME") {
prepData.firstName = true
prepData.lastName = true
}
if ("group" in dragElemData) {
prepData.group = dragElemData.group;
prepData.isGroupExist = false;
// if(uniqGroupIds.includes(dragElemData.group)) {
// prepData.isGroupExist = true;
// } else {
// prepData.isGroupExist = false;
// setUniqGroupIds([...uniqGroupIds, dragElemData.group]);
// }
}
if (prepData.type.toLowerCase() === "temporal") {
prepData.currentTimeStamp = true;
}
if (prepData.type.toLowerCase() === "number") {
prepData.decimalPlace = 0;
prepData.isCurrency = false;
}
// const dataToSet: DroppedItemsObj = {
// ...droppedItems,
// [key]: [...(droppedItems[key] || []),
// prepData],
// }
// const dataToSet: DroppedItemsObj = {
// ...droppedItems,
// [key]: [...(droppedItems[key] || []),
// {
// ...prepData,
// radioGroups: dragElemData.label === "radio" ?
// [
// {
// ...prepData,
// dimensions: {
// ...prepData.dimensions, top: 8, left: 10
// }
// },
// {
// ...prepData, dimensions: {
// ...prepData.dimensions,
// top: 8,
// left: 10
// }
// }
// ]
// :
// []
// }],
// }
// if (dragElemData.label === "radio") {
// setDroppedItems(dataToSet);
// } else {
// setDroppedItems(dataToSet);
// }
if (prepData.type.toLowerCase() === TagsTypes.SIGNATURE || prepData.type.toLowerCase() === TagsTypes.INITIALS) {
const currDocTags = droppedItems.filter((val) => val.pageIndex === prepData.pageIndex)
setSignConfObj({
...signConfObj,
[currDocTags.length]: {
accepted: true,
index: currDocTags.length
}
})
}
setDroppedItems((prev) => [...prev, prepData]);
// setDroppedItemsCounter((prev) => prev + 1);
// setTimeout(() => {
// const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
// handleTagClick(key, tagIndex)
// }, 0)
};
// HANDLE DROP - END
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Allows the drop event to work
};
// HANDLE TAG MOUSE DOWN - START
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
setSelectedTag({ key, index })
event.preventDefault();
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
let targetElement = event?.currentTarget;
if (!targetContainer || !targetElement) return;
const containerRect = targetContainer.getBoundingClientRect();
const elementRect = targetElement.getBoundingClientRect();
// Calculate the initial offset (where the mouse was clicked inside the element)
const offsetX = event.clientX - elementRect.left;
const offsetY = event.clientY - elementRect.top;
const conScrollWidth = targetContainer?.scrollWidth;
const conScrollHeight = targetContainer?.scrollHeight;
let isGroupAttr = event.currentTarget.hasAttribute("group");
let groupId = null;
if (isGroupAttr) {
groupId = event.currentTarget.getAttribute("group");
}
let groupElements: NodeListOf<HTMLElement> | null = null
if (isGroupAttr && groupId) {
groupElements = document.querySelectorAll(`div[group="${groupId}"]`);
}
const handleMouseMove = (event: MouseEvent) => {
// Get new X and Y positions relative to the container
let left = event.clientX - containerRect.left - offsetX;
let top = event.clientY - containerRect.top - offsetY;
// **Restrict movement within container bounds**
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
// Apply the movement to the element
setDroppedItems(
droppedItems.map((item, i) =>
i === index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item
)
);
};
const handleMouseUp = () => {
document.removeEventListener("pointermove", handleMouseMove);
document.removeEventListener("pointerup", handleMouseUp);
};
// Add event listeners
document.addEventListener("pointermove", handleMouseMove);
document.addEventListener("pointerup", handleMouseUp);
};
// HANDLE TAG MOUSE DOWN - END
// Updated handleRemoveTag function with your existing API logic and group handling
const handleRemoveTag = (key: string, targetIndex: number) => {
const tag: DroppedItem = droppedItems[targetIndex];
const recipientId = tag.recipientId;
(async () => {
if ("id" in tag) {
try {
const response = await api.delete(`/api/sign/tags?recipientId=${recipientId}`, { data: [{ tagId: tag.id }] })
if (response.status !== 200) throw "Tag Deletion error"
} catch (err) {
return;
}
}
setSelectedTag(null);
// Check if this item has a group for special group handling
if ('group' in tag && tag.group) {
const groupId = tag.group;
// Find all items in the same group
const groupItems = droppedItems[key]
.map((item, idx) => ({ item, index: idx }))
.filter(({ item }) => 'group' in item && item.group === groupId && !item.isDeleted);
// Sort group items by their top position to identify the parent (top-most)
groupItems.sort((a, b) => a.item.dimensions.top - b.item.dimensions.top);
// Check if the item being removed is the parent (top-most item)
const isParentItem = groupItems.length > 0 && groupItems[0].index === targetIndex;
if (isParentItem && groupItems.length > 1) {
// If removing the parent and there are child items, mark all items in the group as deleted
// Get indices of all group items to mark as deleted
const indicesToDelete = groupItems.map(({ index }) => index);
setDroppedItems(prevState => ({
...prevState,
[key]: prevState[key].map((item, index) =>
indicesToDelete.includes(index) ? { ...item, isDeleted: true } : item
)
}));
// Remove group border elements after state update
// setTimeout(() => {
// removeGroupBorders(groupId);
// }, 100);
} else {
// If removing a child item or the only item in group, just mark this item as deleted
setDroppedItems(prevState => ({
...prevState,
[key]: prevState[key].map((item, index) =>
index === targetIndex ? { ...item, isDeleted: true } : item
)
}));
// Check if this was the last item in the group after state update
// setTimeout(() => {
// const remainingGroupItems = droppedItems[key].filter(item =>
// 'group' in item && item.group === groupId && !item.isDeleted
// );
// if (remainingGroupItems.length === 0) {
// // Remove group borders if no items left
// removeGroupBorders(groupId);
// } else {
// // Update group borders for remaining items
// handleIGroupBorderPosition(groupId);
// }
// }, 100);
}
} else {
// Handle non-group item removal (your existing logic)
setDroppedItems(prevState => prevState.filter((item, idx) => {
return idx !== targetIndex;
}));
}
})()
};
// const [showTerms, setShowTerms] = useState<boolean>(true);
return (
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>
<>
{/* <TermsModal showTerms={showTerms} setShowTerms={setShowTerms} recipientId={recipientId || ""} /> */}
{showCompletionScreen && <SignCompletionScreen />}
<div className="bg-slate-100"
// onSubmit={(e: React.FormEvent<HTMLFormElement>) => handleFormSubmit(e)}
>
{/* Nav Two - Options - START */}
<div className='flex justify-between items-center px-4 py-1 bg-white'>
{/* Left Side */}
<div className='flex justify-start items-center gap-1'>
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide font-semibold text-[1rem] text-[var(--primary)]'>Sign Made Easy</p>
</div>
{/* FIT - START */}
<div
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>Fit</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute bg-white rounded-sm z-50'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
onClick={() => setSelFitIndx(indx)}
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
<div
className='flex items-center justify-between w-full'>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
</div>
</div>)
})
}
</div>}
</div>
{/* FIT - END */}
</div>
<p className="text-xs text-slate-600">Hi! {recipient && recipient.firstName} {recipient && recipient.lastName}</p>
{/* Right Side - STARTS */}
<div className='flex justify-center items-center'>
{/* Documnt Nav */}
<>
{selectedDocuments !== null &&
<div tabIndex={1}
onBlur={() => setNav2DocToggle(false)}>
<div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>{selectedDocuments && selectedDocuments.length} Documents</p>
<MdKeyboardArrowDown className={`${nav2DocToggle ? 'rotate-180' : ''} transition-all`} />
</div>
<div
className={`absolute max-w-max shadow rounded-md mt-1 px-2 py-1 z-20 bg-white ${nav2DocToggle ? '' : 'hidden'}`}>
{selectedDocuments.length && selectedDocuments.map((document, indx) => {
return (<div key={indx} className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-default'>
<div className='flex items-center gap-1'>
{/* <p className='w-3 h-3 rounded-sm' style={{ background: recipient.color }}></p> */}
<IoIosDocument className='text-slate-600 text-lg' />
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[15ch]'>{document.name}</p>
</div>
</div>)
})}
</div>
</div>
}
</>
{/* Send */}
{(!transparentTags && !hideTags) ? <button
onClick={handleFormSubmit}
// onClick={() => handleFormSubmitBtnClick()}
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)] outline-none'>
<p
id="sign-client-side-btn"
className='tracking-wide text-white'>
Sign
</p>
<FaSignature className='text-white text-md' />
</button>
:
<p className="text-xs">Signed Successfully</p>
}
</div>
{/* Right Side - ENDS */}
</div>
{/* Nav Two - Options - End */}
{/* Document Container Start */}
<div className="grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-48px)] overflow-hidden">
{/* Left Side pages start */}
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
<div
id="tags-list-client-side"
className={`col-span-2 h-full p-4 overflow-y-auto ${provideTags ? "bg-white" : "order-last"}`}>
{recipientId && provideTags && <FieldList color={"#8f87f1"} recipientId={Number(recipientId)} recipientIndx={0} acntId={"1"} />}
</div>
{/* Pages - END */}
{/* Main container */}
<div
id="doc-con"
className="zoom-con relative col-span-8 px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth"
>
{
selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => {
return (
<div key={doc.documentId} className='flex flex-col'>
<div className='py-2 text-sm'>
<p className='text-center'>{doc.name}</p>
</div>
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'>
{
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
const key = `${doc.documentId}_${page.index}`;
// const tagJsonData = [...droppedItems];
// debugger
const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index)
return (
<div
key={indx}
id={doc.documentId + indx}
onDrop={(e) => handleDrop(e, doc.documentId, doc.documentId, page.index)}
onDragOver={(e) => handleDragOver(e)}
className='drop-page-in shadow-md relative'
>
{!hideTags && <>
{currDocTags.map((item, index) => {
if (item.pageIndex !== page.index || item.documentId != doc.documentId) return <></>;
return (
<div
tabIndex={1}
onFocus={() => {
setSelectedTag({ key, index })
setFocusElement({ ...item, focusElementIndx: index })
}
}
key={index}
// onPointerDown={(e) => handleTagMouseDown(e, key, index)}
className="absolute rounded dropped-item text-sm"
// onClick={() => handleTagClick(key, index)}
style={{
left: `${item.dimensions.left}px`,
top: `${item.dimensions.top}px`,
width: `${item.dimensions.width}px`,
height: `${item.dimensions.height}px`,
border: `${((!hideTags && focusElement) && !transparentTags && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) ? '2px dashed #1c398e' : 'none'}`,
// display: `${hideTags ? 'none' : 'block'}`
}}
>
{provideTags && !transparentTags && !hideTags && <div
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
// className="absolute top-[-15px] left-[-15px] w-4 h-4 bg-blue-600 rounded-tr cursor-move z-30"
title="Drag"
>
<RiDragMoveFill
className="absolute top-[-15px] left-[-15px] w-4 h-4 bg-slate-100 text-[var(--primary)] cursor-move z-30"
/>
</div>}
<>
{selectedTag?.key && selectedTag.key === key && selectedTag.index === index &&
<div className='absolute flex justify-between items-center w-full max-w-max bg-white top-[-22px] shadow z-20 text-sm rounded-t-sm' >
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
<p className='w-2.5 h-2.5 rounded'
style={{ "background": "#2d2d2d" }}
>
</p>
{/* <p
className=''>{IconComponent}
</p> */}
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
<IoMdClose onClick={(e) => {
e.stopPropagation();
handleRemoveTag(key, index)
}} className='cursor-pointer' />
</div>
</div>}
</>
{/* TEXT FIELD - START */}
{
item.type === "TEXT" &&
<div
id={item.id}
tabIndex={1}
className={`max-h-max text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
// style={{ height: item.dimensions.height + "px" }}
>
{/* <input
name={item.id}
className="w-full px-2 outline-none"
>
</input> */}
{
!item.multiLine && !item.maskType && <p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onBlur={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
>{item.value ?? ""}</p>
}
{
item.multiLine && !item.maskType && <p
suppressContentEditableWarning
onBlur={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
contentEditable={true}
className="w-full outline-none max-h-max"
>{item.value ?? ""}</p>
}
{
"maskType" in item && "maskCharacter" in item && item.maskType && <p
// onBlur={(e) => {
// const val = e.currentTarget.textContent;
// handleTagUpdate(index, val as string);
// }}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
// onFocus={() => {
// }}
onBlur={(e) => {
let val = e.target.textContent;
handleTagUpdate(index, val as string);
if (typeof item.maskType === "string" && typeof val === "string" && item.maskCharacter) {
val = val.replace(/ /g, "");
e.target.textContent = maskUtils(item.maskType, val, item.maskCharacter)
}
}}
contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
></p>
}
</div>
}
{/* TEXT FIELD - END */}
{/* EMAIL FIELD - START */}
{
item.type === "EMAIL" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onInput={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const val = e.currentTarget.textContent?.trim() || "";
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (val && !emailRegex.test(val)) {
alert("Please enter a valid email address");
handleTagUpdate(index, "" as string)
e.currentTarget.textContent = "";
e.currentTarget.focus(); // Optional: return focus if invalid
}
e.preventDefault()
};
}}
onBlur={(e) => {
const val = e.currentTarget.textContent?.trim() || "";
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (val && !emailRegex.test(val)) {
alert("Please enter a valid email address");
handleTagUpdate(index, "" as string)
e.currentTarget.textContent = "";
e.currentTarget.focus(); // Optional: return focus if invalid
}
}}
// contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
>
{item.value ?? ""}
</p>
}
</div>
}
{/* EMAIL FIELD - END */}
{/* NAME FIELD - START */}
{
item.type === "NAME" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onInput={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
};
}}
onBlur={(e) => {
}}
// contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
>
{item.value ?? ""}
</p>
}
</div>
}
{/* NAME FIELD - END */}
{/* NUMBER FIELD - START */}
{
item.type === "NUMBER" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<input
onInput={(e) => {
const val = e.currentTarget.value;
handleTagUpdate(index, val as string);
}}
type="number"
className="outline-none h-full max-w-max"
/>
}
</div>
}
{/* NUMBER FIELD - END */}
{/* COMPANY FIELD - START */}
{
item.type === "COMPANY" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onInput={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
};
}}
onBlur={(e) => {
}}
contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
></p>
}
</div>
}
{/* COMPANY FIELD - END */}
{/* CHECKBOX FIELD - START */}
{
item.type === "CHECKBOX" &&
<div
id={item.id}
className={`h-full w-full rounded flex justify-center items-center ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
<input
name={item.id}
// style={{ height: item.dimensions.height + "px" }}
className="w-full px-2 outline-none"
type="checkbox" />
</div>
}
{/* CHECKBOX FIELD - END */}
{/* SIGNATURE FIELD - START */}
{
item.type === "SIGNATURE" &&
<div
tabIndex={1}
id={item.id}
onClick={() => handleSignClick(index, item.id)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none bg-transparent" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{/* <input type="checkbox" className="" /> */}
{
(!transparentTags && !hideTags && !(saved.save && (index in signConfObj && signConfObj[index].accepted))) ?
<>
<FaSignature className="text-lg text-slate-600" />
</>
:
<img
style={{
width: `${item.dimensions.width}px`,
height: `${item.dimensions.height}px`,
}}
// src={saved.signature as string} />
src={`data:image/png;base64,${saved.signature}`}
/>
}
</div>
}
{/* SIGNATURE FIELD - END */}
{/* INITIALS FIELD - START */}
{
item.type === "INITIALS" &&
<div
tabIndex={1}
id={item.id}
onClick={() => handleSignClick(index, item.id)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{((!transparentTags && !hideTags) && !(saved.save && (index in signConfObj) && signConfObj[index].accepted)) ?
<TbSignature className="text-lg text-slate-600" />
:
<img
style={{
width: `${item.dimensions.width}px`,
height: `${item.dimensions.height}px`,
}}
className="w-[100%] h-full"
// src={saved.initial as string}
src={`data:image/png;base64,${saved.initial}`}
/>
}
</div>
}
{/* INITIALS FIELD - END */}
{/* STAMP FIELD - START */}
{
item.type === "STAMP" &&
<div
tabIndex={1}
id={item.id}
onClick={() => handleStampClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{!transparentTags && !hideTags ?
<FaStamp className="text-lg text-slate-600" />
:
<img className="w-[100%] h-full" src={saved.initial as string} />
}
</div>
}
{/* STAMP FIELD - END */}
{/* IMAGE FIELD - START */}
{
item.type === "FILE" &&
<div
tabIndex={1}
id={item.id}
// onClick={() => handleSignClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{!transparentTags && !hideTags ?
<FaFileAlt className="text-lg text-slate-600" />
:
<img className="w-[100%] h-full" src={saved.initial as string} />
}
</div>
}
{/* IMAGE FIELD - END */}
{/* SELECT FIELD - START */}
{
item.type === "SELECT" &&
<div
id={item.id}
tabIndex={1}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100"}`}
>
<select
name={item.id}
className="outline-none"
onInput={(e) => {
const val = e.currentTarget.value;
handleTagUpdate(index, val as string);
}}
>
<option value="">Select</option>
{item.options && item.options.map((obj, indx) => {
return (
<option
key={indx}
value={obj.value}
>
{obj.label}
</option>
)
})
}
</select>
</div>
}
{/* SELECT FIELD - END */}
{/* TEMPORAL FIELD - START */}
{
item.type === "TEMPORAL" &&
<div
id={item.id}
tabIndex={1}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100"}`}
>
<p
style={{ opacity: `${item.id === isDateOpen ? "0" : "100"}` }}
className="w-full h-full rounded pl-1 pt-1 whitespace-nowrap">
{(item.id && item.id in dateValues) ? dateValues[item.id] : item.value ? item.value : item.dateFormat?.toLowerCase()}
</p>
<input
name={item.id}
onFocus={(e) => {
e.target.showPicker?.()
if (item.id) setIsDateOpen(item.id)
}}
onBlur={(e) => {
let val = e.target.value;
// Adding dummy time in case of time as it giving Invalid date without that
if (item.dateFormat?.toLowerCase() === "time") val = "09/02/2025 " + val
if (!item.id) return;
setIsDateOpen(null);
setDateValues({
...dateValues,
[item.id as string]: dayjs(val).format(item.format)
})
}}
onInput={(e) => {
const val = e.currentTarget.value;
handleTagUpdate(index, val as string);
}}
style={{ opacity: `${item.id === isDateOpen ? "100" : "0"}` }}
className="w-full px-2 outline-none absolute"
onClick={() => {
}}
type={`${item.dateFormat?.toLowerCase() === "date" ? "date" : (item.dateFormat?.toLowerCase() === "time" ? "time" : "datetime-local")}`}
/>
</div>
}
{/* TEMPORAL FIELD - END */}
{/* Attachment Field - START */}
{
item.type.toLowerCase() === TagsTypes.ATTACHMENT &&
<div
tabIndex={1}
id={item.id}
// onClick={() => handleStampClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{!transparentTags && !hideTags ?
(
<p className="text-lg text-slate-600">
{typeToIconMap.ATTACHMENT}
</p>
)
:
(
<p className="text-lg text-slate-600">
{typeToIconMap.ATTACHMENT}
</p>
)
}
</div>
}
{/* Attachment Field - END */}
{/* Focus Show Element */}
{
(!transparentTags && !hideTags) && (focusElement && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) &&
<>
{/* <div
className="absolute left-0 z-50 bg-blue-900 text-xs text-slate-100 px-2 py-[2px] rounded-sm whitespace-nowrap"
style={{ top: -23 + "px" }}
>
<p>{focusElement.label}</p>
</div> */}
<div
className="absolute left-[-10px] z-50 flex max-w-max"
style={{ left: -(item.dimensions.left + 20) + "px", top: ((item.dimensions.height / 2) - 18) + "px" }}
>
{/* <FaArrowRight className="text-[var(--primary)] text-xl" />
*/}
<BsFileArrowDownFill className="text-blue-900 text-4xl rotate-270" />
</div>
</>
}
</div>
)
})
}
</>}
<img
style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
draggable="false"
width={page.width}
height={page.height}
src={page.src}
/>
{/* 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>
{/* Side details of documents Pages and document Name - END */}
</div>
)
})
}
</div>
</div>
)
})
}
{/* tag pagination Start */}
{/* {!transparentTags && !hideTags && <div className="sticky rounded-md bottom-2 left-[50%] translate-[-50%] max-w-max flex bg-slate-900 text-white text-sm opacity-70">
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-l-md transition-all duration-150 "
onClick={() => handleTagNavClick("P")}
>Prev</div>
<div className="px-3 py-1 border-l-1 border-r-1">{focusElement ? focusElement.label : "NotSelected"}</div>
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-r-md transition-all duration-150 "
onClick={() => handleTagNavClick("N")}
>Next</div>
</div>} */}
</div>
<div className={`col-span-2 ${provideTags ? "" : "order-first"} overflow-y-auto`}>
{selectedDocuments !== null && <DocumentPages
selectedDocuments={selectedDocuments}
setDropDownTogglePage={setDropDownTogglePage}
dropDownTogglePage={dropDownTogglePage} />}
{/* {provideTags && selectedTag && selectedTag !== null && "key" in selectedTag && "index" in selectedTag &&
<RightSideBar
droppedItems={droppedItems}
selectedTag={droppedItems[selectedTag.index]}
setDroppedItems={setDroppedItems}
selTagKey={selectedTag.key}
selTagIndx={selectedTag.index}
signClient={true}
/>} */}
</div>
{/* Right Side - START */}
{/* <div className="col-span-2 bg-slate-100 h-full p-4 overflow-y-auto">
{
focusElement && <>
</>
}
</div> */}
{/* Right Side - END */}
</div>
{/* Document Container End */}
</div>
</>
<div style={{ display: `${openSign ? 'block' : 'none'}` }}>
{recipient &&
<SignCon
recipient={recipient}
recipientId={recipientId as string}
setOpenSign={setOpenSign} saved={saved}
setSaved={setSaved}
signTagId={signTagId} />}
</div>
<div style={{ display: `${stamp.open ? 'block' : 'none'}` }}>
<StampUpload stamp={stamp.blob} setStamp={setStamp} />
</div>
{/* <SignatureCon recipientId={recipientId as string} openSign={openSign} setOpenSign={setOpenSign} savedSignature={savedSignature} setSavedSignature={setSavedSignature} />
<InitialCon openInitial={openInitial} setOpenInitial={setOpenInitial} savedInitial={savedInitial} setSavedInitial={setSavedInitial} /> */}
{globalLoader.isLoading && <Loader />}
</div >
)
}
export default SignDocument