This commit is contained in:
=bhagwanSingh 2025-10-07 17:01:26 +05:30
parent 01c89d413f
commit d4b42d3b11
33 changed files with 889 additions and 452 deletions

4
.env
View File

@ -6,5 +6,5 @@
# 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="http://52.10.89.255:8083"
VITE_SSE_URI="http://52.10.89.255:8083"
VITE_BASE_URI="http://52.10.89.255"
VITE_SSE_URI="http://52.10.89.255"

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sign Made Easy</title>
</head>

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -25,6 +25,8 @@ import Contacts from "./components/contacts/Contacts";
import TransactionSentSuccess from "./components/process/CompletionScreen/TransactionSentSuccess";
import CompletionScreen from "./components/client/CompletionScreen";
import SessionExpired from "./components/client/SessionExpired";
import ExpiredPage from "./components/ExpiredPage";
const App = () => {
return (
@ -113,8 +115,10 @@ const App = () => {
<Route path="/session_expired" element={<SessionExpired />} />
<Route path="/transaction-complete" 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 reassgned to another user"} textColorSuccess={false} />} />
<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 reassgned to another user"} textColorSuccess={false} />} />
<Route path="/expired" element={<ExpiredPage />} />
{/* Links work wothout Login */}
</Routes>

View File

@ -0,0 +1,72 @@
"use client"
import { FiClock } from "react-icons/fi"
import { Link } from "react-router-dom"
export interface ExpiredPageProps {
title?: string
message?: string
actionLabel?: string
actionHref?: string
onAction?: () => void
secondaryLabel?: string
secondaryHref?: string
}
export default function ExpiredPage({
title = "Link expired",
message = "This link has expired or is no longer valid. Request a new one to continue.",
actionLabel = "Go back home",
actionHref = "/",
onAction,
secondaryLabel,
secondaryHref,
}: ExpiredPageProps) {
return (
<main className="min-h-[60vh] w-full flex items-center justify-center px-6 py-16">
<section aria-labelledby="expired-title" className="w-full max-w-md text-center space-y-6">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-secondary">
{/* Decorative icon */}
<FiClock className="h-7 w-7 text-primary" aria-hidden="true" />
</div>
<div className="space-y-2">
<h1 id="expired-title" className="text-3xl font-semibold tracking-tight text-balance">
{title}
</h1>
<p className="text-muted-foreground text-pretty">{message}</p>
</div>
<div className="flex flex-col items-center justify-center gap-3 sm:flex-row">
{onAction ? (
// Primary button (handler)
<button
type="button"
onClick={onAction}
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{actionLabel}
</button>
) : (
// Primary button (link)
<Link
to={actionHref}
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{actionLabel}
</Link>
)}
{secondaryHref && secondaryLabel ? (
<Link
to={secondaryHref}
className="text-sm text-muted-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline"
>
{secondaryLabel}
</Link>
) : null}
</div>
</section>
</main>
)
}

View File

@ -12,6 +12,7 @@ import { CgSandClock } from "react-icons/cg";
import TransactionDataTable from "./TransactionDataTable";
import { useSelector } from "react-redux";
import { RootState } from "../../store/store";
import { RiDraftLine } from "react-icons/ri";
interface Transaction {
id: number;
@ -45,15 +46,25 @@ const Account = () => {
shadowColor: "shadow-yellow-100",
navigateTo: "/transactions/waiting_for_response"
},
// {
// title: "Expiring Soon",
// count: data.expiring || 0,
// icon: <CgSandClock className="w-5 h-5 text-red-500" />,
// iconBg: "bg-red-100 border-red-200",
// textColor: "text-red-600",
// gradient: "bg-white",
// shadowColor: "shadow-red-100",
// navigateTo: "/transactions/expiring_soon"
// },
{
title: "Expiring Soon",
count: data.expiring || 0,
icon: <CgSandClock className="w-5 h-5 text-red-500" />,
iconBg: "bg-red-100 border-red-200",
textColor: "text-red-600",
title: "Draft",
count: data.draft || 0,
icon: <RiDraftLine className="w-5 h-5 text-indigo-500" />,
iconBg: "bg-indigo-100 border-indigo-200",
textColor: "text-indigo-600",
gradient: "bg-white",
shadowColor: "shadow-red-100",
navigateTo: "/transactions/expiring_soon"
shadowColor: "shadow-indigo-100",
navigateTo: "/transactions/draft"
},
{
title: "Completed",
@ -185,17 +196,17 @@ const Account = () => {
}
let res = await api.post(`/api/v1/accounts/${data.i_accountName}/invite`, payloadInvite)
if (res?.data) {
toast.success("Invite Sent Successfully...!", {
position: "top-center",
autoClose: 1500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// toast.success("Invite Sent Successfully...!", {
// position: "top-center",
// autoClose: 1500,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
await fetchTransactions()
setIsModalOpen(false);
}

View File

@ -62,17 +62,17 @@ const Accounts = () => {
}
let res = await api.post(`/api/v1/user/accounts`, payloadAccount)
if (res?.data) {
toast.success("Account Add Successfully...!", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// toast.success("Account Add Successfully...!", {
// position: "top-center",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
await fetchAccounts()
setIsModalOpen(false);
}

View File

@ -40,15 +40,15 @@ const DownloadDocument: React.FC<DownloadDocumentInt> = ({ showMenu, setShowMenu
<legend className='text-sm font-medium text-slate-700 mb-3'>Choose file to download</legend>
<div className='space-y-2'>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' value={DownloadTypesEnum.ALL} />
<input type='radio' className='cursor-pointer' name='download-doc-type' defaultChecked value={DownloadTypesEnum.ALL} />
<span>All</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' defaultChecked value={DownloadTypesEnum.COMBINED} />
<input type='radio' className='cursor-pointer' name='download-doc-type' value={DownloadTypesEnum.COMBINED} />
<span>Combined</span>
</label>
<label className='cursor-pointer flex items-center gap-2'>
<input type='radio' className='cursor-pointer' name='download-doc-type' defaultChecked value={DownloadTypesEnum.CERTIFICATE} />
<input type='radio' className='cursor-pointer' name='download-doc-type' value={DownloadTypesEnum.CERTIFICATE} />
<span>Certificate</span>
</label>

View File

@ -52,7 +52,7 @@ const TransactionDataTable = ({ activeAccountId }) => {
let addParams = `?sortBy=createdAt desc&page=${page}&size=${Math.min(size, 30)}`
switch(filter) {
case "action_required":
addParams = addParams+"&filter=inbox"
addParams = addParams+"&filter=inbox&status=SENT"
break;
case "waiting_for_response":
addParams = addParams+"&status=sent"
@ -61,7 +61,10 @@ const TransactionDataTable = ({ activeAccountId }) => {
addParams = addParams+"&filter=inbox"
break;
case "completed":
addParams = addParams+"&status=completed&filter=completed"
addParams = addParams+"&filter=completed"
break;
case "draft":
addParams = addParams+"&filter=draft"
break;
default:
break;

View File

@ -8,6 +8,7 @@ import { AppDispatch } from "../../store/store";
import { fetchUserDetails } from "../../store/slices/userSlice";
import LoginRegisterImg from "../../assets/illustration_1_.png";
import SMELogo from "../../assets/SMELogo.png";
import api from "../../config/api";
function UserAuthUI() {
const [isLogin, setIsLogin] = useState(true);
@ -34,6 +35,12 @@ function UserAuthUI() {
// Your register API call logic here
};
useEffect(() => {
(async () => {
await api.get(`/api/v1/user/details`);
})()
}, []);
return (
<div className="max-h-screen bg-white flex">
{/* Left Side - Branding */}

View File

@ -1,5 +1,6 @@
import React from "react";
import React, { useEffect } from "react";
import { RiFileList3Line } from "react-icons/ri";
import confetti from 'canvas-confetti'
interface CompletionScreenProps {
hit?: string;
@ -14,6 +15,40 @@ 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
}) => {
useEffect(() => {
const duration = 3000;
const end = Date.now() + duration;
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
confetti({
particleCount: 5, // fewer particles per burst for smoothness
angle: 90, // falling straight down
spread: 30, // slight spread
startVelocity: 8, // gentle falling speed
origin: {
x: Math.random(), // random horizontal position across the top
y: 0,
},
scalar: 1,
zIndex: 9999,
});
}, 50); // fire every 50ms for continuous effect
return () => clearInterval(interval);
}, []);
return (
<div className="relative min-h-screen flex flex-col bg-gradient-to-b from-white to-blue-50 overflow-hidden">
@ -49,7 +84,7 @@ 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="font-semibold mt-4 2xl:mt-6 text-md 2xl:text-xl">
<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>

View File

@ -33,6 +33,7 @@ import CompletionScreen from "./CompletionScreen";
import { GrPrint } from "react-icons/gr";
import ClientDownloadBtn from "./ClientDownloadBtn";
import ClientAuditModal from "./ClientAuditModal";
import axiosInstance from "../../config/axiosInstance";
@ -250,9 +251,10 @@ const SignDocument = () => {
// Ping Request - START
useEffect(() => {
const intervalId = setInterval(() => {
axios.get(
`${import.meta.env.VITE_BASE_URI}/api/v1/session/ping?sessionId=${recipientId}`,
axiosInstance.get(
`/api/v1/session/ping?sessionId=${recipientId}`,
{
maxRedirects: 5,
headers: { "ngrok-skip-browser-warning": "69420" },
}
).catch(err => console.error("Ping failed:", err));
@ -333,7 +335,6 @@ const SignDocument = () => {
accepted: true,
index: index
}
setSignConfObj((prev) => {
return (
{ ...prev, [index]: { ...newObj } }
@ -343,8 +344,9 @@ const SignDocument = () => {
}, [saved])
const fetchData = (isSign: boolean = false) => {
axios.get(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}`,
axiosInstance.get(`/api/v1/sign/${recipientId}`,
{
maxRedirects: 5,
headers:
{ "ngrok-skip-browser-warning": "69420", }
})
@ -376,9 +378,35 @@ const SignDocument = () => {
// setShowCompletionScreen(true)
setTransparentTags(true)
setHideTags(true)
}
if (res?.data?.tagJsons && res?.data?.tagJsons.length > 0) {
for (const tag of res?.data?.tagJsons) {
if (tag.type === "SIGNATURE" && "value" in tag && tag.value) {
axiosInstance.get(`/api/v1/sign/${recipientId}/signatures/${tag.value}`).then((res) => {
console.log(res, "line 386")
}).catch((err) => {
console.log(err)
})
// const [saved, setSaved] = useState<SaveType>({
// signatureId: null,
// saveType: null,
// save: false,
// signature: null,
// initial: null,
// });
} else if (tag.type === "INITIALS") {
}
}
}
dispatch(hideLoader());
}
@ -389,9 +417,28 @@ const SignDocument = () => {
}
const handleFormSubmit = async () => {
let newSignatureIdForCreateDefault = null;
if (saved.isDefaultAndClicked) {
try {
const res = await axiosInstance.post(`/api/v1/sign/${recipientId}/signatures`, {
signatureId: saved.signatureId
});
if (res.status === 201 || res.status === 202) {
newSignatureIdForCreateDefault = res.data.id;
}
} catch (err) {
console.log(err);
}
}
const submitData: TagsSubmitDataObj = {
tags: []
}
const createTagsAndSubmit: TagsSubmitDataObj = {
tags: []
}
if (!droppedItems) return;
@ -407,13 +454,13 @@ const SignDocument = () => {
return;
}
for (const tag of droppedItems) {
for (let tag of droppedItems) {
if ("required" in tag && tag.required && ((tag.type !== "SIGNATURE" && tag.type !== "INITIALS") && (tag.value === undefined || tag.value === null || tag.value === "" || !("value" in tag)))) {
// handleToastError(`${tag.label} is required`)
return;
}
if (tag.type === "SIGNATURE" || tag.type === "INITIALS") {
if ("id" in tag && (tag.type === "SIGNATURE" || tag.type === "INITIALS")) {
const tagData: TagSubmitData = {
id: tag.id as string,
type: tag.type,
@ -422,7 +469,7 @@ const SignDocument = () => {
value: saved.signatureId as string
}
submitData.tags.push(tagData)
} else {
} else if ("id" in tag) {
const tagData: TagSubmitData = {
id: tag.id as string,
type: tag.type,
@ -431,20 +478,49 @@ const SignDocument = () => {
value: tag.value ? tag.value as string : ""
}
submitData.tags.push(tagData);
} else if (tag.type === "SIGNATURE" || tag.type === "INITIALS") {
// Not getting recipient id in details it is undefined - not working self sign
tag.recipientId = recipient.id;
tag.documentId = Number(tag.documentId)
createTagsAndSubmit.tags.push(
{
...tag,
type: tag.type,
// subType: saved.saveType as string,
signed: true,
value: newSignatureIdForCreateDefault as string
})
} else {
tag.recipientId = recipient.id
tag.documentId = Number(tag.documentId)
createTagsAndSubmit.tags.push(
{
...tag,
type: tag.type,
// subType: tag.subType ? tag.subType : "",
signed: true,
value: tag.value ? tag.value as string : ""
})
}
}
// return;
try {
dispatch(showLoader("Signing ..."));
await axios.put(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}/tags`, submitData, { headers: { "ngrok-skip-browser-warning": "69420", } })
const signRes = await axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}`, {}, { headers: { "ngrok-skip-browser-warning": "69420", } })
if (submitData.tags.length) {
await axiosInstance.put(`/api/v1/sign/${recipientId}/tags`, submitData, { headers: { "ngrok-skip-browser-warning": "69420", } })
} else if (createTagsAndSubmit.tags.length) {
await axiosInstance.post(`/api/v1/sign/${recipientId}/tags`, createTagsAndSubmit, { headers: { "ngrok-skip-browser-warning": "69420", } })
}
const signRes = await axiosInstance.post(`/api/v1/sign/${recipientId}`, {}, { headers: { "ngrok-skip-browser-warning": "69420", } })
dispatch(hideLoader());
if (signRes.status === 200) {
dispatch(hideLoader());
setTransparentTags(true)
setShowConfetti(true)
// setShowConfetti(true)
// setShowCompletionScreen(true)
setSelectedTag(null)
// navigate("/transaction-complete")
@ -548,7 +624,21 @@ const SignDocument = () => {
useEffect(() => {
const allRequiredFields = droppedItems.filter((item, idx) => {
if (!item.required) return false
if ((item.required && item.value && item.type != "SIGNATURE" && item.type != "INITIALS") || (idx in signConfObj && saved.save)) return false
if ((item.required && item.value && item.type != "SIGNATURE" && item.type != "INITIALS") ||
(
(item.id !== null && item.id !== undefined && item.id !== "")
? (
(`${item.id}`) in signConfObj && signConfObj[`${item.id}`].accepted
) : (
(`${idx}`) in signConfObj && signConfObj[`${idx}`].accepted
)
)
) return false
return true
})
@ -558,7 +648,7 @@ const SignDocument = () => {
}, 0)
setHorizontalFormLoader({ requiredFilled: (totalRequiredFields - allRequiredFields.length), totalRequired: totalRequiredFields })
setRequiredFilled(allRequiredFields.length ? false : true)
setRequiredFilled(allRequiredFields.length || !droppedItems.length ? false : true)
}, [droppedItems, signConfObj])
@ -663,7 +753,7 @@ const SignDocument = () => {
const handleDeclineConfirm = async (reason: string, comments: string) => {
try {
await axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}/decline`, { reason: reason + ", " + comments });
await axiosInstance.post(`/api/v1/sign/${recipientId}/decline`, { reason: reason + ", " + comments });
setSignerConsent((prev) => ({ ...prev, decline: true }));
setRecSignStatus("DECLINED");
// setShowCompletionScreen(true);
@ -673,7 +763,7 @@ const SignDocument = () => {
}
const handleReassignConfirm = (data: { firstName: string; lastName: string; email: string; message: string }) => {
axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}/reassign`, {
axiosInstance.post(`/api/v1/sign/${recipientId}/reassign`, {
name: data.firstName ? data.firstName + " " + data.lastName : data.lastName,
email: data.email,
message: data.message,
@ -845,77 +935,27 @@ const SignDocument = () => {
transactionId={"1"}
/>
{!transparentTags && recipient?.type !== "CC" && <>
<div className={`absolute z-50 w-full shadow-[0_20px_35px_rgba(0,0,0,0.25)] bottom-0 left-0 bg-white px-4 py-2 flex justify-between items-center gap-4`}>
<p className='text-sm text-slate-600'>By Signing I agree Sign Made Easy <a href="" className="text-[var(--mainPrimaryBtn)]">Terms & Conditions.</a></p>
{/* <div className='flex items-center gap-8 cursor-pointer text-xs px-2 py-1 rounded-sm outline-none'>
<FaAngleLeft
onClick={() => handleTagNavClick("P")}
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
<div>
<span className="text-[var(--mainPrimaryBtn)]">{focusElement?.navigationOrder}</span>
<span> / </span>
<span>{droppedItems.length}</span>
<span> Fields</span>
</div>
<FaAngleRight
onClick={() => handleTagNavClick("N")}
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
</div> */}
<div></div>
{/* <p className='cursor-pointer text-sm text-white bg-[var(--mainPrimaryBtn)] px-4 py-2 rounded'
onClick={handleFormSubmit}
>Sign</p> */}
<div>
{/* FIT - START */}
<div
className="hidden sm:block"
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='bg-white flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ml-2'>
<p className='tracking-wide'>Fit</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute top-[120px] bg-white rounded-sm z-50'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
onClick={() => setSelFitIndx(indx)}
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
<div
className='flex items-center justify-between w-full'>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
</div>
</div>)
})
}
</div>}
</div>
{/* FIT - END */}
</div>
</div>
</>}
{!transparentTags && recipient?.type !== "CC" && <>
<div className={`absolute z-50 ${requiredFilled ? "bottom-0 opacity-100" : "bottom-[-100px] opacity-5"} left-0 bg-[var(--primary)] w-full flex justify-around items-center transition-all duration-300 px-2.5`}>
{!transparentTags && recipient?.type !== "CC" && (
<div
className={`absolute z-50 left-0 bottom-0 bg-[var(--primary)] w-full flex justify-around items-center
px-2.5 transition-all duration-500 ease-in-out
${requiredFilled ? "translate-y-0 opacity-100" : "translate-y-full opacity-0"}`}
>
<div className="py-6 text-white">
<p className="text-2xl font-semibold">All Set to Wrap This Up?</p>
<p className="text-sm">You've completed the required fields. Review your work, then select Finish.</p>
<p className="text-sm">
You've completed the required fields. Review your work, then select Finish.
</p>
</div>
<button
onClick={handleFormSubmit}
disabled={requiredFilled}
className="font-semibold text-sm px-8 py-2 bg-slate-100 rounded-md text-[var(--primary)]">Finish</button>
disabled={!requiredFilled}
className="font-semibold text-sm px-8 py-2 bg-slate-100 rounded-md text-[var(--primary)] cursor-pointer"
>
Finish
</button>
</div>
</>}
)}
</div>
@ -944,13 +984,13 @@ const SignDocument = () => {
{/* Document Container Start */}
<div className="grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-40px)] overflow-hidden">
<div className="grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-86px)] overflow-hidden">
{/* Left Side pages start */}
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
<div
id="tags-list-client-side"
className={`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-[100vh] p-4 overflow-y-auto ${recipient?.type !== "CC" ? "bg-white" : ""} ${!provideTags && isFields ? "hidden" : ""}`}>
className={`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" : ""}`}>
{!transparentTags && <div className='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'>
@ -1017,13 +1057,13 @@ const SignDocument = () => {
style={{ "width": page.width, "height": page.height, "maxWidth": "100%", "maxHeight": "100%" }}
draggable="false"
// onLoad={(imgElem) => {
// setPageScales((prev) => ({
// ...prev,
// [page.index]: {
// x: imgElem.clientWidth / page.width,
// y: imgElem.clientHeight / page.height,
// },
// }));
// setPageScales((prev) => ({
// ...prev,
// [page.index]: {
// x: imgElem.clientWidth / page.width,
// y: imgElem.clientHeight / page.height,
// },
// }));
// }}
width={page.width}
height={page.height}
@ -1182,6 +1222,67 @@ const SignDocument = () => {
// onConfirm={({ firstName, lastName, email, message }) => handleReassignConfirm({ firstName, lastName, email, message })}
/>
{!transparentTags && recipient?.type !== "CC" && <>
<div className={`w-full shadow-[0_20px_35px_rgba(0,0,0,0.25)] bottom-0 left-0 bg-white px-4 py-2 flex justify-between items-center gap-4`}>
<p className='text-sm text-slate-600'>By Signing I agree Sign Made Easy <a href="" className="text-[var(--mainPrimaryBtn)]">Terms & Conditions.</a></p>
{/* <div className='flex items-center gap-8 cursor-pointer text-xs px-2 py-1 rounded-sm outline-none'>
<FaAngleLeft
onClick={() => handleTagNavClick("P")}
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
<div>
<span className="text-[var(--mainPrimaryBtn)]">{focusElement?.navigationOrder}</span>
<span> / </span>
<span>{droppedItems.length}</span>
<span> Fields</span>
</div>
<FaAngleRight
onClick={() => handleTagNavClick("N")}
className="bg-[var(--mainPrimaryBtn)] p-1 text-2xl text-white rounded-md" />
</div> */}
<div></div>
{/* <p className='cursor-pointer text-sm text-white bg-[var(--mainPrimaryBtn)] px-4 py-2 rounded'
onClick={handleFormSubmit}
>Sign</p> */}
<div>
{/* FIT - START */}
<div
className="hidden sm:block"
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='bg-white flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ml-2'>
<p className='tracking-wide'>Fit</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute top-[120px] bg-white rounded-sm z-50'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
onClick={() => setSelFitIndx(indx)}
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
<div
className='flex items-center justify-between w-full'>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
</div>
</div>)
})
}
</div>}
</div>
{/* FIT - END */}
</div>
</div>
</>}
</div >
)
}

View File

@ -238,8 +238,8 @@ const TagsCon: React.FC<TagsConProps> = ({
// });
}
const handleSignClick = async (index: number, id: number) => {
const handleSignClick = async (index: number, id: number | undefined) => {
id = (id !== undefined && id !== null) ? id : index
setSignTagId(id)
if (saved.save) {
if (saved.isDefaultAndClicked) {
@ -256,15 +256,15 @@ const TagsCon: React.FC<TagsConProps> = ({
}
if (index in signConfObj) {
if (id in signConfObj) {
setOpenSign(true);
return
};
setSignConfObj({
...signConfObj,
[index]: {
[id]: {
accepted: true,
index: index
index: id
}
})
return
@ -279,9 +279,9 @@ const TagsCon: React.FC<TagsConProps> = ({
// })
// })
const newObj = {
[index]: {
[id]: {
"accepted": false,
index: index
index: id
}
}
setSignConfObj({ ...newObj })
@ -302,10 +302,12 @@ const TagsCon: React.FC<TagsConProps> = ({
}
console.log(signConfObj, "Line 305")
return (
<>
{currDocTags.map((item, index) => {
if (item.pageIndex !== pageIndex || item.documentId != documentId) return <></>;
if (item.pageIndex !== pageIndex || Number(item.documentId) !== Number(documentId)) return <></>;
console.log('pageWidth:', pageWidth, 'computed left:', (item.dimensions.left / pageWidth) * 100);
return (
<div
@ -616,7 +618,16 @@ const TagsCon: React.FC<TagsConProps> = ({
>
{/* <input type="checkbox" className="" /> */}
{
(!transparentTags && !hideTags && !(saved.save && (index in signConfObj && signConfObj[index].accepted))) ? (
(!transparentTags && !hideTags && !(saved.save &&
(
(item.id !== null && item.id !== undefined && item.id !== "")
? (
(`${item.id}`) in signConfObj && signConfObj[`${item.id}`].accepted
) : (
(`${index}`) in signConfObj && signConfObj[`${index}`].accepted
)
)
)) ? (
<FaSignature className="text-lg text-slate-600" />
) : saved.saveType === "SELECT" ? (
<>{saved.fullName}</>
@ -647,7 +658,16 @@ const TagsCon: React.FC<TagsConProps> = ({
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)) ?
{((!transparentTags && !hideTags) && !(saved.save &&
(
(item.id !== null && item.id !== undefined && item.id !== "")
? (
(`${item.id}`) in signConfObj && signConfObj[`${item.id}`].accepted
) : (
(`${index}`) in signConfObj && signConfObj[`${index}`].accepted
)
)
)) ?
<TbSignature className="text-lg text-slate-600" />
:
<img

View File

@ -7,6 +7,7 @@ import { FiFileText, FiClock, FiAlertTriangle, FiCheckCircle } from "react-icons
import { CgSandClock } from "react-icons/cg";
import { useSelector } from 'react-redux';
import { RootState } from '../../store/store';
import { RiDraftLine } from 'react-icons/ri';
interface CardData {
title: string;
@ -44,15 +45,25 @@ const Home = () => {
shadowColor: "shadow-yellow-100",
navigateTo: "/transactions/waiting_for_response"
},
// {
// title: "Expiring Soon",
// count: data.expiring || 0,
// icon: <CgSandClock className="w-5 h-5 text-red-500" />,
// iconBg: "bg-red-100 border-red-200",
// textColor: "text-red-600",
// gradient: "bg-white",
// shadowColor: "shadow-red-100",
// navigateTo: "/transactions/expiring_soon"
// },
{
title: "Expiring Soon",
count: data.expiring || 0,
icon: <CgSandClock className="w-5 h-5 text-red-500" />,
iconBg: "bg-red-100 border-red-200",
textColor: "text-red-600",
title: "Draft",
count: data.draft || 0,
icon: <RiDraftLine className="w-5 h-5 text-indigo-500" />,
iconBg: "bg-indigo-100 border-indigo-200",
textColor: "text-indigo-600",
gradient: "bg-white",
shadowColor: "shadow-red-100",
navigateTo: "/transactions/expiring_soon"
shadowColor: "shadow-indigo-100",
navigateTo: "/transactions/draft"
},
{
title: "Completed",

View File

@ -352,11 +352,14 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("focusin", handleClickOutside); // Also handles tabbing out
handleFocus("firstName");
setOpenContactModal(false)
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("focusin", handleClickOutside);
};
}, []);

View File

@ -282,8 +282,25 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
// if (!field) return; // If not found, exit
const dropZone = event.currentTarget.getBoundingClientRect();
const x = event.clientX - dropZone.left - dragElemData.offsetX;
const y = event.clientY - dropZone.top - dragElemData.offsetY;
let x = event.clientX - dropZone.left - dragElemData.offsetX;
let y = event.clientY - dropZone.top - dragElemData.offsetY;
// Handling Drop Over Right
if(dropZone.width < x + dragElemData.width) {
x -= (x + dragElemData.width) - dropZone.width;
}
// Handling Drop Over Bottom
if(dropZone.height < y + dragElemData.height) {
y -= (y + dragElemData.height) - dropZone.height;
}
// Handling Drop Over Left
if(x < 0) {
x = 0;
}
// Handling Drop Over Top
if(y < 0) {
y = 0;
}
const key = `${docId}_${pageIndex}`;
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;

View File

@ -284,9 +284,21 @@ const DroppedItemsTagsCon = forwardRef(({
const handleMouseMove = (event: MouseEvent) => {
const dropZone: DOMRect = targetContainer?.getBoundingClientRect() as DOMRect;
const droppedElemRect: DOMRect = targetElement?.getBoundingClientRect() as DOMRect;
console.log(dropZone, "dropZone Line 309")
console.log(droppedElemRect, "rect Line 309")
let newWidth = startWidth + (event.clientX - startX);
let newHeight = startHeight + (event.clientY - startY);
if (newHeight > 200 || newWidth > 400) {
if (newHeight > 200) {
newHeight = 200
@ -296,6 +308,13 @@ const DroppedItemsTagsCon = forwardRef(({
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({

View File

@ -153,6 +153,122 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
})
.catch((err) => {
console.error(err.message);
let data = [
{
"editable": true,
"required": false,
"navigationOrder": 1,
"type": "RADIO"
},
{
"editable": true,
"required": true,
"navigationOrder": 10,
"type": "SIGNATURE"
},
{
"editable": true,
"required": false,
"navigationOrder": 9,
"type": "STAMP"
},
{
"editable": true,
"required": false,
"navigationOrder": 9,
"type": "NAME",
"lastName": true,
"firstName": true
},
{
"editable": true,
"required": false,
"navigationOrder": 9,
"type": "TEXT",
"maxLength": 256,
"minLength": 0,
"readOnly": false,
"multiLine": false,
"regex": []
},
{
"editable": true,
"required": false,
"navigationOrder": 1,
"type": "TEMPORAL",
"format": "date",
"dateFormat": "MM/dd/YYYY",
"currentTimeStamp": true
},
{
"editable": true,
"required": false,
"navigationOrder": 1,
"type": "SELECT",
"multiSelect": false
},
{
"editable": true,
"required": false,
"navigationOrder": 1,
"type": "ATTACHMENT"
},
{
"editable": true,
"required": false,
"navigationOrder": 9,
"type": "NUMBER",
"decimalPlace": 0
},
{
"editable": true,
"required": false,
"navigationOrder": 1,
"type": "COMPANY"
},
{
"editable": true,
"required": false,
"navigationOrder": 9,
"type": "CHECKBOX"
},
{
"editable": true,
"required": false,
"navigationOrder": 8,
"type": "EMAIL"
},
{
"editable": true,
"required": true,
"navigationOrder": 10,
"type": "INITIALS"
}
]
const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP'];
data = data.filter((obj, indx) => {
if (obj.type === "STAMP" || obj.type === "RADIO" || obj.type === "CHECKBOX" || obj.type === "ATTACHMENT") return false
return true
});
// Separate prioritized and other tags
const prioritized = data.filter((tag: any) =>
priorityOrder.includes(tag.type)
);
const others = data
.filter((tag: any) => !priorityOrder.includes(tag.type))
.sort((a: any, b: any) => a.type.localeCompare(b.type));
// Maintain the custom order of priority
const prioritizedSorted = priorityOrder
.map((type) => prioritized.find((tag: any) => tag.type === type))
.filter(Boolean); // remove undefined in case not found
setTagsData([...prioritizedSorted, ...others]);
});
}, []);

View File

@ -190,17 +190,17 @@ const NavTwo: React.FC<NavTwoProps> = ({
}
} else {
navigate(`/templates`);
toast.success("Template Saved...!", {
position: "top-center",
autoClose: 1500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// toast.success("Template Saved...!", {
// position: "top-center",
// autoClose: 1500,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
}
}, 1000)
}
@ -328,9 +328,9 @@ const NavTwo: React.FC<NavTwoProps> = ({
className="absolute right-0 mt-2 w-48 rounded-md bg-white shadow-sm ring-1 ring-black/5 z-10">
<div
className="py-1">
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
{/* <button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Save & Close
</button>
</button> */}
<button
onClick={() => {
setMessageModal(true)
@ -347,7 +347,7 @@ const NavTwo: React.FC<NavTwoProps> = ({
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Edit Recipients
</button>
<button
{/* <button
onClick={() => {
setEditRecipientsModal(true)
setShowActionsDropDown(false)
@ -357,7 +357,7 @@ const NavTwo: React.FC<NavTwoProps> = ({
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Advanced Options
</button>
</button> */}
</div>
</div>}
</div>

View File

@ -348,7 +348,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
<p className="text-xs md:text-sm text-gray-400 hidden md:block">The maximum file size for the image is 300kb. File types supported include .png, .jpg, .jpeg</p>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 md:gap-4 pb-2">
<div className="grid md:grid-cols-12 gap-3 md:gap-4 pb-2">
{/* Upload 1 - Full Signature */}
<div className="md:col-span-8">
<label className="block text-xs md:text-sm font-medium text-gray-700 mb-2">Full Name
@ -476,14 +476,14 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
<nav className="flex gap-4">
<nav className="flex gap-1 sm:gap-4">
{navigationItems.map((item) => (
<>
<div className='relative'>
<button
key={item.id}
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
className={`flex items-center justify-start space-x-2 px-3 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap ${activeTab === item.id
className={`flex items-center justify-start space-x-1 px-2 sm:space-x-2 sm:px-3 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap ${activeTab === item.id
? ' text-[var(--primary)]'
: 'text-gray-400'
}`}
@ -509,7 +509,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
{/* Bottom - START */}
<div className="px-4 py-3 flex sm:flex-row justify-between items-center gap-2 md:gap-3 flex-shrink-0">
<div className="px-4 py-3 flex flex-col items-left sm:flex-row justify-between sm:items-center gap-2 md:gap-3 flex-shrink-0">
{/* Save Defaults - START */}
<div className="text-gray-700 flex-shrink-0 px-2">
<label className="inline-flex items-center cursor-pointer text-xs md:text-sm hover:text-gray-900 transition-colors">

View File

@ -236,7 +236,7 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
};
return (
<div className="px-4 lg:px-6 py-4 flex-shrink-0">
<div className="lg:px-6 py-4 flex-shrink-0">
<div className='flex justify-between'>
{/* <h2 className="text-lg sm:text-xl lg:text-2xl font-semibold text-gray-900 mb-1 sm:mb-2 ">Draw Signature</h2> */}
<h2 className="mt-4 mb-5 text-sm">Draw your signature. <span className='text-red-600'>*</span> Are required fields.</h2>
@ -264,9 +264,9 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
</div>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 lg:gap-6">
<div className="grid grid-cols-12 gap-4 lg:gap-6 overflow-x-auto">
{/* Full Signature Canvas */}
<div className="md:col-span-8">
<div className="col-span-8">
<label className="block text-sm font-medium mb-2">Full Name
<span className='text-red-600'> *</span>
</label>
@ -285,7 +285,7 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
</div>
{/* Initials Canvas */}
<div className="md:col-span-4">
<div className="col-span-4">
<label className="block text-sm font-medium mb-2">Initials
<span className='text-red-600'> *</span>
</label>

View File

@ -1,285 +1,289 @@
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import logoRecord from '../../assets/record.svg';
import LoaderUi from '../../helper/LoaderUi';
import api from '../../config/api';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { TemplateInterface } from '../../utlity/types';
import TemplateDeleteWarnModal from './TemplateDeleteWarnModal';
import Loader from '../dashboard/Loader';
import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import logoRecord from "../../assets/record.svg";
import Loader from "../dashboard/Loader";
import api from "../../config/api";
import TemplateDeleteWarnModal from "./TemplateDeleteWarnModal";
interface Template {
id: number;
name: string | null;
description: string | null;
createdAt: string;
lastModifiedAt: string;
createdBy?: { name: string };
lastModifiedBy?: { name: string };
expireAfter?: number;
}
const formatDate = (isoString: string) => {
const date = new Date(isoString);
return isNaN(date.getTime()) ? "-" : date.toLocaleString();
};
const TemplateTable = () => {
const [loading, setLoading] = useState(false)
const [templates, setTemplates] = useState<TemplateInterface[] | []>([])
const accountDetails = useSelector((state: any) => state.user.account);
const accountId = accountDetails.id || "";
const [showMenu, setShowMenu] = useState(null);
const [showTemplateDeleteWarnModal, setShowTemplateDeleteWarnModal] = useState<{ show: boolean, templateId: string }>({ show: false, templateId: "" });
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const navigate = useNavigate();
const location = useLocation();
const getAllTemplates = async () => {
setLoading(true)
const [templates, setTemplates] = useState<Template[]>([]);
const [loading, setLoading] = useState(false);
// Pagination
const [page, setPage] = useState(0); // 0-based
const [size, setSize] = useState(10);
const [totalPages, setTotalPages] = useState(0);
// Menu state
const [showMenu, setShowMenu] = useState<{ open: boolean; templateId: number | null }>({
open: false,
templateId: null,
});
// Delete modal
const [showTemplateDeleteWarnModal, setShowTemplateDeleteWarnModal] = useState<{
show: boolean;
templateId: string;
}>({ show: false, templateId: "" });
const fetchTemplates = async () => {
if (!activeAccountId) return;
setLoading(true);
try {
let res = await api.get(`/api/v1/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc`)
let templateData = res?.data
setTemplates(templateData)
const res = await api.get(
`/api/v1/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc&page=${page}&size=${size}`
);
const { content = [], page: pageData = {} } = res.data;
setTemplates(content);
setTotalPages(pageData.totalPages || 0);
} catch (error) {
console.log(error)
console.error("Error fetching templates:", error);
} finally {
setLoading(false)
setLoading(false);
}
}
useEffect(() => {
if (activeAccountId) {
getAllTemplates()
}
}, [activeAccountId])
// Listen for sessionStorage changes
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'activeAccountId') {
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
setActiveAccountId(newActiveAccountId);
}
};
// Listen for storage events (works for changes from other tabs)
window.addEventListener('storage', handleStorageChange);
// Custom event listener for same-tab changes
const handleCustomStorageChange = (e: CustomEvent) => {
if (e.detail.key === 'activeAccountId') {
setActiveAccountId(e.detail.newValue);
}
};
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
};
}, []);
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
const handleUseClick = async (item: any) => {
useEffect(() => {
fetchTemplates();
}, [activeAccountId, page, size]);
const handleUseClick = async (item: Template) => {
try {
const payload = {
name: item?.name ?? "",
subject: item.subject,
message: item.message,
description: item.description,
name: item.name ?? "",
subject: item.description,
message: "",
expireAfter: item.expireAfter,
enableIdStamping: item.enableIdStamping,
allowViewAudit: item.allowViewAudit,
allowReassign: item.allowReassign,
enableSignatureIdStamping: item.enableSignatureIdStamping,
templateId: item.id,
};
let res = await api.post(`/api/v1/accounts/${activeAccountId}/transactions`, payload)
// /process-step-one/:accountId/:transactionId
const res = await api.post(`/api/v1/accounts/${activeAccountId}/transactions`, payload);
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) {
console.log(error)
console.error(error);
}
}
};
const handleEditClick = (templateId: string) => {
setShowMenu(null);
navigate(`/process-step-one/${accountId}/template/${templateId}`);
}
setShowMenu({ open: false, templateId: null });
navigate(`/process-step-one/${activeAccountId}/template/${templateId}`);
};
const handleDeleteClick = async (templateId: string) => {
setShowMenu(null);
setShowMenu({ open: false, templateId: null });
try {
await api.delete(`api/accounts/${activeAccountId}/templates/${templateId}`)
setTemplates((prev) => {
return prev.filter((temp) => temp.id !== Number(templateId));
});
setShowTemplateDeleteWarnModal({ show: false, templateId: "" })
} catch (e) { }
}
function formatTimestamp(isoString, options = { timeZone: 'local' }) {
// Parse the ISO string to a Date object
const date = new Date(isoString);
if (isNaN(date)) {
return 'Invalid date';
await api.delete(`/api/v1/accounts/${activeAccountId}/templates/${templateId}`);
setTemplates((prev) => prev.filter((temp) => temp.id !== Number(templateId)));
setShowTemplateDeleteWarnModal({ show: false, templateId: "" });
} catch (e) {
console.error(e);
}
};
// Return formatted date string
if (options.timeZone === 'UTC') {
return date.toUTCString(); // e.g. "Mon, 04 Aug 2025 13:46:46 GMT"
} else {
return date.toLocaleString(); // e.g. "8/4/2025, 9:46:46 AM" (based on local time zone)
}
}
return (
<>
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
{/* Header */}
{location.pathname !== '/templates' &&
<div className="flex justify-between items-center px-6 py-4 border-b border-gray-100">
<h2 className="text-sm font-semibold text-gray-800">Recent Template</h2>
<span className=' text-[var(--primary)] text-xs hover:underline cursor-pointer font-semibold'>View all</span>
</div>}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden p-5">
{/* Header */}
{location.pathname !== "/templates" && (
<div className="flex justify-between items-center pb-3">
<h2 className="text-lg font-semibold text-gray-800">Recent Templates</h2>
<span
onClick={() => navigate("/templates")}
className="text-[var(--primary)] text-sm hover:underline cursor-pointer font-medium"
>
View all
</span>
</div>
)}
{/* Scrollable Table Container */}
<div className={`overflow-auto no-scrollbar ${location.pathname === '/templates' ? "min-h-[76vh] max-h-[76vh]" : "min-h-[70vh] max-h-[70vh]"}`}>
<table className="w-full text-sm">
{/* Sticky Header */}
<thead className="bg-gray-50 sticky top-0 z-10">
<tr className="text-gray-600 text-xs uppercase tracking-wider border-b border-gray-200">
<th className="px-6 py-4 text-left font-semibold">Name</th>
{location.pathname === '/templates' && (
<>
<th className="px-6 py-4 text-center font-semibold">Created</th>
<th className="px-6 py-4 text-center font-semibold">Last Updated</th>
</>
)}
{/* <th className="px-6 py-4 text-center font-semibold">Use</th> */}
<th className="px-6 py-4 text-center font-semibold">Action</th>
{/* Table */}
<div
className={`overflow-auto rounded-lg ${location.pathname === "/templates"
? "min-h-[70vh] max-h-[70vh]"
: "min-h-[30vh] max-h-[50vh]"
}`}
>
<table className="w-full text-sm">
<thead className="sticky top-0 bg-gray-50 z-10">
<tr className="text-xs text-gray-600">
<th className="px-6 py-3 text-left font-semibold">Template Name</th>
<th className="px-6 py-3 text-left font-semibold">Created By</th>
<th className="px-6 py-3 text-left font-semibold">Created</th>
<th className="px-6 py-3 text-left font-semibold">Last Updated</th>
<th className="px-6 py-3 text-left font-semibold">Actions</th>
</tr>
</thead>
{loading ? (
<tbody>
<tr>
<td colSpan={6} className="py-10 text-center">
<Loader />
</td>
</tr>
</thead>
{loading ? (
<tbody>
<tr>
<td colSpan="6" className="relative h-100">
<Loader />
</tbody>
) : templates.length > 0 ? (
<tbody>
{templates.map((template, idx) => (
<tr
key={template.id}
className={`${idx % 2 !== 0 ? "bg-gray-50" : "bg-white"
} border-b border-gray-300 hover:bg-gray-100 transition`}
>
<td className="px-6 py-3 text-gray-800 font-medium truncate max-w-xs">
{template.name || "Untitled"}
</td>
</tr>
</tbody>
) : templates.length > 0 ? (
<>
{/* Scrollable Body */}
<tbody className="bg-white divide-y divide-gray-100">
{templates.map((item, index) => (
<tr
key={index}
className="hover:bg-gray-50 transition-colors duration-150"
>
<td className="px-6 py-4 text-left">
<div className="text-gray-800 font-medium truncate max-w-xs">
{item.name || 'Untitled'}
</div>
</td>
<td className="px-6 py-3 text-gray-700">
{template.createdBy?.name || "-"}
</td>
<td className="px-6 py-3 text-gray-600">{formatDate(template.createdAt)}</td>
<td className="px-6 py-3 text-gray-600">
{formatDate(template.lastModifiedAt)}
</td>
<td className="px-6 py-3 text-gray-700">
<div className="relative inline-block text-left">
<button
onClick={() =>
setShowMenu({
open:
showMenu.open && showMenu.templateId === template.id ? false : true,
templateId: template.id,
})
}
className="text-gray-500 hover:text-gray-700 p-1 rounded-md hover:bg-gray-100"
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button>
{location.pathname === '/templates' && (
<>
<td className="px-6 py-4 text-center">
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(item.createdAt)}
</span>
</td>
<td className="px-6 py-4 text-center">
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(item.lastModifiedAt)}
</span>
</td>
</>
)}
{/* <td className="px-6 py-4 text-center">
<button
className="cursor-pointer bg-[var(--primary)] border-gray-300 text-white hover:bg-gray-100 px-3 py-2 rounded-lg font-semibold text-xs transition-colors duration-150"
onClick={() => handleUseClick(item)}
>
USE
</button>
</td> */}
<td className="px-6 py-4 text-center">
<div
className="relative inline-block"
tabIndex={0}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setShowMenu(null);
}
}}
>
{showMenu.open && showMenu.templateId === template.id && (
<div className="absolute right-0 mt-2 w-40 bg-white border border-gray-200 rounded-lg shadow-lg z-20 animate-fade-in">
<button
className="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
onClick={() =>
setShowMenu(showMenu === index ? null : index)
}
onClick={() => handleUseClick(template)}
className="w-full text-left px-4 py-2 text-sm hover:bg-blue-50 hover:text-blue-600"
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
Use
</button>
<button
onClick={() => handleEditClick(template.id + "")}
className="w-full text-left px-4 py-2 text-sm hover:bg-yellow-50 hover:text-yellow-600"
>
Edit
</button>
<button
onClick={() =>
setShowTemplateDeleteWarnModal({
show: true,
templateId: template.id + "",
})
}
className="w-full text-left px-4 py-2 text-sm hover:bg-red-50 hover:text-red-600"
>
Delete
</button>
{showMenu === index && (
<div className="absolute top-7.5 right-0 bg-white border border-gray-200 rounded-xl shadow-lg z-20 min-w-[140px] overflow-hidden animate-fade-in">
<button
onClick={() => handleUseClick(item)}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
>
Use
</button>
<button
onClick={() => handleEditClick(item.id)}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-yellow-50 hover:text-yellow-600 transition-colors"
>
Edit
</button>
<button
onClick={() => {
setShowTemplateDeleteWarnModal({ show: true, templateId: item.id+"" })
}}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-red-50 hover:text-red-600 transition-colors"
>
Delete
</button>
</div>
)}
</div>
</td>
</tr>
))}
</tbody>
</>
) : (
<tbody>
<tr>
<td colSpan="6" className="text-center py-10 text-gray-500">
<div className="flex flex-col items-center justify-center">
<img
src={logoRecord}
alt="Record Not Found"
className="w-70 h-70 mb-4 opacity-70"
/>
)}
</div>
</td>
</tr>
</tbody>
)}
</table>
</div>
{
showTemplateDeleteWarnModal.show && <TemplateDeleteWarnModal
setShowTemplateDeleteWarnModal={setShowTemplateDeleteWarnModal}
onConfirm={() => handleDeleteClick(showTemplateDeleteWarnModal.templateId)}
/>
}
))}
</tbody>
) : (
<tbody>
<tr>
<td colSpan={6} className="text-center py-10 text-gray-500">
<div className="flex flex-col items-center">
<img
src={logoRecord}
alt="No Templates"
className="w-32 h-32 mb-4 opacity-70"
/>
<p>No templates found</p>
</div>
</td>
</tr>
</tbody>
)}
</table>
</div>
</>
)
}
export default TemplateTable
{/* Pagination */}
{templates.length > 0 && (
<div className="flex justify-between items-center mt-4 text-sm text-gray-600">
<div>
Page {page + 1} of {totalPages}
</div>
<div className="flex items-center gap-3">
<select
value={size}
onChange={(e) => {
setSize(Math.min(Number(e.target.value), 30));
setPage(0);
}}
className="border rounded px-2 py-1 text-sm"
>
{[5, 10, 20, 30].map((s) => (
<option key={s} value={s}>
{s} / page
</option>
))}
</select>
<button
onClick={() => setPage((prev) => Math.max(prev - 1, 0))}
disabled={page === 0}
className={`px-3 py-1 rounded border ${page === 0
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "bg-white hover:bg-gray-50"
}`}
>
Prev
</button>
<button
onClick={() => setPage((prev) => prev + 1)}
disabled={page + 1 >= totalPages}
className={`px-3 py-1 rounded border ${page + 1 >= totalPages
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "bg-white hover:bg-gray-50"
}`}
>
Next
</button>
</div>
</div>
)}
{showTemplateDeleteWarnModal.show && (
<TemplateDeleteWarnModal
setShowTemplateDeleteWarnModal={setShowTemplateDeleteWarnModal}
onConfirm={() => handleDeleteClick(showTemplateDeleteWarnModal.templateId)}
/>
)}
</div>
);
};
export default TemplateTable;

View File

@ -114,7 +114,7 @@ const TransactionView = () => {
const handleNavigateToSign = (id: number) => {
signSessionRedirect(accountId, transactionId, id+"" as string)
signSessionRedirect(accountId, transactionId, id + "" as string)
}
const getStatusBadge = (status: string) => {
@ -202,18 +202,21 @@ const TransactionView = () => {
<strong>Host:</strong> {recipient.hostEmail}
</p>
)}
{"message" in recipient && <p className="text-xs text-gray-500 italic">
{recipient.message}
</p>}
</div>
</div>
{/* Right section: status and actions */}
<div className="flex flex-col items-start sm:items-end gap-1 text-sm text-slate-600">
{recipient.type.toLowerCase() !== "cc" && <div className="flex flex-col items-start sm:items-end gap-1 text-sm text-slate-600">
<div className="flex flex-wrap gap-3 items-center relative">
<div className="pl-6 relative">
<AiFillSignature className="absolute left-0 top-0 text-[1rem]" />
<p className={`text-black text-xs font-semibold rounded-lg px-2 py-0.5 ${getStatusBadge(recipient.status)}`}>{recipient.status}</p>
</div>
{transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status !== "COMPLETED" && (
{transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status === "PENDING" && (
<button
onClick={() => handleNavigateToSign(recipient.id)}
className="text-white rounded-sm bg-[var(--primary)] px-3 py-1 font-semibold text-xs hover:bg-opacity-90 transition cursor-pointer"
@ -232,7 +235,7 @@ const TransactionView = () => {
View signed location
</a>
)} */}
</div>
</div>}
</div>
<hr className="my-3 border-slate-200" />
@ -274,12 +277,12 @@ const TransactionView = () => {
<div className='bg-slate-100 p-2 absolute w-full bottom-[0px]'>
<p className='text-[0.7rem] text-slate-500 flex justify-end items-center'>
{/* <span> 1 of {docsD?.pages?.length}</span> */}
<IoMdDownload
<IoMdDownload
className='cursor-pointer'
onClick={() => downloadDocumentUtils(accountId, transactionId, docsD.id)}
size={18} />
</p>
</div>

View File

@ -1,10 +1,9 @@
// src/api.js
import axios from "axios";
import { redirect } from "react-router-dom"; // if using react-router
// or use your preferred navigation method
const api = axios.create({
baseURL: import.meta.env.VITE_BASE_URI,
maxRedirects: 5,
withCredentials: true,
headers: {
"Content-Type": "application/json",

View File

@ -1,31 +1,46 @@
import axios from "axios";
// api/axios.ts
import axios, { AxiosInstance } from "axios";
export const createAxiosInstance = () => {
const defaultHeaders = {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "69420",
};
const persistedRoot = localStorage.getItem('persist:root');
const parsedRoot = JSON.parse(persistedRoot);
const axiosInstance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URI,
withCredentials: true,
headers: defaultHeaders,
validateStatus: (status) => status >= 200 && status < 400,
});
if(!persistedRoot || !("user" in parsedRoot)) {
return axios.create({
baseURL: import.meta.env.VITE_BASE_URI,
withCredentials: true,
headers: {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "69420",
},
});
axiosInstance.interceptors.response.use(
(response) => {
// ✅ Handle backend "soft redirects"
if (response?.data?.redirect) {
const redirectUrl = response.data.redirect;
// Use location.replace to avoid back button returning to invalid page
window.location.replace(redirectUrl);
// OR window.location.href = redirectUrl; if you want history entry
// Optionally return a rejected promise to stop normal handling
return Promise.reject({ redirected: true });
}
const user = JSON.parse(parsedRoot.user);
const accessToken = user.accessToken;
// Otherwise just continue as normal
return response;
},
(error) => {
// Optionally handle backend 302 or 3xx here (if returned as error)
if (error.response?.status === 302 && error.response?.headers?.location) {
window.location.replace(error.response.headers.location);
return Promise.reject({ redirected: true });
}
return axios.create({
baseURL: import.meta.env.VITE_BASE_URI,
withCredentials: true,
headers: {
"ngrok-skip-browser-warning": "69420",
authorization: accessToken,
// Pass through all other errors
return Promise.reject(error);
}
);
},
});
};
export default axiosInstance;

View File

@ -1,8 +1,4 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import axios from "axios";
import { createAxiosInstance } from "../../config/axiosInstance";
const axiosInstance = createAxiosInstance();
export type SignType = "self-sign" | "multiple-sign";