Compare commits

...

10 Commits

49 changed files with 3210 additions and 1106 deletions

2
.env
View File

@ -6,5 +6,7 @@
# VITE_BASE_URI="https://smashing-albacore-engaging.ngrok-free.app"
# VITE_BASE_URI="https://basically-polished-yeti.ngrok-free.app"
# VITE_BASE_URI="https://sacha-countryfied-evolutionistically.ngrok-free.dev"
# VITE_BASE_URI="https://web.signmadeasy.app"
# VITE_SSE_URI="https://web.signmadeasy.app"
VITE_BASE_URI="https://web.signmadeasy.app"
VITE_SSE_URI="https://web.signmadeasy.app"

View File

@ -9,7 +9,7 @@
"build:skip-check": "tsc --noEmit && vite build",
"lint": "eslint .",
"preview": "vite preview",
"deploy": "npm run build:skip-check && scp -i private\\SME-Web.pem -r dist root@52.10.89.255:/var/www/sme"
"deploy": "npm run build:skip-check && pscp -i private\\SME-Web.ppk -r dist root@100.22.185.166:/var/www/sme"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",

View File

@ -114,7 +114,7 @@ const App = () => {
<Route path="/view" element={<DocView />} />
<Route path="/session_expired" element={<SessionExpired />} />
<Route path="/transaction-complete" element={<CompletionScreen />} />
<Route path="/transaction-complete/:recipientId/" element={<CompletionScreen />} />
<Route path="/transaction-declined" element={<CompletionScreen hit="Action Canceled! " title="Maybe Next Time!" message={" Your request has been declined successfully. You can revisit anytime if you change your mind."} textColorSuccess={false} />} />
<Route path="/transaction-reassigned" element={<CompletionScreen hit="Reasssigned Successfully" title=" The transaction has been reassigned!" message={"The transaction has been reassigned to another user"} textColorSuccess={false} />} />

View File

@ -1,6 +1,8 @@
import React, { useEffect } from "react";
import { RiFileList3Line } from "react-icons/ri";
import confetti from 'canvas-confetti'
import handleDownloadDocument from "../../helper/handleDownloadDocumentRecSide";
import { useParams } from "react-router-dom";
interface CompletionScreenProps {
hit?: string;
@ -15,7 +17,7 @@ const CompletionScreen: React.FC<CompletionScreenProps> = ({
message = "Your document is now complete and securely stored. We’ve emailed you a copy for your reference.",
textColorSuccess = true
}) => {
const { recipientId } = useParams();
useEffect(() => {
@ -60,10 +62,11 @@ const CompletionScreen: React.FC<CompletionScreenProps> = ({
{/* Header */}
<header className="px-4 py-3 flex justify-center sm:justify-start items-center bg-white shadow-md z-10">
<div className="flex items-center gap-2">
<RiFileList3Line className="text-[var(--mainPrimaryBtn)] text-2xl" />
{/* <RiFileList3Line className="text-[var(--mainPrimaryBtn)] text-2xl" />
<span className="text-md font-semibold text-[var(--mainPrimaryBtn)]">
Sign made easy
</span>
</span> */}
<img class="py-2 w-[140px]" alt="" src="/sme-colored.png" />
</div>
</header>
@ -76,6 +79,13 @@ const CompletionScreen: React.FC<CompletionScreenProps> = ({
{message}
</p>
<p
onClick={() => { handleDownloadDocument(recipientId || "") }}
className="text-white bg-[var(--mainPrimaryBtn)] px-3 py-2 rounded-sm mt-4 cursor-pointer">
Download Signed Documents
</p>
{/* <button className="flex items-center gap-2 mt-6 bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 transition">
<FiDownload />
Download
@ -84,11 +94,10 @@ const CompletionScreen: React.FC<CompletionScreenProps> = ({
{/* <button className="mt-6 flex items-center gap-2 bg-orange-500 text-white px-6 py-2 rounded-md hover:bg-orange-600 transition">
<FiZap /> Login
</button> */}
<p className="relative z-50 font-semibold mt-4 2xl:mt-6 text-md 2xl:text-xl">
Access your documents anytime — just <a href="/login" className="text-white bg-[var(--mainPrimaryBtn)] px-3 py-1 rounded-sm">Login</a>
<p className="relative z-50 font-semibold mt-2 2xl:mt-4 text-md 2xl:text-xl">
Access your documents anytime — just <a href="/login" className="text-[var(--mainPrimaryBtn)] py-1 border-b-1 border-dashed">Login</a>
</p>
{/* Illustration placeholder */}
<div className="mt-6 2xl:mt-10">
{textColorSuccess ? <img
@ -104,7 +113,8 @@ const CompletionScreen: React.FC<CompletionScreenProps> = ({
</div>
{/* Apps Section */}
<section className="relative z-10 top-6 2xl:top-10 w-full">
{/* <section className="relative z-10 top-6 2xl:top-10 w-full"> */}
<section className="relative z-10 w-full mt-6 2xl:mt-10 pb-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4 2xl:text-2xl">
Boost Efficiency with Our Apps
</h2>
@ -119,7 +129,9 @@ const CompletionScreen: React.FC<CompletionScreenProps> = ({
<div className="w-full">
<div className="w-full flex flex-col justify-start items-start sm:flex-row sm:justify-between sm:items-center">
<img src="/forms-made-easy.png" className="w-[40%] 2xl:w-[60%]" alt="Docs Made Easy" />
<button className="text-xs sm:text-sm md:text-md sm:mt-0 bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 transition">
<button
onClick={() => window.open("https://formsmadeasy.com/", "_blank")}
className="cursor-pointer text-xs sm:text-sm md:text-md sm:mt-0 bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 transition">
Try Now
</button>
</div>
@ -137,7 +149,9 @@ const CompletionScreen: React.FC<CompletionScreenProps> = ({
<div className="w-full">
<div className="w-full flex flex-col justify-start items-start sm:flex-row sm:justify-between sm:items-center">
<img src="/docs-made-easy.png" className="w-[40%] 2xl:w-[60%]" alt="Docs Made Easy" />
<button className="text-xs sm:text-sm md:text-md sm:mt-0 bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 transition">
<button
onClick={() => window.open("https://docsmadeasy.com/", "_blank")}
className="cursor-pointer text-xs sm:text-sm md:text-md sm:mt-0 bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 transition">
Try Now
</button>
</div>

View File

@ -1,103 +1,150 @@
import React, { useState } from 'react'
import { MdKeyboardArrowDown, MdOutlineNavigateNext } from 'react-icons/md';
import React, { useEffect, useState } from 'react'
import { MdKeyboardArrowUp, MdMoreVert } from 'react-icons/md';
import { DocumentType } from './SignDocument';
import { GrFormPrevious } from 'react-icons/gr';
import { DroppedItem } from '../../utlity/types';
import { PiTagSimpleFill } from 'react-icons/pi';
interface DocumentPagesInterface {
selectedDocuments: DocumentType[] | null;
setDropDownTogglePage: React.Dispatch<React.SetStateAction<string>>;
dropDownTogglePage: string;
droppedItems: Record<string, DroppedItem[]> | null;
}
const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownTogglePage }: DocumentPagesInterface) => {
const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownTogglePage, droppedItems }: DocumentPagesInterface) => {
const [activePage, setActivePage] = useState<string | null>(
selectedDocuments?.[0] ? `${selectedDocuments[0].documentId}_0` : null
)
const [documentPagesTagsMapping, setDocumentPagesTagsMapping] = useState<Record<string, Record<number, [number, string]>>>({})
useEffect(() => {
const documentId = selectedDocuments?.[0] ? `${selectedDocuments[0].documentId}_0` : null
if (documentId === null) return;
const key = 'pageDropDown' + 0;
setDropDownTogglePage(dropDownTogglePage === key ? '' : key);
setActivePage(`${documentId}_0`);
window.location.hash = documentId + "0";
}, [selectedDocuments])
const toggleDoc = (indx: number, documentId: string) => {
const key = 'pageDropDown' + indx;
setDropDownTogglePage(dropDownTogglePage === key ? '' : key);
setActivePage(`${documentId}_0`);
window.location.hash = documentId + "0";
}
const goToPage = (documentId: string, pageIndex: number) => {
const key = `${documentId}_${pageIndex}`;
setActivePage(key);
window.location.hash = documentId + "" + pageIndex;
}
useEffect(() => {
setTimeout(() => {
let documentPagesTagsMappingLocal: Record<string, Record<number, [number, string]>> = {}
droppedItems && Object.keys(droppedItems).length > 0 && droppedItems.forEach(item => {
const documentId = item.documentId;
const pageIndex = item.pageIndex;
if (!documentPagesTagsMappingLocal[documentId]) {
documentPagesTagsMappingLocal[documentId] = {};
}
documentPagesTagsMappingLocal[documentId][pageIndex] = documentPagesTagsMappingLocal[documentId][pageIndex] ? [documentPagesTagsMappingLocal[documentId][pageIndex][0] + 1, "#005AB5"] : [1, "#005AB5"]
})
setDocumentPagesTagsMapping(documentPagesTagsMappingLocal)
}, 300)
}, [droppedItems])
const [currentPage, setCurrentPage] = useState(0);
return (
<div className="flex flex-col gap-2 pb-[6rem]">
{selectedDocuments && selectedDocuments.length && selectedDocuments.map((document, indx) => {
<div className="flex flex-col w-full px-2 gap-2 pb-[6rem] bg-slate-100">
{selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((document, indx) => {
const isOpen = dropDownTogglePage === 'pageDropDown' + indx;
return (
<div
key={indx}
<div key={indx} className="border-b border-slate-200">
// tabIndex={1}
// onBlur={() => setDropDownTogglePage('')}
>
{/* Document Header */}
<div
onClick={() => {
setDropDownTogglePage(`${(dropDownTogglePage == 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`);
setCurrentPage(0);
window.location.hash = document.documentId + "" + 0;
}
}
className={`p-2 ${(dropDownTogglePage == 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
{
document.pages != undefined && <img
key={indx}
className='rounded-sm shadow'
src={document.pages[0].src}
alt="" />
}
<div
className={`flex items-center justify-between w-full text-xs rounded-sm cursor-pointer ${(dropDownTogglePage == 'pageDropDown' + indx) ? "text-slate-100" : "text-slate-600"}`}>
<p className={`whitespace-nowrap overflow-hidden text-ellipsis max-w-[18ch]`}>{document.name}</p>
<MdKeyboardArrowDown className={`${(dropDownTogglePage == 'pageDropDown' + indx) ? 'rotate-180' : ''}`} />
onClick={() => toggleDoc(indx, document.documentId)}
className="flex items-start justify-between px-3 py-2.5 cursor-pointer hover:bg-slate-50 transition-colors"
>
<div className="flex-1 min-w-0">
<p className="text-[13px] font-medium text-slate-800 leading-tight truncate max-w-[16ch]">
{document.name}
</p>
<p className="text-[11px] text-slate-400 mt-0.5">
{document.pages?.length} {document.pages?.length === 1 ? 'page' : 'pages'}
</p>
</div>
<MdKeyboardArrowUp
size={20}
className={`text-slate-400 mt-0.5 flex-shrink-0 transition-transform duration-200 ${isOpen ? '' : 'rotate-180'}`}
/>
</div>
{/* Page Thumbnails - DocuSign style */}
{isOpen && (
<div className="flex flex-col px-2">
{document.pages?.map((page, pageIndex) => {
const key = `${document.documentId}_${pageIndex}`;
const isActive = activePage === key;
{
dropDownTogglePage == 'pageDropDown' + indx &&
<div className="my-1 flex justify-center items-center gap-2 flex-wrap">
{/* Prev Button */}
<a
href={"#" + document.documentId + (currentPage - 1)}
className={`cursor-pointer px-2 py-1 shadow rounded-sm text-sm bg-slate-200 hover:border-[var(--primary)] transition-all duration-300 ${currentPage == 0 ? "pointer-events-none opacity-50" : ""
}`}
onClick={(e) => {
if (currentPage == 0) e.preventDefault();
else setCurrentPage(currentPage - 1);
}}
>
<GrFormPrevious />
</a>
return (
<div
key={pageIndex}
onClick={() => goToPage(document.documentId, pageIndex)}
className="flex flex-col items-center py-2 px-1 rounded-md cursor-pointer hover:bg-slate-50 transition-colors"
>
{/* Big Thumbnail */}
<div className={`relative sm:p-2 lg:p-4 bg-slate-100 rounded border flex items-center justify-center overflow-hidden
${isActive ? 'border-blue-500 border-2' : 'border-slate-200'}`}
>
<img
src={page.src}
alt={`Page ${pageIndex + 1}`}
className="w-[140px] h-[180px] h-full object-cover"
draggable="false"
/>
{/* Page Selector */}
<select
className="w-[58px] py-1 text-xs text-slate-400 outline-none"
value={currentPage}
onChange={(e) => {
const newPage = Number(e.target.value);
setCurrentPage(newPage);
window.location.hash = document.documentId + "" + newPage;
}}
>
{document.pages != undefined && document.pages.map((_, pageIndex) => (
<option key={pageIndex} value={pageIndex}>
{pageIndex + 1}
</option>
))}
</select>
{/* Page number + dots */}
<div className="absolute bottom-0 bg-slate-800 flex items-center justify-between w-full pl-4 pr-2 py-3 bg-white">
<span className="text-[13px] text-slate-500">{pageIndex + 1}</span>
{
// <>
// <button
// onClick={(e) => { e.stopPropagation(); /* open context menu */ }}
// className="text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded p-0.5"
// >
// <MdMoreVert size={18} />
// </button>
// </>
}
</div>
<span className="text-xs text-slate-600">
/ {document.pages?.length}
</span>
{documentPagesTagsMapping[document.documentId]?.[pageIndex] && (
<div className='absolute top-4 right-2 rotate-180'>
{/* <span className='absolute text-white text-[10px] font-medium text-slate-500 top-[50%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 pointer-events-none select-none rotate-180'>
{documentPagesTagsMapping[document.documentId][pageIndex][0]}
</span> */}
<PiTagSimpleFill style={{ fontSize: '1.8rem', color: documentPagesTagsMapping[document.documentId][pageIndex][1] }} />
</div>
)}
{/* Next Button */}
<a
href={"#" + document.documentId + (currentPage + 1)}
className={`cursor-pointer px-2 py-1 shadow rounded-sm text-sm bg-slate-200 hover:border-[var(--primary)] transition-all duration-300 ${currentPage == document.pages?.length - 1 ? "pointer-events-none opacity-50" : ""
}`}
onClick={(e) => {
if (currentPage == document.pages?.length - 1) e.preventDefault();
else setCurrentPage(currentPage + 1);
}}
>
<MdOutlineNavigateNext />
</a>
</div>
</div>
)
})}
</div>
}
)}
</div>
)

View File

@ -3,6 +3,8 @@ import { FaSignature, FaDownload, FaUserEdit, FaTimesCircle, FaAngleLeft, FaAngl
import { BsThreeDotsVertical } from "react-icons/bs";
import ClientDownloadDocument from "./ClientDownloadDocument";
import { AiOutlineAudit } from "react-icons/ai";
import { useParams } from "react-router-dom";
import handleDownloadDocument from "../../helper/handleDownloadDocumentRecSide";
interface Props {
transparentTags?: boolean;
@ -30,6 +32,8 @@ export default function SignButton({
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const { recipientId } = useParams();
const [showMenuDownload, setShowMenuDownload] = useState<{ open: boolean, transactionId: string | null }>({
open: false,
transactionId: null
@ -91,6 +95,18 @@ export default function SignButton({
<span>Download Document</span>
</button>
</li> */}
<li>
<button
onClick={() => {
handleDownloadDocument(recipientId || "")
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<AiOutlineAudit />
<span>Download Documents</span>
</button>
</li>
<li>
<button
onClick={() => {

View File

@ -23,7 +23,7 @@ import DocumentPages from "./DocumentPages";
import FieldList from "../process/processStepTwo/FieldList";
import typeToIconMap from "../../helper/iconMap";
import RightSideBar from "../process/processStepTwo/rightSideBar/RightSideBar";
import TagProperties from "../process/processStepTwo/tagProperties/TagProperties";
import TagsCon from "./TagsCon";
import ConsentScreen from "./ConsentScreen";
import SignButton from "./SignButton";
@ -142,6 +142,22 @@ const fitObj = [
label: "200%",
value: "200"
},
{
label: "250%",
value: "250"
},
{
label: "300%",
value: "300"
},
{
label: "350%",
value: "350"
},
{
label: "400%",
value: "400"
},
]
enum RightSideNav {
@ -150,6 +166,7 @@ enum RightSideNav {
}
const SignDocument = () => {
const windowWidth = window.innerWidth;
const { recipientId } = useParams();
const dispatch = useDispatch();
@ -158,6 +175,9 @@ const SignDocument = () => {
// Signer data - START
const [selectedDocuments, setSelectedDocuments] = useState<DocumentType[] | null>(null);
const [imageSizes, setImageSizes] = useState<Record<string, { width: number; height: number }>>({});
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
const [isPages, setIsPages] = useState<boolean>(true);
@ -165,13 +185,14 @@ const SignDocument = () => {
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 [showConfetti, setShowConfetti] = useState<boolean>(false)
const [pageScales, setPageScales] = useState<Record<string, { x: number; y: number }>>({});
const [requiredFilled, setRequiredFilled] = useState<boolean>(false);
const [horizontalFormLoader, setHorizontalFormLoader] = useState<{ requiredFilled: number, totalRequired: number | null }>({ requiredFilled: 0, totalRequired: null });
@ -207,6 +228,55 @@ const SignDocument = () => {
const [provideTags, setProvideTags] = useState(false)
const [showCompletionScreen, setShowCompletionScreen] = useState(false)
const [zoomScale, setZoomScale] = useState<number>(100)
const scaleFactor: number = (zoomScale ?? 100) / 100;
// To center the container on every zoom scale change - START
useEffect(() => {
const container = document.getElementById("doc-con");
if (!container) return;
const prevScrollRatio = container.scrollTop / (container.scrollHeight || 1);
const newScrollTop = prevScrollRatio * container.scrollHeight;
const newScrollLeft = (container.scrollWidth - container.clientWidth) / 2;
container.scrollTo({
top: newScrollTop,
left: newScrollLeft,
behavior: "instant" as ScrollBehavior
});
}, [zoomScale]);
// To center the container on every zoom scale change - END
// Handle Tags and Image Size - START
useEffect(() => {
const observers: ResizeObserver[] = [];
Object.keys(imageRefs.current).forEach((key) => {
const el = imageRefs.current[key];
if (!el) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
setImageSizes((prev) => ({
...prev,
[key]: { width, height },
}));
}
});
observer.observe(el);
observers.push(observer);
});
return () => observers.forEach((o) => o.disconnect());
}, [selectedDocuments]); // re-run when documents load
// Handle Tags anf Image Size = END
const [saved, setSaved] = useState<SaveType>({
signatureId: null,
saveType: null,
@ -228,25 +298,31 @@ const SignDocument = () => {
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 handleFitSelect = (indx: number) => {
setSelFitIndx(indx);
let value = fitObj[indx].value;
if (value === "FIT") value = "100";
setZoomScale?.(Number(value));
}
// const zoom = (percentage: number, contentClass: string) => {
// const content = document.querySelector(`.${contentClass}`);
// if (content) {
// (content as HTMLElement).style.zoom = `${percentage}%`;
// }
// }
// useEffect(() => {
// let indx = fitObj[selFitIndx].value
// if (indx == "FIT") {
// indx = "100";
// }
// zoom(Number(indx), 'zoom-con')
// }, [selFitIndx])
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 ..."));
@ -529,7 +605,7 @@ const SignDocument = () => {
// setShowCompletionScreen(true)
setSelectedTag(null)
// navigate("/transaction-complete")
window.location.href = `/transaction-complete`
window.location.href = `/transaction-complete/${recipientId}`
}
} catch (e) {
dispatch(hideLoader());
@ -893,7 +969,8 @@ const SignDocument = () => {
{fitObj.map((val, indx) => {
return (<div
key={indx}
onClick={() => setSelFitIndx(indx)}
// onClick={() => setSelFitIndx(indx)}
onClick={() => handleFitSelect(indx)}
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
<div
@ -1036,7 +1113,7 @@ const SignDocument = () => {
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
<div
id="tags-list-client-side"
className={`${isPages ? "block" : "hidden"} sm:block absolute w-[220px] left-0 top-[42px] border-r-2 sm:border-none border-slate-200 z-10 sm:static sm:w-auto sm:col-span-2 h-full p-4 overflow-y-auto ${recipient?.type != "CC" ? "bg-white" : ""} ${!provideTags && isFields ? "hidden" : ""}`}>
className={`${isPages ? "block" : "hidden"} bg-slate-100 sm:block absolute w-[220px] left-0 top-[42px] border-r-2 sm:border-none border-slate-200 z-10 sm:static sm:w-auto sm:col-span-2 h-full overflow-y-auto ${recipient?.type != "CC" ? "bg-slate-100" : ""} ${!provideTags && isFields ? "hidden" : ""}`}>
<div
@ -1074,7 +1151,9 @@ const SignDocument = () => {
<DocumentPages
selectedDocuments={selectedDocuments}
setDropDownTogglePage={setDropDownTogglePage}
dropDownTogglePage={dropDownTogglePage} />
dropDownTogglePage={dropDownTogglePage}
droppedItems={droppedItems}
/>
</div>
{/* Pages - END */}
@ -1084,145 +1163,174 @@ const SignDocument = () => {
id="doc-con"
className={`zoom-con relative ${provideTags ? (isFields && selectedTag != null ? "col-span-8" : "col-span-10") : (isFields ? "col-span-12" : "col-span-10")} sm:px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth transition-all duration-200 pb-[4rem]`}
>
{
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];
const currDocTags = droppedItems
.map((val, index) => ({ ...val, originalIndex: index })) // add original index
.filter(val => val.pageIndex === page.index);
let scale: { x: number | null, y: number | null } = { x: null, y: null }
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'
>
<img
// style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
id={`${doc.documentId}-${page.index}`}
// style={{ "width": page.width, "height": page.height, "maxWidth": "100%", "maxHeight": "100%" }}
className={`w-[${page.width / 2}px] h-[${page.height / 2}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
ref={(el) => (imageRefs.current[page.index] = el)}
draggable="false"
// onLoad={(imgElem) => {
// setPageScales((prev) => ({
// ...prev,
// [page.index]: {
// x: imgElem.clientWidth / page.width,
// y: imgElem.clientHeight / page.height,
// },
// }));
// }}
width={page.width}
height={page.height}
src={page.src}
loading='lazy'
/>
{!hideTags && <>
<TagsCon
pageWidth={page.width}
pageHeight={page.height}
renderedWidth={imageRefs.current[page.index]?.offsetWidth || 0}
renderedHeight={imageRefs.current[page.index]?.offsetHeight || 0}
currDocTags={currDocTags}
pageIdkey={key}
provideTags={provideTags}
documentId={doc.documentId}
pageIndex={page.index}
hideTags={hideTags}
transparentTags={transparentTags}
focusElement={focusElement}
selectedTag={selectedTag}
droppedItems={droppedItems}
saved={saved}
signConfObj={signConfObj}
recipientId={recipientId as string}
setSaved={setSaved}
setStamp={setStamp}
setSignConfObj={setSignConfObj}
setOpenSign={setOpenSign}
setSignTagId={setSignTagId}
setDroppedItems={setDroppedItems}
setSelectedTag={setSelectedTag}
setFocusElement={setFocusElement}
handleTagNavClick={handleTagNavClick}
/>
</>}
{/* Side details of documents Pages and document Name - START */}
< p className='hidden sm:block absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-50px] text-slate-600' >
page {indx + 1} of {doc.pages && doc.pages.length}
</p>
{/* Side details of documents Pages and document Name - END */}
{!transparentTags && focusElement != null && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index &&
<div style={{
width: `${100 * scaleFactor}%`, // ← outer wrapper grows to make room
height: `${100 * scaleFactor}%`,
}}>
<div className="zoom-con"
style={{
transform: `scale(${scaleFactor})`,
transformOrigin: 'top left', // ← change to top left
width: `${100 / scaleFactor}%`,
height: scaleFactor < 1
? `${100 / scaleFactor}%` // zoomed out — expand to fill
: 'auto', // zoomed in — let content grow
}}>
{
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];
const currDocTags = droppedItems
.map((val, index) => ({ ...val, originalIndex: index })) // add original index
.filter(val => val.pageIndex === page.index);
let scale: { x: number | null, y: number | null } = { x: null, y: null }
return (
<div
id="tag-navigation-btn"
className={`hidden absolute right-0 translate-x-[100%] sm:flex justify-center gap-0 items-center z-50 cursor-pointer transition-all duration-300`}
style={{ top: (focusElement.dimensions.top) + "px" }}
onClick={() => handleTagNavClick("N")}
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'
>
<svg width="100" height="35" viewBox="0 0 112 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.87927 20.8542L15.0554 2.53357C15.7668 1.54438 16.9108 0.958008 18.1292 0.958008H108.049C110.14 0.958008 111.835 2.65317 111.835 4.74426V41.0235C111.835 43.1146 110.14 44.8098 108.049 44.8098H18.1084C16.9016 44.8098 15.7671 44.2345 15.054 43.261L1.89871 25.3024C0.930133 23.9802 0.92229 22.1849 1.87927 20.8542Z" fill="#005AB5" />
{/* <path d="M53.2649 16.7474V28.3838H51.3899L45.907 20.4577H45.8104V28.3838H43.7024V16.7474H45.5888L51.0661 24.6792H51.1683V16.7474H53.2649ZM58.8545 28.5542C57.9795 28.5542 57.2238 28.3724 56.5875 28.0088C55.9549 27.6414 55.4682 27.1224 55.1272 26.452C54.7863 25.7777 54.6159 24.9842 54.6159 24.0713C54.6159 23.1736 54.7863 22.3857 55.1272 21.7077C55.4719 21.0258 55.953 20.4955 56.5704 20.1167C57.1879 19.7342 57.9132 19.5429 58.7466 19.5429C59.2844 19.5429 59.792 19.63 60.2693 19.8042C60.7504 19.9747 61.1746 20.2398 61.542 20.5997C61.9132 20.9595 62.2049 21.4179 62.417 21.9747C62.6291 22.5277 62.7352 23.1868 62.7352 23.952V24.5827H55.5818V23.1963H60.7636C60.7598 22.8023 60.6746 22.452 60.5079 22.1452C60.3413 21.8345 60.1083 21.5902 59.8091 21.4122C59.5136 21.2342 59.1689 21.1452 58.775 21.1452C58.3545 21.1452 57.9852 21.2474 57.667 21.452C57.3488 21.6527 57.1007 21.9179 56.9227 22.2474C56.7485 22.5732 56.6594 22.9311 56.6557 23.3213V24.5315C56.6557 25.0391 56.7485 25.4747 56.9341 25.8383C57.1197 26.1982 57.3791 26.4747 57.7125 26.6679C58.0458 26.8573 58.436 26.952 58.8829 26.952C59.1822 26.952 59.453 26.9103 59.6954 26.827C59.9379 26.7398 60.1481 26.613 60.3261 26.4463C60.5041 26.2796 60.6386 26.0732 60.7295 25.827L62.65 26.0429C62.5288 26.5505 62.2977 26.9936 61.9568 27.3724C61.6197 27.7474 61.1879 28.0391 60.6613 28.2474C60.1348 28.452 59.5325 28.5542 58.8545 28.5542ZM65.4896 19.6565L67.2509 22.8781L69.0407 19.6565H71.2168L68.5862 24.0202L71.2623 28.3838H69.0975L67.2509 25.2417L65.4214 28.3838H63.2396L65.8987 24.0202L63.3078 19.6565H65.4896ZM76.6857 19.6565V21.2474H71.6687V19.6565H76.6857ZM72.9073 17.5656H74.9641V25.7588C74.9641 26.0353 75.0058 26.2474 75.0891 26.3952C75.1762 26.5391 75.2899 26.6376 75.43 26.6906C75.5702 26.7436 75.7255 26.7702 75.8959 26.7702C76.0247 26.7702 76.1421 26.7607 76.2482 26.7417C76.3581 26.7228 76.4414 26.7058 76.4982 26.6906L76.8448 28.2986C76.735 28.3364 76.5778 28.3781 76.3732 28.4236C76.1725 28.469 75.9262 28.4955 75.6346 28.5031C75.1194 28.5183 74.6554 28.4406 74.2425 28.2702C73.8296 28.0959 73.502 27.827 73.2596 27.4633C73.0209 27.0997 72.9035 26.6452 72.9073 26.0997V17.5656Z" fill="white" /> */}
<img
// style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
id={`${doc.documentId}-${page.index}`}
// style={{ "width": page.width, "height": page.height, "maxWidth": "100%", "maxHeight": "100%" }}
className={`w-[${page.width / 2}px] h-[${page.height / 2}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
ref={(el) => (imageRefs.current[page.index] = el)}
onLoad={(e) => {
const el = e.currentTarget;
setImageSizes((prev) => ({
...prev,
[page.index]: {
width: el.offsetWidth,
height: el.offsetHeight,
},
}));
}}
draggable="false"
// onLoad={(imgElem) => {
// setPageScales((prev) => ({
// ...prev,
// [page.index]: {
// x: imgElem.clientWidth / page.width,
// y: imgElem.clientHeight / page.height,
// },
// }));
// }}
width={page.width}
height={page.height}
src={page.src}
loading='lazy'
/>
<text className="font-semibold capitalize" x="55%" y="54%" fill="white" font-size="14" font-family="Arial, sans-serif" dominant-baseline="middle" text-anchor="middle">
{requiredFilled && focusElement.required == true ? "Submit" : focusElement.type.toLowerCase()}
</text>
</svg>
{!hideTags && <>
<TagsCon
zoomScale={zoomScale}
pageWidth={page.width}
pageHeight={page.height}
renderedWidth={imageSizes[page.index]?.width || imageRefs.current[page.index]?.offsetWidth || 0}
renderedHeight={imageSizes[page.index]?.height || imageRefs.current[page.index]?.offsetHeight || 0}
currDocTags={currDocTags}
pageIdkey={key}
provideTags={provideTags}
documentId={doc.documentId}
pageIndex={page.index}
hideTags={hideTags}
transparentTags={transparentTags}
focusElement={focusElement}
selectedTag={selectedTag}
droppedItems={droppedItems}
saved={saved}
signConfObj={signConfObj}
recipientId={recipientId as string}
setSaved={setSaved}
setStamp={setStamp}
setSignConfObj={setSignConfObj}
setOpenSign={setOpenSign}
setSignTagId={setSignTagId}
setDroppedItems={setDroppedItems}
setSelectedTag={setSelectedTag}
setFocusElement={setFocusElement}
handleTagNavClick={handleTagNavClick}
/>
</>}
{/* NEXT Button - END */}
</div>}
</div>
)
})
}
{/* Side details of documents Pages and document Name - START */}
< p className='hidden sm:block absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-50px] text-slate-600' >
page {indx + 1} of {doc.pages && doc.pages.length}
</p>
{/* Side details of documents Pages and document Name - END */}
{!transparentTags && focusElement != null && focusElement && Object.keys(focusElement).length > 1 && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index &&
<div
id="tag-navigation-btn"
className={`hidden absolute right-0 translate-x-[100%] sm:flex justify-center gap-0 items-center z-50 cursor-pointer transition-all duration-300`}
style={{ top: (focusElement.dimensions.top) + "px" }}
onClick={() => handleTagNavClick("N")}
>
<svg width="100" height="35" viewBox="0 0 112 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.87927 20.8542L15.0554 2.53357C15.7668 1.54438 16.9108 0.958008 18.1292 0.958008H108.049C110.14 0.958008 111.835 2.65317 111.835 4.74426V41.0235C111.835 43.1146 110.14 44.8098 108.049 44.8098H18.1084C16.9016 44.8098 15.7671 44.2345 15.054 43.261L1.89871 25.3024C0.930133 23.9802 0.92229 22.1849 1.87927 20.8542Z" fill="#005AB5" />
{/* <path d="M53.2649 16.7474V28.3838H51.3899L45.907 20.4577H45.8104V28.3838H43.7024V16.7474H45.5888L51.0661 24.6792H51.1683V16.7474H53.2649ZM58.8545 28.5542C57.9795 28.5542 57.2238 28.3724 56.5875 28.0088C55.9549 27.6414 55.4682 27.1224 55.1272 26.452C54.7863 25.7777 54.6159 24.9842 54.6159 24.0713C54.6159 23.1736 54.7863 22.3857 55.1272 21.7077C55.4719 21.0258 55.953 20.4955 56.5704 20.1167C57.1879 19.7342 57.9132 19.5429 58.7466 19.5429C59.2844 19.5429 59.792 19.63 60.2693 19.8042C60.7504 19.9747 61.1746 20.2398 61.542 20.5997C61.9132 20.9595 62.2049 21.4179 62.417 21.9747C62.6291 22.5277 62.7352 23.1868 62.7352 23.952V24.5827H55.5818V23.1963H60.7636C60.7598 22.8023 60.6746 22.452 60.5079 22.1452C60.3413 21.8345 60.1083 21.5902 59.8091 21.4122C59.5136 21.2342 59.1689 21.1452 58.775 21.1452C58.3545 21.1452 57.9852 21.2474 57.667 21.452C57.3488 21.6527 57.1007 21.9179 56.9227 22.2474C56.7485 22.5732 56.6594 22.9311 56.6557 23.3213V24.5315C56.6557 25.0391 56.7485 25.4747 56.9341 25.8383C57.1197 26.1982 57.3791 26.4747 57.7125 26.6679C58.0458 26.8573 58.436 26.952 58.8829 26.952C59.1822 26.952 59.453 26.9103 59.6954 26.827C59.9379 26.7398 60.1481 26.613 60.3261 26.4463C60.5041 26.2796 60.6386 26.0732 60.7295 25.827L62.65 26.0429C62.5288 26.5505 62.2977 26.9936 61.9568 27.3724C61.6197 27.7474 61.1879 28.0391 60.6613 28.2474C60.1348 28.452 59.5325 28.5542 58.8545 28.5542ZM65.4896 19.6565L67.2509 22.8781L69.0407 19.6565H71.2168L68.5862 24.0202L71.2623 28.3838H69.0975L67.2509 25.2417L65.4214 28.3838H63.2396L65.8987 24.0202L63.3078 19.6565H65.4896ZM76.6857 19.6565V21.2474H71.6687V19.6565H76.6857ZM72.9073 17.5656H74.9641V25.7588C74.9641 26.0353 75.0058 26.2474 75.0891 26.3952C75.1762 26.5391 75.2899 26.6376 75.43 26.6906C75.5702 26.7436 75.7255 26.7702 75.8959 26.7702C76.0247 26.7702 76.1421 26.7607 76.2482 26.7417C76.3581 26.7228 76.4414 26.7058 76.4982 26.6906L76.8448 28.2986C76.735 28.3364 76.5778 28.3781 76.3732 28.4236C76.1725 28.469 75.9262 28.4955 75.6346 28.5031C75.1194 28.5183 74.6554 28.4406 74.2425 28.2702C73.8296 28.0959 73.502 27.827 73.2596 27.4633C73.0209 27.0997 72.9035 26.6452 72.9073 26.0997V17.5656Z" fill="white" /> */}
<text className="font-semibold capitalize" x="55%" y="54%" fill="white" font-size="14" font-family="Arial, sans-serif" dominant-baseline="middle" text-anchor="middle">
{requiredFilled && focusElement.required == true ? "Submit" : focusElement.type.toLowerCase()}
</text>
</svg>
{/* NEXT Button - END */}
</div>}
</div>
)
})
}
</div>
</div>
)
})
}
{(navigationTagPos == NavigationTagPosEnum.ABOVE || navigationTagPos == NavigationTagPosEnum.BELOW) &&
<div
onClick={() => scrollToElement(focusElement?.navigationOrder)}
className={`hidden sm:block cursor-pointer fixed ${navigationTagPos == NavigationTagPosEnum.ABOVE ? "top-[45px]" : requiredFilled ? "bottom-[82px]" : "bottom-[35px]"} right-[8%] flex flex-col items-center`}>
{/* Arrow pointing up */}
{navigationTagPos == NavigationTagPosEnum.ABOVE && <div className="w-0 h-0 border-l-6 border-r-6 border-b-6 border-l-transparent border-r-transparent border-b-[var(--primary)]"></div>}
{/* Label box */}
<div
className="bg-[var(--primary)] text-white text-sm px-3 py-1 rounded-md shadow-md flex items-center gap-1">
<span className="capitalize">{focusElement?.type.toLowerCase()}</span>
</div>
{navigationTagPos == NavigationTagPosEnum.BELOW && <div className="w-0 h-0 border-l-6 border-r-6 border-b-6 border-l-transparent border-r-transparent border-b-[var(--primary)] rotate-[180deg]"></div>}
</div>}
</div>
)
})
}
{(navigationTagPos == NavigationTagPosEnum.ABOVE || navigationTagPos == NavigationTagPosEnum.BELOW) &&
<div
onClick={() => scrollToElement(focusElement?.navigationOrder)}
className={`hidden sm:block cursor-pointer fixed ${navigationTagPos == NavigationTagPosEnum.ABOVE ? "top-[45px]" : requiredFilled ? "bottom-[82px]" : "bottom-[35px]"} right-[8%] flex flex-col items-center`}>
{/* Arrow pointing up */}
{navigationTagPos == NavigationTagPosEnum.ABOVE && <div className="w-0 h-0 border-l-6 border-r-6 border-b-6 border-l-transparent border-r-transparent border-b-[var(--primary)]"></div>}
{/* Label box */}
<div
className="bg-[var(--primary)] text-white text-sm px-3 py-1 rounded-md shadow-md flex items-center gap-1">
<span className="capitalize">{focusElement?.type.toLowerCase()}</span>
</div>
{navigationTagPos == NavigationTagPosEnum.BELOW && <div className="w-0 h-0 border-l-6 border-r-6 border-b-6 border-l-transparent border-r-transparent border-b-[var(--primary)] rotate-[180deg]"></div>}
</div>}
</div>
</div>
</div>
{provideTags && selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag &&
<div className={`col-span-2 ${provideTags ? "" : "order-first"} h-full ${!isFields ? "bg-white" : isFields && provideTags ? "bg-white" : ""} overflow-y-auto`}>
{/* Right Side Attributes Section if providedTags - START */}
<RightSideBar
<TagProperties
droppedItems={droppedItems}
selectedTag={droppedItems[selectedTag.index]}
setDroppedItems={setDroppedItems}
@ -1244,6 +1352,7 @@ const SignDocument = () => {
<div style={{ display: `${openSign ? 'block' : 'none'}` }}>
{recipient &&
<ClientSignCon
zoomScale={zoomScale}
recipient={recipient}
recipientId={recipientId as string}
setOpenSign={setOpenSign} saved={saved}
@ -1326,7 +1435,7 @@ const SignDocument = () => {
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
</div> */}
<div>
{!transparentTags && focusElement != null &&
{!transparentTags && focusElement != null && Object.keys(focusElement).length > 1 &&
<div
id="tag-navigation-btn-mb"
className={`sm:hidden flex justify-center gap-0 items-center z-50 cursor-pointer transition-all duration-300`}

View File

@ -14,6 +14,7 @@ import axios from 'axios';
interface TagsConProps {
zoomScale: number,
pageWidth: number,
pageHeight: number,
renderedWidth: number,
@ -43,6 +44,7 @@ interface TagsConProps {
}
const TagsCon: React.FC<TagsConProps> = ({
zoomScale,
pageWidth,
pageHeight,
renderedWidth,
@ -71,60 +73,76 @@ const TagsCon: React.FC<TagsConProps> = ({
handleTagNavClick
}) => {
const scaleFactor: number = (zoomScale ?? 100) / 100;
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
const [isDateOpen, setIsDateOpen] = useState<string | null>(null);
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
setSelectedTag({ key, index })
setSelectedTag({ key, index });
event.preventDefault();
const scale = scaleFactor ?? 1; // zoom factor
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
let targetElement = event?.currentTarget;
let targetElement = event?.currentTarget?.parentElement;
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 scaleX = renderedWidth && pageWidth ? renderedWidth / pageWidth : 1;
const scaleY = renderedHeight && pageHeight ? renderedHeight / pageHeight : 1;
const conScrollWidth = targetContainer?.scrollWidth;
const conScrollHeight = targetContainer?.scrollHeight;
// Click offset in screen pixels
const clickOffsetX = event.clientX - elementRect.left;
const clickOffsetY = event.clientY - elementRect.top;
let isGroupAttr = event.currentTarget.hasAttribute("group");
let groupId = null;
if (isGroupAttr) {
groupId = event.currentTarget.getAttribute("group");
}
if (isGroupAttr) groupId = event.currentTarget.getAttribute("group");
let groupElements: NodeListOf<HTMLElement> | null = null
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;
const currentContainerRect = targetContainer!.getBoundingClientRect();
// Mouse position relative to container in screen pixels
const mouseX = event.clientX - currentContainerRect.left;
const mouseY = event.clientY - currentContainerRect.top;
// **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));
// Convert to rendered pixel space (undo zoom)
const renderedLeft = (mouseX - clickOffsetX) / scale;
const renderedTop = (mouseY - clickOffsetY) / scale;
// Convert to original coord space for storage
const originalLeft = renderedLeft / scaleX;
const originalTop = renderedTop / scaleY;
if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
if (groupElements != null && groupElements.length) {
handleGroupBorderPosition(groupElements, currentContainerRect, clickOffsetX, clickOffsetY);
}
// Apply the movement to the element
// Update state in original coord space
setDroppedItems(
droppedItems.map((item, i) =>
i == index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item
i === index
? {
...item,
dimensions: {
...item.dimensions,
left: Math.max(0, originalLeft),
top: Math.max(0, originalTop),
},
update: true
}
: item
)
);
};
const handleMouseUp = () => {
@ -132,7 +150,6 @@ const TagsCon: React.FC<TagsConProps> = ({
document.removeEventListener("pointerup", handleMouseUp);
};
// Add event listeners
document.addEventListener("pointermove", handleMouseMove);
document.addEventListener("pointerup", handleMouseUp);
};
@ -308,9 +325,6 @@ const TagsCon: React.FC<TagsConProps> = ({
}
}
console.log(signConfObj, saved, "Line 305")
return (
<>
{currDocTags.map((item, index) => {
@ -681,6 +695,49 @@ const TagsCon: React.FC<TagsConProps> = ({
{/* CHECKBOX FIELD - END */}
{/* RADIO FIELD - START */}
{
item.type == "RADIO" &&
<div
id={item.navigationOrder}
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 opacity-80"}`}
>
<input
type="radio"
name={item.group ?? `radio-group-${item.recipientId}`} // group by recipient or group id
value={item.id}
checked={item.checked ?? false}
onChange={(e) => {
if (!e.target.checked) return;
// Uncheck all other radio tags in same group, check this one
setDroppedItems(
droppedItems.map((tag, i) => {
if (tag.type !== "RADIO") return tag;
// Same group = same name attribute
const sameGroup = (item.group && tag.group)
? item.group === tag.group
: tag.recipientId === item.recipientId; // fallback
if (!sameGroup) return tag;
return {
...tag,
checked: tag.id === item.id,
update: true
};
})
);
}}
className="w-4 h-4 cursor-pointer accent-[var(--primary)]"
/>
</div>
}
{/* RADIO FIELD - END */}
{/* SIGNATURE FIELD - START */}
{
item.type == "SIGNATURE" &&

View File

@ -137,7 +137,7 @@ function Dashboard() {
{/* Close Icon */}
<button
onClick={() => setShowConfetti(false)} // or whatever logic hides the modal
className="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
className="absolute top-2 right-2 text-gray-500 hover:text-gray-100"
aria-label="Close"
>
<IoClose className="text-2xl" />

View File

@ -6,6 +6,12 @@ import { RootState } from '../../store/store';
import api from '../../config/api';
import TransactionChoiceModal from './TransactionChoiceModal';
import { resetRecipients } from '../../store/slices/selectedRecipientsSlice';
import { IoMdArrowDropdown } from 'react-icons/io';
import { MdArrowRight } from 'react-icons/md';
import { FaDesktop } from 'react-icons/fa';
import { GrTemplate } from 'react-icons/gr';
import { HiOutlineTemplate, HiTemplate } from 'react-icons/hi';
import { LuLayoutTemplate } from 'react-icons/lu';
const StartNow = () => {
@ -141,11 +147,11 @@ const StartNow = () => {
}}
/>}
<div
onClick={() => fileInputRef.current?.click()}
// onClick={() => fileInputRef.current?.click()}
onDrop={handleDrop}
onDragOver={handleDragOver}
id='start-now-upload-con'
className="group col-span-1 cursor-pointer border-1 border-dashed border-[#D3D3D3] rounded-lg bg-[var(--documentUploadBg)] transition-all duration-200 p-4 flex flex-col items-center justify-center gap-2 text-center shadow-sm hover:shadow-md">
className="group col-span-1 cursor-pointer border-1 border-dashed border-[#D3D3D3] rounded-lg bg-[var(--documentUploadBg)] transition-all duration-200 p-4 flex flex-col items-center justify-center gap-2 text-center shadow-sm">
{/* Upload icon */}
<svg className="mt-4" width="44" height="46" viewBox="0 0 62 46" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -163,11 +169,70 @@ const StartNow = () => {
PDF, DOCX (max 10MB)
</p>
<button
{/* <button
className="text-xs mt-4 mb-6 text-white bg-[var(--dashBoardStartNowBtn)] px-8 py-2 rounded shadow-xs transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-blue-400 cursor-pointer"
>
Upload File
</button>
</button> */}
<div className="relative inline-block group mt-4 mb-6">
<button
className="flex text-lg font-medium items-center gap-1 text-xs text-white bg-[var(--dashBoardStartNowBtn)] pl-3 pr-2 py-2 rounded shadow-xs transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-blue-400 cursor-pointer"
>
<p>Upload File</p>
<IoMdArrowDropdown className='text-lg' />
</button>
{/* Dropdown */}
<div
className="
absolute
left-0
top-full
mt-1
min-w-[180px]
bg-white
border
border-slate-200
rounded-md
shadow-lg
overflow-hidden
z-50
opacity-0
invisible
translate-y-[-5px]
scale-[0.98]
transition-all
duration-200
ease-out
group-hover:opacity-100
group-hover:visible
group-hover:translate-y-0
group-hover:scale-100
"
>
<button
onClick={() => fileInputRef.current?.click()}
className="w-full text-left px-4 py-2.5 text-xs hover:bg-slate-50 cursor-pointer flex gap-2 items-center"
>
<FaDesktop className='text-[var(--primary)]' />
<span>Desktop</span>
</button>
<button
onClick={() => navigate("/templates")}
className="w-full text-left px-4 py-2.5 text-xs hover:bg-slate-50 border-t border-slate-100 cursor-pointer flex gap-2 items-center"
>
<HiTemplate className='text-[var(--primary)]' />
<span>Use a Template</span>
</button>
</div>
</div>
{filesError && <span className="text-red-500 text-xs">{filesError}</span>}

View File

@ -91,83 +91,380 @@ const TransactionSentSuccess = () => {
</div>
{/* Content */}
<div className="flex flex-col p-4 justify-center items-center w-full">
<div className="mx-auto w-full max-w-6xl">
{/* Success banner */}
<div className="flex flex-col justify-center items-center gap-3 mb-4 p-4 rounded-md">
{/* <div className="w-12 h-12 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center shadow-sm">
<FaCheck className="text-white text-2xl" />
</div> */}
<div className="relative z-10 w-full px-4 py-6 sm:px-6">
<div className="max-w-7xl mx-auto">
<svg width="70" height="70" viewBox="0 0 114 109" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M114 54.3837L101.095 40.1811L103.115 21.1673L84.2441 17.2075L74.6181 0.631836L57 8.43682L54.6221 54.3837L57 100.33L74.6183 108.135L84.2441 91.56L103.115 87.5999L101.095 68.5861L114 54.3837Z" fill="#0ED678" />
<path d="M39.3816 0.631836L29.7558 17.2075L10.8847 21.1675L12.9053 40.1811L0 54.3837L12.9053 68.5861L10.8847 87.5999L29.7558 91.56L39.3819 108.135L57.0001 100.33V8.43682L39.3816 0.631836Z" fill="#04EB84" />
<path d="M80.8957 42.013L75.9064 37.5986L57 58.5946L54.6221 63.8124L57 68.5499L80.8957 42.013Z" fill="#F7F0EB" />
<path d="M37.1311 49.3916L32.4023 54.0791L52.2842 73.7868L57.0003 68.5495V58.5942L52.0082 64.138L37.1311 49.3916Z" fill="#FFFBF5" />
</svg>
{/* Success Hero */}
{/* Success Hero */}
<div
className="
relative
overflow-hidden
bg-white
border
border-slate-200
rounded-md
px-6
py-8
lg:px-10
lg:py-10
"
>
<div>
<p className="text-lg sm:text-4xl font-bold text-[var(--mainPrimaryBtn)]">
Your Document is sent for signature
</p>
<p className="text-lg text-slate-600 text-center font-medium">All signers will be notified via email</p>
</div>
</div>
{/* Main two-column layout */}
<div className="flex flex-col lg:flex-row gap-4 mt-6 lg:justify-center lg:items-stretch">
{/* Left: Document Preview */}
<div className="flex-1 bg-slate-100 rounded-md overflow-hidden">
<div className="w-full h-[50vh] overflow-auto">
<div className="py-2 bg-white px-4 shadow-sm rounded-sm sticky top-0 z-10 border-1 border-gray-200">
<p className="text-sm font-medium">Quick Preview</p>
</div>
<div className="px-4">
<DocumentContainer isPreview={true} aId={accountId} tId={transactionId} />
</div>
</div>
</div>
{/* Right: Signers list + actions */}
{/* subtle background */}
<div
className={`flex-1 transition-all rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'
}`}
>
<div className="p-4 h-full bg-white rounded-md shadow-2xl border-1 border-gray-100">
className="
absolute
right-[-80px]
top-[-80px]
w-[220px]
h-[220px]
bg-emerald-50
rounded-full
blur-3xl
opacity-70
"
/>
<div className='overflow-auto'>
<SignersList signers={selectedRecipients} />
<div
className="
relative
z-10
flex
flex-col
lg:flex-row
items-start
lg:items-center
justify-between
gap-8
"
>
{/* Left */}
<div className="flex-1">
{/* success badge */}
<div
className="
inline-flex
items-center
gap-2
px-4
py-2
rounded-full
bg-emerald-50
border
border-emerald-100
mb-5
"
>
<div
className="
w-5
h-5
rounded-full
bg-emerald-500
flex
items-center
justify-center
"
>
<FaCheck className="text-white text-[11px]" />
</div>
<span
className="
text-sm
font-medium
text-emerald-700
"
>
Transaction Sent Successfully
</span>
</div>
<div className="flex flex-col sm:flex-row gap-2 max-h-max relative top-auto bottom-0">
<Link
to="/"
className="flex-1 p-2 text-center rounded-md bg-slate-200 hover:shadow-sm transition-all duration-150"
{/* title */}
<h1
className="
text-3xl
sm:text-4xl
font-bold
text-slate-900
leading-tight
tracking-[-0.02em]
max-w-3xl
"
>
Your document has been sent for signature
</h1>
{/* description */}
<p
className="
mt-4
text-base
leading-relaxed
text-slate-600
max-w-2xl
"
>
All recipients will receive an email notification with
secure access to review and sign the document.
</p>
{/* actions */}
<div
className="
mt-7
flex
flex-col
sm:flex-row
gap-3
"
>
{/* <button
onClick={() => {
handleDownloadDocument(recipientId || "")
}}
className="
h-11
px-5
rounded-sm
bg-[var(--mainPrimaryBtn)]
text-white
font-medium
hover:opacity-90
transition
shadow-sm
cursor-pointer
"
>
Back to Home
</Link>
Download Documents
</button> */}
<button
onClick={() => {
window.location.href = "/"
}}
className="
h-11
px-5
rounded-sm
bg-white
text-slate-700
font-medium
hover:opacity-90
transition
shadow-sm
cursor-pointer
"
>
Back To Home
</button>
{selfUserSign.sign && (
<p
onClick={() => signSessionRedirect(accountId, transactionId, selfUserSign.recipientId + "")}
className="flex-1 p-2 text-center rounded-md bg-[var(--mainPrimaryBtn)] text-white hover:shadow-sm transition-all duration-150 cursor-pointer"
<button
onClick={() =>
signSessionRedirect(
accountId,
transactionId,
selfUserSign.recipientId + ""
)
}
className="
h-11
px-5
rounded-sm
border
border-slate-300
bg-[var(--primary)]
text-white
font-medium
opacity-80
hover:opacity-100
transition
cursor-pointer
"
>
Sign Now
</p>
</button>
)}
</div>
</div>
{/* Right */}
<div
className="
hidden
lg:flex
items-center
justify-center
shrink-0
"
>
{/* Optional: Signing Order */}
{/* <div className="mt-4">
<SigningOrderDiagram signers={selectedRecipients} />
</div> */}
<div
className="
w-[260px]
h-[220px]
rounded-md
bg-gradient-to-br
from-emerald-50
to-blue-50
border
border-slate-200
flex
items-center
justify-center
relative
overflow-hidden
"
>
<img
src="/completion-screen-1.png"
alt=""
className="
w-44
object-contain
relative
z-10
"
/>
</div>
</div>
</div>
</div>
{/* Main Section */}
<div
className="
grid
grid-cols-1
xl:grid-cols-[1.3fr_0.7fr]
gap-6
mt-6
items-start
"
>
{/* Preview */}
<div
className="
bg-white
rounded-md
border
border-slate-200
shadow-sm
overflow-hidden
"
>
{/* Header */}
<div
className="
flex
items-center
justify-between
px-5
py-4
border-b
border-slate-200
bg-slate-50
"
>
<div>
<p className="font-semibold text-slate-800">
Document Preview
</p>
<p className="text-sm text-slate-500">
Review the final document before signing
</p>
</div>
</div>
{/* Preview Body */}
<div className="h-[70vh] overflow-auto bg-slate-100">
<div className="px-4 py-4">
{
selectedRecipients.length > 0 &&
selectedDocuments.length > 0 && (
<DocumentContainer
isPreview={true}
aId={accountId}
tId={transactionId}
/>
)
}
</div>
</div>
</div>
{/* Right Sidebar */}
<div
className={`
transition-all
duration-700
${isVisible
? 'translate-y-0 opacity-100'
: 'translate-y-8 opacity-0'
}
`}
>
<div
className="
sticky
top-5
bg-white
rounded-md
border
border-slate-200
shadow-sm
overflow-hidden
"
>
{/* Header */}
<div
className="
px-5
py-4
border-b
border-slate-200
bg-slate-50
"
>
<p className="font-semibold text-slate-800">
Signers
</p>
<p className="text-sm text-slate-500 mt-1">
Recipients who will receive this document
</p>
</div>
{/* Body */}
<div className="max-h-[70vh] overflow-auto p-4">
<SignersList signers={selectedRecipients} />
</div>
</div>
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@ import { RootState } from "../../../store/store";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { addRecipientAsync, fetchRecipients, recipientType, removeAllRecipients, removeRecipientAsync, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from "../../../store/slices/selectedRecipientsSlice";
import { addRecipientAsync, fetchRecipients, recipientType, removeAllRecipients, removeRecipientAsync, resetRecipients, setSigningOrder, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientOrder } from "../../../store/slices/selectedRecipientsSlice";
import { TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
import { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
import Loader from "../../dashboard/Loader";
@ -78,6 +78,8 @@ const ProcessStepOne = () => {
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder)
const [showSelfSignWarnModal, setShowSelfSignWarnModal] = useState(false)
const [editRecipientCurrentEmail, setEditRecipientCurrentEmail] = useState('')
@ -141,6 +143,12 @@ const ProcessStepOne = () => {
if (submitError.duplicateEmailSameRoutingOrder) setSubmitError(prev => ({ ...prev, duplicateEmailSameRoutingOrder: false }))
if (submitError.documents) setSubmitError(prev => ({ ...prev, documents: false }))
if (submitError.recipients) setSubmitError(prev => ({ ...prev, recipients: false }))
// Handle Signing Order for template and for transction - fill on refresh based on recipients routing order or prefill - START
let isSigningOrder = signingOrder;
selectedRecipients && selectedRecipients.length > 1 && selectedRecipients.some(recipient => recipient.routingOrder && recipient.routingOrder > 1) ? isSigningOrder = true : "";
isSigningOrder && handleSigningOrderChange(isSigningOrder);
// Handle Signing Order for template and for transction - fill on refresh based on recipients routing order or prefill - END
}, [selectedRecipients])
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@ -391,6 +399,9 @@ const ProcessStepOne = () => {
useEffect(() => {
validationCheck()
selectedRecipients.forEach((recipient, index) => {
})
}, [selectedDocuments, selectedRecipients, emailDtls])
const handleSelfSignCb = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -414,13 +425,28 @@ const ProcessStepOne = () => {
)
}
function handleSigningOrderChange(checked?: boolean) {
dispatch(setSigningOrder(checked ?? !signingOrder))
if (checked ?? !signingOrder) {
selectedRecipients.forEach((recipient, index) => {
const id = recipient.id || recipient.uuid || "1"
dispatch(updateRecipientOrder({ id, routingOrder: index + 1 }));
})
} else {
selectedRecipients.forEach((recipient, index) => {
const id = recipient.id || recipient.uuid || "1"
dispatch(updateRecipientOrder({ id, routingOrder: 1 }));
})
}
}
return (
<>
<div className={`relative w-full h-screen bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none overflow-hidden' : ''}`}>
{/* Top Navigation - Fixed */}
<div className='fixed top-0 left-0 right-0 w-full bg-white shadow z-50 px-4 py-1.5 lg:py-3'>
<div className='fixed top-0 left-0 right-0 w-full bg-white shadow z-50 px-4 py-1.5 lg:py-3 z-50'>
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 flex-wrap'>
{/* Left Section */}
@ -489,7 +515,7 @@ const ProcessStepOne = () => {
{/* Documents */}
<section>
<h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2>
<h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Add documents</h2>
<div id="documents_con" className={`${submitError && submitError.documents && submitError.showError ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} />
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError && submitError.documents && submitError.showError && "At least one document is required."}</p>
@ -499,21 +525,40 @@ const ProcessStepOne = () => {
{/* Recipients */}
<section className="mt-8">
<p className="border-t border-gray-200"></p>
{!isTemplate &&
transactionTemplateToggle != TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES &&
<div className="flex justify-between items-center">
<h2 className=" text-lg font-medium mb-4 pt-6 text-[var(--processStepOneTextHead)]">Signers</h2>
{/* <div className="flex items-center mr-3">
<div className="flex justify-between items-center">
<h2 className=" text-lg font-medium mb-4 pt-6 text-[var(--processStepOneTextHead)]">Add recipients</h2>
{!isTemplate &&
transactionTemplateToggle != TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES &&
<>
{/* <div className="flex items-center mr-3">
<input
checked={signType == SignTypeEnum.selfSign}
onChange={(e) => handleSelfSignCb(e)}
id="default-checkbox" type="checkbox" value="" className="w-4 h-4 checked:bg-[var(--mainPrimaryBtn)] bg-gray-100 border-gray-300 rounded-sm outline-none"
checked={signType == SignTypeEnum.selfSign}
onChange={(e) => handleSelfSignCb(e)}
id="default-checkbox" type="checkbox" value="" className="w-4 h-4 checked:bg-[var(--mainPrimaryBtn)] bg-gray-100 border-gray-300 rounded-sm outline-none"
/>
<label htmlFor="default-checkbox" className="ms-2 text-sm flex gap-1 text-slate-600">
I AM THE ONLY SIGNER</label>
</div> */}
</div>
}
I AM THE ONLY SIGNER</label>
</div> */}
</>
}
{/* HERE WE AREEEEEEEE */}
{selectedRecipients.length > 1 && <div className="flex items-center">
<input
// checked={signType == SignTypeEnum.selfSign}
onChange={(e) => handleSigningOrderChange(e.target.checked)}
checked={signingOrder}
id="default-checkbox" type="checkbox" value="" className="w-4 h-4 checked:bg-[var(--mainPrimaryBtn)] bg-gray-100 border-gray-300 rounded-sm outline-none"
/>
<label htmlFor="default-checkbox" className="ms-2 text-sm flex gap-1 text-slate-600">
Set signing order
</label>
</div>}
{/* HERE WE AREEEEEEEE */}
</div>
{
(signType != SignTypeEnum.selfSign || showSelfSignWarnModal) &&
@ -533,7 +578,7 @@ const ProcessStepOne = () => {
{/* Email */}
<section className="mt-8">
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2>
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Add Message</h2>
{/* Subject */}
<div className="border-1 rounded-xl border-gray-300 px-6 py-8">

View File

@ -67,6 +67,8 @@ const RecipientCards = ({
let { accountId, transactionId } = useParams<{ accountId: string; transactionId: string }>();
const dispatch = useDispatch<AppDispatch>();
const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder)
if (!transactionId) {
transactionId = templateId;
}
@ -355,10 +357,10 @@ const RecipientCards = ({
};
return (
<div className="w-full rounded-md bg-slate-50">
<div className="w-full rounded-md">
{recipients.length > 0 ? (
<>
{recipients.length > 1 && (
{/* {recipients.length > 1 && (
<div className="flex sm:flex-row sm:items-center justify-between px-3 sm:px-4 py-2 border-b border-gray-300 gap-2">
<h3 className="text-sm font-semibold text-gray-700">
Recipients ({recipients.length})
@ -371,7 +373,7 @@ const RecipientCards = ({
<FaUsersLine className="text-[var(--primary)] mt-0.5 text-lg" />
</label>
</div>
)}
)} */}
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
@ -379,7 +381,7 @@ const RecipientCards = ({
items={recipients.map((r) => r.id || r.uuid)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
<div className="flex flex-col max-h-[47vh] overflow-y-auto custom-scrollbar">
{recipients.map((recipient, idx) => (
<SortableRecipient
key={recipient.id || recipient.uuid}
@ -389,230 +391,249 @@ const RecipientCards = ({
>
<div
key={recipient.email + idx}
className={`relative flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 transition-colors gap-3 lg:gap-0 ${(recipient.uuid || recipient.id) == editRecipientCurrentEmail
className={`relative flex flex-col lg:flex-row lg:items-center justify-between transition-colors gap-3 lg:gap-0 ${(recipient.uuid || recipient.id) == editRecipientCurrentEmail
? "bg-neutral-100 opacity-40"
: ""
}`}
>
{/* Main Content Section */}
<div className="flex items-start sm:items-center gap-2 sm:gap-3 min-w-0 flex-1">
{/* Order Number */}
{recipients.length > 1 ? (
<div className="relative flex-shrink-0">
<input
type="text"
value={recipient.routingOrder == 0 ? "" : recipient.routingOrder}
onChange={(e) => {
const value = e.target.value;
if (/^\d*$/.test(value)) {
const num = Number(value);
handleOrderChange(recipient.id || recipient.uuid, num);
}
}}
onKeyDown={(e) => {
if (!/[0-9]/.test(e.key) && e.key != "Backspace" && e.key != "Tab") {
e.preventDefault();
}
}}
className="w-8 sm:w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400"
/>
{idx == 0 && recipients.length > 1 && <p className="group">
<BiSolidHelpCircle className="absolute top-[-12px] right-[-10px] text-gray-400 cursor-pointer" />
{/* Tooltip */}
<span className="absolute top-[2px] left-[48px] hidden group-hover:block w-56 p-2 rounded-lg text-sm bg-white text-slate-600 shadow-lg z-10">
Signing order: Recipients receive emails one after another.
Same order means they all receive at once.
</span>
</p>}
<>
{/* Main Content Section */}
<div className="flex items-start sm:items-center gap-2 sm:gap-3 min-w-0 flex-1">
{/* Order Number */}
<div className="">
{recipients.length > 1 && signingOrder ? (
<div className="relative flex-shrink-0">
<input
type="text"
value={recipient.routingOrder == 0 ? "" : recipient.routingOrder}
onChange={(e) => {
const value = e.target.value;
if (/^\d*$/.test(value)) {
const num = Number(value);
handleOrderChange(recipient.id || recipient.uuid, num);
}
}}
onKeyDown={(e) => {
if (!/[0-9]/.test(e.key) && e.key != "Backspace" && e.key != "Tab") {
e.preventDefault();
}
}}
className="w-8 sm:w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400"
/>
<>
{idx == 0 && recipients.length > 1 && <p className="group">
<>
{/* <BiSolidHelpCircle className="absolute top-[-12px] right-[-10px] text-gray-400 cursor-pointer" />
<span className="absolute top-[2px] left-[48px] hidden group-hover:block w-56 p-2 rounded-lg text-sm bg-white text-slate-600 shadow-lg z-10">
Signing order: Recipients receive emails one after another.
Same order means they all receive at once.
</span> */}
</>
</p>}
</>
</div>
) : (
<>{signingOrder &&
<input
type="number"
disabled
value={recipient.routingOrder}
min={1}
onChange={(e) =>
handleOrderChange(
recipient.id || recipient.uuid,
Number(e.target.value)
)
}
className="w-6 sm:w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)] flex-shrink-0 border-2 border-slate-200"
/>}
</>
)}
</div>
) : (
<input
type="number"
disabled
value={recipient.routingOrder}
min={1}
onChange={(e) =>
handleOrderChange(
recipient.id || recipient.uuid,
Number(e.target.value)
)
}
className="w-6 sm:w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)] flex-shrink-0"
/>
)}
{/* Avatar */}
{/* <div className="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
{/* Avatar */}
{/* <div className="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
</div> */}
{/* Form Fields Section */}
<div className="min-w-0 flex-1">
{/* Role Name and Permissions Row */}
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
{<div className="flex lg:w-1/2">
{transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ?
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "roleName", "Role name")}
</div>
:
<div className="font-semibold text-gray-800 text-sm px-2">
{recipient.roleName}
</div>
}
</div>}
{/* Form Fields Section */}
<div className={`min-w-0 py-4 ${signingOrder ? "pl-8" : "pl-2"} flex-1 border-l-8 ${idx % 2 == 0 ? 'border-[#A5EDFC]' : 'border-[#84EFC3]'}`}>
{/* Permissions - Mobile: Below role, Desktop: Right side */}
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
{transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="flex ps-2.5 sm:flex-row items-start sm:items-center gap-2 sm:gap-4">
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToEdit: !recipient.allowSenderToEdit,
uuid: getRecipientIdentifier(recipient),
})
)
{/* Role Name and Permissions Row */}
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
{<div className="flex lg:w-1/2">
{transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ?
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "roleName", "Role name")}
</div>
:
<div className="font-semibold text-gray-800 text-sm px-2">
{recipient.roleName}
</div>
}
</div>}
{/* Permissions - Mobile: Below role, Desktop: Right side */}
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
{transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="flex ps-2.5 sm:flex-row items-start sm:items-center gap-2 sm:gap-4">
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToEdit: !recipient.allowSenderToEdit,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-blue-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Edit</span>
</label>
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToDelete: !recipient.allowSenderToDelete,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Delete</span>
</label>
</div>
)}
</div>
</div>
{/* Name and Email Fields */}
<div className="mb-2 w-[100%]">
{/* Mobile: Stack vertically, Tablet+: Side by side */}
<div className="flex flex-col justify-center items-center sm:flex-row gap-1 sm:gap-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2 sm:flex-1">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "firstName", "First Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "lastName", "Last Name")}
</div>
</div>
</div>
<div className="w-full sm:flex-1">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
</div>
</div>
{/* Action Buttons - START*/}
{signType != "self-sign" && (
<div className="ml-4">
<button
type="button"
title="Remove recipient"
aria-label={`Remove recipient ${recipient.firstName}`}
onClick={() => handleRemoveRecipient(recipient)}
disabled={
transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
? false
: !recipient.newOne && !recipient.allowSenderToDelete
}
className="accent-blue-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Edit</span>
</label>
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToDelete: !recipient.allowSenderToDelete,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Delete</span>
</label>
</div>
)}
</div>
</div>
{/* Name and Email Fields */}
<div className="mb-2 w-[92%]">
{/* Mobile: Stack vertically, Tablet+: Side by side */}
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2 sm:flex-1">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "firstName", "First Name")}
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50 p-1"
>
<ImBin size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "lastName", "Last Name")}
)}
{/* Action Buttons - END*/}
</div>
</div>
{/* Host Fields (for In-Person Signers) */}
{recipient.type == recipientType.INPERSONSIGNER && (
<div className="mb-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostName", "Host Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
</div>
</div>
</div>
</div>
<div className="w-full sm:flex-1">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
</div>
</div>
</div>
</div>
)}
{/* Host Fields (for In-Person Signers) */}
{recipient.type == recipientType.INPERSONSIGNER && (
<div className="mb-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostName", "Host Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
</div>
</div>
</div>
{/* Recipient Type Radio Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 my-1.5 mx-1 sm:mx-2">
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.SIGNER}
checked={recipient.type == recipientType.SIGNER}
onChange={() => handleTypeChange(recipient, recipientType.SIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.INPERSONSIGNER}
checked={recipient.type == recipientType.INPERSONSIGNER}
onChange={() => handleTypeChange(recipient, recipientType.INPERSONSIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm whitespace-nowrap">In Person Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.CC}
checked={recipient.type == recipientType.CC}
onChange={() => handleTypeChange(recipient, recipientType.CC)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">CC</span>
</label>
</div>
)}
{/* Recipient Type Radio Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 my-1.5 mx-1 sm:mx-2">
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.SIGNER}
checked={recipient.type == recipientType.SIGNER}
onChange={() => handleTypeChange(recipient, recipientType.SIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.INPERSONSIGNER}
checked={recipient.type == recipientType.INPERSONSIGNER}
onChange={() => handleTypeChange(recipient, recipientType.INPERSONSIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm whitespace-nowrap">In Person Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.CC}
checked={recipient.type == recipientType.CC}
onChange={() => handleTypeChange(recipient, recipientType.CC)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">CC</span>
</label>
</div>
</div>
</div>
</>
{/* Action Buttons */}
{signType != "self-sign" && (
<div className="lg:ml-2 absolute top-[-8px] right-[-5px] px-1.5 py-1.5">
<button
type="button"
title="Remove recipient"
aria-label={`Remove recipient ${recipient.firstName}`}
onClick={() => handleRemoveRecipient(recipient)}
disabled={
transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
? false
: !recipient.newOne && !recipient.allowSenderToDelete
}
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50 p-1"
>
<ImBin size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
)}
</div>
</SortableRecipient>

View File

@ -19,7 +19,7 @@ interface RecipientsConIntProps {
userData: UserState;
isModal: boolean;
submitError?: FormSubmitInt;
setEditRecipientsModal?: React.Dispatch<boolean>
setEditRecipientsModal?: React.Dispatch<boolean>;
}
const RecipientsCon: React.FC<RecipientsConIntProps> = ({ transactionTemplateToggle, accountId, transactionId, userData, submitError, isModal = true, setEditRecipientsModal }) => {

View File

@ -7,6 +7,7 @@ import { RootState } from "../../../store/store";
const SortableRecipient = ({ recipient, idx, editRecipientCurrentEmail, children }: any) => {
const selectedRecipientsLen = useSelector((state: RootState) => state.selectedRecipients.recipients.length);
const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder)
const {
attributes,
@ -22,12 +23,12 @@ const SortableRecipient = ({ recipient, idx, editRecipientCurrentEmail, children
transition,
opacity: isDragging ? 0.6 : 1,
};
return (
<div
ref={setNodeRef}
style={style}
className={`relative flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 transition-colors gap-3 lg:gap-0 ${(recipient.uuid || recipient.id) == editRecipientCurrentEmail ? "bg-neutral-100 opacity-40" : ""}
className={`relative flex flex-col lg:flex-row lg:items-center justify-between gap-3 lg:gap-0 my-3 ${(recipient.uuid || recipient.id) == editRecipientCurrentEmail ? "bg-neutral-100 opacity-40" : ""}
${isDragging ? "bg-blue-50 shadow-lg" : ""}
`}
>
@ -35,9 +36,11 @@ const SortableRecipient = ({ recipient, idx, editRecipientCurrentEmail, children
{selectedRecipientsLen && <div
{...attributes}
{...listeners}
className="cursor-grab text-gray-500 flex items-center pr-2 absolute top-0 left-0 z-50"
className="cursor-grab text-gray-500 flex items-center pr-2 absolute top-0 left-0 z-10 h-full"
>
<MdDragIndicator className="absolute top-[10px] left-[10px] text-xl text-gray-300" />
{signingOrder && <MdDragIndicator className="absolute top-[50%] left-[70px] transform -translate-y-1/2 text-gray-300" />}
</div>}
{/* Your card content */}

View File

@ -0,0 +1,49 @@
const CloseConfirmationModal: React.FC<{ open: boolean, onClose: () => void, onConfirm: () => void }> = ({
open,
onClose,
onConfirm,
}) => {
if (!open) return <></>;
return (
<div className="fixed inset-0 z-100 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative z-10 w-full max-w-sm rounded-sm bg-white p-6 shadow-2xl">
<h2 className="text-lg font-semibold text-slate-800">
Leave this page?
</h2>
<p className="mt-2 text-sm text-slate-600">
Your progress may not be saved.
</p>
<div className="mt-6 flex justify-end gap-3">
<button
onClick={onClose}
className="rounded-sm border border-slate-300 px-4 py-2 hover:bg-slate-100 cursor-pointer"
>
Cancel
</button>
<button
onClick={onConfirm}
className="rounded-sm bg-[var(--primary)] px-4 py-2 text-white cursor-pointer"
>
Confirm
</button>
</div>
</div>
</div>
);
};
export default CloseConfirmationModal;

View File

@ -4,7 +4,7 @@ import { RootState } from '../../../store/store'
import api from '../../../config/api';
import RightSideBar, { MASKTYPE, TEXT } from './rightSideBar/RightSideBar';
import TagProperties, { MASKTYPE, TEXT } from './tagProperties/TagProperties';
import { DroppedItem, TagsTypes } from '../../../utlity/types';
import { useNavigate, useParams } from 'react-router-dom';
@ -16,6 +16,13 @@ import typeToIconMap from '../../../helper/iconMap';
import Loader from '../../dashboard/Loader';
import { hideLoader, showLoader } from '../../../store/slices/loaderSlice';
import DroppedItemsTagsCon from './DroppedItemsTagsCon';
import DocumentPages from './documentPages/DocumentPages';
import {
initializeDroppedItems,
setDroppedItems,
clearTransactionTags,
incrementCounter,
} from '../../../store/slices/documentTagsSlice';
export interface DroppedItemsObj {
[key: string]: DroppedItem[]
@ -35,6 +42,9 @@ export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = {
}
interface DocumentContainerProps {
isPages: boolean;
setIsPages: React.Dispatch<React.SetStateAction<boolean>>;
zoomScale: number;
transactionSaving?: boolean;
setTransactionSaving?: React.Dispatch<React.SetStateAction<boolean>>;
isSend?: boolean | null;
@ -42,6 +52,7 @@ interface DocumentContainerProps {
isPreview?: boolean;
aId?: string;
tId?: string;
selectedRecipientIndx?: number;
}
let copiedTags: DroppedItem[] = []
@ -54,10 +65,11 @@ export interface DocumentContainerRef {
handleTransactionProcess: (isSave?: boolean, isNavigate?: boolean, navigateTo?: string) => void;
}
const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProps>(
({ transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null }, ref) => {
({ isPages, zoomScale, transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null, selectedRecipientIndx = 0, setIsPages }, ref) => {
const scaleFactor: number = (zoomScale ?? 100) / 100;
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch();
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
@ -86,11 +98,45 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
const imageRefs = useRef<{ [key: string]: HTMLImageElement | null }>({});
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const [droppedItems, setDroppedItems] = useState<DroppedItemsObj>({});
// handle Tag - Document Pos and size for Responsiveness - START
const [imageSizes, setImageSizes] = useState<Record<string, { width: number; height: number }>>({});
useEffect(() => {
const observers: ResizeObserver[] = [];
Object.keys(imageRefs.current).forEach((key) => {
const el = imageRefs.current[key];
if (!el) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
setImageSizes((prev) => ({
...prev,
[key]: { width, height },
}));
}
});
observer.observe(el);
observers.push(observer);
});
return () => observers.forEach((o) => o.disconnect());
}, [selectedDocuments]);
// handle Tag - Document Pos and size for Responsiveness - START
// Redux state for dropped items
const transactionKey = `${accountId}_${transactionId}`;
const droppedItems = useSelector((state: RootState) => state.documentTags.items[transactionKey] || {});
const droppedItemsCounter = useSelector((state: RootState) => state.documentTags.counter[transactionKey] || 0);
interface HotKeysObj {
isActive: boolean;
@ -141,7 +187,11 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
}
}
setDroppedItems(recTags)
// Dispatch to Redux instead of local state
dispatch(initializeDroppedItems({
transactionKey,
items: recTags
}));
// Handle your response here
} catch (err) {
console.error(err);
@ -155,7 +205,6 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
}, [selectedRecipients, isPreview]);
// FETCHING TAG - END
const [droppedItemsCounter, setDroppedItemsCounter] = useState<number>(1);
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
// NAVIGATING to Dashboard if Transaction is not created - START
@ -195,13 +244,17 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
if ("update" in tag && tag.update) {
updateTags.push(tag)
}
if (updateTags.length && "update" in updateTags[updateTags.length - 1]) {
delete updateTags[updateTags.length - 1].update
delete updateTags[updateTags.length - 1].icon
if (updateTags.length) {
const lastTag = updateTags[updateTags.length - 1];
if ("update" in lastTag || "icon" in lastTag) {
// Create new object without update and icon properties
const { update, icon, ...cleanTag } = lastTag as any;
updateTags[updateTags.length - 1] = cleanTag;
}
}
}
updatedData[recipientId] = updateTags.filter((tag, indx) => !tag.isDeleted);
updatedData[recipientId] = updateTags.filter((tag) => !tag.isDeleted);
}
const requests = Object.entries(updatedData)
@ -222,13 +275,18 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
try {
const responses = await Promise.all(requests);
// Dispatch Redux action instead of local state
const droppedItemsUpdateFalse = Object.fromEntries(
Object.entries(droppedItems).map(([recipientId, tags]) => {
return [recipientId, tags.map(tag => ({ ...tag, update: false }))];
})
)
setDroppedItems(droppedItemsUpdateFalse);
dispatch(setDroppedItems({
transactionKey,
items: droppedItemsUpdateFalse
}));
return responses;
} catch (error) {
console.error("Error submitting recipient tags:", error);
@ -256,7 +314,7 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
let tagsData = groupByRecipientId(updatedTags);
const responses = await sendTagsToRecipients(tagsData, accountId, transactionId);
if(responses && "status" in responses && responses?.status !== 200) return
if (responses && "status" in responses && responses?.status !== 200) return
// return
// SENDING Transaction - START
if (!save) {
@ -300,10 +358,11 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
// HANDLE DROP - START
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number, originalWidth: number, originalHeight: number) => {
event.preventDefault();
const imageElement = document.getElementById(`${docId}-${pageIndex}`) as HTMLImageElement;
const pageRefKey = `${docId}_${pageIndex}`;
const imageElement = imageRefs.current[pageRefKey] || document.getElementById(`${docId}-${pageIndex}`) as HTMLImageElement;
const currentWidth = imageElement.offsetWidth;
const currentHeight = imageElement.offsetHeight;
const currentWidth = imageElement?.offsetWidth || originalWidth;
const currentHeight = imageElement?.offsetHeight || originalHeight;
const scaleX = currentWidth / originalWidth; // 350 / 700 = 0.5
const scaleY = currentHeight / originalHeight; // 225 / 450 = 0.5
@ -333,8 +392,11 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
clientY = event.clientY;
}
let x = clientX - dropZone.left - (dragElemData.width / 2);
let y = clientY - dropZone.top - (dragElemData.height / 2);
// let x = (clientX - dropZone.left) - (dragElemData.width / 2);
// let y = (clientY - dropZone.top) - (dragElemData.height / 2);
let x = (clientX - dropZone.left) / scaleFactor - (dragElemData.width / 2);
let y = (clientY - dropZone.top) / scaleFactor - (dragElemData.height / 2);
// Handling Drop Over Right
// if (dropZone.width < x + dragElemData.width) {
@ -353,8 +415,11 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
// y = 0;
// }
x = Math.max(0, Math.min(x, dropZone.width - dragElemData.width));
y = Math.max(0, Math.min(y, dropZone.height - dragElemData.height));
// x = Math.max(0, Math.min(x, dropZone.width - dragElemData.width));
// y = Math.max(0, Math.min(y, dropZone.height - dragElemData.height));
x = Math.max(0, Math.min(x, originalWidth - dragElemData.width));
y = Math.max(0, Math.min(y, originalHeight - dragElemData.height));
const key = `${docId}_${pageIndex}`;
@ -369,10 +434,10 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
label: dragElemData.label,
// subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '',
dimensions: {
left: x,
top: y,
width: dragElemData.width,
height: dragElemData.height,
left: x / scaleX,
top: y / scaleY,
width: dragElemData.width / scaleX,
height: dragElemData.height / scaleY,
},
styles: null,
documentId: docId,
@ -407,19 +472,22 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
prepData],
}
setDroppedItems(dataToSet);
setDroppedItemsCounter((prev) => prev + 1);
dispatch(setDroppedItems({
transactionKey,
items: dataToSet
}));
dispatch(incrementCounter(transactionKey));
setTimeout(() => {
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
setSelectedTag({ key, index: tagIndex })
}, 1000)
// setTimeout(() => {
// const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
// setSelectedTag({ key, index: tagIndex })
// }, 1000)
};
// HANDLE DROP - END
useEffect(() => {
if (!hotKeysObj.isActive) return
if((!selectedTag?.key || `${selectedTag?.key}` in droppedItems && droppedItems[selectedTag?.key].length == 0)) return;
if ((!selectedTag?.key || `${selectedTag?.key}` in droppedItems && droppedItems[selectedTag?.key].length == 0)) return;
const docCon = document.getElementById("doc-con");
@ -438,9 +506,12 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
console.log("copiedTags", copiedTags)
console.log("updatedKey", updatedKey)
setDroppedItems(prevState => ({
...prevState,
[selectedDoc.key]: updatedKey
dispatch(setDroppedItems({
transactionKey,
items: {
...droppedItems,
[selectedDoc.key]: updatedKey
}
}));
// Handle Ctrl+V
@ -530,103 +601,159 @@ const DocumentContainer = forwardRef<DocumentContainerRef, DocumentContainerProp
return (
<>
{/* Center Doc Container - START */}
<div id="doc-con" className={`zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth`}>
<div id="doc-con"
className={`sm:col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth`}>
<div style={{
width: `${100 * scaleFactor}%`, // ← outer wrapper grows to make room
height: `${100 * scaleFactor}%`,
}}>
<div className="zoom-con"
style={{
transform: `scale(${scaleFactor})`,
transformOrigin: 'top left', // ← change to top left
width: `${100 / scaleFactor}%`,
height: scaleFactor < 1
? `${100 / scaleFactor}%` // zoomed out — expand to fill
: 'auto', // zoomed in — let content grow
}}>
{
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
return (
<div key={indxDoc} className='flex flex-col overflow-x-auto'>
<div
className='py-2 text-sm'>
<p className='text-center'>{doc.name}</p>
</div>
{
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
return (
<div key={indxDoc} className='flex flex-col overflow-x-auto'>
<div
className='py-2 text-sm'>
<p className='text-center'>{doc.name}</p>
</div>
<div className={`flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%] ${isPreview ? "pointer-events-none" : ""}`}>
{
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
const key = `${doc.id}_${page.index}`;
return (
<div
tabIndex={-1}
onFocus={() => handleDocumentFocus(key)}
key={indx}
id={`${doc.id}${indx}`}
onDrop={(e) => handleDrop(e, doc.id, indxDoc, page.index, page.width, page.height)}
onDragOver={handleDragOver}
className='drop-page-in shadow-md relative'
style={{ aspectRatio: `${page.width}/${page.height}` }}
>
<div className={`flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%] ${isPreview ? "pointer-events-none" : ""}`}>
{
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
const key = `${doc.id}_${page.index}`;
return (
<div
tabIndex={-1}
onFocus={() => handleDocumentFocus(key)}
key={indx}
id={`${doc.id}${indx}`}
onDrop={(e) => handleDrop(e, doc.id, indxDoc, page.index, page.width, page.height)}
onDragOver={handleDragOver}
className='drop-page-in shadow-md relative'
style={{ aspectRatio: `${page.width}/${page.height}` }}
>
<img
// style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
// style={{ "width": page.width, "height": page.height, "maxWidth": "100%", "maxHeight": "100%" }}
// style={{ "width": "100%", "height": "100%", "maxWidth": "100%", "maxHeight": "100%" }}
// className={`w-[${(page.width / 2)}px] h-[${(page.height / 2)}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
id={`${doc.id}-${page.index}`}
className={`w-[${page.width / 2}px] h-[${page.height / 2}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
ref={(el) => {
const pageRefKey = `${doc.id}_${page.index}`;
imageRefs.current[pageRefKey] = el;
}}
onLoad={(event) => {
const pageRefKey = `${doc.id}_${page.index}`;
const el = event.currentTarget;
setImageSizes((prev) => ({
...prev,
[pageRefKey]: {
width: el.offsetWidth,
height: el.offsetHeight,
},
}));
}}
draggable="false"
width={page.width}
height={page.height}
// src={`http://10.10.1.99:8083/api/v1/accounts/${accountId}/transactions/${transactionId}/documents/${doc.id}/pages/${page.id}`}
src={page.src}
loading='lazy'
data-doc-img="true"
/>
<img
// style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
// style={{ "width": page.width, "height": page.height, "maxWidth": "100%", "maxHeight": "100%" }}
// style={{ "width": "100%", "height": "100%", "maxWidth": "100%", "maxHeight": "100%" }}
// className={`w-[${(page.width / 2)}px] h-[${(page.height / 2)}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
id={`${doc.id}-${page.index}`}
className={`w-[${page.width / 2}px] h-[${page.height / 2}px] sm:w-[${page.width}px] sm:h-[${page.height}px]`}
ref={(el) => (imageRefs.current[page.index] = el)}
draggable="false"
width={page.width}
height={page.height}
// src={`http://10.10.1.99:8083/api/v1/accounts/${accountId}/transactions/${transactionId}/documents/${doc.id}/pages/${page.id}`}
src={page.src}
loading='lazy'
/>
<DroppedItemsTagsCon
scaleFactor={scaleFactor}
pageWidth={page.width}
pageHeight={page.height}
renderedWidth={imageSizes[key]?.width || imageRefs.current[key]?.offsetWidth || 0}
renderedHeight={imageSizes[key]?.height || imageRefs.current[key]?.offsetHeight || 0}
ref={childRef}
droppedItems={droppedItems}
pageIdkey={key}
selectedTag={selectedTag}
selectedRecipients={selectedRecipients}
setSelectedTag={setSelectedTag}
setDroppedItems={(updateFn: any) => {
const newItems = typeof updateFn === 'function'
? updateFn(droppedItems)
: updateFn;
dispatch(setDroppedItems({
transactionKey,
items: newItems
}));
}}
accountId={accountId}
transactionId={transactionId}
transactionTemplateToggle={transactionTemplateToggle}
tagUndoRef={tagUndoRef}
transactionKey={transactionKey}
/>
<DroppedItemsTagsCon
pageWidth={page.width}
pageHeight={page.height}
renderedWidth={imageRefs.current[page.index]?.offsetWidth || 0}
renderedHeight={imageRefs.current[page.index]?.offsetHeight || 0}
ref={childRef}
droppedItems={droppedItems}
pageIdkey={key}
selectedTag={selectedTag}
selectedRecipients={selectedRecipients}
setSelectedTag={setSelectedTag}
setDroppedItems={setDroppedItems}
accountId={accountId}
transactionId={transactionId}
transactionTemplateToggle={transactionTemplateToggle}
tagUndoRef={tagUndoRef}
/>
{/* Side details of documents Pages and document Name - START */}
<p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-60px] text-slate-600'>
page {indx + 1} of {doc.pages && doc.pages.length}
</p>
{/* <p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs right-[-140px] text-slate-600'>
{/* Side details of documents Pages and document Name - START */}
<p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-60px] text-slate-600'>
page {indx + 1} of {doc.pages && doc.pages.length}
</p>
{/* <p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs right-[-140px] text-slate-600'>
Document {indxDoc + 1} {doc.name}
</p> */}
{/* Side details of documents Pages and document Name - END */}
{/* Side details of documents Pages and document Name - END */}
</div>
)
})
}
</div>
</div>
)
}) : <Loader />
}
</div>
)
})
}
</div>
</div>
)
}) : <Loader />
}
</div>
</div>
</div>
{selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag && <RightSideBar
droppedItems={droppedItems}
selectedTag={droppedItems[selectedTag.key][selectedTag.index]}
setDroppedItems={setDroppedItems}
selTagKey={selectedTag.key}
selTagIndx={selectedTag.index}
signClient={false}
emptySelectedTag={() => setSelectedTag(null)}
/>
}
{/* Note Check Here - Work on Tag Propertieres Apppearrrrrrrrrrrrrrrr ------------------------- */}
<>
{selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag && droppedItems[selectedTag.key] && droppedItems[selectedTag.key][selectedTag.index] && <TagProperties
droppedItems={droppedItems}
selectedTag={droppedItems[selectedTag.key][selectedTag.index]}
setDroppedItems={(updateFn: any) => {
const newItems = typeof updateFn === 'function'
? updateFn(droppedItems)
: updateFn;
dispatch(setDroppedItems({
transactionKey,
items: newItems
}));
}}
selTagKey={selectedTag.key}
selTagIndx={selectedTag.index}
signClient={false}
emptySelectedTag={() => setSelectedTag(null)}
/>
}
</>
{selectedDocuments && selectedDocuments.length > 0 && <DocumentPages
isPages={isPages}
setIsPages={setIsPages}
droppedItems={droppedItems}
/>}
{/* Center Doc Container - END */}
</>
)

View File

@ -9,6 +9,8 @@
// -- handleAddGroupItemClicked
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { useDispatch } from 'react-redux'
import type { AppDispatch } from '../../../store/store'
import { DroppedItemsObj } from './DocumentContainer';
import typeToIconMap from '../../../helper/iconMap';
import { IoMdClose } from 'react-icons/io';
@ -17,8 +19,10 @@ import { FaQuestionCircle } from 'react-icons/fa';
import api from '../../../config/api';
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
import { DroppedItem } from '../../../utlity/types';
import { setDroppedItems as setDroppedItemsAction } from '../../../store/slices/documentTagsSlice';
interface DroppedItemsTagsConProps {
scaleFactor: number,
pageWidth: number,
pageHeight: number,
renderedWidth: number,
@ -28,15 +32,17 @@ interface DroppedItemsTagsConProps {
selectedTag: { key: string; index: number } | null;
selectedRecipients: Recipient[];
setSelectedTag: React.Dispatch<React.SetStateAction<{ key: string; index: number } | null>>;
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>;
setDroppedItems: (updateFn: ((prevState: DroppedItemsObj) => DroppedItemsObj) | DroppedItemsObj) => void;
accountId: string;
transactionId: string;
transactionTemplateToggle: string;
tagUndoRef: React.MutableRefObject<DroppedItem[]>;
transactionKey: string;
}
// 👶 Child component with forwardRef
const DroppedItemsTagsCon = forwardRef(({
scaleFactor,
pageWidth,
pageHeight,
renderedWidth,
@ -50,9 +56,12 @@ const DroppedItemsTagsCon = forwardRef(({
accountId,
transactionId,
transactionTemplateToggle,
tagUndoRef
tagUndoRef,
transactionKey
}: DroppedItemsTagsConProps, ref) => {
const dispatch = useDispatch();
const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 });
const handleTagClick = (key: string, index: number) => {
@ -156,146 +165,217 @@ const DroppedItemsTagsCon = forwardRef(({
handleRemoveTag
}));
// HANDLE TAG MOUSE DOWN - START
// Updated handleTagMouseDown function with proper group border positioning
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
event.preventDefault();
const scale = scaleFactor ?? 1;
const scaleX = renderedWidth && pageWidth ? renderedWidth / pageWidth : 1;
const scaleY = renderedHeight && pageHeight ? renderedHeight / pageHeight : 1;
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
let targetElement = event?.currentTarget?.parentElement;
if (!targetContainer || !targetElement) return;
const containerRect = targetContainer.getBoundingClientRect();
const elementRect = targetElement.getBoundingClientRect();
let clientX = event.clientX
let clientY = event.clientY
const currentItem = droppedItems[key][index];
const tagOriginalLeft = currentItem.dimensions.left;
const tagOriginalTop = currentItem.dimensions.top;
if ((event as any).changedTouches?.[0]) {
clientX = (event as any).changedTouches[0].pageX;
clientY = (event as any).changedTouches[0].pageY;
}
const tagScreenLeft = containerRect.left + tagOriginalLeft * scaleX * scale;
const tagScreenTop = containerRect.top + tagOriginalTop * scaleY * scale;
// Calculate the initial offset (where the mouse was clicked inside the element)
const offsetY = event.clientY - elementRect.top;
const offsetX = event.clientX - elementRect.left;
const clickOffsetX = event.clientX - tagScreenLeft;
const clickOffsetY = event.clientY - tagScreenTop;
positionRef.current = { left: event.clientX - containerRect.left - offsetX, top: event.clientY - containerRect.top - offsetY };
// Track whether mouse actually moved
let hasMoved = false;
const conScrollWidth = targetContainer?.scrollWidth;
const conScrollHeight = targetContainer?.scrollHeight;
// Initialize positionRef with CURRENT position, not 0,0
positionRef.current = { left: tagOriginalLeft, top: tagOriginalTop };
let isGroupAttr = event.currentTarget.hasAttribute("group");
let groupId = null;
if (isGroupAttr) {
groupId = event.currentTarget.getAttribute("group");
}
if (isGroupAttr) groupId = event.currentTarget.getAttribute("group");
let groupElements: NodeListOf<HTMLElement> | null = null;
if (isGroupAttr && groupId) {
groupElements = document.querySelectorAll(`div[group="${groupId}"]`);
// Show initial border position during drag start
handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
handleGroupBorderPosition(groupElements, containerRect, clickOffsetX, clickOffsetY);
}
// let timeOut = null
const handleMouseMove = (event: MouseEvent) => {
// Get new X and Y positions relative to the container
let clientX = event.clientX
let clientY = event.clientY
hasMoved = true; // Marking that it ge tmoved
if ((event as any).changedTouches?.[0]) {
clientX = (event as any).changedTouches[0].pageX;
clientY = (event as any).changedTouches[0].pageY;
}
const currentContainerRect = targetContainer!.getBoundingClientRect();
let left = clientX - containerRect.left - offsetX;
let top = clientY - containerRect.top - offsetY;
const mouseScreenX = event.clientX - currentContainerRect.left;
const mouseScreenY = event.clientY - currentContainerRect.top;
// 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));
const tagScreenX = mouseScreenX - clickOffsetX;
const tagScreenY = mouseScreenY - clickOffsetY;
// Save in ref (fast, no re-render)
positionRef.current = { left, top };
const originalLeft = tagScreenX / scale / scaleX;
const originalTop = tagScreenY / scale / scaleY;
// Update DOM directly
targetElement!.style.top = `${top}px`;
targetElement!.style.left = `${left}px`;
positionRef.current = { left: originalLeft, top: originalTop };
targetElement!.style.left = `${originalLeft * scaleX}px`;
targetElement!.style.top = `${originalTop * scaleY}px`;
console.log(top, left, "top, left Line 283")
// Apply the movement to the element first
// setDroppedItems(prevState => ({
// ...prevState,
// [key]: Array.isArray(prevState[key])
// ? prevState[key].map((item, i) =>
// i == index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item
// )
// : [],
// }));
// Update group border position during drag if it's a group element
if (groupElements != null && groupElements.length) {
// Use a small timeout to ensure DOM is updated before recalculating borders
requestAnimationFrame(() => {
handleGroupBorderPosition(groupElements!, containerRect, offsetX, offsetY);
handleGroupBorderPosition(groupElements!, currentContainerRect, clickOffsetX, clickOffsetY);
});
}
};
const handleMouseUp = () => {
// CRITICAL: Ensure group borders are properly positioned after drag ends
if (groupElements != null && groupElements.length && groupId) {
// Multiple approaches to ensure the positioning works:
// 1. Immediate update
setTimeout(() => {
handleIGroupBorderPosition(groupId);
}, 10);
// 2. Secondary update to ensure it's properly positioned
setTimeout(() => {
handleIGroupBorderPosition(groupId);
}, 100);
// 3. Final update using requestAnimationFrame for DOM sync
requestAnimationFrame(() => {
setTimeout(() => {
handleIGroupBorderPosition(groupId);
}, 50);
});
const handleMouseUp = (e: MouseEvent) => {
// Just clean not update - NOTE
if (!hasMoved) {
document.removeEventListener("pointermove", handleMouseMove);
document.removeEventListener("pointerup", handleMouseUp);
return;
}
const { left, top } = positionRef.current;
console.log(top, left, "HandleTag Mouse Up top, left Line 283")
let hoveredDocId: any = ""
let hoveredPageIndex: string = ""
let { left, top } = positionRef.current;
const [currentDocumentId, currentPageIndex] = key.split("_");
setDroppedItems(prevState => ({
...prevState,
[key]: Array.isArray(prevState[key])
? prevState[key].map((item, i) =>
i == index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item
)
: [],
}));
let tagDocumentUpdate = {
update: false,
updatingPage: false,
updatingDocument: false,
pageIndex: 0,
documentId: ""
}
targetElement.style.pointerEvents = "none";
const el = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
const hoveredContainer = el?.closest(".drop-page-in");
targetElement.style.pointerEvents = "auto";
if (hoveredContainer) {
const imgElem: HTMLElement | null = hoveredContainer.querySelector("[data-doc-img]");
hoveredDocId = imgElem ? imgElem.getAttribute("id") || "" : "";
hoveredDocId = hoveredDocId.split("-");
hoveredPageIndex = hoveredDocId[hoveredDocId.length - 1];
hoveredDocId.pop();
hoveredDocId = hoveredDocId.join("-");
}
if (hoveredContainer === null || hoveredDocId != currentDocumentId) {
targetElement!.style.left = `${tagOriginalLeft * scaleX}px`;
targetElement!.style.top = `${tagOriginalTop * scaleY}px`;
document.removeEventListener("pointermove", handleMouseMove);
document.removeEventListener("pointerup", handleMouseUp);
return;
}
if (key in droppedItems && index in droppedItems[key]) {
const documentId = droppedItems[key][index]["documentId"];
const pageIndex = droppedItems[key][index]["pageIndex"];
tagDocumentUpdate.pageIndex = pageIndex;
tagDocumentUpdate.documentId = documentId;
if (hoveredDocId == documentId) {
if (hoveredPageIndex != pageIndex) {
tagDocumentUpdate.updatingPage = true;
tagDocumentUpdate.pageIndex = hoveredPageIndex;
tagDocumentUpdate.documentId = documentId;
}
} else {
tagDocumentUpdate.updatingDocument = true;
tagDocumentUpdate.updatingPage = true;
tagDocumentUpdate.pageIndex = hoveredPageIndex;
tagDocumentUpdate.documentId = hoveredDocId;
}
}
if (groupElements != null && groupElements.length && groupId) {
setTimeout(() => { handleIGroupBorderPosition(groupId); }, 10);
setTimeout(() => { handleIGroupBorderPosition(groupId); }, 100);
requestAnimationFrame(() => { setTimeout(() => { handleIGroupBorderPosition(groupId); }, 50); });
}
setDroppedItems(prevState => {
const isSameDocument = currentDocumentId == tagDocumentUpdate.documentId;
if (!isSameDocument) return prevState;
const isSamePage = currentPageIndex == tagDocumentUpdate.pageIndex;
if (isSameDocument && isSamePage) {
const clampedLeft = Math.max(0, Math.min(left, pageWidth - currentItem.dimensions.width));
const clampedTop = Math.max(0, Math.min(top, pageHeight - currentItem.dimensions.height));
return {
...prevState,
[key]: Array.isArray(prevState[key])
? prevState[key].map((item, i) =>
i === index ? {
...item,
documentId: tagDocumentUpdate.documentId,
pageIndex: tagDocumentUpdate.pageIndex,
dimensions: { ...item.dimensions, left: clampedLeft, top: clampedTop },
update: true,
} : item
) : [],
};
}
const newContainerRect = hoveredContainer?.getBoundingClientRect() as DOMRect;
const newMouseScreenX = e.clientX - newContainerRect.left;
const newMouseScreenY = e.clientY - newContainerRect.top;
left = Math.max(0, Math.min(
(newMouseScreenX - clickOffsetX) / scale / scaleX,
pageWidth - currentItem.dimensions.width
));
top = Math.max(0, Math.min(
(newMouseScreenY - clickOffsetY) / scale / scaleY,
pageHeight - currentItem.dimensions.height
));
const targetKey = `${tagDocumentUpdate.documentId}_${tagDocumentUpdate.pageIndex}`;
const currentItems = Array.isArray(prevState[key]) ? [...prevState[key]] : [];
const movedItem = currentItems[index];
if (!movedItem) return prevState;
currentItems.splice(index, 1);
return {
...prevState,
[key]: currentItems,
[targetKey]: [
...(Array.isArray(prevState[targetKey]) ? prevState[targetKey] : []),
{ ...movedItem, documentId: tagDocumentUpdate.documentId, pageIndex: tagDocumentUpdate.pageIndex, dimensions: { ...movedItem.dimensions, left, top }, update: true },
],
};
});
document.removeEventListener("pointermove", handleMouseMove);
document.removeEventListener("pointerup", handleMouseUp);
};
// Add event listeners
document.addEventListener("pointermove", handleMouseMove);
document.addEventListener("pointerup", handleMouseUp);
};
// HANDLE TAG MOUSE DOWN - END
// HANDLE EXPAND TAG - START
const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
event.preventDefault(); // Prevent text selection issues
event.preventDefault();
const scale = scaleFactor ?? 1;
const scaleX = renderedWidth && pageWidth ? renderedWidth / pageWidth : 1;
const scaleY = renderedHeight && pageHeight ? renderedHeight / pageHeight : 1;
// Combined scale: zoom * render ratio
const combinedScaleX = scaleX * scale;
const combinedScaleY = scaleY * scale;
const expandingElemIcon = event.currentTarget;
const targetElement = expandingElemIcon.nextElementSibling?.parentElement;
@ -308,49 +388,41 @@ const DroppedItemsTagsCon = forwardRef(({
const startX = event.clientX;
const startY = event.clientY;
const startWidth = elementRect.width;
const startHeight = elementRect.height;
const startWidth = elementRect.width / combinedScaleX;
const startHeight = elementRect.height / combinedScaleY;
const handleMouseMove = (event: MouseEvent) => {
const dropZone = targetContainer.getBoundingClientRect();
const deltaX = (event.clientX - startX) / combinedScaleX;
const deltaY = (event.clientY - startY) / combinedScaleY;
const dropZone: DOMRect = targetContainer?.getBoundingClientRect() as DOMRect;
const droppedElemRect: DOMRect = targetElement?.getBoundingClientRect() as DOMRect;
let newWidth = startWidth + deltaX;
let newHeight = startHeight + deltaY;
console.log(dropZone, "dropZone Line 309")
console.log(droppedElemRect, "rect Line 309")
if (newWidth > 400) newWidth = 400;
if (newHeight > 200) newHeight = 200;
if (newWidth < 20) newWidth = 20;
if (newHeight < 10) newHeight = 10;
let newWidth = startWidth + (event.clientX - startX);
let newHeight = startHeight + (event.clientY - startY);
const maxWidth = dropZone.width / combinedScaleX;
const maxHeight = dropZone.height / combinedScaleY;
const currentLeft = droppedItems[key][index].dimensions.left;
const currentTop = droppedItems[key][index].dimensions.top;
if (currentLeft + newWidth > maxWidth) newWidth = maxWidth - currentLeft;
if (currentTop + newHeight > maxHeight) newHeight = maxHeight - currentTop;
if (newHeight > 200 || newWidth > 400) {
if (newHeight > 200) {
newHeight = 200
}
if (newWidth > 400) {
newWidth = 400
}
}
if ((dropZone.right - (droppedElemRect.left + newWidth)) < 0) {
newWidth = startWidth
}
if ((dropZone.bottom - (droppedElemRect.bottom + newHeight)) < 0) {
newHeight = startHeight
}
// Apply the new width & height
setDroppedItems({
...droppedItems,
[key]: Array.isArray(droppedItems[key]) // Ensure it's an array
[key]: Array.isArray(droppedItems[key])
? droppedItems[key].map((item, i) =>
i == index ? { ...item, dimensions: { ...item.dimensions, width: newWidth, height: newHeight }, update: true } : item
i === index
? { ...item, dimensions: { ...item.dimensions, width: newWidth, height: newHeight }, update: true }
: item
)
: [], // Provide a fallback in case it's undefined or not an array
: [],
});
};
const handleMouseUp = () => {
@ -364,14 +436,15 @@ const DroppedItemsTagsCon = forwardRef(({
// HANDLE EXPAND TAG - END
const removeGroupBorders = (groupId: string) => {
console.log(`Removing borders for group: ${groupId}`);
const borderPositions = ['top', 'right', 'bottom', 'left'];
borderPositions.forEach(position => {
const borderElement = document.querySelector(`[border-group='${groupId}'][border-pos='${position}']`);
if (borderElement && borderElement.parentNode) {
borderElement.parentNode.removeChild(borderElement);
if (borderElement) {
borderElement.remove?.();
if (!borderElement.remove && borderElement.parentNode && borderElement.parentNode.contains(borderElement)) {
borderElement.parentNode.removeChild(borderElement);
}
}
});
};
@ -384,34 +457,46 @@ const DroppedItemsTagsCon = forwardRef(({
return;
}
let targetContainer = groupElements[0].parentElement?.parentElement;
if (!targetContainer) {
console.log("No target container found");
return;
}
const containerRect = targetContainer.getBoundingClientRect();
// Use consistent offset values
const offsetX = 10;
const offsetY = 10;
// Ensure DOM is fully updated before positioning
requestAnimationFrame(() => {
requestAnimationFrame(() => {
handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
let targetContainer = groupElements[0].parentElement?.parentElement;
if (!targetContainer) return;
let containerRect = targetContainer.getBoundingClientRect();
handleGroupBorderPosition(groupElements, containerRect, 10, 10);
});
});
};
const handleGroupBorderPosition = (groupElements: NodeListOf<HTMLElement>, containerRect: DOMRect, offsetX: number, offsetY: number) => {
useEffect(() => {
if (!selectedTag) {
const borderElems = document.querySelectorAll("[border-group]")
if (borderElems && borderElems.length) {
borderElems.forEach(elem => {
elem.style.display = "none";
})
}
}
}, [selectedTag])
const handleGroupBorderPosition = (
groupElements: NodeListOf<HTMLElement>,
containerRect: DOMRect,
offsetX: number,
offsetY: number
) => {
if (!groupElements || groupElements.length == 0) return;
const groupId = groupElements[0].getAttribute('group');
if (!groupId) return;
// Calculate the bounding box of all group elements
const scale = scaleFactor ?? 1;
const scaleX = renderedWidth && pageWidth ? renderedWidth / pageWidth : 1;
const scaleY = renderedHeight && pageHeight ? renderedHeight / pageHeight : 1;
const combinedScaleX = scaleX * scale;
const combinedScaleY = scaleY * scale;
let minLeft = Infinity, minTop = Infinity, maxRight = -Infinity, maxBottom = -Infinity;
groupElements.forEach(element => {
@ -419,10 +504,11 @@ const DroppedItemsTagsCon = forwardRef(({
if (!parentElement) return;
const rect = parentElement.getBoundingClientRect();
const relativeLeft = rect.left - containerRect.left;
const relativeTop = rect.top - containerRect.top;
const relativeRight = relativeLeft + rect.width;
const relativeBottom = relativeTop + rect.height;
const relativeLeft = (rect.left - containerRect.left) / combinedScaleX;
const relativeTop = (rect.top - containerRect.top) / combinedScaleY;
const relativeRight = relativeLeft + rect.width / combinedScaleX;
const relativeBottom = relativeTop + rect.height / combinedScaleY;
minLeft = Math.min(minLeft, relativeLeft);
minTop = Math.min(minTop, relativeTop);
@ -430,7 +516,6 @@ const DroppedItemsTagsCon = forwardRef(({
maxBottom = Math.max(maxBottom, relativeBottom);
});
// Add some padding around the group
const padding = 10;
minLeft -= padding;
minTop -= padding;
@ -440,39 +525,36 @@ const DroppedItemsTagsCon = forwardRef(({
const width = maxRight - minLeft;
const height = maxBottom - minTop;
// Update or create border elements
const borderPositions = ['top', 'right', 'bottom', 'left'];
borderPositions.forEach(position => {
let borderElement = document.querySelector(`[border-group='${groupId}'][border-pos='${position}']`) as HTMLElement;
let borderElement = document.querySelector(
`[border-group='${groupId}'][border-pos='${position}']`
) as HTMLElement;
if (!borderElement) {
// Create border element if it doesn't exist
borderElement = document.createElement('div');
borderElement.className = `group-border ${position == 'bottom' ? 'bottom-border' : ''}`;
borderElement.setAttribute('border-group', groupId);
borderElement.setAttribute('border-pos', position);
// Add the + icon for bottom border
if (position == 'bottom') {
const addIcon = document.createElement('div');
addIcon.innerHTML = '<IoAdd />'; // You might need to adjust this based on your icon system
addIcon.innerHTML = '<IoAdd />';
addIcon.onclick = () => {
// Find the original item to pass to handleAddGroupItemClicked
const originalItem = droppedItems[key]?.find(item => 'group' in item && item.group == groupId);
if (originalItem) {
handleAddGroupItemClicked(key, originalItem);
}
const originalItem = droppedItems[pageIdkey]?.find(
item => 'group' in item && item.group == groupId
);
if (originalItem) handleAddGroupItemClicked(pageIdkey, originalItem);
};
borderElement.appendChild(addIcon);
}
// Append to container
groupElements[0].parentElement?.parentElement?.appendChild(borderElement);
}
// Position the border element
borderElement.style.position = 'absolute';
borderElement.style.display = 'block';
borderElement.style.pointerEvents = position == 'bottom' ? 'auto' : 'none';
switch (position) {
@ -587,24 +669,35 @@ const DroppedItemsTagsCon = forwardRef(({
const IconComponent = typeToIconMap[type] || <FaQuestionCircle />;
const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id == item.recipientId);
const scaleX = renderedWidth / Math.round(pageWidth);
const scaleY = renderedHeight / Math.round(pageHeight);
const scaleX = renderedWidth && pageWidth ? renderedWidth / pageWidth : 1;
const scaleY = renderedHeight && pageHeight ? renderedHeight / pageHeight : 1;
return (
<React.Fragment key={index}>
<div
// key={index}
tabIndex={0}
draggable="false"
data-tag-id={index}
data-drop-key={pageIdkey}
data-tag-selected={index == selectedTag?.index}
className="absolute rounded dropped-item"
onClick={() => handleTagClick(pageIdkey, index)}
className="absolute rounded dropped-item z-50"
onClick={(e) => {
e.currentTarget.focus();
handleTagClick(pageIdkey, index)
}}
onBlur={(e: React.FocusEvent) => {
const nextElement = e.relatedTarget as HTMLElement | null;
if (nextElement?.hasAttribute("data-type-group") || nextElement?.closest("[data-tag-properties]")) return;
setSelectedTag(null);
}}
style={{
left: `${item.dimensions.left * scaleX}px`,
top: `${item.dimensions.top * scaleY}px`,
width: item.type == "radio" ? `${item.dimensions.width + 20}px` : `${item.dimensions.width * scaleX}px`,
height: item.type == "radio" ? `max-content` : `${item.dimensions.height * scaleY}px`
// width: item.type == "radio" ? `${item.dimensions.width + 20}px` : `${item.dimensions.width * scaleX}px`,
// height: item.type == "radio" ? `max-content` : `${item.dimensions.height * scaleY}px`
width: `${item.dimensions.width * scaleX}px`,
height: `${item.dimensions.height * scaleY}px`
}}
// style={{
@ -640,10 +733,14 @@ const DroppedItemsTagsCon = forwardRef(({
{/* Expand Circle - STARTS */}
{item.type != "radio" && item.type != "checkbox" && <div
{/* {item.type != "radio" && item.type != "checkbox" && <div
onPointerDown={(e) => handleExpandTag(e, pageIdkey, index)}
className='w-2.5 h-2.5 absolute bg-red-400 rounded-full bottom-[-8px] right-[-8px] cursor-nw-resize hover:bg-blue-400'>
</div>}
</div>} */}
<div
onPointerDown={(e) => handleExpandTag(e, pageIdkey, index)}
className='w-2.5 h-2.5 absolute bg-red-400 rounded-full bottom-[-8px] right-[-8px] cursor-nw-resize hover:bg-blue-400'>
</div>
{/* Expand Circle - ENDS */}
{/* Tag - START */}
@ -671,15 +768,22 @@ const DroppedItemsTagsCon = forwardRef(({
{
"group" in item && item.group && "isGroupExist" in item && !item.isGroupExist &&
<>
<div className="group-border" border-group={item.group} border-pos="top"></div>
<div className="group-border" border-group={item.group} border-pos="right"></div>
<div className="group-border bottom-border" border-group={item.group} border-pos="bottom">
<div
onClick={() => handleAddGroupItemClicked(pageIdkey, item)}>
<div className="group-border z-50" border-group={item.group} border-pos="top"></div>
<div className="group-border z-50" border-group={item.group} border-pos="right"></div>
<div className="group-border z-50 bottom-border" border-group={item.group} border-pos="bottom">
<button
data-type-group="true"
onClick={() => handleAddGroupItemClicked(pageIdkey, item)}
onBlur={(e: React.FocusEvent) => {
const nextElement = e.relatedTarget as HTMLElement | null;
if (nextElement?.hasAttribute("groupp")) return;
setSelectedTag(null);
}}
>
<IoAdd />
</div>
</button>
</div>
<div className="group-border" border-group={item.group} border-pos="left"></div>
<div className="group-border z-50" border-group={item.group} border-pos="left"></div>
</>
}

View File

@ -132,7 +132,7 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP'];
const data = res.data.filter((obj, indx) => {
if (obj.type == "STAMP" || obj.type == "RADIO" || obj.type == "CHECKBOX" || obj.type == "ATTACHMENT" || obj.type == "SELECT") return false
if (obj.type == "STAMP" || obj.type == "RADIO" || obj.type == "CHECKBOX" || obj.type == "ATTACHMENT" || obj.type == "SELECT") return true
return true
});
@ -308,7 +308,7 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
// );
return (
<div className='flex flex-row sm:flex-wrap gap-2 justify-start items-start w-full overflow-x-auto'>
<div className='flex flex-row sm:flex-wrap gap-2 justify-start items-start overflow-x-auto w-[90vw] sm:w-full px-2'>
{tagsData.map((field, index) => {
const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
@ -316,26 +316,29 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
if (!IconComponent) return null;
return (
// <div key={index} draggable={true} onDragStart={(e) => handleDragStart(e, field.label, recipientId)} className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300">
<div
draggable={true}
key={index}
onPointerDown={(e) => handlePointerDown(e, field.type.toLowerCase(), recipientId, recipientIndx, field)}
className={`border-[1px] flex flex-col justify-center bg-white p-2 w-[75px] h-[75px] items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300`}
style={{ borderColor: color }}
>
<>
{/* <div key={index} draggable={true} onDragStart={(e) => handleDragStart(e, field.label, recipientId)} className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300"> */}
<div
draggable={true}
key={index}
onPointerDown={(e) => handlePointerDown(e, field.type.toLowerCase(), recipientId, recipientIndx, field)}
className={`border-[1px] flex w-full justify-left bg-white items-center gap-1 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300`}
style={{ borderColor: color }}
>
<div
className={`p-1 rounded-sm text-white`}
// style={{ background: color }}
>
{IconComponent}
</div>
<p
className="capitalize text-xs">
{field.type.toLowerCase()}
</p>
</div>
<div
className={`p-1 rounded-sm text-white`}
// style={{ background: color }}
>
{IconComponent}
</div>
<p
className="capitalize text-xs">
{field.type.toLowerCase()}
</p>
</div>
{/* </div> */}
</>
)
}

View File

@ -6,13 +6,11 @@ import FieldList from './FieldList';
import { addColorsToRecipient, fetchRecipients, recipientType, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice';
import { IoCheckmark } from 'react-icons/io5'
import { MdKeyboardArrowDown, MdOutlineNavigateNext } from 'react-icons/md'
import { MdKeyboardArrowDown } from 'react-icons/md'
import { useParams } from 'react-router-dom';
import { fetchDocuments } from '../../../store/slices/documentSlice';
import { GrFormPrevious } from 'react-icons/gr';
import { LuTableOfContents } from 'react-icons/lu';
import { IoIosDocument, IoMdClose } from 'react-icons/io';
import { FaAngleUp, FaChevronRight } from 'react-icons/fa';
import { FaAngleUp } from 'react-icons/fa';
import { SlDocs } from "react-icons/sl";
interface LeftSideBarProps {
isFields: boolean;
@ -23,7 +21,7 @@ interface LeftSideBarProps {
handleSetIsPages: (setTo: boolean) => void;
}
const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRecipientIndx, setSelectedRecipient, handleSetIsFields, handleSetIsPages }) => {
const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, selectedRecipientIndx, setSelectedRecipient, handleSetIsFields, handleSetIsPages }) => {
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch();
@ -44,8 +42,6 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const [currentPage, setCurrentPage] = useState(0);
useEffect(() => {
if (selectedRecipients.length == 0) {
dispatch(fetchRecipients({ accountId, transactionId }));
@ -60,7 +56,6 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
// For Recipient
const [dropDownToggle, setDropDownToggle] = useState(false);
const [dropDownTogglePage, setDropDownTogglePage] = useState<string>("");
const selectedRecipient = selectedRecipients[selectedRecipientIndx];
@ -84,26 +79,26 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
}, [selectedRecipient])
if (selectedRecipient && selectedRecipient.type == recipientType.CC) {
return <div className='col-span-2 bg-white h-full p-4 overflow-y-auto'></div>;
return <div className='sm:col-span-2 bg-white h-full p-4 overflow-y-auto'></div>;
}
return (
<>
{/* Left Side Bar - START */}
<div className='col-span-2'>
<div className='sm:col-span-2'>
{
(isPages || isFields) && <div className={`sm:block p-4 absolute sm:top-[0] rounded-tr-md z-50 sm:relative sm:col-span-2 bg-white border-t-1 border-r-1 border-slate-300 sm:h-[calc(100vh-40px)] overflow-y-auto sm:w-auto ${isFields ? "h-[150px] bottom-0 overflow-x-hidden w-full" : "h-[calc(100vh-40px)]"} `}>
<div
(isFields || window.innerWidth > 638) && <div className={`sm:block p-4 absolute sm:top-[0] rounded-tr-md z-50 sm:relative sm:col-span-2 bg-white border-t-1 border-r-1 border-slate-300 sm:h-[calc(100vh-40px)] overflow-y-auto sm:w-auto ${(isFields || window.innerWidth > 638) ? "h-[100px] bottom-0 overflow-x-hidden w-full" : "h-[calc(100vh-40px)]"} `}>
{/* <div
onClick={() => {
handleSetIsPages(false)
// handleSetIsPages(false)
handleSetIsFields(false)
}}
className='absolute sm:hidden top-[5px] right-[10px] bg-white rounded-full text-2xl z-50'>
<IoMdClose />
</div>
</div> */}
<div className='hidden sm:flex flex-col md:flex-row justify-between items-center bg-slate-100 p-1.5 rounded-2xl border-1 border-gray-200 mb-3'>
{/* <div className='hidden sm:flex flex-col md:flex-row justify-between items-center bg-slate-100 p-1.5 rounded-2xl border-1 border-gray-200 mb-3'>
<div
onClick={() => handleSetIsFields(!isFields)}
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isFields ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300`}>
@ -112,15 +107,15 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
</div>
<div
onClick={() => handleSetIsPages(!isPages)}
// onClick={() => handleSetIsPages(!isPages)}
className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isPages ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300`}>
<IoIosDocument />
<p className='tracking-wide'>Pages</p>
</div>
</div>
</div> */}
{selectedRecipients.length > 0 && isFields && <>
{selectedRecipients.length > 0 && (isFields || window.innerWidth > 638) && <>
{/* Fields List and Recipeint - Start */}
{/* Side Bar showing Recipients */}
<div className='sm:relative sm:top-0 sm:left-0 fixed w-full'>
@ -183,7 +178,10 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
<div
id='tags-con-dash-left'
className='mt-2 w-full h-[400px] sm:h-auto'>
<FieldList color={selectedRecipient?.color || '#2d2d2d'} recipientId={selectedRecipient.id} recipientIndx={selectedRecipientIndx} />
<FieldList
color={selectedRecipient?.color || '#2d2d2d'}
recipientId={selectedRecipient.id}
recipientIndx={selectedRecipientIndx} />
</div>
</div>
@ -191,99 +189,6 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
{/* Fields List and Recipeint - End */}
</>}
{isPages && <>
{/* Pages - START */}
<div className='flex flex-col gap-2'>
{selectedDocuments.length && selectedDocuments.map((document, indx) => {
return (
<div
key={indx}
// tabIndex={1}
// onBlur={() => setDropDownTogglePage('')}
>
<div
onClick={() => {
setDropDownTogglePage(`${(dropDownTogglePage == 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`);
setCurrentPage(0);
window.location.hash = document.id + "" + 0;
}}
className={`p-2 ${(dropDownTogglePage == 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
{
document.pages != undefined && <img
key={indx}
className='rounded-sm shadow'
src={"data:image/png;base64," + document.thumbnail}
alt="" />
}
<div
className='flex items-center justify-between w-full text-xs rounded-sm cursor-pointer text-slate-100'>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[18ch]'>{document.name}</p>
<MdKeyboardArrowDown className={`${(dropDownTogglePage == 'pageDropDown' + indx) ? 'rotate-180' : ''}`} />
</div>
</div>
{
dropDownTogglePage == 'pageDropDown' + indx &&
<div className="my-1 flex justify-center items-center gap-2 flex-wrap">
{/* Prev Button */}
<a
href={"#" + document.id + (currentPage - 1)}
className={`cursor-pointer px-2 py-1 shadow rounded-sm text-sm bg-slate-200 hover:border-[var(--primary)] transition-all duration-300 ${currentPage == 0 ? "pointer-events-none opacity-50" : ""
}`}
onClick={(e) => {
if (currentPage == 0) e.preventDefault();
else setCurrentPage(currentPage - 1);
}}
>
<GrFormPrevious />
</a>
{/* Page Selector */}
<select
className="w-[58px] py-1 text-xs text-slate-400 outline-none"
value={currentPage}
onChange={(e) => {
const newPage = Number(e.target.value);
setCurrentPage(newPage);
window.location.hash = document.id + "" + newPage;
}}
>
{document.pages != undefined && document.pages.map((_, pageIndex) => (
<option key={pageIndex} value={pageIndex}>
{pageIndex + 1}
</option>
))}
</select>
<span className="text-xs text-slate-600">
/ {document.pages?.length}
</span>
{/* Next Button */}
<a
href={"#" + document.id + (currentPage + 1)}
className={`cursor-pointer px-2 py-1 shadow rounded-sm text-sm bg-slate-200 hover:border-[var(--primary)] transition-all duration-300 ${currentPage == document.pages?.length - 1 ? "pointer-events-none opacity-50" : ""
}`}
onClick={(e) => {
if (currentPage == document.pages?.length - 1) e.preventDefault();
else setCurrentPage(currentPage + 1);
}}
>
<MdOutlineNavigateNext />
</a>
</div>
}
</div>
)
})}
</div>
{/* Pages - END */}
</>}
</div>
}
{/* Left Side Bar - END */}
@ -292,29 +197,30 @@ const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, isPages, selectedRe
<div>
{/* Left Side Pages */}
{!isPages && <div
{<div
onClick={() => {
handleSetIsPages(true)
handleSetIsFields(false)
// handleSetIsFields(false)
}}
className='sm:hidden fixed top-[8%] left-0 bg-[var(--primary)] z-50 flex items-center text-white rounded-tr-md rounded-br-md cursor-pointer opacity-50 hover:opacity-100 transition-all duration-300'>
<div className='py-2 pl-2 pr-1'>
<FaAngleUp className='rotate-90' />
<p className='flex flex-col justify-center items-center text-xs tracking-widest mt-1 pt-1 border-t-1 border-white'>
className='sm:hidden fixed top-[8%] left-0 bg-[var(--primary)] z-50 flex items-center text-white rounded-tr-sm rounded-br-sm cursor-pointer opacity-50 hover:opacity-100 transition-all duration-300'>
<div className='py-2 pl-2 pr-2'>
{/* <FaAngleUp className='rotate-90' /> */}
<SlDocs />
{/* <p className='flex flex-col justify-center items-center text-xs tracking-widest mt-1 pt-1 border-t-1 border-white'>
<span>P</span>
<span>A</span>
<span>G</span>
<span>E</span>
<span>S</span>
</p>
</p> */}
</div>
</div>}
{!isFields && <div
className='sm:hidden fixed bottom-0 left-0 rounded-tr-2xl rounded-tl-2xl bg-[var(--primary)] z-50 w-full flex justify-center text-white cursor-pointer opacity-50 hover:opacity-100 transition-all duration-300'
{(!isFields && window.innerWidth <= 638) && <div
className='sm:hidden fixed bottom-0 left-0 rounded-tr-sm rounded-tl-sm bg-[var(--primary)] z-50 w-full flex justify-center text-white cursor-pointer opacity-50 hover:opacity-100 transition-all duration-300'
onClick={() => {
handleSetIsPages(false)
// handleSetIsPages(false)
handleSetIsFields(true)
}}
>

View File

@ -3,18 +3,19 @@ import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../store/store'
import { FaCheck, FaSignature } from 'react-icons/fa'
import { IoIosDocument } from 'react-icons/io'
import { LuTableOfContents } from 'react-icons/lu'
import { FaAngleRight, FaCheck, FaRegCopy, FaSignature } from 'react-icons/fa'
import { IoIosDocument, IoMdHome } from 'react-icons/io'
import { LuRedo2, LuUndo2 } from 'react-icons/lu'
import { MdKeyboardArrowDown } from 'react-icons/md'
import { RiResetRightLine } from 'react-icons/ri'
import { fetchRecipients, recipientType, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'
import { fetchDocuments } from '../../../store/slices/documentSlice'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { Bounce, toast } from 'react-toastify'
import { FiChevronDown } from 'react-icons/fi'
import { useNavigate, useParams } from 'react-router-dom'
import RecipientsCon from '../processStepOne/RecipientsCon'
import MessageCon from '../processStepOne/MessageCon'
import { IoArrowBackSharp } from 'react-icons/io5'
import { FaRegPaste } from 'react-icons/fa6'
import ConfirmSendModal from './TransactionSendConfirmationModal'
import CloseConfirmationModal from './CloseCofirmationModal'
interface NavTwoProps {
transactionSaving?: boolean;
@ -29,6 +30,7 @@ interface NavTwoProps {
aId?: string | null
tId?: string | null
onSendClick?: (isSave?: boolean, navigate?: boolean, navigateTo?: string) => void;
onZoomChange?: React.Dispatch<React.SetStateAction<number>>
}
@ -54,20 +56,37 @@ const fitObj = [
label: "200%",
value: "200"
},
{
label: "250%",
value: "250"
},
{
label: "300%",
value: "300"
},
{
label: "350%",
value: "350"
},
{
label: "400%",
value: "400"
},
]
const NavTwo: React.FC<NavTwoProps> = ({
transactionSaving = false,
isFields,
setIsFields,
isPages,
setIsPages,
isSend,
setIsSend,
// isFields,
// setIsFields,
// isPages,
// setIsPages,
// isSend,
// setIsSend,
isPreview = false,
aId = null,
tId = null,
onSendClick,
onZoomChange
}) => {
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch();
@ -75,6 +94,11 @@ const NavTwo: React.FC<NavTwoProps> = ({
const [showActionsDropDown, setShowActionsDropDown] = useState<boolean>(false);
const [editRecipientsModal, setEditRecipientsModal] = useState<boolean>(false);
const [messageModal, setMessageModal] = useState<boolean>(false);
const [confirmSendModalOpen, setConfirmSendModalOpen] = useState({
"open": false,
"confirm": false,
"loading": false
});
console.log(editRecipientsModal)
@ -100,6 +124,8 @@ const NavTwo: React.FC<NavTwoProps> = ({
// const [transactionSaving, setTransactionSaving] = useState<boolean>(false);
const [dotCount, setDotCount] = useState(0);
const [closePage, setClosePage] = useState(false);
const [isFitDd, setIsFitDd] = useState<boolean>(false)
const [selFitIndx, setSelFitIndx] = useState<number>(0);
const [selfUserSign, setSelfUserSign] = useState({ sign: false, recipientId: 0 });
@ -169,6 +195,17 @@ const NavTwo: React.FC<NavTwoProps> = ({
return transactionSendValidationObj;
}
// Remove the zoom() function and the useEffect that calls it
// Instead:
const handleFitSelect = (indx: number) => {
setSelFitIndx(indx);
let value = fitObj[indx].value;
if (value === "FIT") value = "100";
onZoomChange?.(Number(value));
}
function handleSend(isSignSend: boolean = false) {
// dispatch(showLoader("Loading ..."));
if (onSendClick === undefined) return;
@ -184,6 +221,7 @@ const NavTwo: React.FC<NavTwoProps> = ({
// setIsSend(!isSend);
// setTimeout(() => {
setConfirmSendModalOpen({ open: false, confirm: false, loading: false })
if (transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS) {
if (isSignSend) {
// SELF SIGN CHECK HERE
@ -198,20 +236,24 @@ const NavTwo: React.FC<NavTwoProps> = ({
// }, 1000)
}
const zoom = (percentage: number, contentClass: string) => {
const content = document.querySelector(`.${contentClass}`);
if (content) {
(content as HTMLElement).style.zoom = `${percentage}%`;
}
}
useEffect(() => {
let indx = fitObj[selFitIndx].value
if (indx == "FIT") {
indx = "100";
}
zoom(Number(indx), 'zoom-con')
}, [selFitIndx])
if (confirmSendModalOpen.confirm) handleSend()
}, [confirmSendModalOpen])
// const zoom = (percentage: number, contentClass: string) => {
// const content = document.querySelector(`.${contentClass}`);
// if (content) {
// (content as HTMLElement).style.zoom = `${percentage}%`;
// }
// }
// useEffect(() => {
// let indx = fitObj[selFitIndx].value
// if (indx == "FIT") {
// indx = "100";
// }
// zoom(Number(indx), 'zoom-con')
// }, [selFitIndx])
useEffect(() => {
if (!transactionSaving) return;
@ -227,50 +269,121 @@ const NavTwo: React.FC<NavTwoProps> = ({
return (
<>
{closePage && <CloseConfirmationModal
open={closePage}
onClose={() => setClosePage(false)} // Cancel not closing
onConfirm={() => navigate("/")}
/>}
<ConfirmSendModal
confirmSendModalOpen={confirmSendModalOpen.open}
confirmSendModalOnClose={() => { setConfirmSendModalOpen({ open: false, confirm: false, loading: false }) }}
confirmSendModalOnConfirm={() => { setConfirmSendModalOpen({ open: true, confirm: true, loading: true }) }}
confirmSendModalLoading={confirmSendModalOpen.loading}
documents={selectedDocuments}
recipients={selectedRecipients}
/>
{/* Nav Two - Oprions - START */}
<div className='flex justify-between items-center px-4 py-1 bg-white'>
<div className='relative flex justify-between items-center px-4 py-1 bg-white'>
{/* Left Side */}
<div className='flex justify-start items-center gap-2'>
<div>
{/* <div>
<Link to={"/"}>
<img width={120} src="/sme-colored.png" alt="Sign Made Easy" />
</Link>
</div> */}
<div className='flex items-center gap-3'>
<div className='flex gap-4 text-xl text-slate-600'>
<button
className='hover:text-[var(--primary)] durartion-300 cursor-pointer'
onClick={() => setClosePage(true)}
>
<IoMdHome className='text-[var(--primary)] sm:text-slate-600' />
{/* <img width={120} src="/sme-colored.png" alt="Sign Made Easy" /> */}
</button>
<button
className='hover:text-[var(--primary)] durartion-300 cursor-pointer'
onClick={() => navigate(-1)}
>
<IoArrowBackSharp />
</button>
</div>
<div className='w-[0px] md:w-[1px] h-[28px] bg-slate-200'></div>
<div className='hidden md:flex items-center gap-2 text-xs'>
<button
onClick={() => navigate(-1)}
className='underline font-thin cursor-pointer'>
Set Up Transaction
</button>
<FaAngleRight className='font-thin' />
<p className='font-normal'>Add Fields</p>
</div>
</div>
<div
tabIndex={1}
onBlur={() => setIsFitDd(false)}
className='hidden absolute gap-2 lg:flex items-center left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-50'
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='hidden sm:flex md: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 className='flex'>
<p className='p-2 hover:bg-slate-100 cursor-pointer rounded-sm'>
<LuUndo2 className='text-sm text-slate-600' />
</p>
<p className='p-2 hover:bg-slate-100 cursor-pointer rounded-sm'>
<LuRedo2 className='text-sm text-slate-600' />
</p>
</div>
{isFitDd && <div className='absolute bg-white rounded-sm z-[1000] shadow'>
{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
<div className='w-full h-[20px] border-[1px] border-slate-200'></div>
<div className='flex'>
<p className='p-2 hover:bg-slate-100 cursor-pointer rounded-sm'>
<FaRegCopy className='text-sm text-slate-600' />
</p>
<p className='p-2 hover:bg-slate-100 cursor-pointer rounded-sm'>
<FaRegPaste className='text-sm text-slate-600' />
</p>
</div>
<div className='w-full h-[20px] border-[1px] border-slate-200'></div>
<div className='border border-slate-400 rounded-sm ml-1'>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='hidden sm:flex md:flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'
>
<p className='tracking-wide'>{fitObj[selFitIndx]?.label}</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute bg-white rounded-sm z-[1000] shadow'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
// onClick={() => setSelFitIndx(indx)}
onClick={() => handleFitSelect(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>
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>
@ -311,7 +424,7 @@ const NavTwo: React.FC<NavTwoProps> = ({
setShowActionsDropDown(false)
}}
className="cursor-pointer w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Save
Save Changes
</button>
<button
onClick={() => {
@ -319,7 +432,7 @@ const NavTwo: React.FC<NavTwoProps> = ({
setShowActionsDropDown(false);
}}
className="cursor-pointer w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Save & Close
Save & Exit
</button>
<button
onClick={() => {
@ -402,7 +515,7 @@ const NavTwo: React.FC<NavTwoProps> = ({
{!isPreview && <>
<div
id='send-sign-dash-btn'
onClick={() => handleSend()}
onClick={() => setConfirmSendModalOpen({ open: true, confirm: false, loading: false })}
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
<p className='tracking-wide text-white'>
{transactionTemplateToggle == TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? "Save" : "Send"}
@ -422,8 +535,9 @@ const NavTwo: React.FC<NavTwoProps> = ({
{editRecipientsModal && accountId && transactionId && <>
<div className="w-full h-full absolute top-0 left-0 z-[1000] flex justify-center items-center">
<div className="w-full h-full bg-slate-100 opacity-85 blur-2xl absolute top-0 left-0"></div>
<div className="w-full h-[100vh] absolute top-0 left-0 z-[1000] flex justify-center items-center">
{/* <div className="w-full h-full bg-slate-100 opacity-40 blur-2xl absolute top-0 left-0"></div> */}
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50" />
<div className="z-50 bg-white p-4 rounded-sm shadow-md">
<RecipientsCon
transactionTemplateToggle={transactionTemplateToggle}
@ -437,8 +551,9 @@ const NavTwo: React.FC<NavTwoProps> = ({
</>}
{messageModal && accountId && transactionId && <>
<div className="w-full h-full absolute top-0 left-0 z-[1000] flex justify-center items-center">
<div className="w-full h-full bg-slate-100 opacity-85 blur-2xl absolute top-0 left-0"></div>
<div className="w-full h-[100vh] absolute top-0 left-0 z-[1000] flex justify-center items-center">
{/* <div className="w-full h-full bg-slate-100 opacity-40 blur-2xl absolute top-0 left-0"></div> */}
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50" />
<div className="z-50 bg-white p-4 rounded-sm shadow-md">
<MessageCon
transactionTemplateToggle={transactionTemplateToggle}

View File

@ -38,7 +38,7 @@ const ProcessStepTwo = () => {
const windowWidth = window.innerWidth;
const [isFields, setIsFields] = useState(windowWidth > 638 ? true : false);
const [isPages, setIsPages] = useState(false);
const [isPages, setIsPages] = useState(windowWidth > 638 ? true : false);
const [selectedRecipient, setSelectedRecipient] = useState(0);
@ -70,21 +70,13 @@ const ProcessStepTwo = () => {
const handleSetIsFields = (setTo: boolean) => {
if (setTo != undefined && setTo != isFields) {
setIsFields(setTo)
if (setTo == true) {
setIsPages(false)
}
};
setIsFields(true)
setIsPages(false)
}
const handleSetIsPages = (setTo: boolean) => {
if (setTo != undefined && setTo != isPages) {
setIsPages(setTo)
if (setTo == true) {
setIsFields(false)
}
};
setIsFields(false)
setIsPages(true)
}
const docRef = useRef<DocumentContainerRef>(null);
@ -93,13 +85,59 @@ const ProcessStepTwo = () => {
docRef.current?.handleTransactionProcess(isSave, isNavigate, navigateTo);
};
const [zoomScale, setZoomScale] = useState<number>(100); // percentage
// To center the container on every zoom scale change - START
useEffect(() => {
const container = document.getElementById("doc-con");
if (!container) return;
const prevScrollRatio = container.scrollTop / (container.scrollHeight || 1);
const newScrollTop = prevScrollRatio * container.scrollHeight;
const newScrollLeft = (container.scrollWidth - container.clientWidth) / 2;
container.scrollTo({
top: newScrollTop,
left: newScrollLeft,
behavior: "instant" as ScrollBehavior
});
}, [zoomScale]);
// To center the container on every zoom scale change - END
return (
<div className={`min-w-full relative h-screen bg-slate-100 overflow-hidden ${(globalLoader.isLoading) ? 'pointer-events-none' : ''}`}>
<NavTwo transactionSaving={transactionSaving} isFields={isFields} isPages={isPages} setIsPages={setIsPages} setIsFields={setIsFields} isSend={isSend} setIsSend={setIsSend} onSendClick={handleSendClick} />
<NavTwo
onZoomChange={setZoomScale}
transactionSaving={transactionSaving}
// isFields={isFields}
// isPages={isPages}
// setIsPages={setIsPages}
// setIsFields={setIsFields}
// isSend={isSend}
// setIsSend={setIsSend}
onSendClick={handleSendClick} />
<div className='sm:grid sm:grid-cols-12 sm:gap-1 mt-1 h-[calc(100vh-46px)] overflow-hidden'>
<LeftSideBar isFields={isFields} isPages={isPages} selectedRecipientIndx={selectedRecipient} setSelectedRecipient={setSelectedRecipient} handleSetIsFields={handleSetIsFields} handleSetIsPages={handleSetIsPages} />
<DocumentContainer ref={docRef} transactionSaving={transactionSaving} setTransactionSaving={setTransactionSaving} isSend={isSend} setIsSend={setIsSend} />
<LeftSideBar
isFields={isFields}
isPages={isPages}
selectedRecipientIndx={selectedRecipient}
setSelectedRecipient={setSelectedRecipient}
handleSetIsFields={handleSetIsFields}
handleSetIsPages={handleSetIsPages}
/>
<DocumentContainer
isPages={isPages}
setIsPages={setIsPages}
ref={docRef}
zoomScale={zoomScale}
selectedRecipientIndx={selectedRecipient}
transactionSaving={transactionSaving}
setTransactionSaving={setTransactionSaving}
isSend={isSend}
setIsSend={setIsSend}
/>
</div>
{globalLoader.isLoading && <Loader />}

View File

@ -0,0 +1,351 @@
import React, { useEffect } from "react";
import { IoClose } from "react-icons/io5";
import { FiFileText, FiUsers } from "react-icons/fi";
interface Recipient {
id: string;
name: string;
email: string;
color?: string;
}
interface DocumentItem {
id: string;
name: string;
}
interface ConfirmSendModalProps {
confirmSendModalOpen: boolean;
confirmSendModalOnClose: () => void;
confirmSendModalOnConfirm: () => void;
confirmSendModalLoading?: boolean;
documents: DocumentItem[];
recipients: Recipient[];
}
const ConfirmSendModal = ({
confirmSendModalOpen,
confirmSendModalOnClose,
confirmSendModalOnConfirm,
confirmSendModalLoading = false,
documents,
recipients,
}: ConfirmSendModalProps) => {
// Close on ESC
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape") {
confirmSendModalOnClose();
}
};
if (confirmSendModalOpen) {
document.addEventListener("keydown", handleEsc);
}
return () => {
document.removeEventListener("keydown", handleEsc);
};
}, [confirmSendModalOpen]);
// Prevent body scroll when modal open
useEffect(() => {
if (confirmSendModalOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
return () => {
document.body.style.overflow = "auto";
};
}, [confirmSendModalOpen]);
if (!confirmSendModalOpen) return null;
return (
<div
onClick={confirmSendModalOnClose}
className="
fixed
inset-0
z-[9999]
flex
items-center
justify-center
bg-black/50
backdrop-blur-sm
p-3
sm:p-4
"
>
{/* Modal */}
<div
onClick={(e) => e.stopPropagation()}
className="
w-full
max-w-xl
max-h-[90vh]
flex
flex-col
rounded-sm
bg-white
shadow-2xl
overflow-hidden
animate-in
fade-in
zoom-in
duration-200
"
>
{/* Header */}
<div className="flex items-start justify-between border-b border-slate-200 p-5 sm:p-6">
<div className="pr-4">
<h2 className="text-xl sm:text-2xl font-semibold text-slate-800">
Review & Send
</h2>
<p className="mt-1 text-sm text-slate-500 leading-relaxed">
Please review the documents and recipient details
before sending this transaction for signing.
</p>
</div>
<button
onClick={confirmSendModalOnClose}
className="
shrink-0
p-2
rounded-sm
hover:bg-slate-100
transition
cursor-pointer
"
>
<IoClose className="text-xl text-slate-500" />
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-5">
{/* Documents */}
<div className="rounded-sm border border-slate-200 overflow-hidden">
<div className="flex items-center gap-2 border-b border-slate-200 bg-slate-50 px-4 py-3">
<FiFileText className="text-slate-600" />
<h3 className="font-medium text-slate-800">
Documents ({documents.length})
</h3>
</div>
<div className="max-h-[220px] overflow-y-auto p-3 space-y-2">
{documents.map((document) => (
<div
key={document.id}
className="
flex
items-center
gap-3
rounded-sm
border
border-slate-200
bg-white
px-3
py-3
"
>
<div
className="
flex
items-center
justify-center
w-10
h-10
rounded-sm
bg-slate-100
shrink-0
"
>
<FiFileText className="text-slate-600" />
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-slate-800 truncate">
{document.name}
</p>
</div>
</div>
))}
</div>
</div>
{/* Recipients */}
<div className="rounded-sm border border-slate-200 overflow-hidden">
<div className="flex items-center gap-2 border-b border-slate-200 bg-slate-50 px-4 py-3">
<FiUsers className="text-slate-600" />
<h3 className="font-medium text-slate-800">
Recipients ({recipients.length})
</h3>
</div>
<div className="max-h-[260px] overflow-y-auto p-3 space-y-2">
{recipients.map((recipient) => (
<div
key={recipient.id}
className="
flex
items-center
justify-between
gap-3
rounded-sm
border
border-slate-200
bg-white
px-3
py-3
"
>
<div className="flex items-center gap-3 min-w-0">
<div
className="w-3 h-3 rounded-full shrink-0"
style={{
background:
recipient.color || "#6366f1",
}}
/>
<div className="min-w-0">
<p className="text-sm font-medium text-slate-800 truncate">
{recipient.name}
</p>
<p className="text-xs text-slate-500 truncate">
{recipient.email}
</p>
</div>
</div>
<div
className="
shrink-0
rounded-full
bg-amber-50
border
border-amber-200
px-2.5
py-1
text-xs
font-medium
text-amber-700
"
>
Pending
</div>
</div>
))}
</div>
</div>
{/* Info */}
<div
className="
rounded-sm
border
border-indigo-100
bg-indigo-50
px-4
py-4
"
>
<p className="text-sm leading-relaxed text-indigo-900">
Once confirmed, recipients will receive an email
invitation to review and sign the documents.
</p>
</div>
</div>
{/* Footer */}
<div
className="
flex
flex-col-reverse
sm:flex-row
items-stretch
sm:items-center
justify-end
gap-3
border-t
border-slate-200
bg-white
p-4
sm:p-5
"
>
<button
onClick={confirmSendModalOnClose}
className="
w-full
sm:w-auto
text-sm
px-4
py-2.5
rounded-sm
border
border-slate-300
text-slate-700
hover:bg-slate-100
transition
cursor-pointer
font-medium
"
>
Continue Reviewing
</button>
<button
onClick={confirmSendModalOnConfirm}
disabled={confirmSendModalLoading}
className="
w-full
sm:w-auto
text-sm
px-5
py-2.5
rounded-sm
bg-[var(--primary)]
text-white
hover:opacity-100
transition
opacity-80
disabled:opacity-50
disabled:cursor-not-allowed
cursor-pointer
font-medium
"
>
{confirmSendModalLoading
? "Sending..."
: "Confirm & Send"}
</button>
</div>
</div>
</div>
);
};
export default ConfirmSendModal;

View File

@ -0,0 +1,263 @@
import React, { useEffect, useState } from 'react'
import { RootState } from '../../../../store/store';
import { useSelector } from 'react-redux';
import { DroppedItemsObj } from '../DocumentContainer';
import { MdKeyboardArrowUp, MdMoreVert } from 'react-icons/md';
import { PiTagSimpleFill } from 'react-icons/pi';
const DocumentPages: React.FC<{
isPages: boolean;
droppedItems: DroppedItemsObj;
setIsPages: React.Dispatch<React.SetStateAction<boolean>>;
}> = ({ isPages, droppedItems, setIsPages }) => {
type RecipientPageTagInfo = {
color: string;
recipientIndex: number;
recipientTagCount?: number;
};
type DocumentTagsMapping = {
[documentId: string]: {
[pageIndex: string]: RecipientPageTagInfo[];
};
};
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const [documentPagesTagsMapping, setDocumentPagesTagsMapping] = useState<DocumentTagsMapping>({});
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
const [openDocs, setOpenDocs] = useState<Record<string, boolean>>(
() => Object.fromEntries(selectedDocuments.map((d, i) => [d.id, i === 0]))
)
const [activePage, setActivePage] = useState<string | null>(
selectedDocuments[0] ? `${selectedDocuments[0].id}_0` : null
)
const toggleDoc = (docId: string) => {
setOpenDocs(prev => ({ ...prev, [docId]: !prev[docId] }))
}
const goToPage = (docId: string, pageIndex: number) => {
const key = `${docId}_${pageIndex}`
setActivePage(key)
window.location.hash = docId + "" + pageIndex
}
// useEffect(() => {
// setTimeout(() => {
// let documentPagesTagsMappingLocal: {
// [documentId: string]: {
// [pageIndex: number]: [number, string];
// };
// }[] = [];
// recipients && recipients.length && recipients.forEach((selectedRecipient, selectedRecipientIndx) => {
// const selectedRecipientColor = selectedRecipient.color
// let activeRecipientTagsCount = 0;
// droppedItems && Object.keys(droppedItems).length > 0 && Object.keys(droppedItems).forEach(key => {
// activeRecipientTagsCount = (droppedItems[key] && droppedItems[key].length) > 0 ? droppedItems[key].reduce((acc, item) => !item.isDeleted && item.recipientIndx == selectedRecipientIndx ? acc + 1 : acc, 0) : 0;
// if (activeRecipientTagsCount == 0) return;
// const [documentId, pageIndex] = key.split("_");
// if (!documentPagesTagsMappingLocal[documentId]) {
// documentPagesTagsMappingLocal[documentId] = {};
// }
// documentPagesTagsMappingLocal.push({ [documentId]: { [pageIndex]: [activeRecipientTagsCount, selectedRecipientColor] } })
// })
// })
// setDocumentPagesTagsMapping(documentPagesTagsMappingLocal)
// }, 300)
// // }, [droppedItems, selectedRecipientIndx])
// }, [droppedItems])
useEffect(() => {
setTimeout(() => {
let documentTagsMappingLocal: DocumentTagsMapping = {};
recipients &&
recipients.length &&
recipients.forEach((selectedRecipient, selectedRecipientIndx) => {
const selectedRecipientColor = selectedRecipient.color;
droppedItems &&
Object.keys(droppedItems).length > 0 &&
Object.keys(droppedItems).forEach(key => {
const activeRecipientTagsCount =
(droppedItems[key] && droppedItems[key].length) > 0
? droppedItems[key].reduce((acc, item) => {
return !item.isDeleted &&
item.recipientIndx == selectedRecipientIndx
? acc + 1
: acc;
}, 0)
: 0;
if (activeRecipientTagsCount === 0) return;
const [documentId, pageIndex] = key.split("_");
// Create document object
if (!documentTagsMappingLocal[documentId]) {
documentTagsMappingLocal[documentId] = {};
}
// Create page array
if (!documentTagsMappingLocal[documentId][pageIndex]) {
documentTagsMappingLocal[documentId][pageIndex] = [];
}
// Push recipient info
documentTagsMappingLocal[documentId][pageIndex].push({
color: selectedRecipientColor,
recipientIndex: selectedRecipientIndx,
recipientTagCount: activeRecipientTagsCount,
});
});
});
setDocumentPagesTagsMapping(documentTagsMappingLocal);
}, 300);
}, [droppedItems]);
useEffect(() => {
setTimeout(() => {
if (!isPages) return
const docPagesCon = document.getElementById("document-pages");
if (docPagesCon) {
docPagesCon.focus();
}
}, 0)
}, [isPages]);
return (
<div id='document-pages'
tabIndex={-1}
onBlur={() => window.innerWidth <= 638 && setIsPages(false)}
className={`h-[100vh] absolute ${isPages || window.innerWidth > 638 ? "" : "hidden"} top-0 left-0 sm:static max-w-max bg-slate-100 z-50 sm:col-span-2 flex flex-col w-full overflow-y-auto`}>
{selectedDocuments.map((document, indx) => (
<div key={indx} className="border-b border-slate-200 pb-3 mb-1">
{/* Section label (if any) */}
{document.section && (
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-400 px-3 pt-2 pb-1">
{document.section}
</p>
)}
{/* Document Header */}
<div
onClick={() => toggleDoc(document.id)}
className="flex items-start justify-between px-3 py-2.5 cursor-pointer hover:bg-slate-50 transition-colors"
>
<div>
<p className="text-[13px] font-medium text-slate-800 leading-tight">{document.name}</p>
<p className="text-[11px] text-slate-400 mt-0.5">
{document.pages?.length} {document.pages?.length === 1 ? 'page' : 'pages'}
</p>
</div>
<MdKeyboardArrowUp
size={20}
className={`text-slate-400 mt-0.5 flex-shrink-0 transition-transform duration-200 ${openDocs[document.id] ? '' : 'rotate-180'}`}
/>
</div>
{/* Page Thumbnails */}
{openDocs[document.id] && (
<div className="flex flex-col px-2">
{document.pages?.map((_: any, pageIndex: number) => {
const key = `${document.id}_${pageIndex}`
const isActive = activePage === key
return (
<div
key={pageIndex}
onClick={() => goToPage(document.id, pageIndex)}
className="flex flex-col items-center py-2 px-1 rounded-md cursor-pointer hover:bg-slate-50 transition-colors"
>
{/* Big Thumbnail */}
<div className={`relative sm:p-2 lg:p-4 bg-slate-100 rounded border flex items-center justify-center overflow-hidden
${isActive ? 'border-blue-500 border-2' : 'border-slate-200'}`}
>
{document.thumbnail ? (
<img
src={"data:image/png;base64," + document.thumbnail}
alt={`Page ${pageIndex + 1}`}
className="w-[140px] h-[180px] h-full object-cover"
/>
) : (
<div className="text-slate-300 text-4xl">Document</div>
)}
{/* Page number + dots menu */}
<div className="absolute bottom-0 bg-slate-800 flex items-center justify-between w-full pl-4 pr-2 py-3 bg-white">
<span className="text-[13px] text-slate-500">{pageIndex + 1}</span>
{
// <>
// <button
// onClick={(e) => { e.stopPropagation(); /* open context menu */ }}
// className="text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded p-0.5"
// >
// <MdMoreVert size={18} />
// </button>
// </>
}
</div>
<div className='absolute top-4 left-2 h-[80%] overflow-y-auto overflow-x-hidden'>
{documentPagesTagsMapping[document.id]?.[pageIndex]?.length > 0 && (
<>
{documentPagesTagsMapping[document.id][pageIndex].map((tagInfo, indx) => (
<div key={indx} className='relative'>
{/* Count */}
{/* Optional */}
{/* <span className='absolute text-white text-[9px] font-medium top-[50%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 pointer-events-none select-none z-10'>
{tagInfo.recipientTagCount}
</span> */}
{/* Tag */}
<PiTagSimpleFill
style={{
fontSize: '1.2rem',
color: tagInfo.color,
}}
/>
</div>
))}
</>
)}
</div>
</div>
</div>
)
})}
</div>
)}
</div>
))}
</div>
)
}
export default DocumentPages

View File

@ -19,6 +19,8 @@ import TextValueFIll from "./attributes/TextValueFIll";
import CurrTimeStampAttr from "./attributes/CurrTimeStampAttr";
import ToolTipAttr from "./attributes/ToolTipAttr";
import "./style.css";
export interface DroppedItemProps {
droppedItems: DroppedItem[]
selectedTag: DroppedItem
@ -91,7 +93,7 @@ const defaultStyles: StylesInterface = {
textSize: 14
};
const RightSideBar: React.FC<DroppedItemProps> = (
const TagProperties: React.FC<DroppedItemProps> = (
{
droppedItems,
selectedTag,
@ -140,16 +142,32 @@ const RightSideBar: React.FC<DroppedItemProps> = (
setDropDownToggle(null)
}
// useEffect(() => {
// if (selectedTag.label == "temporal" && !("dateFormat" in selectedTag)) {
// handlePropertyChange("dateFormat", TEMPORAL.DATE);
// handlePropertyChange("format", "DD/MM/YY");
// }
// if (selectedTag.label == TagsTypes.TEXT && !("multiLine" in selectedTag)) {
// handlePropertyChange("multiLine", true);
// }
// }, [selectedTag])
useEffect(() => {
if (selectedTag.label == "temporal" && !("dateFormat" in selectedTag)) {
if (!selectedTag) return;
if (selectedTag.label === "temporal" && !("dateFormat" in selectedTag)) {
handlePropertyChange("dateFormat", TEMPORAL.DATE);
handlePropertyChange("format", "DD/MM/YY");
return;
}
if (selectedTag.label == TagsTypes.TEXT && !("multiLine" in selectedTag)) {
if (selectedTag.label === TagsTypes.TEXT && !("multiLine" in selectedTag)) {
handlePropertyChange("multiLine", true);
}
}, [selectedTag])
}, [selectedTag?.id, selectedTag?.label]);
const handleDecimalPlace = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = Number(e.target.value)
@ -301,7 +319,10 @@ const RightSideBar: React.FC<DroppedItemProps> = (
return (
<>
{/* Right Side Bar Start */}
<div className='absolute border-l-1 border-t-1 border-slate-300 rounded-tl-lg right-0 top-[48px] sm:rounded-none sm:top-0 sm:border-0 sm:relative sm:col-span-2 sm:w-full h-full bg-white text-sm overflow-y-auto'>
<div
tabIndex={0}
data-tag-properties="true"
className='animate-slide-in-right z-50 absolute border-l-1 transition-all duration-600 border-t-1 border-slate-300 rounded-tl-lg right-0 top-[48px] sm:rounded-none sm:top-0 sm:border-0 sm:relative sm:col-span-2 sm:w-full h-full bg-white text-sm overflow-y-auto'>
<div className="flex flex-row-reverse justify-end items-center">
<p className="p-2 font-semibold text-md"><span className="capitalize">{selectedTag.type.toLowerCase()}</span></p>
<p className="ml-2.5 cursor-pointer" >
@ -485,4 +506,4 @@ const RightSideBar: React.FC<DroppedItemProps> = (
)
}
export default RightSideBar
export default TagProperties

View File

@ -1,6 +1,6 @@
import { MdKeyboardArrowDown } from 'react-icons/md';
import { DROPDOWNTOGGLE, MASKCHARACTER, MASKTYPE, NUMBER, SELECT, TEMPORAL, TEXT } from '../RightSideBar';
import { DROPDOWNTOGGLE, MASKCHARACTER, MASKTYPE, NUMBER, SELECT, TEMPORAL, TEXT } from '../TagProperties';
import { DroppedItem, TagsTypes } from '../../../../../utlity/types';
import { dateFormats, timeFormats } from '../../../../../utlity/dateTimeFormats';
import { FaCommentsDollar } from 'react-icons/fa';

View File

@ -1,5 +1,5 @@
import { MdKeyboardArrowDown } from "react-icons/md";
import { DROPDOWNTOGGLE, MASKCHARACTER, MASKTYPE } from "../RightSideBar";
import { DROPDOWNTOGGLE, MASKCHARACTER, MASKTYPE } from "../TagProperties";
import { LuAsterisk } from "react-icons/lu";
import { GoDotFill } from "react-icons/go";
import { IoClose } from "react-icons/io5";

View File

@ -1,5 +1,5 @@
import { useState } from "react";
import { DROPDOWNTOGGLE } from "./RightSideBar";
import { DROPDOWNTOGGLE } from "./TagProperties";
import { DroppedItemsObj } from "../DocumentContainer";
import { IoMdClose } from "react-icons/io";
import { TagOptionAttrType } from "../../../../utlity/types";

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { dateFormats, dateTimeFormats, monthFormats, timeFormats, weekFormats, yearFormats } from "../../../../../utlity/dateTimeFormats";
import { DROPDOWNTOGGLE, TEMPORAL } from "../RightSideBar";
import { DROPDOWNTOGGLE, TEMPORAL } from "../TagProperties";
import { MdKeyboardArrowDown } from "react-icons/md";
import dayjs from "dayjs";
import { DroppedItem } from "../../../../../utlity/types";

View File

@ -0,0 +1,16 @@
/* globals.css */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards;
}

View File

@ -49,6 +49,7 @@ interface Signature {
}
interface ClientSignConProps {
zoomScale: number,
setOpenSign?: (value: boolean) => void;
getAllSignatures: () => Promise<void>;
// editingSignature?: Signature | null;
@ -90,6 +91,7 @@ const fontsMap = [
const ClientSignCon: React.FC<ClientSignConProps> = ({
zoomScale,
recipient,
setOpenSign,
recipientId,
@ -98,7 +100,8 @@ const ClientSignCon: React.FC<ClientSignConProps> = ({
signTagId
}) => {
console.log("savedModal", recipient)
// const scaleFactor: number = (zoomScale ?? 100) / 100;
const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD' | 'SAVED'>('DRAW');
const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState('');

View File

@ -133,7 +133,7 @@ const TransactionView = () => {
<>
{transactionDetails &&
<>
<div className='mt-2 grid grid-cols-1 lg:grid-cols-12 items-stretch max-w-90vw min-h-[88vh] h-full bg-white rounded-lg shadow-sm mx-3'>
<div className='mt-2 grid grid-cols-1 lg:grid-cols-12 items-stretch max-w-[95vw] min-h-[88vh] h-full bg-white rounded-lg shadow-sm mx-3'>
<div className="lg:col-span-10 w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
{/* Header */}
<h1 className='text-lg font-semibold text-gray-800'>
@ -264,12 +264,12 @@ const TransactionView = () => {
<div className='lg:col-span-2 text-xs rounded-sm max-h-[88vh] overflow-y-auto no-scrollbar'>
{documentsData.map((docsD, index) => (
<div className='p-4 flex flex-col gap-2 cursor-pointer' key={docsD.id}>
<div className='p-4 flex flex-col gap-2 cursor-pointer items-center' key={docsD.id}>
<p>{docsD.name}</p>
<div className='border-3 rounded-sm border-slate-300 relative text-xs'>
<div className='border-3 rounded-sm border-slate-300 relative text-xs w-[160px] h-[168px]'>
{docsD?.thumbnail ? (
<img
style={{ width: "200px", height: "160px" }}
style={{ width: "160px", height: "160px" }}
src={`data:image/jpeg;base64,${docsD.thumbnail}`} // Adjust MIME type if needed
alt=""
loading="lazy"

View File

@ -0,0 +1,33 @@
import axiosInstance from "../config/axiosInstance";
const handleDownloadDocument = async (recipientId: string): Promise<void> => {
try {
const response = await axiosInstance.get(
`/api/v1/sign/${recipientId}/documents`,
{
responseType: "blob",
}
);
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
// IMPORTANT
link.download = "document.zip";
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
console.error(err);
}
};
export default handleDownloadDocument;

View File

@ -32,30 +32,30 @@ import { FaUser } from 'react-icons/fa';
const typeToIconMap = {
TEXT:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36.6673 18.8333H25.0007C24.5586 18.8333 24.1347 19.0089 23.8221 19.3215C23.5096 19.634 23.334 20.058 23.334 20.5V22.8333C23.334 23.2753 23.5096 23.6993 23.8221 24.0118C24.1347 24.3244 24.5586 24.5 25.0007 24.5C25.4427 24.5 25.8666 24.3244 26.1792 24.0118C26.4917 23.6993 26.6673 23.2753 26.6673 22.8333V22.1666H29.1673V31.6666H27.334C26.892 31.6666 26.468 31.8422 26.1555 32.1548C25.8429 32.4674 25.6673 32.8913 25.6673 33.3333C25.6673 33.7753 25.8429 34.1993 26.1555 34.5118C26.468 34.8244 26.892 35 27.334 35H34.334C34.776 35 35.1999 34.8244 35.5125 34.5118C35.8251 34.1993 36.0007 33.7753 36.0007 33.3333C36.0007 32.8913 35.8251 32.4674 35.5125 32.1548C35.1999 31.8422 34.776 31.6666 34.334 31.6666H32.5007V22.1666H35.0007V22.8333C35.0007 23.2753 35.1762 23.6993 35.4888 24.0118C35.8014 24.3244 36.2253 24.5 36.6673 24.5C37.1093 24.5 37.5333 24.3244 37.8458 24.0118C38.1584 23.6993 38.334 23.2753 38.334 22.8333V20.5C38.334 20.058 38.1584 19.634 37.8458 19.3215C37.5333 19.0089 37.1093 18.8333 36.6673 18.8333Z" fill="#FBC249" />
<path d="M22.5927 31.6667H18.3327V8.33333H28.3327V9.62833C28.3327 10.0704 28.5083 10.4943 28.8208 10.8068C29.1334 11.1194 29.5573 11.295 29.9993 11.295C30.4414 11.295 30.8653 11.1194 31.1779 10.8068C31.4904 10.4943 31.666 10.0704 31.666 9.62833V6.66667C31.666 6.22464 31.4904 5.80072 31.1779 5.48816C30.8653 5.17559 30.4414 5 29.9993 5H3.33268C2.89065 5 2.46673 5.17559 2.15417 5.48816C1.84161 5.80072 1.66602 6.22464 1.66602 6.66667V9.62833C1.66602 10.0704 1.84161 10.4943 2.15417 10.8068C2.46673 11.1194 2.89065 11.295 3.33268 11.295C3.77471 11.295 4.19863 11.1194 4.51119 10.8068C4.82375 10.4943 4.99935 10.0704 4.99935 9.62833V8.33333H14.9993V31.6667H10.7393C10.2973 31.6667 9.8734 31.8423 9.56084 32.1548C9.24828 32.4674 9.07268 32.8913 9.07268 33.3333C9.07268 33.7754 9.24828 34.1993 9.56084 34.5118C9.8734 34.8244 10.2973 35 10.7393 35H22.5927C23.0347 35 23.4586 34.8244 23.7712 34.5118C24.0838 34.1993 24.2593 33.7754 24.2593 33.3333C24.2593 32.8913 24.0838 32.4674 23.7712 32.1548C23.4586 31.8423 23.0347 31.6667 22.5927 31.6667Z" fill="#0D6EFD" />
</svg>
,
SIGNATURE:
<svg width="40" height="28" viewBox="0 0 40 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.0801 17.5987C21.5418 19.6325 18.4018 20.647 14.7059 20.647C13.0573 20.647 11.6623 20.2612 10.5509 19.6122C9.23383 20.2583 7.82453 20.647 6.47062 20.647C2.39344 20.647 0 18.453 0 14.7647C0 14.1149 0.52672 13.5882 1.17649 13.5882C1.82625 13.5882 2.35297 14.1149 2.35297 14.7647C2.35297 17.1057 3.64945 18.2941 6.47062 18.2941C7.1664 18.2941 7.89992 18.1431 8.62586 17.8792C7.57188 16.4205 7.22195 14.5556 7.68218 12.7146C8.42484 9.74382 10.361 7.70585 12.9412 7.70585C16.0278 7.70585 17.2912 11.6365 15.7581 14.7026C15.151 15.9167 14.1543 17.0938 12.9456 18.0758C13.4687 18.2168 14.0559 18.2941 14.7059 18.2941C17.8277 18.2941 20.4095 17.4789 22.4945 15.8528L24.1591 9.74928C24.2129 9.55178 24.3174 9.3717 24.4622 9.22694L33.2857 0.403428C33.7451 -0.0560254 34.49 -0.0560254 34.9495 0.403428C35.4089 0.862881 35.4089 1.6078 34.9495 2.06718L27.3366 9.68007C27.98 10.045 28.5577 10.4822 29.0671 10.9917C29.5766 11.5012 30.0138 12.0789 30.3787 12.7222L37.9916 5.10936C38.451 4.64991 39.1959 4.64991 39.6554 5.10936C40.1148 5.56874 40.1148 6.31366 39.6554 6.77311L30.8319 15.5966C30.6871 15.7414 30.507 15.8458 30.3095 15.8997L24.0801 17.5987ZM26.0741 11.6702L25.2062 14.8526L28.3886 13.9848C28.1148 13.4805 27.7864 13.0384 27.4034 12.6554C27.0204 12.2725 26.5784 11.9441 26.0741 11.6702ZM13.6536 13.6503C14.5119 11.9338 13.9092 10.0588 12.9412 10.0588C11.6096 10.0588 10.4722 11.2561 9.96492 13.2854C9.64672 14.5583 9.92117 15.831 10.7534 16.7749C12.0156 15.9148 13.0857 14.7862 13.6536 13.6503Z" fill="#0D6EFD" />
<path d="M0 26.1765C0 26.8262 0.52672 27.353 1.17649 27.353H21.1765C21.8263 27.353 22.353 26.8262 22.353 26.1765C22.353 25.5267 21.8263 25 21.1765 25H1.17649C0.52672 25 0 25.5267 0 26.1765Z" fill="#FBC249" />
</svg>
,
INITIALS: <svg width="39" height="36" viewBox="0 0 39 36" fill="none" xmlns="http://www.w3.org/2000/svg">
INITIALS: <svg width="20" height="20" viewBox="0 0 39 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.91844 27.3C4.31844 27.3 3.10844 26.85 2.28844 25.95C1.46844 25.03 1.05844 23.72 1.05844 22.02V20.82H4.17844V22.26C4.17844 23.62 4.74844 24.3 5.88844 24.3C6.44844 24.3 6.86844 24.14 7.14844 23.82C7.44844 23.48 7.59844 22.94 7.59844 22.2C7.59844 21.32 7.39844 20.55 6.99844 19.89C6.59844 19.21 5.85844 18.4 4.77844 17.46C3.41844 16.26 2.46844 15.18 1.92844 14.22C1.38844 13.24 1.11844 12.14 1.11844 10.92C1.11844 9.26 1.53844 7.98 2.37844 7.08C3.21844 6.16 4.43844 5.7 6.03844 5.7C7.61844 5.7 8.80844 6.16 9.60844 7.08C10.4284 7.98 10.8384 9.28 10.8384 10.98V11.85H7.71844V10.77C7.71844 10.05 7.57844 9.53 7.29844 9.21C7.01844 8.87 6.60844 8.7 6.06844 8.7C4.96844 8.7 4.41844 9.37 4.41844 10.71C4.41844 11.47 4.61844 12.18 5.01844 12.84C5.43844 13.5 6.18844 14.3 7.26844 15.24C8.64844 16.44 9.59844 17.53 10.1184 18.51C10.6384 19.49 10.8984 20.64 10.8984 21.96C10.8984 23.68 10.4684 25 9.60844 25.92C8.76844 26.84 7.53844 27.3 5.91844 27.3ZM12.7905 6H17.5005L19.6005 21.03H19.6605L21.7605 6H26.4705V27H23.3505V11.1H23.2905L20.8905 27H18.1305L15.7305 11.1H15.6705V27H12.7905V6ZM28.9331 6H37.9331V9H32.2331V14.55H36.7631V17.55H32.2331V24H37.9331V27H28.9331V6Z" fill="#0D6EFD" />
<path d="M0.398438 29.4H38.5931V31.8H0.398438V29.4Z" fill="#0D6EFD" />
<rect y="29" width="39" height="3" fill="#FBC249" />
</svg>,
TEMPORAL:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3333 3.33334V8.33334M26.6667 3.33334V8.33334M5.83333 15.15H34.1667M35 14.1667V28.3333C35 33.3333 32.5 36.6667 26.6667 36.6667H13.3333C7.5 36.6667 5 33.3333 5 28.3333V14.1667C5 9.16668 7.5 5.83334 13.3333 5.83334H26.6667C32.5 5.83334 35 9.16668 35 14.1667Z" stroke="#0D6EFD" stroke-width="2.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" />
<path d="M26.1583 22.8333H26.1733M26.1583 27.8333H26.1733M19.9916 22.8333H20.0083M19.9916 27.8333H20.0083M13.8233 22.8333H13.84M13.8233 27.8333H13.84" stroke="#FBC249" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
,
NUMBER:
<svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.6157 2L21.8867 34.5769" stroke="#FBC249" stroke-width="3.38462" stroke-linecap="round" />
<path d="M15.6528 2L6.92383 34.5769" stroke="#FBC249" stroke-width="3.38462" stroke-linecap="round" />
<path d="M34.9991 13H3.69141" stroke="#0D6EFD" stroke-width="3.38462" stroke-linecap="round" />
@ -63,7 +63,7 @@ const typeToIconMap = {
</svg>
,
IMAGE: <FaRegImage />,
STAMP: <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
STAMP: <svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_575_574)">
<mask id="mask0_575_574" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40">
<path d="M0 3.8147e-06H40V40H0V3.8147e-06Z" fill="white" />
@ -82,13 +82,13 @@ const typeToIconMap = {
</svg>
,
CHECKBOX:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3341 26.25C18.1699 26.2504 18.0072 26.2182 17.8556 26.1553C17.704 26.0923 17.5664 25.9999 17.4507 25.8833L12.4507 20.8833C12.2299 20.6464 12.1097 20.333 12.1154 20.0091C12.1211 19.6853 12.2523 19.3763 12.4814 19.1473C12.7104 18.9183 13.0193 18.7871 13.3432 18.7814C13.667 18.7757 13.9804 18.8959 14.2174 19.1167L18.3341 23.2317L27.4507 14.1167C27.6877 13.8959 28.0011 13.7757 28.3249 13.7814C28.6488 13.7871 28.9577 13.9183 29.1868 14.1473C29.4158 14.3763 29.547 14.6853 29.5527 15.0091C29.5584 15.333 29.4382 15.6464 29.2174 15.8833L19.2174 25.8833C19.1018 25.9999 18.9641 26.0923 18.8125 26.1553C18.6609 26.2182 18.4982 26.2504 18.3341 26.25Z" fill="#FBC249" />
<path d="M30.0007 37.9167H10.0007C7.90183 37.914 5.88973 37.0791 4.40564 35.595C2.92156 34.1109 2.08663 32.0988 2.08398 30V10C2.08663 7.90119 2.92156 5.88909 4.40564 4.405C5.88973 2.92091 7.90183 2.08599 10.0007 2.08334H30.0007C32.0995 2.08599 34.1116 2.92091 35.5957 4.405C37.0798 5.88909 37.9147 7.90119 37.9173 10V30C37.9147 32.0988 37.0798 34.1109 35.5957 35.595C34.1116 37.0791 32.0995 37.914 30.0007 37.9167ZM10.0007 4.58334C8.5646 4.58511 7.18788 5.15636 6.17244 6.1718C5.157 7.18724 4.58575 8.56396 4.58398 10V30C4.58575 31.4361 5.157 32.8128 6.17244 33.8282C7.18788 34.8437 8.5646 35.4149 10.0007 35.4167H30.0007C31.4367 35.4149 32.8134 34.8437 33.8289 33.8282C34.8443 32.8128 35.4156 31.4361 35.4173 30V10C35.4156 8.56396 34.8443 7.18724 33.8289 6.1718C32.8134 5.15636 31.4367 4.58511 30.0007 4.58334H10.0007Z" fill="#0D6EFD" />
</svg>
,
SELECT:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 34.1666C19.6667 34.1666 19.3333 34 19 33.8333L5.66666 23.8333C4.99999 23.3333 4.83332 22.1666 5.33332 21.5C5.83332 20.8333 6.99999 20.6666 7.66666 21.1666L20 30.3333L32.3333 21.1666C33 20.6666 34.1667 20.8333 34.6667 21.5C35.1667 22.1666 35 23.3333 34.3333 23.8333L21 33.8333C20.6667 34 20.3333 34.1666 20 34.1666Z" fill="#0D6EFD" />
<path d="M33.3333 9.16665H6.66667C5.66667 9.16665 5 8.49998 5 7.49998C5 6.49998 5.66667 5.83331 6.66667 5.83331H33.3333C34.3333 5.83331 35 6.49998 35 7.49998C35 8.49998 34.3333 9.16665 33.3333 9.16665Z" fill="#FBC249" />
<path d="M33.3333 15.8333H6.66667C5.66667 15.8333 5 15.1667 5 14.1667C5 13.1667 5.66667 12.5 6.66667 12.5H33.3333C34.3333 12.5 35 13.1667 35 14.1667C35 15.1667 34.3333 15.8333 33.3333 15.8333Z" fill="#FBC249" />
@ -96,31 +96,31 @@ const typeToIconMap = {
,
FILE: <FaFileAlt />,
ATTACHMENT:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.3329 19.6667L17.9829 22.0167C16.6829 23.3167 16.6829 25.4333 17.9829 26.7333C19.2829 28.0333 21.3996 28.0333 22.6996 26.7333L26.3996 23.0333C27.6482 21.7811 28.3493 20.085 28.3493 18.3167C28.3493 16.5484 27.6482 14.8522 26.3996 13.6C25.1474 12.3514 23.4513 11.6503 21.6829 11.6503C19.9146 11.6503 18.2185 12.3514 16.9663 13.6L12.9329 17.6333C12.4019 18.1639 11.9806 18.794 11.6932 19.4875C11.4058 20.181 11.2578 20.9243 11.2578 21.675C11.2578 22.4257 11.4058 23.169 11.6932 23.8625C11.9806 24.556 12.4019 25.1861 12.9329 25.7167" stroke="#FBC249" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M15.0007 36.6667H25.0007C33.334 36.6667 36.6673 33.3333 36.6673 25V15C36.6673 6.66668 33.334 3.33334 25.0007 3.33334H15.0007C6.66732 3.33334 3.33398 6.66668 3.33398 15V25C3.33398 33.3333 6.66732 36.6667 15.0007 36.6667Z" stroke="#0D6EFD" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
,
RADIO:
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 1.125C27.3139 1.125 34.875 8.68612 34.875 18C34.875 27.3139 27.3139 34.875 18 34.875C8.68612 34.875 1.125 27.3139 1.125 18C1.125 8.68612 8.68612 1.125 18 1.125ZM18 3.375C9.92812 3.375 3.375 9.92812 3.375 18C3.375 26.0719 9.92812 32.625 18 32.625C26.0719 32.625 32.625 26.0719 32.625 18C32.625 9.92812 26.0719 3.375 18 3.375Z" fill="#0D6EFD" />
<path d="M27 18C27 13.0331 22.9669 9 18 9C13.0331 9 9 13.0331 9 18C9 22.9669 13.0331 27 18 27C22.9669 27 27 22.9669 27 18Z" fill="#FBC249" />
</svg>
,
EMAIL:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8903 4.5H26.1098C28.845 4.49996 31.0496 4.49992 32.7836 4.73304C34.5838 4.97508 36.0994 5.49286 37.3034 6.6967C38.5072 7.90054 39.025 9.41628 39.267 11.2165C39.5 12.9504 39.5 15.1551 39.5 17.8903V22.1098C39.5 24.845 39.5 27.0496 39.267 28.7836C39.025 30.5838 38.5072 32.0994 37.3034 33.3034C36.0994 34.5072 34.5838 35.025 32.7836 35.267C31.0496 35.5 28.845 35.5 26.1098 35.5H13.8903C11.1551 35.5 8.95044 35.5 7.2165 35.267C5.41628 35.025 3.90054 34.5072 2.6967 33.3034C1.49287 32.0994 0.975079 30.5838 0.733047 28.7836C0.499925 27.0496 0.499959 24.845 0.500001 22.1098V17.8903C0.499959 15.1551 0.499925 12.9504 0.733047 11.2165C0.975079 9.41628 1.49287 7.90054 2.6967 6.6967C3.90054 5.49286 5.41628 4.97508 7.2165 4.73304C8.95044 4.49992 11.1551 4.49996 13.8903 4.5ZM7.61624 7.7063C6.1487 7.9036 5.37154 8.2645 4.81802 8.81802C4.2645 9.37154 3.9036 10.1487 3.7063 11.6162C3.50318 13.1269 3.5 15.1292 3.5 18V22C3.5 24.8708 3.50318 26.873 3.7063 28.3838C3.9036 29.8514 4.2645 30.6284 4.81802 31.182C5.37154 31.7356 6.1487 32.0964 7.61624 32.2938C9.12694 32.4968 11.1292 32.5 14 32.5H26C28.8708 32.5 30.873 32.4968 32.3838 32.2938C33.8514 32.0964 34.6284 31.7356 35.182 31.182C35.7356 30.6284 36.0964 29.8514 36.2938 28.3838C36.4968 26.873 36.5 24.8708 36.5 22V18C36.5 15.1292 36.4968 13.1269 36.2938 11.6162C36.0964 10.1487 35.7356 9.37154 35.182 8.81802C34.6284 8.2645 33.8514 7.9036 32.3838 7.7063C30.873 7.50318 28.8708 7.5 26 7.5H14C11.1292 7.5 9.12694 7.50318 7.61624 7.7063Z" fill="#0D6EFD" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75183 13.168C7.21135 12.4787 8.14267 12.2924 8.83195 12.7519L14.1756 16.3143C15.816 17.4079 16.9479 18.1601 17.8927 18.6516C18.8049 19.1261 19.4187 19.2889 19.9999 19.2889C20.5811 19.2889 21.1949 19.1261 22.1071 18.6516C23.0519 18.1601 24.1839 17.4079 25.8243 16.3143L31.1679 12.7519C31.8571 12.2924 32.7885 12.4787 33.2479 13.168C33.7075 13.8572 33.5213 14.7886 32.8319 15.2481L27.4191 18.8567C25.8647 19.893 24.5979 20.7374 23.4917 21.313C22.3365 21.914 21.2353 22.289 19.9999 22.289C18.7645 22.289 17.6634 21.914 16.5082 21.313C15.4019 20.7374 14.1351 19.893 12.5807 18.8566L7.16785 15.2481C6.47857 14.7886 6.29231 13.8572 6.75183 13.168Z" fill="#FBC249" />
</svg>
,
NAME:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.3169 36.6667C34.3169 30.2167 27.9003 25 20.0003 25C12.1003 25 5.68359 30.2167 5.68359 36.6667" stroke="#0D6EFD" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M25.8919 17.5592C24.3291 19.122 22.2095 20 19.9993 20C17.7892 20 15.6696 19.122 14.1068 17.5592C12.544 15.9964 11.666 13.8768 11.666 11.6666C11.666 9.45651 12.544 7.33689 14.1068 5.77409C15.6696 4.21129 17.7892 3.33331 19.9993 3.33331C22.2095 3.33331 24.3291 4.21129 25.8919 5.77409C27.4547 7.33689 28.3327 9.45651 28.3327 11.6666C28.3327 13.8768 27.4547 15.9964 25.8919 17.5592Z" stroke="#FBC249" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
,
COMPANY:
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.5 36.25H36.25V5C36.25 2.9375 34.5625 1.25 32.5 1.25H17.5C15.4375 1.25 13.75 2.9375 13.75 5V7.5H7.5C5.4375 7.5 3.75 9.1875 3.75 11.25V36.25H2.5C1.8125 36.25 1.25 36.8125 1.25 37.5C1.25 38.1875 1.8125 38.75 2.5 38.75H37.5C38.1875 38.75 38.75 38.1875 38.75 37.5C38.75 36.8125 38.1875 36.25 37.5 36.25ZM26.25 33.75H33.75V36.25H26.25V33.75ZM16.25 5C16.25 4.3125 16.8125 3.75 17.5 3.75H32.5C33.1875 3.75 33.75 4.3125 33.75 5V31.25H26.25V11.25C26.25 9.1875 24.5625 7.5 22.5 7.5H16.25V5ZM6.25 11.25C6.25 10.5625 6.8125 10 7.5 10H22.5C23.1875 10 23.75 10.5625 23.75 11.25V36.25H20V29.375C20.6875 29.375 21.25 28.8125 21.25 28.125C21.25 27.4375 20.6875 26.875 20 26.875H10C9.3125 26.875 8.75 27.4375 8.75 28.125C8.75 28.8125 9.3125 29.375 10 29.375V36.25H6.25V11.25ZM17.5 29.375V36.25H12.5V29.375H17.5Z" fill="#0D6EFD" />
<path d="M9.375 14.375H10.0375C10.725 14.375 11.2875 13.8125 11.2875 13.125C11.2875 12.4375 10.725 11.875 10.0375 11.875H9.375C8.6875 11.875 8.125 12.4375 8.125 13.125C8.125 13.8125 8.6875 14.375 9.375 14.375Z" fill="#FBC249" />
<path d="M14.6758 14.375H15.3383C16.0258 14.375 16.5883 13.8125 16.5883 13.125C16.5883 12.4375 16.0258 11.875 15.3383 11.875H14.6758C13.9883 11.875 13.4258 12.4375 13.4258 13.125C13.4258 13.8125 13.9883 14.375 14.6758 14.375Z" fill="#FBC249" />

View File

@ -185,7 +185,7 @@ input[type=number] {
/* pointer-events: none; */
}
.bottom-border>div {
.bottom-border>button {
position: absolute;
top: -10px;
left: 50%;
@ -197,7 +197,7 @@ input[type=number] {
border-radius: 5px;
}
.bottom-border>div:hover {
.bottom-border>button:hover {
background-color: var(--primaryLight2);
opacity: 0.9;
transition: all 0.3s ease-in-out;
@ -428,7 +428,7 @@ input[type=number] {
}
}
.bottom-border>div:hover {
.bottom-border>button:hover {
background-color: var(--primaryLight2);
opacity: 0.9;
transition: all 0.3s ease-in-out;

View File

@ -0,0 +1,218 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Define the interface matching DocumentContainer.tsx usage
export interface DroppedItem {
id?: string;
icon?: any;
label?: string;
type: string;
subType?: string;
dimensions: {
left: number;
top: number;
width: number;
height: number;
};
styles?: any;
documentId: string;
pageIndex: number;
recipientId: string | number;
recipientIndx?: number;
required?: boolean;
update?: boolean;
isDeleted?: boolean;
group?: string;
isGroupExist?: boolean;
currentTimeStamp?: boolean;
decimalPlace?: number;
isCurrency?: boolean;
maskType?: string;
maskCharacter?: string;
format?: string;
regex?: string[];
[key: string]: any; // Allow other properties
}
export interface DroppedItemsObj {
[key: string]: DroppedItem[];
}
export interface DocumentTagsState {
items: {
[transactionKey: string]: DroppedItemsObj; // Key: `${accountId}_${transactionId}`
};
counter: {
[transactionKey: string]: number;
};
isLoading: {
[transactionKey: string]: boolean;
};
}
const initialState: DocumentTagsState = {
items: {},
counter: {},
isLoading: {},
};
const documentTagsSlice = createSlice({
name: 'documentTags',
initialState,
reducers: {
/**
* Initialize or replace all dropped items for a transaction
* Used when fetching tags from API
*/
initializeDroppedItems: (
state,
action: PayloadAction<{
transactionKey: string;
items: DroppedItemsObj;
}>
) => {
const { transactionKey, items } = action.payload;
state.items[transactionKey] = items;
state.isLoading[transactionKey] = false;
},
/**
* Set all dropped items (replaces existing)
* Used when user adds/modifies tags in current session
*/
setDroppedItems: (
state,
action: PayloadAction<{
transactionKey: string;
items: DroppedItemsObj;
}>
) => {
const { transactionKey, items } = action.payload;
state.items[transactionKey] = items;
},
/**
* Add a dropped item to specific page
*/
addDroppedItem: (
state,
action: PayloadAction<{
transactionKey: string;
pageKey: string;
item: DroppedItem;
}>
) => {
const { transactionKey, pageKey, item } = action.payload;
if (!state.items[transactionKey]) {
state.items[transactionKey] = {};
}
if (!state.items[transactionKey][pageKey]) {
state.items[transactionKey][pageKey] = [];
}
state.items[transactionKey][pageKey].push(item);
state.counter[transactionKey] = (state.counter[transactionKey] || 0) + 1;
},
/**
* Update dropped items for a specific page
*/
updatePageItems: (
state,
action: PayloadAction<{
transactionKey: string;
pageKey: string;
items: DroppedItem[];
}>
) => {
const { transactionKey, pageKey, items } = action.payload;
if (!state.items[transactionKey]) {
state.items[transactionKey] = {};
}
state.items[transactionKey][pageKey] = items;
},
/**
* Remove an item by index from a page
*/
removeDroppedItem: (
state,
action: PayloadAction<{
transactionKey: string;
pageKey: string;
index: number;
}>
) => {
const { transactionKey, pageKey, index } = action.payload;
if (state.items[transactionKey]?.[pageKey]) {
state.items[transactionKey][pageKey].splice(index, 1);
}
},
/**
* Update a specific item by index
*/
updateDroppedItem: (
state,
action: PayloadAction<{
transactionKey: string;
pageKey: string;
index: number;
updates: Partial<DroppedItem>;
}>
) => {
const { transactionKey, pageKey, index, updates } = action.payload;
if (state.items[transactionKey]?.[pageKey]?.[index]) {
state.items[transactionKey][pageKey][index] = {
...state.items[transactionKey][pageKey][index],
...updates,
};
}
},
/**
* Clear all tags for a specific transaction
*/
clearTransactionTags: (state, action: PayloadAction<string>) => {
const transactionKey = action.payload;
delete state.items[transactionKey];
delete state.counter[transactionKey];
delete state.isLoading[transactionKey];
},
/**
* Increment counter for a transaction
*/
incrementCounter: (state, action: PayloadAction<string>) => {
const transactionKey = action.payload;
state.counter[transactionKey] = (state.counter[transactionKey] || 0) + 1;
},
/**
* Set loading state
*/
setLoading: (
state,
action: PayloadAction<{
transactionKey: string;
isLoading: boolean;
}>
) => {
const { transactionKey, isLoading } = action.payload;
state.isLoading[transactionKey] = isLoading;
},
},
});
export const {
initializeDroppedItems,
setDroppedItems,
addDroppedItem,
updatePageItems,
removeDroppedItem,
updateDroppedItem,
clearTransactionTags,
incrementCounter,
setLoading,
} = documentTagsSlice.actions;
export default documentTagsSlice.reducer;

View File

@ -0,0 +1,176 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
/* TYPES */
export interface Position {
top: number;
left: number;
width: number;
height: number;
}
export interface Mask {
maskType: string;
maskCharacter: string;
}
export interface Tag {
id: string;
label: string;
position: Position;
mask: Mask;
type: string;
subType: string | null;
error: string | null;
regex: string[];
format: string;
documentIndex: number;
pageNumber: number;
signerIndex: number;
style: Record<string, any>;
isDeleted?: boolean;
}
/* PAYLOAD TYPES */
interface AddTagPayload {
recipientId: string;
tag: Tag;
}
interface UpdateTagPayload {
recipientId: string;
tagId: string;
updatedData: Partial<Tag>;
}
interface RemoveTagPayload {
recipientId: string;
tagId: string;
}
interface ReplaceRecipientTagsPayload {
recipientId: string;
tags: Tag[];
}
/* STATE */
/*
STATE STRUCTURE
{
recipientId: {
tagId: Tag
}
}
*/
export interface LocalTagsState {
[recipientId: string]: {
[tagId: string]: Tag;
};
}
const initialState: LocalTagsState = {};
/* SLICE */
const localTagsSlice = createSlice({
name: "localTags",
initialState,
reducers: {
/* ------------------------------ ADD TAG ----------------------------- */
addTag: (state, action: PayloadAction<AddTagPayload>) => {
const { recipientId, tag } = action.payload;
if (!state[recipientId]) {
state[recipientId] = {};
}
state[recipientId][tag.id] = tag;
},
/* ---------------------------- UPDATE TAG ---------------------------- */
updateTag: (state, action: PayloadAction<UpdateTagPayload>) => {
const { recipientId, tagId, updatedData } = action.payload;
if (!state[recipientId]) return;
if (!state[recipientId][tagId]) return;
state[recipientId][tagId] = {
...state[recipientId][tagId],
...updatedData,
};
},
/* ---------------------------- REMOVE TAG ---------------------------- */
removeTag: (state, action: PayloadAction<RemoveTagPayload>) => {
const { recipientId, tagId } = action.payload;
if (!state[recipientId]) return;
delete state[recipientId][tagId];
// Optional cleanup
if (Object.keys(state[recipientId]).length === 0) {
delete state[recipientId];
}
},
/* ------------------------- REPLACE ALL TAGS ------------------------- */
replaceRecipientTags: (
state,
action: PayloadAction<ReplaceRecipientTagsPayload>
) => {
const { recipientId, tags } = action.payload;
state[recipientId] = {};
tags.forEach((tag) => {
state[recipientId][tag.id] = tag;
});
},
/* -------------------------- CLEAR RECIPIENT ------------------------- */
clearRecipientTags: (
state,
action: PayloadAction<string>
) => {
delete state[action.payload];
},
/* ----------------------------- CLEAR ALL ---------------------------- */
clearTags: () => {
return initialState;
},
},
});
/* EXPORTS */
export const {
addTag,
updateTag,
removeTag,
replaceRecipientTags,
clearRecipientTags,
clearTags,
} = localTagsSlice.actions;
export default localTagsSlice.reducer;

View File

@ -268,11 +268,11 @@ const selectedRecipientsSlice = createSlice({
})
.addCase(fetchRecipients.rejected, (state, action) => {
console.error("Failed to fetch recipients:", action.payload);
toast.error("Failed to fetch recipients", {
position: "bottom-right",
autoClose: 5000,
theme: "light",
});
// toast.error("Failed to fetch recipients", {
// position: "bottom-right",
// autoClose: 5000,
// theme: "light",
// });
})
.addCase(addRecipientAsync.pending, () => { })
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
@ -283,11 +283,11 @@ const selectedRecipientsSlice = createSlice({
})
.addCase(addRecipientAsync.rejected, (state, action) => {
console.error("Failed to add recipients:", action.payload);
toast.error("Failed to add recipients", {
position: "bottom-right",
autoClose: 5000,
theme: "light",
});
// toast.error("Failed to add recipients", {
// position: "bottom-right",
// autoClose: 5000,
// theme: "light",
// });
})
.addCase(removeRecipientAsync.pending, () => { })
.addCase(removeRecipientAsync.fulfilled, (state, action: PayloadAction<string | number>) => {
@ -297,11 +297,11 @@ const selectedRecipientsSlice = createSlice({
})
.addCase(removeRecipientAsync.rejected, (state, action) => {
console.error("Failed to delete recipient:", action.payload);
toast.error("Failed to delete recipient", {
position: "bottom-right",
autoClose: 5000,
theme: "light",
});
// toast.error("Failed to delete recipient", {
// position: "bottom-right",
// autoClose: 5000,
// theme: "light",
// });
});
}
});

View File

@ -72,6 +72,7 @@ import documentReducer from "./slices/documentSlice";
import tagsReducer from "./slices/tagsSlice";
import transactionReducer from "./slices/transactionSlice";
import signatureReducer from "./slices/signatureSlice";
import documentTagsReducer from "./slices/documentTagsSlice";
const rootReducer = combineReducers({
user: userReducer,
@ -83,6 +84,7 @@ const rootReducer = combineReducers({
tags: tagsReducer,
transaction: transactionReducer,
signature: signatureReducer,
documentTags: documentTagsReducer,
});
export const store = configureStore({

View File

@ -1,4 +1,4 @@
import { MASKCHARACTER } from "../components/process/processStepTwo/rightSideBar/RightSideBar";
import { MASKCHARACTER } from "../components/process/processStepTwo/tagProperties/TagProperties";
const type_all = (str: string, changeWith: string): string => {
return str.replace(/./g, changeWith);

View File

@ -7,7 +7,9 @@ export default defineConfig({
base: '/',
plugins: [react(), tailwindcss()],
server: {
port: 5173,
allowedHosts: [
'smashing-albacore-engaging.ngrok-free.app',
'quality-friendly-goldfish.ngrok-free.app',
'officially-powerful-goat.ngrok-free.app',
'89f0-49-36-189-120.ngrok-free.app',
@ -15,6 +17,7 @@ export default defineConfig({
'c76f1251c173.ngrok-free.app',
'enabled-upright-boxer.ngrok-free.app',
'basically-polished-yeti.ngrok-free.app',
'grace-antral-shari.ngrok-free.dev',
'10.10.1.248:8083'
],
},