self_Sign & completion screen navigate to back & without email and lastname show error toast

This commit is contained in:
JaiPandey425 2025-08-07 16:06:32 +05:30
parent 4de7c2540e
commit 002c8e6110
11 changed files with 242 additions and 65 deletions

View File

@ -22,7 +22,6 @@ import Templates from "./components/templates/Templates";
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
import JoyrideController from "./utlity/JoyRideTour";
import Contacts from "./components/contacts/Contacts";
const App = () => {
return (

View File

@ -17,6 +17,32 @@ const SignCompletionScreen: React.FC = () => {
const [showDownloadModal, setShowDownloadModal] = useState(false);
const [animationPhase, setAnimationPhase] = useState(0);
useEffect(() => {
const currentPath = window.location.pathname;
const handlePopState = (event) => {
// Show warning
// alert("Back navigation is blocked on this screen.");
// Push the current URL back to prevent navigating away
window.history.pushState(null, "", currentPath);
};
// Push extra states to build history buffer
for (let i = 0; i < 50; i++) {
window.history.pushState(null, "", currentPath);
}
// Add popstate listener
window.addEventListener("popstate", handlePopState);
// Cleanup on unmount
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
useEffect(() => {
// Trigger animation phases sequentially
const timers = [
@ -321,7 +347,7 @@ const SignCompletionScreen: React.FC = () => {
{/* Additional Info */}
<div className={`mt-8 pt-3 border-t border-gray-200 border-opacity-50`}>
<p className="text-sm text-gray-500">
Your document is signed and ready. You can download it or return to the sign made easy.
Your document is signed and saved. Download it or return to Sign Made Easy to continue — navigation is paused until an action is taken.
</p>
</div>
</div>

View File

@ -161,6 +161,7 @@ const SignDocument = () => {
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
console.log(recSignStatus, "recSignStatus")
// Handling Date Opacity
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
@ -261,6 +262,8 @@ const SignDocument = () => {
}
}, [saved])
const handleSignClick = (index: number, id: number) => {
setSignTagId(id)
if (saved.save) {
@ -305,6 +308,35 @@ const SignDocument = () => {
}
}
// Step 2: Prevent browser back when status === 'COMPLETED'
// useEffect(() => {
// if (recSignStatus !== "COMPLETED") return;
// const currentPath = window.location.pathname;
// const handlePopState = (event) => {
// // Show warning
// alert("Back navigation is blocked on this screen.");
// // Push the current URL back to prevent navigating away
// window.history.pushState(null, "", currentPath);
// };
// // Push extra states to build history buffer
// for (let i = 0; i < 50; i++) {
// window.history.pushState(null, "", currentPath);
// }
// // Add popstate listener
// window.addEventListener("popstate", handlePopState);
// // Cleanup on unmount
// return () => {
// window.removeEventListener("popstate", handlePopState);
// };
// }, [recSignStatus]);
const fetchData = (isSign: boolean = false) => {
axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign?recipientId=${recipientId}`,
{
@ -315,17 +347,17 @@ const SignDocument = () => {
if (res.status === 200) {
setSelectedDocuments(res.data.documents)
setRecipient(res.data.details)
setDroppedItems(res.data.tagJsons)
setTransaction(res.data.transaction)
setRecSignStatus(res.data.status)
setSelectedDocuments(res?.data?.documents)
setRecipient(res?.data?.details)
setDroppedItems(res?.data?.tagJsons)
setTransaction(res?.data?.transaction)
setRecSignStatus(res?.data?.status)
if ("provideOwnTags" in res.data.details && res.data.details.provideOwnTags) {
if ("provideOwnTags" in res?.data?.details && res?.data?.details.provideOwnTags) {
setProvideTags(true)
}
if (res.data.status === "COMPLETED") {
if (res?.data?.status === "COMPLETED") {
setShowCompletionScreen(true)
}
@ -337,6 +369,7 @@ const SignDocument = () => {
})
.catch((err) => {
console.log(err)
});
}
@ -867,6 +900,8 @@ const SignDocument = () => {
};
// const [showTerms, setShowTerms] = useState<boolean>(true);
return (
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>

View File

@ -134,7 +134,7 @@ const Contacts = () => {
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[79vh] border border-gray-200 rounded shadow-sm no-scrollbar">
<div className="overflow-y-auto max-h-[79vh] border border-gray-200 rounded shadow-sm no-scrollbar ">
<table className="min-w-full text-sm bg-white">
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider sticky top-0 z-10">
<tr>
@ -162,7 +162,7 @@ const Contacts = () => {
</td>
</tr>
) : (
contacts.map((contact) => (
contacts.map((contact, index) => (
<tr key={contact.id} className="hover:bg-gray-50">
<td className="px-4 py-3">{contact.firstName}</td>
<td className="px-4 py-3">{contact.lastName}</td>
@ -181,7 +181,7 @@ const Contacts = () => {
</button>
{openMenuId === contact.id && (
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px]">
<div className={`absolute mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px] ${(contacts.length - 1) === index ? "bottom-1 right-4" : "bottom-0 right-5"}`}>
<button
onMouseDown={() => handleEdit(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import {
FaCheck,
FaFileAlt,
@ -15,12 +15,13 @@ import {
import { fetchRecipients } from "../../../store/slices/selectedRecipientsSlice";
import { fetchDocuments } from "../../../store/slices/documentSlice";
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
import Loader from "../../dashboard/Loader";
import confetti from 'canvas-confetti'
import { IoClose } from "react-icons/io5";
import { IoMdClose, IoMdHome } from "react-icons/io";
import { Bounce, toast } from "react-toastify";
import api from "../../../config/api";
type Signer = {
id: number;
@ -200,7 +201,9 @@ const DocumentCompletionScreen = () => {
{ title: 'Document 10', pages: 11 },
{ title: 'Document 11', pages: 4 }, // Won't be shown due to slice
];
const location = useLocation();
const [status, setStatus] = useState(null);
console.log("location.pathname", location.pathname, status)
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const [showConfetti, setShowConfetti] = React.useState(true)
if (!accountId || !transactionId) {
@ -265,6 +268,77 @@ const DocumentCompletionScreen = () => {
}, [showConfetti]);
console.log("History length:", window.history.length);
// Step 1: Fetch transaction status
useEffect(() => {
const fetchTransaction = async () => {
// /api/accounts/{accountId}/transactions/{trxId}
try {
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
setStatus(res?.data?.status);
} catch (error) {
console.error("Failed to fetch transaction:", error);
}
};
if (accountId && transactionId) {
fetchTransaction();
}
}, [accountId, transactionId]);
// Step 2: Prevent browser back when status === 'SENT'
useEffect(() => {
if (status === "SENT") {
const currentPath = window.location.pathname;
const handlePopState = (event) => {
// Show warning
alert("Back navigation is blocked on this screen.");
// Push the current URL back to prevent navigating away
window.history.pushState(null, "", currentPath);
};
// Push extra states to build history buffer
for (let i = 0; i < 50; i++) {
window.history.pushState(null, "", currentPath);
}
// Add popstate listener
window.addEventListener("popstate", handlePopState);
// Cleanup on unmount
return () => {
window.removeEventListener("popstate", handlePopState);
};
}
}, [status]);
// useEffect(() => {
// if (status === 'SENT') {
// // Step 1: Push a dummy state to trap the user
// window.history.pushState(null, '', window.location.href);
// // Step 2: Intercept back button
// const handlePopState = (event) => {
// // Push the same dummy state again to block back
// window.history.pushState(null, '', window.location.href);
// };
// window.addEventListener('popstate', handlePopState);
// return () => {
// window.removeEventListener('popstate', handlePopState);
// };
// }
// }, [status]);
return (
<>

View File

@ -60,6 +60,7 @@ const ProcessStepOne = () => {
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const [showSelfSignWarnModal, setShowSelfSignWarnModal] = useState(false)
const [editRecipientCurrentEmail, setEditRecipientCurrentEmail] = useState('')
console.log()
const userData = useSelector((state: RootState) => state.user)
@ -142,7 +143,8 @@ const ProcessStepOne = () => {
let formError = {
documents: false,
recipients: false,
subject: false
subject: false,
recipientEmailAndLastName:false
}
if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
@ -150,20 +152,32 @@ const ProcessStepOne = () => {
formError.documents = true
}
if (!selectedRecipients.length) {
formError.recipients = true
formError.recipients = true;
} else {
// Check if all recipients have email and lastName
const isValid = selectedRecipients.every(
(item) => item.email?.trim() !== '' && item.lastName?.trim() !== ''
);
if (!isValid) {
// alert('All recipients must have both Email and Last Name filled.');
formError.recipientEmailAndLastName = true;
}
}
if (!emailDtls.subject.length) {
formError.subject = true
}
if (SignTypeEnum.selfSign === signType) formError.recipients = false;
if (formError.documents || formError.recipients || formError.subject) {
if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName) {
console.log(selectedDocuments, 'line 193')
toast.dismiss();
toast.error(`${formError.documents ? "Add Document" : ""}
${(formError.documents && formError.recipients) ? " | " : ""}
${formError.recipients ? "Add Recipient" : ""}
${formError.recipientEmailAndLastName ? "All recipients must have both Email and Last Name." : ""}
${(formError.documents || formError.recipients) ? " | " : ""}
${formError.subject ? "Add Subject" : ""}`, {
position: "top-center",
@ -434,7 +448,8 @@ const ProcessStepOne = () => {
{
(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) &&
<div className="flex flex-col gap-4 align-start">
<RecipientCards editRecipient={editRecipient} transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} />
<RecipientCards transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} editRecipient={editRecipient} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} editRecipientCurrentEmail={editRecipientCurrentEmail} />
{/* {signType !== "self-sign" && <RecipientAddCard />} */}
<div
@ -442,7 +457,7 @@ const ProcessStepOne = () => {
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none h-screen overflow-hidden' : ''}`}
>
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} />
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
</div>
</div>

View File

@ -22,10 +22,11 @@ interface ContactType {
export interface ContactTypeObj {
type: string;
contacts: ContactType[];
setEditRecipientCurrentEmail : any
}
const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase, setEditRecipient, setEditCase, topRef }) => {
const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase, setEditRecipient, setEditCase, topRef , setEditRecipientCurrentEmail }) => {
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
@ -241,6 +242,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
type: false,
});
setEditRecipientCurrentEmail("")
setEditCase("");
setManualFocusTriggered(false);
formRef.current?.reset();

View File

@ -30,12 +30,17 @@ type CardsData = {
setEditRecipient: (recipient: any) => void; // Replace `any` with the actual type if known
setEditCase: (string: any) => void;
transactionTemplateToggle: string
editRecipient: any
setEditRecipientCurrentEmail: any,
editRecipientCurrentEmail: string
};
const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transactionTemplateToggle }: CardsData) => {
const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateToggle, editRecipient, editCase, setEditRecipientCurrentEmail, editRecipientCurrentEmail }: CardsData) => {
const templateId = useParams<{ templateId: string }>().templateId;
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const dispatch = useDispatch();
if (!transactionId) {
@ -57,6 +62,11 @@ const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transact
// alert(JSON.stringify(recipient))
setEditCase('Edit')
setEditRecipient(recipient)
if (recipient.uuid) {
setEditRecipientCurrentEmail(recipient.uuid)
}else{
setEditRecipientCurrentEmail(recipient.id)
}
}
const handleRemoveRecipient = (recipient) => {
@ -128,39 +138,35 @@ const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transact
)}
<div className="flex flex-col divide-y divide-gray-200">
{recipients.map((recipient, idx) => {
// if(recipient.id === editRecipient.id) return
return (
<div
key={recipient.email + idx}
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3 min-w-0">
{/* {signingOrder && ( */}
{recipients.length > 1 ? <div className="relative">
<input
type="number"
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400 bg-slate-100"
/>
{/* <RiPencilFill className="absolute top-1.5 right-0 text-[14px] text-gray-500" /> */}
</div>
: <input
type="number"
disabled
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
/>}
{/* )} */}
{recipients.map((recipient, idx) => (
<div
key={recipient.email + idx}
className={`flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail ? "bg-gray-200 opacity-40" : ""}`}
>
<div className="flex items-center gap-3 min-w-0">
{/* {signingOrder && ( */}
{recipients.length > 1 ? <div className="relative">
<input
type="number"
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400 bg-slate-100"
/>
{/* <RiPencilFill className="absolute top-1.5 right-0 text-[14px] text-gray-500" /> */}
</div>
: <input
type="number"
disabled
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
/>}
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
@ -219,7 +225,7 @@ const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transact
)}
</div>
)
})}
)}
</div>
</>
) : (

View File

@ -63,6 +63,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
return null;
}
// const [transactionSaving, setTransactionSaving] = useState<boolean>(false);
const [dotCount, setDotCount] = useState(0);
@ -125,10 +126,10 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
setTimeout(() => {
if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS) {
if (isSignSend) {
window.open(`./../../../sign/${selfUserSign.recipientId}`, "_self");
navigate(`./../../../sign/${selfUserSign.recipientId}`,{ replace: true });
}
else {
navigate(`/document-completion-screen/${accountId}/${transactionId}`)
navigate(`/document-completion-screen/${accountId}/${transactionId}`, { replace: true })
}
} else {
navigate(`/templates`);
@ -311,7 +312,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
return (<div key={indx} className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-default'>
<div className='flex items-center gap-1.5'>
<p className='w-3 h-3 rounded-sm' style={{ background: recipient.color }}></p>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{recipient.firstName} {recipient.lastName} segfsdtzgszrdtgrs serdgvsrdgs dtrgv</p>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{recipient.firstName} {recipient.lastName}</p>
</div>
</div>)
})}

View File

@ -9,6 +9,7 @@ import { RootState } from '../../../store/store';
import Loader from '../../dashboard/Loader';
import { hideLoader } from '../../../store/slices/loaderSlice';
import { useNavigate } from 'react-router-dom';
import api from '../../../config/api';
export interface IsSendInterface {
send: boolean | null;
@ -25,7 +26,7 @@ const ProcessStepTwo = () => {
const [isSend, setIsSend] = useState<boolean | null>(null);
const globalLoader = useSelector((state: RootState) => state.loader)
const dispatch = useDispatch();
const navigate = useNavigate();
const navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
@ -34,6 +35,8 @@ const ProcessStepTwo = () => {
}, [])
return (
<div className={`min-w-full relative h-screen bg-slate-100 overflow-hidden ${(globalLoader.isLoading) ? 'pointer-events-none' : ''}`}>
<NavTwo transactionSaving={transactionSaving} isFields={isFields} setIsFields={setIsFields} isSend={isSend} setIsSend={setIsSend} />

View File

@ -90,6 +90,22 @@ const Templates = () => {
navigate(`/process-step-one/${accountId}/template/${templateId}`);
}
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 (
<div className="p-3 text-sm">
{/* Header */}
@ -159,8 +175,8 @@ const Templates = () => {
</div>
</td>
<td className="px-4 py-2 text-sm text-center">{item.name || 'Untitled'}</td>
<td className="px-4 py-2 text-sm text-center">{formatDate(item.createdAt)}</td>
<td className="px-4 py-2 text-sm text-center">{formatDate(item.lastModifiedAt)}</td>
<td className="px-4 py-2 text-sm text-center">{formatTimestamp(item.createdAt)}</td>
<td className="px-4 py-2 text-sm text-center">{formatTimestamp(item.lastModifiedAt)}</td>
<td className="px-4 py-2 text-sm text-center" onClick={() => handleUseClick(item)}><span className="bg-[var(--lightGreyCol)] cursor-pointer border-gray-300 text-gray-600 hover:bg-gray-100 px-3 py-1 rounded-lg font-semibold text-[12px]">Use</span></td>
{/* Dropdown Trigger + Tooltip */}
<td className="px-4 py-2 text-center relative">
@ -173,7 +189,7 @@ const Templates = () => {
{showMenu === index && (
<div
className="absolute right-15 top-5 mt-2 w-32 bg-white border border-gray-200 rounded shadow-lg z-10 transition-opacity duration-300 ease-in-out"
className="absolute right-21 top-0 mt-2 w-20 bg-white border border-gray-200 rounded shadow-lg z-10 transition-opacity duration-300 ease-in-out"
tabIndex={0} // Make it focusable
onBlur={(e) => {
// Ensure it only closes if focus truly leaves the container
@ -183,7 +199,7 @@ const Templates = () => {
}}
>
<button
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
className="block w-full text-left px-4 py-1 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => handleEditClick(item.id)}
>
Edit