diff --git a/.env b/.env index 98c85da..cdb5098 100644 --- a/.env +++ b/.env @@ -1,3 +1,7 @@ -VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app" +VITE_BASE_URI="https://wired-terminally-muskrat.ngrok-free.app" +# VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app" # VITE_BASE_URI="https://python-amused-adder.ngrok-free.app" -# VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app" \ No newline at end of file +# VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app" + + +# VITE_BASE_URI="http://192.168.2.176:8083" \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 6a6a7e3..37da9ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import Templates from "./components/templates/Templates"; import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen"; import JoyrideController from "./utlity/JoyRideTour"; import Contacts from "./components/contacts/Contacts"; +import TransactionSentSuccess from "./components/process/CompletionScreen/TransactionSentSuccess"; const App = () => { return ( @@ -84,10 +85,16 @@ const App = () => { } /> - + } /> */} + + + + } /> {/* Links work wothout Login */} diff --git a/src/assets/SMELogo2.png b/src/assets/SMELogo2.png new file mode 100644 index 0000000..65b729c Binary files /dev/null and b/src/assets/SMELogo2.png differ diff --git a/src/components/CommonNav.tsx b/src/components/CommonNav.tsx index 07e52d2..c948d83 100644 --- a/src/components/CommonNav.tsx +++ b/src/components/CommonNav.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa"; import api from '../config/api'; +import { IoSettings, IoSettingsOutline } from 'react-icons/io5'; interface AccountNavProps { accountName?: string | null; @@ -365,6 +366,18 @@ const CommonNav: React.FC = ({ +
+ +
diff --git a/src/components/client/TagsCon.tsx b/src/components/client/TagsCon.tsx new file mode 100644 index 0000000..025ee43 --- /dev/null +++ b/src/components/client/TagsCon.tsx @@ -0,0 +1,806 @@ +import React, { useState } from 'react' +import { DroppedItem, TagsTypes } from '../../utlity/types'; +import { RiDragMoveFill } from 'react-icons/ri'; +import { IoMdClose } from 'react-icons/io'; +import maskUtils from '../../utlity/maskUtils'; +import { FaFileAlt, FaSignature, FaStamp } from 'react-icons/fa'; +import { TbSignature } from 'react-icons/tb'; +import { BsFileArrowDownFill } from 'react-icons/bs'; +import { handleGroupBorderPosition } from '../../utlity/tagGroupConUtils'; +import api from '../../config/api'; +import { SaveType, SignConfTypeObj, StampTypeObj } from './SignDocument'; +import dayjs from 'dayjs'; +import typeToIconMap from '../../helper/iconMap'; + + +interface TagsConProps { + currDocTags: DroppedItem[] + pageIdkey: string; + provideTags: boolean + documentId: string; + pageIndex: number; + hideTags: boolean, + transparentTags: boolean; + focusElement: DroppedItem | null; + selectedTag: { key: string; index: number } | null; + droppedItems: DroppedItem[] + saved: SaveType; + signConfObj: SignConfTypeObj | {}; + setStamp: React.Dispatch>; + setSignConfObj: React.Dispatch>; + setOpenSign: React.Dispatch>, + setSignTagId: React.Dispatch>; + setDroppedItems: React.Dispatch>; + setSelectedTag: React.Dispatch> + setFocusElement: React.Dispatch> +} + +const TagsCon: React.FC = ({ + currDocTags, + pageIdkey, + provideTags, + documentId, + pageIndex, + hideTags, + transparentTags, + focusElement, + selectedTag, + droppedItems, + saved, + signConfObj, + setStamp, + setSignConfObj, + setOpenSign, + setSignTagId, + setDroppedItems, + setSelectedTag, + setFocusElement +}) => { + + const [dateValues, setDateValues] = useState<{ [key: string]: string }>({}); + const [isDateOpen, setIsDateOpen] = useState(null); + + const handleTagMouseDown = (event: React.MouseEvent, key: string, index: number) => { + setSelectedTag({ key, index }) + + event.preventDefault(); + + let targetContainer = event?.currentTarget?.parentElement?.parentElement; + let targetElement = event?.currentTarget; + + if (!targetContainer || !targetElement) return; + + const containerRect = targetContainer.getBoundingClientRect(); + const elementRect = targetElement.getBoundingClientRect(); + + // Calculate the initial offset (where the mouse was clicked inside the element) + const offsetX = event.clientX - elementRect.left; + const offsetY = event.clientY - elementRect.top; + + const conScrollWidth = targetContainer?.scrollWidth; + const conScrollHeight = targetContainer?.scrollHeight; + + let isGroupAttr = event.currentTarget.hasAttribute("group"); + let groupId = null; + if (isGroupAttr) { + groupId = event.currentTarget.getAttribute("group"); + } + + let groupElements: NodeListOf | null = null + if (isGroupAttr && groupId) { + groupElements = document.querySelectorAll(`div[group="${groupId}"]`); + } + + const handleMouseMove = (event: MouseEvent) => { + // Get new X and Y positions relative to the container + let left = event.clientX - containerRect.left - offsetX; + let top = event.clientY - containerRect.top - offsetY; + + + // **Restrict movement within container bounds** + left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width)); + top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height)); + + + if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY); + + // Apply the movement to the element + setDroppedItems( + droppedItems.map((item, i) => + i === index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item + ) + ); + + }; + + const handleMouseUp = () => { + document.removeEventListener("pointermove", handleMouseMove); + document.removeEventListener("pointerup", handleMouseUp); + }; + + // Add event listeners + document.addEventListener("pointermove", handleMouseMove); + document.addEventListener("pointerup", handleMouseUp); + }; + + const handleRemoveTag = (key: string, targetIndex: number) => { + + const tag: DroppedItem = droppedItems[targetIndex]; + const recipientId = tag.recipientId; + + (async () => { + if ("id" in tag) { + try { + const response = await api.delete(`/api/sign/tags?recipientId=${recipientId}`, { data: [{ tagId: tag.id }] }) + if (response.status !== 200) throw "Tag Deletion error" + } catch (err) { + return; + } + } + + setSelectedTag(null); + + // Check if this item has a group for special group handling + if ('group' in tag && tag.group) { + const groupId = tag.group; + + // Find all items in the same group + const groupItems = droppedItems[key] + .map((item, idx) => ({ item, index: idx })) + .filter(({ item }) => 'group' in item && item.group === groupId && !item.isDeleted); + + // Sort group items by their top position to identify the parent (top-most) + groupItems.sort((a, b) => a.item.dimensions.top - b.item.dimensions.top); + + // Check if the item being removed is the parent (top-most item) + const isParentItem = groupItems.length > 0 && groupItems[0].index === targetIndex; + + if (isParentItem && groupItems.length > 1) { + // If removing the parent and there are child items, mark all items in the group as deleted + + // Get indices of all group items to mark as deleted + const indicesToDelete = groupItems.map(({ index }) => index); + + setDroppedItems(prevState => ({ + ...prevState, + [key]: prevState[key].map((item, index) => + indicesToDelete.includes(index) ? { ...item, isDeleted: true } : item + ) + })); + + // Remove group border elements after state update + // setTimeout(() => { + // removeGroupBorders(groupId); + // }, 100); + + } else { + // If removing a child item or the only item in group, just mark this item as deleted + + setDroppedItems(prevState => ({ + ...prevState, + [key]: prevState[key].map((item, index) => + index === targetIndex ? { ...item, isDeleted: true } : item + ) + })); + + // Check if this was the last item in the group after state update + // setTimeout(() => { + // const remainingGroupItems = droppedItems[key].filter(item => + // 'group' in item && item.group === groupId && !item.isDeleted + // ); + + // if (remainingGroupItems.length === 0) { + // // Remove group borders if no items left + // removeGroupBorders(groupId); + // } else { + // // Update group borders for remaining items + // handleIGroupBorderPosition(groupId); + // } + // }, 100); + } + } else { + // Handle non-group item removal (your existing logic) + setDroppedItems(prevState => prevState.filter((item, idx) => { + return idx !== targetIndex; + })); + } + })() + }; + + const handleTagUpdate = (updateIndx: number, updateValue: string) => { + if (droppedItems && droppedItems.length) { + const updatedTag = droppedItems.map((val, indx) => { + if (indx === updateIndx) { + return { + ...val, + value: updateValue + } + } + return val; + }) + setDroppedItems(updatedTag); + } + + // setTagJson((prev) => { + // const newTagJson = [...prev]; + // if (newTagJson[updateIndx]) { + // newTagJson[updateIndx].label = updateValue; + // } + // return newTagJson; + // }); + } + + + const handleSignClick = (index: number, id: number) => { + setSignTagId(id) + if (saved.save) { + if (index in signConfObj) { + setOpenSign(true); + return + }; + setSignConfObj({ + ...signConfObj, + [index]: { + accepted: true, + index: index + } + }) + return + } + setOpenSign(true); + + const newObj = { + [index]: { + "accepted": false, + index: index + } + } + setSignConfObj({ ...newObj }) + + } + + const handleStampClick = (index: number) => { + setStamp({ + open: true, + blob: "" + }) + + } + + const handleSignAcknowledgeClick = (index: number) => { + if (saved.save && signConfObj === null) { + setSignConfObj([...signConfObj, index]) + } + } + + return ( + <> + {currDocTags.map((item, index) => { + if (item.pageIndex !== pageIndex || item.documentId != documentId) return <>; + return ( +
{ + setSelectedTag({ key: pageIdkey, index }) + // setFocusElement({ ...item, focusElementIndx: index }) + } + } + key={index} + // onPointerDown={(e) => handleTagMouseDown(e, key, index)} + className="absolute rounded dropped-item text-sm" + // onClick={() => handleTagClick(key, index)} + style={{ + left: `${item.dimensions.left}px`, + top: `${item.dimensions.top}px`, + width: `${item.dimensions.width}px`, + height: `${item.dimensions.height}px`, + border: `${((!hideTags && focusElement) && !transparentTags && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) ? '2px dashed #1c398e' : 'none'}`, + // display: `${hideTags ? 'none' : 'block'}` + }} + > + {provideTags && !transparentTags && !hideTags &&
handleTagMouseDown(e, pageIdkey, index)} + // className="absolute top-[-15px] left-[-15px] w-4 h-4 bg-blue-600 rounded-tr cursor-move z-30" + title="Drag" + > + +
} + + <> + {selectedTag?.key && selectedTag.key === pageIdkey && selectedTag.index === index && +
+
+

+

+ {/*

{IconComponent} +

*/} +

{item.label}

+ { + e.stopPropagation(); + handleRemoveTag(pageIdkey, index) + }} className='cursor-pointer' /> +
+
} + + + {/* TEXT FIELD - START */} + { + item.type === "TEXT" && +
+ + {/* + */} + + { + !item.multiLine && !item.maskType &&

{ + const val = e.currentTarget.textContent; + handleTagUpdate(index, val as string); + }} + onKeyDown={(e) => { + if (e.key === "Enter") e.preventDefault(); + }} + contentEditable={true} + className="outline-none whitespace-nowrap w-full max-h-max" + >{item.value ?? ""}

+ } + { + item.multiLine && !item.maskType &&

{ + const val = e.currentTarget.textContent; + handleTagUpdate(index, val as string); + }} + contentEditable={true} + className="w-full outline-none max-h-max" + >{item.value ?? ""}

+ } + { + "maskType" in item && "maskCharacter" in item && item.maskType &&

{ + // const val = e.currentTarget.textContent; + // handleTagUpdate(index, val as string); + // }} + onKeyDown={(e) => { + if (e.key === "Enter") e.preventDefault(); + }} + // onFocus={() => { + // }} + onBlur={(e) => { + let val = e.target.textContent; + handleTagUpdate(index, val as string); + if (typeof item.maskType === "string" && typeof val === "string" && item.maskCharacter) { + val = val.replace(/ /g, ""); + e.target.textContent = maskUtils(item.maskType, val, item.maskCharacter) + } + }} + contentEditable={true} + className="outline-none whitespace-nowrap w-full max-h-max" + >

+ } +
+ } + {/* TEXT FIELD - END */} + + {/* EMAIL FIELD - START */} + { + item.type === "EMAIL" && +
+ { +

{ + const val = e.currentTarget.textContent; + handleTagUpdate(index, val as string); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + const val = e.currentTarget.textContent?.trim() || ""; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (val && !emailRegex.test(val)) { + alert("Please enter a valid email address"); + handleTagUpdate(index, "" as string) + e.currentTarget.textContent = ""; + e.currentTarget.focus(); // Optional: return focus if invalid + } + e.preventDefault() + }; + }} + + onBlur={(e) => { + const val = e.currentTarget.textContent?.trim() || ""; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (val && !emailRegex.test(val)) { + alert("Please enter a valid email address"); + handleTagUpdate(index, "" as string) + e.currentTarget.textContent = ""; + e.currentTarget.focus(); // Optional: return focus if invalid + } + }} + // contentEditable={true} + className="outline-none whitespace-nowrap w-full max-h-max" + > + {item.value ?? ""} +

+ } +
+ } + {/* EMAIL FIELD - END */} + + + {/* NAME FIELD - START */} + { + item.type === "NAME" && +
+ { +

{ + const val = e.currentTarget.textContent; + handleTagUpdate(index, val as string); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + }; + }} + + onBlur={(e) => { + + }} + // contentEditable={true} + className="outline-none whitespace-nowrap w-full max-h-max" + > + {item.value ?? ""} +

+ } +
+ } + {/* NAME FIELD - END */} + + + {/* NUMBER FIELD - START */} + { + item.type === "NUMBER" && +
+ { + { + const val = e.currentTarget.value; + handleTagUpdate(index, val as string); + }} + type="number" + className="outline-none h-full max-w-max" + /> + } +
+ } + {/* NUMBER FIELD - END */} + + + + {/* COMPANY FIELD - START */} + { + item.type === "COMPANY" && +
+ { +

{ + const val = e.currentTarget.textContent; + handleTagUpdate(index, val as string); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + }; + }} + + onBlur={(e) => { + + }} + contentEditable={true} + className="outline-none whitespace-nowrap w-full max-h-max" + >

+ } +
+ } + {/* COMPANY FIELD - END */} + + + {/* CHECKBOX FIELD - START */} + { + item.type === "CHECKBOX" && +
+ +
+ } + {/* CHECKBOX FIELD - END */} + + + {/* SIGNATURE FIELD - START */} + { + item.type === "SIGNATURE" && +
handleSignClick(index, item.id)} + className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none bg-transparent" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`} + // style={{ height: item.dimensions.height + "px" }} + > + {/* */} + { + (!transparentTags && !hideTags && !(saved.save && (index in signConfObj && signConfObj[index].accepted))) ? + <> + + + : + + src={`data:image/png;base64,${saved.signature}`} + /> + } +
+ } + {/* SIGNATURE FIELD - END */} + + + {/* INITIALS FIELD - START */} + { + item.type === "INITIALS" && +
handleSignClick(index, item.id)} + className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`} + // style={{ height: item.dimensions.height + "px" }} + > + {((!transparentTags && !hideTags) && !(saved.save && (index in signConfObj) && signConfObj[index].accepted)) ? + + : + + } +
+ } + {/* INITIALS FIELD - END */} + + + {/* STAMP FIELD - START */} + { + item.type === "STAMP" && +
handleStampClick(index)} + className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`} + // style={{ height: item.dimensions.height + "px" }} + > + {!transparentTags && !hideTags ? + + : + + } +
+ } + {/* STAMP FIELD - END */} + + {/* IMAGE FIELD - START */} + { + item.type === "FILE" && +
handleSignClick(index)} + className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`} + // style={{ height: item.dimensions.height + "px" }} + > + {!transparentTags && !hideTags ? + + : + + } +
+ } + {/* IMAGE FIELD - END */} + + + {/* SELECT FIELD - START */} + { + item.type === "SELECT" && +
+ +
+ } + {/* SELECT FIELD - END */} + + + + + {/* TEMPORAL FIELD - START */} + + { + item.type === "TEMPORAL" && +
+

+ {(item.id && item.id in dateValues) ? dateValues[item.id] : item.value ? item.value : item.dateFormat?.toLowerCase()} +

+ { + e.target.showPicker?.() + if (item.id) setIsDateOpen(item.id) + }} + onBlur={(e) => { + let val = e.target.value; + + // Adding dummy time in case of time as it giving Invalid date without that + if (item.dateFormat?.toLowerCase() === "time") val = "09/02/2025 " + val + + if (!item.id) return; + setIsDateOpen(null); + setDateValues({ + ...dateValues, + [item.id as string]: dayjs(val).format(item.format) + }) + }} + onInput={(e) => { + const val = e.currentTarget.value; + handleTagUpdate(index, val as string); + }} + style={{ opacity: `${item.id === isDateOpen ? "100" : "0"}` }} + className="w-full px-2 outline-none absolute" + onClick={() => { + + }} + type={`${item.dateFormat?.toLowerCase() === "date" ? "date" : (item.dateFormat?.toLowerCase() === "time" ? "time" : "datetime-local")}`} + /> + +
+ } + + {/* TEMPORAL FIELD - END */} + + + {/* Attachment Field - START */} + { + item.type.toLowerCase() === TagsTypes.ATTACHMENT && +
handleStampClick(index)} + className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`} + // style={{ height: item.dimensions.height + "px" }} + > + {!transparentTags && !hideTags ? + ( +

+ {typeToIconMap.ATTACHMENT} +

+ ) + : + ( +

+ {typeToIconMap.ATTACHMENT} +

+ ) + } +
+ } + {/* Attachment Field - END */} + + + + {/* Focus Show Element */} + { + (!transparentTags && !hideTags) && (focusElement && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) && + <> + {/*
+

{focusElement.label}

+
*/} +
+ {/* + */} + +
+ + } +
+ ) + })} + + ) +} + +export default TagsCon \ No newline at end of file diff --git a/src/components/dashboard/Loader.tsx b/src/components/dashboard/Loader.tsx index 48a0be7..1920bd8 100644 --- a/src/components/dashboard/Loader.tsx +++ b/src/components/dashboard/Loader.tsx @@ -1,6 +1,8 @@ import { LuSignature } from "react-icons/lu"; import { useSelector } from 'react-redux'; import { RootState } from '../../store/store'; +import SMELogo from "../../assets/SMELogo2.png"; +import { FaFileSignature } from "react-icons/fa"; const Loader = () => { @@ -16,7 +18,7 @@ const Loader = () => { {/* Loader */}
- +

{loaderData.message != undefined ? `${loaderData.message}` : 'Loading ...'}

diff --git a/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx b/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx index b13e260..3899f5a 100644 --- a/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx +++ b/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx @@ -33,9 +33,10 @@ import confetti from 'canvas-confetti' import { IoClose } from "react-icons/io5"; import { IoMdClose, IoMdHome } from "react-icons/io"; import { Bounce, toast } from "react-toastify"; +import NavTwo from '../processStepTwo/NavTwo'; -type Signer = { +export type Signer = { id: number; name: string; email: string; @@ -44,7 +45,7 @@ type Signer = { }; // Signers List Component -const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => { +export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => { return ( //
@@ -198,6 +199,7 @@ const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => { //
); }; + const DocumentSubmissionOptions = () => { const location = useLocation(); const [status, setStatus] = useState(null); @@ -224,18 +226,6 @@ const DocumentSubmissionOptions = () => { }, []) useEffect(() => { - // toast.dismiss(); - // toast.success("Your document has been successfully sent for signature.", { - // position: "top-center", - // autoClose: 1500, - // hideProgressBar: false, - // closeOnClick: false, - // pauseOnHover: true, - // draggable: true, - // progress: undefined, - // theme: "light", - // transition: Bounce, - // }); if (showConfetti) { const duration = 3000; @@ -314,25 +304,6 @@ const DocumentSubmissionOptions = () => { } }, [status]); - - // useEffect(() => { - // if (status === 'SENT') { - // // Step 1: Push a dummy state to trap the user - // window.history.pushState(null, '', window.location.href); - - // // Step 2: Intercept back button - // const handlePopState = (event) => { - // // Push the same dummy state again to block back - // window.history.pushState(null, '', window.location.href); - // }; - - // window.addEventListener('popstate', handlePopState); - - // return () => { - // window.removeEventListener('popstate', handlePopState); - // }; - // } - // }, [status]); const [selectedOption, setSelectedOption] = useState('success'); const [isVisible, setIsVisible] = useState(false); const [previewData, setPreviewData] = useState({ @@ -440,95 +411,6 @@ const DocumentSubmissionOptions = () => {
- {/* Header */} - - - {/*
-
- - -
-
-

- Choose an action after a signer completes the document. -

-
- {options.map((option) => { - const Icon = option.icon; - const isSelected = selectedOption === option.id; - - return ( -
setSelectedOption(option.id)} - className={` - relative cursor-pointer p-4 rounded-xl border-2 transition-all duration-200 group - ${isSelected - ? 'border-[var(--primary)] bg-blue-50 shadow-md scale-[1.02]' - : option.color + ' border-2' - } - hover:shadow-md hover:scale-[1.01] transform - `} - > -
-
- -
-
-
-

- {option.title} -

- {option.isPremium && ( - ★ - )} -
-

- {option.description} -

-
-
- - - {isSelected && ( -
-
- -
-
- )} -
- ); - })} -
-
-
- - -
-
-
-
-

Live Preview

-

See how your users will experience the completion

-
- -
-
- {getPreviewContent()} -
-
-
-
-
*/} -
diff --git a/src/components/process/CompletionScreen/SignerList.tsx b/src/components/process/CompletionScreen/SignerList.tsx new file mode 100644 index 0000000..67032c1 --- /dev/null +++ b/src/components/process/CompletionScreen/SignerList.tsx @@ -0,0 +1,48 @@ +import { FaCheck, FaClock, FaUsers } from "react-icons/fa"; +import { Signer } from "./DocumentCompletionScreen"; + +export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => { + return ( + //
+
+
+

+ {/* */} + Recipients +

+ + {signers.filter(s => s.completed).length} of {signers.length} signed + +
+ +
+ {signers.map((signer) => ( +
+
+ {signer.name.length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.email.split("").map(n => n[0].toUpperCase()).join("")} +
+
+
{signer.name}
+
{signer.email}
+
Order: {signer.routingOrder}
+
+
+ {signer.completed ? ( +
+ + Signed +
+ ) : ( +
+ + Pending +
+ )} +
+
+ ))} +
+
+ //
+ ); +}; \ No newline at end of file diff --git a/src/components/process/CompletionScreen/SigningOrderDiagram.tsx b/src/components/process/CompletionScreen/SigningOrderDiagram.tsx new file mode 100644 index 0000000..046874e --- /dev/null +++ b/src/components/process/CompletionScreen/SigningOrderDiagram.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import { FaCheck, FaClock, FaEnvelope, FaLock } from "react-icons/fa"; + +type Signer = { + id: number; + name: string; + firstName: string; + email: string; + routingOrder: number; + completed: boolean; +}; + +export const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => { + const groupByRoutingOrder = (data: Signer[]) => { + const grouped: Record = {}; + data.forEach((signer) => { + if (!grouped[signer.routingOrder]) { + grouped[signer.routingOrder] = []; + } + grouped[signer.routingOrder].push(signer); + }); + return grouped; + }; + + const getInitials = (name: string) => { + return name + .split(" ") + .map((n) => n[0].toUpperCase()) + .join(""); + }; + + const grouped = groupByRoutingOrder(signers); + const sortedOrders = Object.keys(grouped) + .map(Number) + .sort((a, b) => a - b); + + const currentProcessingOrder = + sortedOrders.find((order) => + grouped[order].some((signer) => !signer.completed) + ) || -1; + + return ( + //
+
+

+ Signing Flow

+
+
+ {/* Mail icon */} +
+
+ +
+

Sent

+
+ + {/* Dotted line */} +
+
+
+ + {/* Signers by routing order */} +
+ {sortedOrders.map((order, index) => ( + +
+
+ Step {order} +
+
+ {grouped[order].map((signer) => { + let bgColor = ""; + let icon = null; + + if (signer.completed) { + bgColor = "bg-[var(--primary)]"; + icon = ; + } else if (order === currentProcessingOrder) { + bgColor = "bg-yellow-500"; + icon = ; + } else { + bgColor = "bg-gray-400"; + icon = ; + } + + return ( +
+
+ {getInitials(signer.name)} + + {icon} + +
+

+ {signer.name.split(" ")[0] || signer.firstName.split(0, 1)} +

+
+ ); + })} +
+
+ + {index < sortedOrders.length - 1 && ( +
+
+
+ )} +
+ ))} +
+
+
+
+ //
+ ); +}; \ No newline at end of file diff --git a/src/components/process/CompletionScreen/TransactionSentSuccess.tsx b/src/components/process/CompletionScreen/TransactionSentSuccess.tsx new file mode 100644 index 0000000..03c3c55 --- /dev/null +++ b/src/components/process/CompletionScreen/TransactionSentSuccess.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react' +import DocumentContainer from '../processStepTwo/DocumentContainer' +import { Link, useParams } from 'react-router-dom' +import { IoMdHome } from 'react-icons/io' +import { FaCheck } from 'react-icons/fa' +import { useDispatch, useSelector } from 'react-redux' +import { AppDispatch, RootState } from '../../../store/store' +import { fetchRecipients, recipientType } from '../../../store/slices/selectedRecipientsSlice' +import { fetchDocuments } from '../../../store/slices/documentSlice' +import { SigningOrderDiagram } from './SigningOrderDiagram' +import { SignersList } from './SignerList' +import { IoCheckmarkDoneCircle } from 'react-icons/io5' + +const TransactionSentSuccess = () => { + const [isVisible, setIsVisible] = useState(false); + const [isSignNow, setIsSignNow] = useState(false); + + const dispatch = useDispatch(); + + const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); + const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); + const userDetails = useSelector((state: RootState) => state.user); + const [selfUserSign, setSelfUserSign] = useState({ sign: false, recipientId: 0 }); + + if (!accountId || !transactionId) { + window.location.href = "/"; + return null; + } + // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) + const selectedDocuments = useSelector((state: RootState) => state.documents.documents) + // const [transactionData, setTransactionData] = useState({}) + + + useEffect(() => { + selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId })); + selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId })); + setIsVisible(true) + }, []) + + + useEffect(() => { + if (selectedRecipients.length === 0) return; + + let minRoutingOrder: number = Infinity; + let userEmailPresent = { + present: false, + routingOrder: 0, + recipientId: 0, + type: recipientType.CC + }; + // Getting Minimum RoutingOrder + for (let i = 0; i < selectedRecipients.length; i++) { + const recipient = selectedRecipients[i] + if (recipient.routingOrder < minRoutingOrder) { + minRoutingOrder = recipient.routingOrder; + } + + if (recipient.email === userDetails.email) { + userEmailPresent.present = true; + userEmailPresent.routingOrder = recipient.routingOrder; + userEmailPresent.recipientId = Number(recipient.id); + userEmailPresent.type = recipient.type; + } + } + + if (userEmailPresent.present && userEmailPresent.routingOrder === minRoutingOrder && userEmailPresent.type !== recipientType.CC && userEmailPresent.type !== recipientType.INPERSONSIGNER) { + setSelfUserSign({ sign: true, recipientId: userEmailPresent.recipientId }) + } + + }, [selectedRecipients]) + + return ( + <> +
+ +
+ + + +
+

🎉 Awesome! The document’s been sent for signature. 🎉

+
+ + +
+ +
+
+
+ +
+
+

Your Document is send for signature

+

All signers will be notified via email

+
+
+ +
+
+
+
+

QuickPreview

+
+ {/* */} +
+ +
+
+
+ + + {/* Right Side */} +
+ + {/*
+
+ +
+
*/} + + {/*
+

Document Send Successfully!

+
*/} +
+ +
+ +
+ {selfUserSign.sign && Sign Now} + Home +
+ + {/*
*/} + {/*
+ +
*/} + {/*
*/} + +
+
+ +
+
+ + +
+ + ) +} + +export default TransactionSentSuccess \ No newline at end of file diff --git a/src/components/process/preview/FinalPreview.tsx b/src/components/process/preview/FinalPreview.tsx new file mode 100644 index 0000000..824bd05 --- /dev/null +++ b/src/components/process/preview/FinalPreview.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import NavTwo from '../processStepTwo/NavTwo' + +const FinalPreview = () => { + return ( + <> + + + ) +} + +export default FinalPreview \ No newline at end of file diff --git a/src/components/process/processStepTwo/DocumentContainer.tsx b/src/components/process/processStepTwo/DocumentContainer.tsx index 29c111d..7522128 100644 --- a/src/components/process/processStepTwo/DocumentContainer.tsx +++ b/src/components/process/processStepTwo/DocumentContainer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useState } from 'react' +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { RootState } from '../../../store/store' @@ -35,17 +35,23 @@ export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = { } interface DocumentContainerProps { - transactionSaving: boolean; - setTransactionSaving: React.Dispatch>; - isSend: boolean | null; - setIsSend: React.Dispatch>; + transactionSaving?: boolean; + setTransactionSaving?: React.Dispatch>; + isSend?: boolean | null; + setIsSend?: React.Dispatch>; + isPreview?: boolean; + aId?: string; + tId?: string; } -const DocumentContainer: React.FC = ({ transactionSaving, setTransactionSaving, isSend }) => { +let copiedTags: DroppedItem[] = [] +let selectedDoc: { [key: string]: string | null } = { key: null } + +const DocumentContainer: React.FC = ({ transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null }) => { let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; const dispatch = useDispatch(); const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>(); - const accountId = params.accountId; + let accountId = params.accountId; let transactionId = params.transactionId if (!transactionId) { transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; @@ -57,8 +63,15 @@ const DocumentContainer: React.FC = ({ transactionSaving return null; } + // If is preview is enable + if (isPreview && aId !== null && tId !== null) { + accountId = aId; + transactionId = tId; + } + // const transactionId = useSelector((state: RootState) => state.transaction.transactionId) const navigate = useNavigate(); + const childRef = useRef(null); const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) @@ -66,7 +79,6 @@ const DocumentContainer: React.FC = ({ transactionSaving const [droppedItems, setDroppedItems] = useState({}); - interface HotKeysObj { isActive: boolean; key: string | null; @@ -213,9 +225,7 @@ const DocumentContainer: React.FC = ({ transactionSaving }) ) - console.log(droppedItemsUpdateFalse, "Line 182") - - // setDroppedItems(droppedItemsUpdateFalse); + setDroppedItems(droppedItemsUpdateFalse); } catch (error) { console.error("Error submitting recipient tags:", error); } @@ -356,9 +366,24 @@ const DocumentContainer: React.FC = ({ transactionSaving switch (hotKeysObj.hotKey) { case "Ctrl+c": - // Handle Ctrl+C + const tempCopy = { ...droppedItems[selectedTag?.key][selectedTag?.index] } + delete tempCopy.id; + copiedTags = [{ ...tempCopy, dimensions: { ...tempCopy.dimensions, top: tempCopy.dimensions.top + 10, left: tempCopy.dimensions.left + 10 } }] + console.log("from Copy copiedTags", copiedTags) break; case "Ctrl+v": + console.log(selectedDoc.key, "keyss") + let updatedKey = droppedItems[selectedDoc.key] + updatedKey.push(...copiedTags) + + console.log("copiedTags", copiedTags) + console.log("updatedKey", updatedKey) + + setDroppedItems(prevState => ({ + ...prevState, + [selectedDoc.key]: updatedKey + })); + // Handle Ctrl+V break; case "Ctrl+x": @@ -372,12 +397,11 @@ const DocumentContainer: React.FC = ({ transactionSaving break; case "Ctrl+a": docCon?.querySelectorAll("[data-tag-id]").forEach(tag => { - tag.classList.add("border-2", "border-slate-800") + tag.classList.add("border-2", "border-sky-700") tag.setAttribute("all-select-group", "true") docCon.addEventListener("click", () => { - console.log("focus in", tag) - tag.classList.remove("border-2", "border-slate-800") + tag.classList.remove("border-2", "border-sky-800") tag.removeAttribute("all-select-group") }) // docCon.addEventListener("focusout", () => { @@ -388,9 +412,15 @@ const DocumentContainer: React.FC = ({ transactionSaving }); break; case "delete": - docCon?.querySelectorAll("[all-select-group]").forEach((tag) => { - console.log(tag, "TAGID") + const allselectedTags = docCon?.querySelectorAll("[all-select-group]") ?? [] + allselectedTags.forEach((tag) => { + const tagIndex = tag.getAttribute("data-tag-id") ?? ""; + const key = tag.getAttribute("data-drop-key") ?? ""; + + childRef.current.handleRemoveTag(key, Number(tagIndex)) }) + + if (allselectedTags.length === 0) childRef.current.handleRemoveTag(selectedTag?.key, selectedTag?.index) break; default: @@ -399,10 +429,11 @@ const DocumentContainer: React.FC = ({ transactionSaving }, [hotKeysObj]) const handleDocumentFocus = (key: string) => { - const id = key.replace("_", ""); - const targetCon = document.getElementById(id); + // const id = key.replace("_", ""); + // const targetCon = document.getElementById(id); + selectedDoc.key = key; - targetCon?.addEventListener("keydown", (event) => { + document?.addEventListener("keydown", (event) => { event.preventDefault(); const ctrl = event.ctrlKey; const alt = event.altKey; @@ -425,7 +456,7 @@ const DocumentContainer: React.FC = ({ transactionSaving setHotKeysObj({ isActive: true, - key: pressedKey, + key: key, tagId: null, hotKey: hotKey }); @@ -439,18 +470,18 @@ const DocumentContainer: React.FC = ({ transactionSaving return ( <> {/* Center Doc Container - START */} -
+
{ selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => { return ( -
+

{doc.name}

-
+
{ doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => { const key = `${doc.id}_${page.index}`; @@ -466,6 +497,7 @@ const DocumentContainer: React.FC = ({ transactionSaving > = ({ +// 👶 Child component with forwardRef +const DroppedItemsTagsCon = forwardRef(({ droppedItems, pageIdkey, selectedTag, @@ -39,7 +40,7 @@ const DroppedItemsTagsCon: React.FC = ({ accountId, transactionId, transactionTemplateToggle -}) => { +}: DroppedItemsTagsConProps, ref) => { const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 }); @@ -48,9 +49,11 @@ const DroppedItemsTagsCon: React.FC = ({ } const handleRemoveTag = (key: string, targetIndex: number) => { + if(key === undefined || targetIndex === undefined) return + const tag = droppedItems[key][targetIndex]; const recipientId = tag.recipientId; - + (async () => { if ("id" in tag) { try { @@ -136,6 +139,10 @@ const DroppedItemsTagsCon: React.FC = ({ })() }; + useImperativeHandle(ref, () => ({ + handleRemoveTag + })); + // HANDLE TAG MOUSE DOWN - START // Updated handleTagMouseDown function with proper group border positioning const handleTagMouseDown = (event: React.MouseEvent, key: string, index: number) => { @@ -536,6 +543,7 @@ const DroppedItemsTagsCon: React.FC = ({ // key={index} 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)} @@ -621,6 +629,6 @@ const DroppedItemsTagsCon: React.FC = ({ {/* Render only the dropped items for this specific docId and page - ENDS */} ) -} +}) export default DroppedItemsTagsCon \ No newline at end of file diff --git a/src/components/process/processStepTwo/NavTwo.tsx b/src/components/process/processStepTwo/NavTwo.tsx index 5be3af9..7fa9612 100644 --- a/src/components/process/processStepTwo/NavTwo.tsx +++ b/src/components/process/processStepTwo/NavTwo.tsx @@ -12,14 +12,18 @@ import { fetchRecipients, recipientType, toggleToTemplates, TRANSACTION_TEMPLATE import { fetchDocuments } from '../../../store/slices/documentSlice' import { useNavigate, useParams } from 'react-router-dom' import { Bounce, toast } from 'react-toastify' +import { isPending } from '@reduxjs/toolkit' interface NavTwoProps { - transactionSaving: boolean; + transactionSaving?: boolean; // setTransactionSaving: React.Dispatch> - isFields: boolean; - setIsFields: React.Dispatch> - isSend: boolean | null; - setIsSend: React.Dispatch> + isFields?: boolean; + setIsFields?: React.Dispatch> + isSend?: boolean | null; + setIsSend?: React.Dispatch> + isPreview?: boolean + aId?: string | null + tId?: string | null } @@ -47,11 +51,11 @@ const fitObj = [ }, ] -const NavTwo: React.FC = ({ transactionSaving, isFields, setIsFields, isSend, setIsSend }) => { +const NavTwo: React.FC = ({ transactionSaving = false, isFields, setIsFields, isSend, setIsSend, isPreview = false, aId = null, tId = null }) => { let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; const dispatch = useDispatch(); const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>(); - const accountId = params.accountId; + let accountId = params.accountId; let transactionId = params.transactionId if (!transactionId) { transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; @@ -63,7 +67,12 @@ const NavTwo: React.FC = ({ transactionSaving, isFields, setIsField return null; } - + // If is preview is enable + if (isPreview && aId !== null && tId !== null) { + accountId = aId; + transactionId = tId; + } + // const [transactionSaving, setTransactionSaving] = useState(false); const [dotCount, setDotCount] = useState(0); @@ -130,8 +139,8 @@ const NavTwo: React.FC = ({ transactionSaving, isFields, setIsField if (isSignSend) { window.open(`./../../../sign/${selfUserSign.recipientId}`, "_self"); } - else { - navigate(`/document-completion-screen/${accountId}/${transactionId}`, { replace: true }) + else { + navigate(`/transaction-sent/${accountId}/${transactionId}`, { replace: true }) } } else { navigate(`/templates`); @@ -190,12 +199,12 @@ const NavTwo: React.FC = ({ transactionSaving, isFields, setIsField {/* Left Side */}
-
handleSetIsFields(true)} className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${isFields ? 'bg-slate-100' : ''}`}>

Fields

-
+
}
handleSetIsFields(false)} @@ -234,12 +243,12 @@ const NavTwo: React.FC = ({ transactionSaving, isFields, setIsField
-
+ {!isPreview &&

{transactionSaving ? `Saving ${dots}` : "Save"}

-
+
}
@@ -293,24 +302,26 @@ const NavTwo: React.FC = ({ transactionSaving, isFields, setIsField
{/* Send */} -
handleSend()} - className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'> -

- {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? "Save" : "Send"} -

- -
- {transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign &&
handleSend(selfUserSign.sign)} - className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'> -

- Sign & Send -

- -
} + {!isPreview && <> +
handleSend()} + className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'> +

+ {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? "Save" : "Send"} +

+ +
+ {transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign &&
handleSend(selfUserSign.sign)} + className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'> +

+ Sign & Send +

+ +
} + } diff --git a/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx b/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx index 6267aed..7efcaa8 100644 --- a/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx +++ b/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx @@ -302,9 +302,7 @@ const RightSideBar: React.FC = ({ droppedItems, selectedTag, s } - {selectedTag.label && - } - + { diff --git a/src/helper/LoaderUi.tsx b/src/helper/LoaderUi.tsx index 0113300..26250e2 100644 --- a/src/helper/LoaderUi.tsx +++ b/src/helper/LoaderUi.tsx @@ -1,4 +1,7 @@ import { LuSignature } from "react-icons/lu"; +import SMELogo from "../assets/SMELogo2.png"; +import { FaFileSignature } from "react-icons/fa"; + interface StyleProps { classStyle?: string; @@ -11,8 +14,8 @@ const LoaderUi = ({ classStyle = '' }: StyleProps) => {
{/* Loader box */} -
- +
+

Loading...!

diff --git a/src/index.css b/src/index.css index 6786b51..1aab2b4 100644 --- a/src/index.css +++ b/src/index.css @@ -115,7 +115,7 @@ canvas { } 50% { - color: #41644A; + color: var(--primary); /* Tailwind blue-500 */ } }