1733 lines
101 KiB
TypeScript
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
|