07-10
4
.env
|
|
@ -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"
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 822 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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"}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 >
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||