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://smashing-albacore-engaging.ngrok-free.app"
# VITE_BASE_URI="https://basically-polished-yeti.ngrok-free.app" # VITE_BASE_URI="https://basically-polished-yeti.ngrok-free.app"
# VITE_BASE_URI="https://sacha-countryfied-evolutionistically.ngrok-free.dev" # VITE_BASE_URI="https://sacha-countryfied-evolutionistically.ngrok-free.dev"
VITE_BASE_URI="http://52.10.89.255:8083" VITE_BASE_URI="http://52.10.89.255"
VITE_SSE_URI="http://52.10.89.255:8083" VITE_SSE_URI="http://52.10.89.255"

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sign Made Easy</title> <title>Sign Made Easy</title>
</head> </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 TransactionSentSuccess from "./components/process/CompletionScreen/TransactionSentSuccess";
import CompletionScreen from "./components/client/CompletionScreen"; import CompletionScreen from "./components/client/CompletionScreen";
import SessionExpired from "./components/client/SessionExpired"; import SessionExpired from "./components/client/SessionExpired";
import ExpiredPage from "./components/ExpiredPage";
const App = () => { const App = () => {
return ( return (
@ -113,8 +115,10 @@ const App = () => {
<Route path="/session_expired" element={<SessionExpired />} /> <Route path="/session_expired" element={<SessionExpired />} />
<Route path="/transaction-complete" element={<CompletionScreen />} /> <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-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-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 */} {/* Links work wothout Login */}
</Routes> </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 TransactionDataTable from "./TransactionDataTable";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../store/store"; import { RootState } from "../../store/store";
import { RiDraftLine } from "react-icons/ri";
interface Transaction { interface Transaction {
id: number; id: number;
@ -45,15 +46,25 @@ const Account = () => {
shadowColor: "shadow-yellow-100", shadowColor: "shadow-yellow-100",
navigateTo: "/transactions/waiting_for_response" 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", title: "Draft",
count: data.expiring || 0, count: data.draft || 0,
icon: <CgSandClock className="w-5 h-5 text-red-500" />, icon: <RiDraftLine className="w-5 h-5 text-indigo-500" />,
iconBg: "bg-red-100 border-red-200", iconBg: "bg-indigo-100 border-indigo-200",
textColor: "text-red-600", textColor: "text-indigo-600",
gradient: "bg-white", gradient: "bg-white",
shadowColor: "shadow-red-100", shadowColor: "shadow-indigo-100",
navigateTo: "/transactions/expiring_soon" navigateTo: "/transactions/draft"
}, },
{ {
title: "Completed", title: "Completed",
@ -185,17 +196,17 @@ const Account = () => {
} }
let res = await api.post(`/api/v1/accounts/${data.i_accountName}/invite`, payloadInvite) let res = await api.post(`/api/v1/accounts/${data.i_accountName}/invite`, payloadInvite)
if (res?.data) { if (res?.data) {
toast.success("Invite Sent Successfully...!", { // toast.success("Invite Sent Successfully...!", {
position: "top-center", // position: "top-center",
autoClose: 1500, // autoClose: 1500,
hideProgressBar: false, // hideProgressBar: false,
closeOnClick: false, // closeOnClick: false,
pauseOnHover: true, // pauseOnHover: true,
draggable: true, // draggable: true,
progress: undefined, // progress: undefined,
theme: "light", // theme: "light",
transition: Bounce, // transition: Bounce,
}); // });
await fetchTransactions() await fetchTransactions()
setIsModalOpen(false); setIsModalOpen(false);
} }

View File

@ -62,17 +62,17 @@ const Accounts = () => {
} }
let res = await api.post(`/api/v1/user/accounts`, payloadAccount) let res = await api.post(`/api/v1/user/accounts`, payloadAccount)
if (res?.data) { if (res?.data) {
toast.success("Account Add Successfully...!", { // toast.success("Account Add Successfully...!", {
position: "top-center", // position: "top-center",
autoClose: 5000, // autoClose: 5000,
hideProgressBar: false, // hideProgressBar: false,
closeOnClick: false, // closeOnClick: false,
pauseOnHover: true, // pauseOnHover: true,
draggable: true, // draggable: true,
progress: undefined, // progress: undefined,
theme: "light", // theme: "light",
transition: Bounce, // transition: Bounce,
}); // });
await fetchAccounts() await fetchAccounts()
setIsModalOpen(false); 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> <legend className='text-sm font-medium text-slate-700 mb-3'>Choose file to download</legend>
<div className='space-y-2'> <div className='space-y-2'>
<label className='cursor-pointer flex items-center gap-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> <span>All</span>
</label> </label>
<label className='cursor-pointer flex items-center gap-2'> <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> <span>Combined</span>
</label> </label>
<label className='cursor-pointer flex items-center gap-2'> <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> <span>Certificate</span>
</label> </label>

View File

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

View File

@ -8,6 +8,7 @@ import { AppDispatch } from "../../store/store";
import { fetchUserDetails } from "../../store/slices/userSlice"; import { fetchUserDetails } from "../../store/slices/userSlice";
import LoginRegisterImg from "../../assets/illustration_1_.png"; import LoginRegisterImg from "../../assets/illustration_1_.png";
import SMELogo from "../../assets/SMELogo.png"; import SMELogo from "../../assets/SMELogo.png";
import api from "../../config/api";
function UserAuthUI() { function UserAuthUI() {
const [isLogin, setIsLogin] = useState(true); const [isLogin, setIsLogin] = useState(true);
@ -34,6 +35,12 @@ function UserAuthUI() {
// Your register API call logic here // Your register API call logic here
}; };
useEffect(() => {
(async () => {
await api.get(`/api/v1/user/details`);
})()
}, []);
return ( return (
<div className="max-h-screen bg-white flex"> <div className="max-h-screen bg-white flex">
{/* Left Side - Branding */} {/* 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 { RiFileList3Line } from "react-icons/ri";
import confetti from 'canvas-confetti'
interface CompletionScreenProps { interface CompletionScreenProps {
hit?: string; 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.", message = "Your document is now complete and securely stored. We’ve emailed you a copy for your reference.",
textColorSuccess = true 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 ( return (
<div className="relative min-h-screen flex flex-col bg-gradient-to-b from-white to-blue-50 overflow-hidden"> <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"> {/* <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 <FiZap /> Login
</button> */} </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> Access your documents anytime — just <a href="/login" className="text-white bg-[var(--mainPrimaryBtn)] px-3 py-1 rounded-sm">Login</a>
</p> </p>

View File

@ -33,6 +33,7 @@ import CompletionScreen from "./CompletionScreen";
import { GrPrint } from "react-icons/gr"; import { GrPrint } from "react-icons/gr";
import ClientDownloadBtn from "./ClientDownloadBtn"; import ClientDownloadBtn from "./ClientDownloadBtn";
import ClientAuditModal from "./ClientAuditModal"; import ClientAuditModal from "./ClientAuditModal";
import axiosInstance from "../../config/axiosInstance";
@ -250,9 +251,10 @@ const SignDocument = () => {
// Ping Request - START // Ping Request - START
useEffect(() => { useEffect(() => {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
axios.get( axiosInstance.get(
`${import.meta.env.VITE_BASE_URI}/api/v1/session/ping?sessionId=${recipientId}`, `/api/v1/session/ping?sessionId=${recipientId}`,
{ {
maxRedirects: 5,
headers: { "ngrok-skip-browser-warning": "69420" }, headers: { "ngrok-skip-browser-warning": "69420" },
} }
).catch(err => console.error("Ping failed:", err)); ).catch(err => console.error("Ping failed:", err));
@ -333,7 +335,6 @@ const SignDocument = () => {
accepted: true, accepted: true,
index: index index: index
} }
setSignConfObj((prev) => { setSignConfObj((prev) => {
return ( return (
{ ...prev, [index]: { ...newObj } } { ...prev, [index]: { ...newObj } }
@ -343,8 +344,9 @@ const SignDocument = () => {
}, [saved]) }, [saved])
const fetchData = (isSign: boolean = false) => { 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: headers:
{ "ngrok-skip-browser-warning": "69420", } { "ngrok-skip-browser-warning": "69420", }
}) })
@ -376,8 +378,34 @@ const SignDocument = () => {
// setShowCompletionScreen(true) // setShowCompletionScreen(true)
setTransparentTags(true) setTransparentTags(true)
setHideTags(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()); dispatch(hideLoader());
} }
@ -389,9 +417,28 @@ const SignDocument = () => {
} }
const handleFormSubmit = async () => { 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 = { const submitData: TagsSubmitDataObj = {
tags: [] tags: []
} }
const createTagsAndSubmit: TagsSubmitDataObj = {
tags: []
}
if (!droppedItems) return; if (!droppedItems) return;
@ -407,13 +454,13 @@ const SignDocument = () => {
return; 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)))) { 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`) // handleToastError(`${tag.label} is required`)
return; return;
} }
if (tag.type === "SIGNATURE" || tag.type === "INITIALS") { if ("id" in tag && (tag.type === "SIGNATURE" || tag.type === "INITIALS")) {
const tagData: TagSubmitData = { const tagData: TagSubmitData = {
id: tag.id as string, id: tag.id as string,
type: tag.type, type: tag.type,
@ -422,7 +469,7 @@ const SignDocument = () => {
value: saved.signatureId as string value: saved.signatureId as string
} }
submitData.tags.push(tagData) submitData.tags.push(tagData)
} else { } else if ("id" in tag) {
const tagData: TagSubmitData = { const tagData: TagSubmitData = {
id: tag.id as string, id: tag.id as string,
type: tag.type, type: tag.type,
@ -431,20 +478,49 @@ const SignDocument = () => {
value: tag.value ? tag.value as string : "" value: tag.value ? tag.value as string : ""
} }
submitData.tags.push(tagData); 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; // return;
try { try {
dispatch(showLoader("Signing ...")); 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()); dispatch(hideLoader());
if (signRes.status === 200) { if (signRes.status === 200) {
dispatch(hideLoader()); dispatch(hideLoader());
setTransparentTags(true) setTransparentTags(true)
setShowConfetti(true) // setShowConfetti(true)
// setShowCompletionScreen(true) // setShowCompletionScreen(true)
setSelectedTag(null) setSelectedTag(null)
// navigate("/transaction-complete") // navigate("/transaction-complete")
@ -548,7 +624,21 @@ const SignDocument = () => {
useEffect(() => { useEffect(() => {
const allRequiredFields = droppedItems.filter((item, idx) => { const allRequiredFields = droppedItems.filter((item, idx) => {
if (!item.required) return false 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 return true
}) })
@ -558,7 +648,7 @@ const SignDocument = () => {
}, 0) }, 0)
setHorizontalFormLoader({ requiredFilled: (totalRequiredFields - allRequiredFields.length), totalRequired: totalRequiredFields }) setHorizontalFormLoader({ requiredFilled: (totalRequiredFields - allRequiredFields.length), totalRequired: totalRequiredFields })
setRequiredFilled(allRequiredFields.length ? false : true) setRequiredFilled(allRequiredFields.length || !droppedItems.length ? false : true)
}, [droppedItems, signConfObj]) }, [droppedItems, signConfObj])
@ -663,7 +753,7 @@ const SignDocument = () => {
const handleDeclineConfirm = async (reason: string, comments: string) => { const handleDeclineConfirm = async (reason: string, comments: string) => {
try { 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 })); setSignerConsent((prev) => ({ ...prev, decline: true }));
setRecSignStatus("DECLINED"); setRecSignStatus("DECLINED");
// setShowCompletionScreen(true); // setShowCompletionScreen(true);
@ -673,7 +763,7 @@ const SignDocument = () => {
} }
const handleReassignConfirm = (data: { firstName: string; lastName: string; email: string; message: string }) => { 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, name: data.firstName ? data.firstName + " " + data.lastName : data.lastName,
email: data.email, email: data.email,
message: data.message, message: data.message,
@ -845,77 +935,27 @@ const SignDocument = () => {
transactionId={"1"} transactionId={"1"}
/> />
{!transparentTags && recipient?.type !== "CC" && <> {!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 <div
className="hidden sm:block" className={`absolute z-50 left-0 bottom-0 bg-[var(--primary)] w-full flex justify-around items-center
tabIndex={1} px-2.5 transition-all duration-500 ease-in-out
onBlur={() => setIsFitDd(false)} ${requiredFilled ? "translate-y-0 opacity-100" : "translate-y-full opacity-0"}`}
> >
<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`}>
<div className="py-6 text-white"> <div className="py-6 text-white">
<p className="text-2xl font-semibold">All Set to Wrap This Up?</p> <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> </div>
<button <button
onClick={handleFormSubmit} onClick={handleFormSubmit}
disabled={requiredFilled} disabled={!requiredFilled}
className="font-semibold text-sm px-8 py-2 bg-slate-100 rounded-md text-[var(--primary)]">Finish</button> className="font-semibold text-sm px-8 py-2 bg-slate-100 rounded-md text-[var(--primary)] cursor-pointer"
>
Finish
</button>
</div> </div>
</>} )}
</div> </div>
@ -944,13 +984,13 @@ const SignDocument = () => {
{/* Document Container Start */} {/* 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 */} {/* Left Side pages start */}
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */} {/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
<div <div
id="tags-list-client-side" 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'> {!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'>
@ -1182,6 +1222,67 @@ const SignDocument = () => {
// onConfirm={({ firstName, lastName, email, message }) => handleReassignConfirm({ firstName, lastName, email, message })} // 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 > </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) setSignTagId(id)
if (saved.save) { if (saved.save) {
if (saved.isDefaultAndClicked) { if (saved.isDefaultAndClicked) {
@ -256,15 +256,15 @@ const TagsCon: React.FC<TagsConProps> = ({
} }
if (index in signConfObj) { if (id in signConfObj) {
setOpenSign(true); setOpenSign(true);
return return
}; };
setSignConfObj({ setSignConfObj({
...signConfObj, ...signConfObj,
[index]: { [id]: {
accepted: true, accepted: true,
index: index index: id
} }
}) })
return return
@ -279,9 +279,9 @@ const TagsCon: React.FC<TagsConProps> = ({
// }) // })
// }) // })
const newObj = { const newObj = {
[index]: { [id]: {
"accepted": false, "accepted": false,
index: index index: id
} }
} }
setSignConfObj({ ...newObj }) setSignConfObj({ ...newObj })
@ -302,10 +302,12 @@ const TagsCon: React.FC<TagsConProps> = ({
} }
console.log(signConfObj, "Line 305")
return ( return (
<> <>
{currDocTags.map((item, index) => { {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); console.log('pageWidth:', pageWidth, 'computed left:', (item.dimensions.left / pageWidth) * 100);
return ( return (
<div <div
@ -616,7 +618,16 @@ const TagsCon: React.FC<TagsConProps> = ({
> >
{/* <input type="checkbox" className="" /> */} {/* <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" /> <FaSignature className="text-lg text-slate-600" />
) : saved.saveType === "SELECT" ? ( ) : saved.saveType === "SELECT" ? (
<>{saved.fullName}</> <>{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"}`} 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" }} // 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" /> <TbSignature className="text-lg text-slate-600" />
: :
<img <img

View File

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

View File

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

View File

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

View File

@ -284,9 +284,21 @@ const DroppedItemsTagsCon = forwardRef(({
const handleMouseMove = (event: MouseEvent) => { 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 newWidth = startWidth + (event.clientX - startX);
let newHeight = startHeight + (event.clientY - startY); let newHeight = startHeight + (event.clientY - startY);
if (newHeight > 200 || newWidth > 400) { if (newHeight > 200 || newWidth > 400) {
if (newHeight > 200) { if (newHeight > 200) {
newHeight = 200 newHeight = 200
@ -297,6 +309,13 @@ const DroppedItemsTagsCon = forwardRef(({
} }
} }
if ((dropZone.right - (droppedElemRect.left + newWidth)) < 0) {
newWidth = startWidth
}
if ((dropZone.bottom - (droppedElemRect.bottom + newHeight)) < 0) {
newHeight = startHeight
}
// Apply the new width & height // Apply the new width & height
setDroppedItems({ setDroppedItems({
...droppedItems, ...droppedItems,

View File

@ -153,6 +153,122 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
}) })
.catch((err) => { .catch((err) => {
console.error(err.message); 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 { } else {
navigate(`/templates`); navigate(`/templates`);
toast.success("Template Saved...!", { // toast.success("Template Saved...!", {
position: "top-center", // position: "top-center",
autoClose: 1500, // autoClose: 1500,
hideProgressBar: false, // hideProgressBar: false,
closeOnClick: false, // closeOnClick: false,
pauseOnHover: true, // pauseOnHover: true,
draggable: true, // draggable: true,
progress: undefined, // progress: undefined,
theme: "light", // theme: "light",
transition: Bounce, // transition: Bounce,
}); // });
} }
}, 1000) }, 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"> className="absolute right-0 mt-2 w-48 rounded-md bg-white shadow-sm ring-1 ring-black/5 z-10">
<div <div
className="py-1"> 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 Save & Close
</button> </button> */}
<button <button
onClick={() => { onClick={() => {
setMessageModal(true) 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"> className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Edit Recipients Edit Recipients
</button> </button>
<button {/* <button
onClick={() => { onClick={() => {
setEditRecipientsModal(true) setEditRecipientsModal(true)
setShowActionsDropDown(false) setShowActionsDropDown(false)
@ -357,7 +357,7 @@ const NavTwo: React.FC<NavTwoProps> = ({
</button> </button>
<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">
Advanced Options Advanced Options
</button> </button> */}
</div> </div>
</div>} </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> <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>
<div className="flex-1 min-h-0 overflow-y-auto"> <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 */} {/* Upload 1 - Full Signature */}
<div className="md:col-span-8"> <div className="md:col-span-8">
<label className="block text-xs md:text-sm font-medium text-gray-700 mb-2">Full Name <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) => ( {navigationItems.map((item) => (
<> <>
<div className='relative'> <div className='relative'>
<button <button
key={item.id} key={item.id}
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')} 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-[var(--primary)]'
: 'text-gray-400' : 'text-gray-400'
}`} }`}
@ -509,7 +509,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
{/* Bottom - START */} {/* 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 */} {/* Save Defaults - START */}
<div className="text-gray-700 flex-shrink-0 px-2"> <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"> <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 ( 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'> <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="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> <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>
<div className="flex-1 overflow-y-auto"> <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 */} {/* 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 <label className="block text-sm font-medium mb-2">Full Name
<span className='text-red-600'> *</span> <span className='text-red-600'> *</span>
</label> </label>
@ -285,7 +285,7 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
</div> </div>
{/* Initials Canvas */} {/* Initials Canvas */}
<div className="md:col-span-4"> <div className="col-span-4">
<label className="block text-sm font-medium mb-2">Initials <label className="block text-sm font-medium mb-2">Initials
<span className='text-red-600'> *</span> <span className='text-red-600'> *</span>
</label> </label>

View File

@ -1,268 +1,227 @@
import { useEffect, useState } from 'react' import React, { useEffect, useState } from "react";
import { useSelector } from 'react-redux'; import { useLocation, useNavigate } from "react-router-dom";
import { useNavigate } from 'react-router-dom'; import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import logoRecord from '../../assets/record.svg'; import logoRecord from "../../assets/record.svg";
import LoaderUi from '../../helper/LoaderUi'; import Loader from "../dashboard/Loader";
import api from '../../config/api'; import api from "../../config/api";
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; import TemplateDeleteWarnModal from "./TemplateDeleteWarnModal";
import { TemplateInterface } from '../../utlity/types';
import TemplateDeleteWarnModal from './TemplateDeleteWarnModal'; interface Template {
import Loader from '../dashboard/Loader'; 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 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>( const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null') JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
); );
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const getAllTemplates = async () => { const [templates, setTemplates] = useState<Template[]>([]);
setLoading(true) 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 { try {
let res = await api.get(`/api/v1/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc`) const res = await api.get(
let templateData = res?.data `/api/v1/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc&page=${page}&size=${size}`
setTemplates(templateData) );
const { content = [], page: pageData = {} } = res.data;
setTemplates(content);
setTotalPages(pageData.totalPages || 0);
} catch (error) { } catch (error) {
console.log(error) console.error("Error fetching templates:", error);
} finally { } finally {
setLoading(false) setLoading(false);
}
} }
};
useEffect(() => { useEffect(() => {
if (activeAccountId) { fetchTemplates();
getAllTemplates() }, [activeAccountId, page, size]);
}
}, [activeAccountId])
const handleUseClick = async (item: Template) => {
// 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) => {
try { try {
const payload = { const payload = {
name: item?.name ?? "", name: item.name ?? "",
subject: item.subject, subject: item.description,
message: item.message, message: "",
description: item.description,
expireAfter: item.expireAfter, expireAfter: item.expireAfter,
enableIdStamping: item.enableIdStamping,
allowViewAudit: item.allowViewAudit,
allowReassign: item.allowReassign,
enableSignatureIdStamping: item.enableSignatureIdStamping,
templateId: item.id, templateId: item.id,
}; };
let res = await api.post(`/api/v1/accounts/${activeAccountId}/transactions`, payload) const res = await api.post(`/api/v1/accounts/${activeAccountId}/transactions`, payload);
// /process-step-one/:accountId/:transactionId
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`); navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) { } catch (error) {
console.log(error) console.error(error);
}
} }
};
const handleEditClick = (templateId: string) => { const handleEditClick = (templateId: string) => {
setShowMenu(null); setShowMenu({ open: false, templateId: null });
navigate(`/process-step-one/${accountId}/template/${templateId}`); navigate(`/process-step-one/${activeAccountId}/template/${templateId}`);
} };
const handleDeleteClick = async (templateId: string) => { const handleDeleteClick = async (templateId: string) => {
setShowMenu(null); setShowMenu({ open: false, templateId: null });
try { try {
await api.delete(`api/accounts/${activeAccountId}/templates/${templateId}`) await api.delete(`/api/v1/accounts/${activeAccountId}/templates/${templateId}`);
setTemplates((prev) => { setTemplates((prev) => prev.filter((temp) => temp.id !== Number(templateId)));
return prev.filter((temp) => temp.id !== Number(templateId)); setShowTemplateDeleteWarnModal({ show: false, templateId: "" });
}); } catch (e) {
setShowTemplateDeleteWarnModal({ show: false, templateId: "" }) console.error(e);
} 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';
}
// 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 ( return (
<> <div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden p-5">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
{/* Header */} {/* Header */}
{location.pathname !== '/templates' && {location.pathname !== "/templates" && (
<div className="flex justify-between items-center px-6 py-4 border-b border-gray-100"> <div className="flex justify-between items-center pb-3">
<h2 className="text-sm font-semibold text-gray-800">Recent Template</h2> <h2 className="text-lg font-semibold text-gray-800">Recent Templates</h2>
<span className=' text-[var(--primary)] text-xs hover:underline cursor-pointer font-semibold'>View all</span> <span
</div>} onClick={() => navigate("/templates")}
className="text-[var(--primary)] text-sm hover:underline cursor-pointer font-medium"
{/* Scrollable Table Container */} >
<div className={`overflow-auto no-scrollbar ${location.pathname === '/templates' ? "min-h-[76vh] max-h-[76vh]" : "min-h-[70vh] max-h-[70vh]"}`}> View all
<table className="w-full text-sm"> </span>
{/* Sticky Header */} </div>
<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> </tr>
</thead> </thead>
{loading ? ( {loading ? (
<tbody> <tbody>
<tr> <tr>
<td colSpan="6" className="relative h-100"> <td colSpan={6} className="py-10 text-center">
<Loader /> <Loader />
</td> </td>
</tr> </tr>
</tbody> </tbody>
) : templates.length > 0 ? ( ) : templates.length > 0 ? (
<> <tbody>
{/* Scrollable Body */} {templates.map((template, idx) => (
<tbody className="bg-white divide-y divide-gray-100">
{templates.map((item, index) => (
<tr <tr
key={index} key={template.id}
className="hover:bg-gray-50 transition-colors duration-150" className={`${idx % 2 !== 0 ? "bg-gray-50" : "bg-white"
} border-b border-gray-300 hover:bg-gray-100 transition`}
> >
<td className="px-6 py-4 text-left"> <td className="px-6 py-3 text-gray-800 font-medium truncate max-w-xs">
<div className="text-gray-800 font-medium truncate max-w-xs"> {template.name || "Untitled"}
{item.name || 'Untitled'}
</div>
</td> </td>
<td className="px-6 py-3 text-gray-700">
{location.pathname === '/templates' && ( {template.createdBy?.name || "-"}
<>
<td className="px-6 py-4 text-center">
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(item.createdAt)}
</span>
</td> </td>
<td className="px-6 py-4 text-center"> <td className="px-6 py-3 text-gray-600">{formatDate(template.createdAt)}</td>
<span className="text-gray-600 whitespace-nowrap"> <td className="px-6 py-3 text-gray-600">
{formatTimestamp(item.lastModifiedAt)} {formatDate(template.lastModifiedAt)}
</span>
</td> </td>
</> <td className="px-6 py-3 text-gray-700">
)} <div className="relative inline-block text-left">
{/* <td className="px-6 py-4 text-center">
<button <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);
}
}}
>
<button
className="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
onClick={() => onClick={() =>
setShowMenu(showMenu === index ? null : index) 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" /> <PiDotsThreeOutlineVerticalFill className="text-lg" />
</button> </button>
{showMenu === index && ( {showMenu.open && showMenu.templateId === template.id && (
<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"> <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 <button
onClick={() => handleUseClick(item)} onClick={() => handleUseClick(template)}
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" className="w-full text-left px-4 py-2 text-sm hover:bg-blue-50 hover:text-blue-600"
> >
Use Use
</button> </button>
<button <button
onClick={() => handleEditClick(item.id)} onClick={() => handleEditClick(template.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" className="w-full text-left px-4 py-2 text-sm hover:bg-yellow-50 hover:text-yellow-600"
> >
Edit Edit
</button> </button>
<button <button
onClick={() => { onClick={() =>
setShowTemplateDeleteWarnModal({ show: true, templateId: item.id+"" }) setShowTemplateDeleteWarnModal({
}} show: true,
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" templateId: template.id + "",
})
}
className="w-full text-left px-4 py-2 text-sm hover:bg-red-50 hover:text-red-600"
> >
Delete Delete
</button> </button>
</div> </div>
)} )}
</div> </div>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</>
) : ( ) : (
<tbody> <tbody>
<tr> <tr>
<td colSpan="6" className="text-center py-10 text-gray-500"> <td colSpan={6} className="text-center py-10 text-gray-500">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center">
<img <img
src={logoRecord} src={logoRecord}
alt="Record Not Found" alt="No Templates"
className="w-70 h-70 mb-4 opacity-70" className="w-32 h-32 mb-4 opacity-70"
/> />
<p>No templates found</p>
</div> </div>
</td> </td>
</tr> </tr>
@ -271,15 +230,60 @@ const TemplateTable = () => {
</table> </table>
</div> </div>
{ {/* Pagination */}
showTemplateDeleteWarnModal.show && <TemplateDeleteWarnModal {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} setShowTemplateDeleteWarnModal={setShowTemplateDeleteWarnModal}
onConfirm={() => handleDeleteClick(showTemplateDeleteWarnModal.templateId)} onConfirm={() => handleDeleteClick(showTemplateDeleteWarnModal.templateId)}
/> />
} )}
</div> </div>
</> );
) };
}
export default TemplateTable export default TemplateTable;

View File

@ -114,7 +114,7 @@ const TransactionView = () => {
const handleNavigateToSign = (id: number) => { const handleNavigateToSign = (id: number) => {
signSessionRedirect(accountId, transactionId, id+"" as string) signSessionRedirect(accountId, transactionId, id + "" as string)
} }
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
@ -202,18 +202,21 @@ const TransactionView = () => {
<strong>Host:</strong> {recipient.hostEmail} <strong>Host:</strong> {recipient.hostEmail}
</p> </p>
)} )}
{"message" in recipient && <p className="text-xs text-gray-500 italic">
{recipient.message}
</p>}
</div> </div>
</div> </div>
{/* Right section: status and actions */} {/* 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="flex flex-wrap gap-3 items-center relative">
<div className="pl-6 relative"> <div className="pl-6 relative">
<AiFillSignature className="absolute left-0 top-0 text-[1rem]" /> <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> <p className={`text-black text-xs font-semibold rounded-lg px-2 py-0.5 ${getStatusBadge(recipient.status)}`}>{recipient.status}</p>
</div> </div>
{transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status !== "COMPLETED" && ( {transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status === "PENDING" && (
<button <button
onClick={() => handleNavigateToSign(recipient.id)} 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" 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 View signed location
</a> </a>
)} */} )} */}
</div> </div>}
</div> </div>
<hr className="my-3 border-slate-200" /> <hr className="my-3 border-slate-200" />

View File

@ -1,10 +1,9 @@
// src/api.js // src/api.js
import axios from "axios"; import axios from "axios";
import { redirect } from "react-router-dom"; // if using react-router
// or use your preferred navigation method
const api = axios.create({ const api = axios.create({
baseURL: import.meta.env.VITE_BASE_URI, baseURL: import.meta.env.VITE_BASE_URI,
maxRedirects: 5,
withCredentials: true, withCredentials: true,
headers: { headers: {
"Content-Type": "application/json", "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 = {
const persistedRoot = localStorage.getItem('persist:root');
const parsedRoot = JSON.parse(persistedRoot);
if(!persistedRoot || !("user" in parsedRoot)) {
return axios.create({
baseURL: import.meta.env.VITE_BASE_URI,
withCredentials: true,
headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"ngrok-skip-browser-warning": "69420", "ngrok-skip-browser-warning": "69420",
}, };
});
}
const user = JSON.parse(parsedRoot.user); const axiosInstance: AxiosInstance = axios.create({
const accessToken = user.accessToken;
return axios.create({
baseURL: import.meta.env.VITE_BASE_URI, baseURL: import.meta.env.VITE_BASE_URI,
withCredentials: true, withCredentials: true,
headers: { headers: defaultHeaders,
"ngrok-skip-browser-warning": "69420", validateStatus: (status) => status >= 200 && status < 400,
authorization: accessToken, });
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 });
}
// 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 });
}
// 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 { 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"; export type SignType = "self-sign" | "multiple-sign";