Merge branch 'jai'

This commit is contained in:
=bhagwanSingh 2025-08-12 16:26:30 +05:30
commit ac6c42e50a
16 changed files with 788 additions and 190 deletions

View File

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

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { FaCheck, FaLock, FaClock, FaEnvelope, FaTimes } from "react-icons/fa"; import { FaCheck, FaLock, FaClock, FaEnvelope, FaTimes, FaUserTimes } from "react-icons/fa";
import { IoCloseOutline } from "react-icons/io5"; import { IoCloseOutline } from "react-icons/io5";
type Signer = { type Signer = {
@ -48,8 +48,10 @@ const groupByRoutingOrder = (data: Signer[]) => {
}; };
const getInitials = (name: string) => { const getInitials = (name: string) => {
if (!name.trim()) return "";
return name return name
.split(" ") .split(" ")
.filter(n => n)
.map((n) => n[0].toUpperCase()) .map((n) => n[0].toUpperCase())
.join(""); .join("");
}; };
@ -63,6 +65,8 @@ const SigningOrderDiagram : React.FC<Signer[]> = (signers) => {
.map(Number) .map(Number)
.sort((a, b) => a - b); .sort((a, b) => a - b);
console.log("sortedOrders",sortedOrders,grouped)
const currentProcessingOrder = const currentProcessingOrder =
sortedOrders.find((order) => sortedOrders.find((order) =>
grouped[order].some((signer) => !signer.completed) grouped[order].some((signer) => !signer.completed)
@ -109,14 +113,14 @@ const SigningOrderDiagram : React.FC<Signer[]> = (signers) => {
<div key={signer.id} className="relative"> <div key={signer.id} className="relative">
<div <div
className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${bgColor}`} className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${bgColor}`}
title={signer.name} title={signer?.name}
> >
{getInitials(signer.name)} {(signer?.name !== '' || signer?.roleName) ? getInitials(signer?.name || signer?.roleName) : ""}
<span className="absolute bottom-[12px] right-[-12px] p-1 rounded-full bg-white shadow"> <span className="absolute bottom-[12px] right-[-12px] p-1 rounded-full bg-white shadow">
{icon} {icon}
</span> </span>
</div> </div>
<p className="mt-1 text-xs text-gray-600 text-center">{signer.name.split(" ")[0]}</p> <p className="mt-1 text-xs text-gray-600 text-center">{(signer?.name?.split(" ")[0] || signer?.roleName)}</p>
</div> </div>
); );
})} })}

View File

@ -17,6 +17,32 @@ const SignCompletionScreen: React.FC = () => {
const [showDownloadModal, setShowDownloadModal] = useState(false); const [showDownloadModal, setShowDownloadModal] = useState(false);
const [animationPhase, setAnimationPhase] = useState(0); 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(() => { useEffect(() => {
// Trigger animation phases sequentially // Trigger animation phases sequentially
const timers = [ const timers = [
@ -321,7 +347,7 @@ const SignCompletionScreen: React.FC = () => {
{/* Additional Info */} {/* Additional Info */}
<div className={`mt-8 pt-3 border-t border-gray-200 border-opacity-50`}> <div className={`mt-8 pt-3 border-t border-gray-200 border-opacity-50`}>
<p className="text-sm text-gray-500"> <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> </p>
</div> </div>
</div> </div>

View File

@ -157,6 +157,7 @@ const SignDocument = () => {
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" }); const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
console.log(recSignStatus, "recSignStatus")
// Handling Date Opacity // Handling Date Opacity
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({}); const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
@ -259,6 +260,8 @@ const SignDocument = () => {
} }
}, [saved]) }, [saved])
const handleSignClick = (index: number, id: number) => { const handleSignClick = (index: number, id: number) => {
setSignTagId(id) setSignTagId(id)
if (saved.save) { if (saved.save) {
@ -303,6 +306,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) => { const fetchData = (isSign: boolean = false) => {
axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign?recipientId=${recipientId}`, axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign?recipientId=${recipientId}`,
{ {
@ -313,17 +345,17 @@ const SignDocument = () => {
if (res.status === 200) { if (res.status === 200) {
setSelectedDocuments(res.data.documents) setSelectedDocuments(res?.data?.documents)
setRecipient(res.data.details) setRecipient(res?.data?.details)
setDroppedItems(res.data.tagJsons) setDroppedItems(res?.data?.tagJsons)
setTransaction(res.data.transaction) setTransaction(res?.data?.transaction)
setRecSignStatus(res.data.status) 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) setProvideTags(true)
} }
if (res.data.status === "COMPLETED") { if (res?.data?.status === "COMPLETED") {
setShowCompletionScreen(true) setShowCompletionScreen(true)
} }
@ -335,6 +367,7 @@ const SignDocument = () => {
}) })
.catch((err) => { .catch((err) => {
console.log(err)
}); });
} }
@ -866,6 +899,8 @@ const SignDocument = () => {
}; };
// const [showTerms, setShowTerms] = useState<boolean>(true); // const [showTerms, setShowTerms] = useState<boolean>(true);
return ( return (
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}> <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> </button>
</div> </div>
</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"> <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"> <thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider sticky top-0 z-10">
<tr> <tr>
@ -162,7 +162,7 @@ const Contacts = () => {
</td> </td>
</tr> </tr>
) : ( ) : (
contacts.map((contact) => ( contacts.map((contact, index) => (
<tr key={contact.id} className="hover:bg-gray-50"> <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.firstName}</td>
<td className="px-4 py-3">{contact.lastName}</td> <td className="px-4 py-3">{contact.lastName}</td>
@ -181,7 +181,7 @@ const Contacts = () => {
</button> </button>
{openMenuId === contact.id && ( {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 <button
onMouseDown={() => handleEdit(contact.id)} onMouseDown={() => handleEdit(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600" className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { FiUpload } from 'react-icons/fi' import { FiUpload } from 'react-icons/fi'
import { uploadDocuments } from '../../store/slices/documentSlice'; import { resetDocuments, uploadDocuments } from '../../store/slices/documentSlice';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -37,7 +37,8 @@ const StartNow = () => {
}); });
}; };
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
await dispatch(resetDocuments())
const selectedFiles = e.target.files ? Array.from(e.target.files) : []; const selectedFiles = e.target.files ? Array.from(e.target.files) : [];
if (selectedFiles.length > 0) { if (selectedFiles.length > 0) {
setFiles((prev) => [...prev, ...selectedFiles]); setFiles((prev) => [...prev, ...selectedFiles]);

View File

@ -1,27 +1,40 @@
import React, { useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { import {
FaArrowLeft,
FaCheck, FaCheck,
FaFileAlt, FaExternalLinkAlt,
FaEye,
FaEyeSlash,
FaRedo,
FaImage,
FaUsers, FaUsers,
FaFileAlt,
FaEnvelope, FaEnvelope,
FaClock, FaCog,
FaShieldAlt,
FaBell,
FaDownload, FaDownload,
FaShare, FaShare,
FaShieldAlt, FaEdit,
FaLock, FaSave,
FaPen, FaClock,
FaCheckCircle FaLock
} from 'react-icons/fa'; } from 'react-icons/fa';
import { RootState } from '../../../store/store';
import api from '../../../config/api';
import { fetchRecipients } from "../../../store/slices/selectedRecipientsSlice"; import { fetchRecipients } from "../../../store/slices/selectedRecipientsSlice";
import { fetchDocuments } from "../../../store/slices/documentSlice"; import { fetchDocuments } from "../../../store/slices/documentSlice";
import { useDispatch, useSelector } from "react-redux"; 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 Loader from "../../dashboard/Loader";
import confetti from 'canvas-confetti' import confetti from 'canvas-confetti'
import { IoClose } from "react-icons/io5"; import { IoClose } from "react-icons/io5";
import { IoMdClose, IoMdHome } from "react-icons/io"; import { IoMdClose, IoMdHome } from "react-icons/io";
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
type Signer = { type Signer = {
id: number; id: number;
name: string; name: string;
@ -33,7 +46,7 @@ type Signer = {
// Signers List Component // Signers List Component
const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => { const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
return ( return (
<div className="bg-white/60 rounded-md shadow-sm border border-gray-100"> // <div className="bg-white/60 rounded-md shadow-sm border border-gray-100 ">
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2"> <h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
@ -45,9 +58,9 @@ const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
</span> </span>
</div> </div>
<div className="space-y-3 max-h-[29vh] overflow-y-auto no-scrollbar"> <div className="space-y-3 max-h-[50vh] overflow-y-auto no-scrollbar">
{signers.map((signer) => ( {signers.map((signer) => (
<div key={signer.id} className="flex items-center gap-4 p-3 bg-white/40 rounded-lg"> <div key={signer.id} className="flex items-center gap-4 p-3 bg-gradient-to-br from-gray-50 to-blue-100 rounded-lg">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium"> <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium">
{signer.name.length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.email.split("").map(n => n[0].toUpperCase()).join("")} {signer.name.length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.email.split("").map(n => n[0].toUpperCase()).join("")}
</div> </div>
@ -73,7 +86,7 @@ const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
))} ))}
</div> </div>
</div> </div>
</div> // </div>
); );
}; };
@ -108,10 +121,10 @@ const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
) || -1; ) || -1;
return ( return (
<div className="bg-white/60 rounded-md shadow-sm border border-gray-100"> // <div className="bg-white/60 rounded-md shadow-sm border border-gray-100">
<div className="p-6"> <div className="p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Signing Flow</h3> <h3 className="text-lg font-semibold text-gray-800 mb-4">Signing Flow</h3>
<div className="overflow-x-auto overflow-y-auto h-[69vh] no-scrollbar flex justify-center items-center"> <div className="overflow-x-auto overflow-y-auto h-[50vh] no-scrollbar flex justify-center items-center">
<div className="flex justify-center items-center gap-4 min-w-max"> <div className="flex justify-center items-center gap-4 min-w-max">
{/* Mail icon */} {/* Mail icon */}
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@ -162,7 +175,7 @@ const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
</span> </span>
</div> </div>
<p className="mt-1 text-xs text-gray-600 text-center max-w-16 truncate"> <p className="mt-1 text-xs text-gray-600 text-center max-w-16 truncate">
{signer.name.split(" ")[0]} {signer.name.split(" ")[0] || signer.firstName.split(0,1)}
</p> </p>
</div> </div>
); );
@ -181,26 +194,13 @@ const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
</div> </div>
</div> </div>
</div> </div>
</div> // </div>
); );
}; };
const DocumentSubmissionOptions = () => {
// Main DocumentCompletionScreen Component const location = useLocation();
const DocumentCompletionScreen = () => { const [status, setStatus] = useState(null);
const documentList = [ console.log("location.pathname", location.pathname, status)
{ title: 'Document 1', pages: 12 },
{ title: 'Document 2', pages: 8 },
{ title: 'Document 3', pages: 5 },
{ title: 'Document 4', pages: 22 },
{ title: 'Document 5', pages: 10 },
{ title: 'Document 6', pages: 15 },
{ title: 'Document 7', pages: 7 },
{ title: 'Document 8', pages: 9 },
{ title: 'Document 9', pages: 6 },
{ title: 'Document 10', pages: 11 },
{ title: 'Document 11', pages: 4 }, // Won't be shown due to slice
];
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const [showConfetti, setShowConfetti] = React.useState(true) const [showConfetti, setShowConfetti] = React.useState(true)
if (!accountId || !transactionId) { if (!accountId || !transactionId) {
@ -215,6 +215,8 @@ const DocumentCompletionScreen = () => {
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents) const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
console.log('Line 218', selectedDocuments)
const [transactionData,setTransactionData] = useState({})
useEffect(() => { useEffect(() => {
@ -265,93 +267,278 @@ const DocumentCompletionScreen = () => {
}, [showConfetti]); }, [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}`);
if (res?.data) {
setStatus(res?.data?.status);
setTransactionData(res?.data)
}
} 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]);
const [selectedOption, setSelectedOption] = useState('success');
const [isVisible, setIsVisible] = useState(false);
const [previewData, setPreviewData] = useState({
documentName: "Contract Agreement.pdf",
submissionCount: 1,
signerName: "Sumit Kumar",
signerEmail: "sumit.kumar@hicglobalsolutions.com",
submissionTime: new Date().toLocaleString()
});
useEffect(() => {
setIsVisible(true);
}, []);
const options = [
{
id: 'success',
icon: FaCheck,
title: 'Success screen',
description: 'Display confirmation message after successful document submission',
color: 'bg-green-50 border-green-200 hover:bg-green-100',
iconColor: 'text-green-600',
isPremium: false
},
{
id: 'redirect',
icon: FaExternalLinkAlt,
title: 'Signing Flow',
description: 'Navigate user to view their transaction details',
color: 'bg-blue-50 border-blue-200 hover:bg-blue-100',
iconColor: 'text-blue-600',
isPremium: true
},
{
id: 'public',
icon: FaEye,
title: 'Signature status',
description: 'Show transaction status on public dashboard',
color: 'bg-purple-50 border-purple-200 hover:bg-purple-100',
iconColor: 'text-purple-600',
isPremium: true
},
];
const getPreviewContent = () => {
switch (selectedOption) {
case 'success':
return (
<div className="text-center space-y-6">
<div className="w-24 h-24 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-6 mx-auto shadow-lg animate-bounce">
<FaCheck className="text-white text-4xl" />
</div>
<div>
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
{selectedDocuments?.map((docs) => (
<p className="text-gray-600">Your document "{docs.name}" has been sent for signature.</p>
) )}
</div>
<div className="bg-gray-50 rounded-lg p-4 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500">createdBy:</span>
<span className="font-medium">{transactionData?.createdBy?.name}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Email:</span>
<span className="font-medium">{transactionData?.createdBy?.email}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">createdAt:</span>
<span className="font-medium">{transactionData.createdAt}</span>
</div>
</div>
</div>
);
case 'redirect':
return ( return (
<> <>
<div className="min-h-screen bg-[var(--mainViewBg)]">
{/* Top Banner */} <SigningOrderDiagram signers={selectedRecipients} />
<div className='fixed top-0 w-full flex items-center justify-between bg-white shadow z-50 px-4 py-3'> </>
<div className='flex items-center gap-2'> );
<Link to={"/"}> case 'public':
return (
<>
<SignersList signers={selectedRecipients} />
</>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50 p-6">
<div className={`max-w-6xl mx-auto transition-all duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
{/* Header */}
<div className="flex items-center space-x-4 mb-5">
<Link to={'/'} className="flex items-center space-x-2 text-gray-600 hover:text-[var(--primary)] transition-colors duration-200 group">
<IoMdHome <IoMdHome
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition' className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
/> />
</Link> </Link>
<p className="text-base font-normal text-gray-700 border-l pl-4">🎉 Awesome! The document’s been sent for signature. 🎉</p> <div className="h-8 w-px bg-gray-300"></div>
</div> <h1 className="text-2xl font-bold text-gray-800">🎉 Awesome! The document’s been sent for signature. 🎉</h1>
<div
id="add-tags-btn-step-one"
className='flex items-center gap-3'>
<Link to={`/accounts/${accountId}/transactions/${transactionId}`} className="flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]">
{/* <FaPen className=" text-white text-md" /> */}
<span className="tracking-wide text-white">View Transaction</span>
</Link>
{/* <button
onClick={() => handleNextClick(false)}
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
>
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"}
</button> */}
</div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 ">
{/* Left Panel - Configuration */}
<div className="space-y-6 ">
{/* Action Selection */}
{(selectedRecipients.length > 0 && selectedDocuments.length > 0) ? <div className="bg-white rounded-xl shadow-sm border border-gray-200 min-h-[81vh] ">
<div className="p-3 mt-[55px]"> <div className="p-6">
<div className="max-w-[97vw] mx-auto"> <h3 className="text-lg font-semibold text-gray-800 mb-4">
{/* Header */} Choose an action after a signer completes the document.
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
{/* Left Section - Document Details */}
<div className="lg:col-span-6 space-y-2">
{/* Document Preview Card */}
<div className="bg-white/60 rounded-md shadow-sm border border-gray-100 overflow-hidden max-h-[88vh] no-scrollbar overflow-y-auto">
<div className="p-3 ">
{selectedDocuments?.map((item, index) => (
<div key={item.id} className="flex items-start justify-between mb-2 px-2 py-3">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
{/* <FaFileAlt className="text-blue-600 text-xl" /> */}
<img src={item.pages[0].src} alt="document" className="h-10 w-10" />
</div>
<div>
<h3 className="text-md font-semibold text-gray-800">
Name : {item.name ?? '--'}
</h3> </h3>
<div className="text-gray-600 text-sm"> <div className="space-y-3">
Status : {item.status ?? '--'} {options.map((option) => {
</div> const Icon = option.icon;
const isSelected = selectedOption === option.id;
return (
<div
key={option.id}
onClick={() => setSelectedOption(option.id)}
className={`
relative cursor-pointer p-4 rounded-xl border-2 transition-all duration-200 group
${isSelected
? 'border-[var(--primary)] bg-blue-50 shadow-md scale-[1.02]'
: option.color + ' border-2'
}
hover:shadow-md hover:scale-[1.01] transform
`}
>
<div className="flex items-start space-x-4">
<div className={`
w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200
${isSelected
? 'bg-[var(--primary)] text-white shadow-lg'
: 'bg-white ' + option.iconColor + ' group-hover:scale-110'
}
`}>
<Icon className="text-lg" />
</div> </div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<h4 className={`font-semibold transition-colors duration-200 ${isSelected ? 'text-[var(--primary)]' : 'text-gray-800'
}`}>
{option.title}
</h4>
{option.isPremium && (
<span className="text-purple-500 text-sm">★</span>
)}
</div> </div>
</div> <p className="text-sm text-gray-600 mt-1">
))} {option.description}
</p>
</div> </div>
</div> </div>
<div><SignersList signers={selectedRecipients} /></div> {/* Selection indicator */}
{isSelected && (
<div className="absolute top-4 right-4">
<div className="w-6 h-6 bg-[var(--primary)] rounded-full flex items-center justify-center">
<FaCheck className="text-white text-xs" />
</div>
</div>
)}
</div>
);
})}
</div>
</div>
</div> </div>
{/* Right Section - Signer Components */} {/* Custom Image Section */}
<div className="lg:col-span-6 space-y-1.5"> </div>
{/* Signing Order Diagram */}
<SigningOrderDiagram signers={selectedRecipients} />
{/* Signers List */} {/* Right Panel - Preview */}
<div className="space-y-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden sticky top-6">
<div className="bg-gradient-to-r from-gray-50 to-blue-50 px-6 py-2 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-800">Live Preview</h3>
<p className="text-sm text-gray-500">See how your users will experience the completion</p>
</div>
<div className="p-3 min-h-[70vh] flex justify-center">
<div className="w-full transition-all duration-500 transform">
{getPreviewContent()}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
: <Loader />}
</div> </div>
</>
); );
}; };
export default DocumentCompletionScreen; export default DocumentSubmissionOptions;

View File

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

View File

@ -22,10 +22,11 @@ interface ContactType {
export interface ContactTypeObj { export interface ContactTypeObj {
type: string; type: string;
contacts: ContactType[]; 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>( const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null') JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
); );
@ -112,7 +113,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
hostName: editRecipient.hostName || "", hostName: editRecipient.hostName || "",
hostEmail: editRecipient.hostEmail || "", hostEmail: editRecipient.hostEmail || "",
roleName: editRecipient.roleName || "", roleName: editRecipient.roleName || "",
type: editRecipient.type || recipientType.SIGNER, type: editRecipient.type,
allowSenderToEdit: editRecipient.allowSenderToEdit ?? true, allowSenderToEdit: editRecipient.allowSenderToEdit ?? true,
allowSenderToDelete: editRecipient.allowSenderToDelete ?? true, allowSenderToDelete: editRecipient.allowSenderToDelete ?? true,
uuid: String(editRecipient?.id ?? editRecipient?.uuid ?? ''), uuid: String(editRecipient?.id ?? editRecipient?.uuid ?? ''),
@ -209,11 +210,22 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
submitForm(); submitForm();
function submitForm() { function submitForm() {
if (editCase === 'Edit') { let updatedRecipient = { ...recipient };
dispatch(updateRecipient(recipient));
// Clear hostName and hostEmail if type is SIGNER or CC
if (
updatedRecipient.type === recipientType.SIGNER ||
updatedRecipient.type === recipientType.CC
) {
updatedRecipient.hostName = "";
updatedRecipient.hostEmail = "";
}
if (editCase === "Edit") {
dispatch(updateRecipient(updatedRecipient));
} else { } else {
dispatch(addRecipient({ dispatch(addRecipient({
...recipient, ...updatedRecipient,
uuid: uuidv4(), uuid: uuidv4(),
newOne: true, newOne: true,
})); }));
@ -241,6 +253,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
type: false, type: false,
}); });
setEditRecipientCurrentEmail("")
setEditCase(""); setEditCase("");
setManualFocusTriggered(false); setManualFocusTriggered(false);
formRef.current?.reset(); formRef.current?.reset();
@ -578,7 +591,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
type="radio" type="radio"
name="recipient-type" name="recipient-type"
value={option.value} value={option.value}
defaultChecked={option.value === recipientType.SIGNER} checked={recipient.type === option.value}
onClick={(e) => setRecipients({ ...recipient, type: e.target.value })} onClick={(e) => setRecipients({ ...recipient, type: e.target.value })}
className="accent-[var(--primary)] cursor-pointer" className="accent-[var(--primary)] cursor-pointer"
/> />

View File

@ -30,12 +30,17 @@ type CardsData = {
setEditRecipient: (recipient: any) => void; // Replace `any` with the actual type if known setEditRecipient: (recipient: any) => void; // Replace `any` with the actual type if known
setEditCase: (string: any) => void; setEditCase: (string: any) => void;
transactionTemplateToggle: string 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; const templateId = useParams<{ templateId: string }>().templateId;
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const dispatch = useDispatch(); const dispatch = useDispatch();
if (!transactionId) { if (!transactionId) {
@ -57,6 +62,11 @@ const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transact
// alert(JSON.stringify(recipient)) // alert(JSON.stringify(recipient))
setEditCase('Edit') setEditCase('Edit')
setEditRecipient(recipient) setEditRecipient(recipient)
if (recipient.uuid) {
setEditRecipientCurrentEmail(recipient.uuid)
}else{
setEditRecipientCurrentEmail(recipient.id)
}
} }
const handleRemoveRecipient = (recipient) => { const handleRemoveRecipient = (recipient) => {
@ -128,13 +138,10 @@ const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transact
)} )}
<div className="flex flex-col divide-y divide-gray-200"> <div className="flex flex-col divide-y divide-gray-200">
{recipients.map((recipient, idx) => { {recipients.map((recipient, idx) => (
// if(recipient.id === editRecipient.id) return
return (
<div <div
key={recipient.email + idx} key={recipient.email + idx}
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors" className={`flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail ? "bg-neutral-100 opacity-40 " : ""}`}
> >
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
{/* {signingOrder && ( */} {/* {signingOrder && ( */}
@ -160,7 +167,6 @@ const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transact
} }
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]" 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"> <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 || ""}`)} {getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
@ -219,7 +225,7 @@ const RecipientCards = ({ editRecipient, setEditRecipient, setEditCase, transact
)} )}
</div> </div>
) )
})} )}
</div> </div>
</> </>
) : ( ) : (

View File

@ -63,6 +63,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
return null; return null;
} }
// const [transactionSaving, setTransactionSaving] = useState<boolean>(false); // const [transactionSaving, setTransactionSaving] = useState<boolean>(false);
const [dotCount, setDotCount] = useState(0); const [dotCount, setDotCount] = useState(0);
@ -125,10 +126,10 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
setTimeout(() => { setTimeout(() => {
if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS) { if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS) {
if (isSignSend) { if (isSignSend) {
navigate(`./../../../sign/${selfUserSign.recipientId}`); navigate(`./../../../sign/${selfUserSign.recipientId}`,{ replace: true });
} }
else { else {
navigate(`/document-completion-screen/${accountId}/${transactionId}`) navigate(`/document-completion-screen/${accountId}/${transactionId}`, { replace: true })
} }
} else { } else {
navigate(`/templates`); 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'> 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'> <div className='flex items-center gap-1.5'>
<p className='w-3 h-3 rounded-sm' style={{ background: recipient.color }}></p> <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>
</div>) </div>)
})} })}

View File

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

View File

@ -55,7 +55,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
getAllSignatures, getAllSignatures,
editingSignature editingSignature
}) => { }) => {
const [activeTab, setActiveTab] = useState<'TYPE' | 'DRAW' | 'UPLOAD' >('TYPE'); const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD'>('SELECT');
const [fullName, setFullName] = useState(''); const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState(''); const [initials, setInitials] = useState('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
@ -107,7 +107,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
// Navigation items // Navigation items
const navigationItems = [ const navigationItems = [
{ id: 'TYPE', label: 'TYPE', icon: <GrCheckboxSelected className="w-4 h-4" /> }, { id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> }, { id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> }, { id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
// { id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> }, // { id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
@ -118,7 +118,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
if (editingSignature) { if (editingSignature) {
setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`); setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`); setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
setActiveTab(editingSignature.signatureType as 'TYPE' | 'DRAW' | 'UPLOAD'); setActiveTab(editingSignature.signatureType as 'SELECT' | 'DRAW' | 'UPLOAD');
// Set font for SELECT tab // Set font for SELECT tab
const fontOption = signatureOptions.find(option => option.style === editingSignature.font); const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
@ -193,11 +193,13 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
const handleSubmit = async () => { const handleSubmit = async () => {
setIsSubmitting(true); setIsSubmitting(true);
if (activeTab === 'DRAW' || activeTab === 'UPLOAD') {
if (!signatures.fullSignature || !signatures.initials) { if (!signatures.fullSignature || !signatures.initials) {
alert('Please provide both signature and initials'); alert('Please provide both signature and initials');
setIsSubmitting(false); setIsSubmitting(false);
return; return;
} }
}
try { try {
const payload = { const payload = {
@ -207,7 +209,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
firstName: fullName.split(' ')[0] || user.firstName, firstName: fullName.split(' ')[0] || user.firstName,
lastName: fullName.split(' ')[1] || user.lastName, lastName: fullName.split(' ')[1] || user.lastName,
font: selectedSignature.style, font: selectedSignature.style,
default:checked default: checked
}; };
let res; let res;
@ -273,7 +275,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
const renderContent = () => { const renderContent = () => {
switch (activeTab) { switch (activeTab) {
case 'TYPE': case 'SELECT':
return ( return (
<div className="h-full flex flex-col "> <div className="h-full flex flex-col ">
<div className="mb-6"> <div className="mb-6">
@ -481,7 +483,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
{navigationItems.map((item) => ( {navigationItems.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => setActiveTab(item.id as 'TYPE' | 'DRAW' | 'UPLOAD')} onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
? 'bg-blue-50 text-blue-700 border border-blue-200' ? 'bg-blue-50 text-blue-700 border border-blue-200'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
@ -534,9 +536,9 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
</div> </div>
<p className="text-sm text-gray-700 opacity-80 ms-1"> <p className="text-sm text-gray-700 opacity-80 ms-1">
{checked
? 'Signature will be used by default.' Signature will be used by default.
: 'Signature will not be used by default.'}
</p> </p>
</label> </label>

View File

@ -90,6 +90,22 @@ const Templates = () => {
navigate(`/process-step-one/${accountId}/template/${templateId}`); 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 ( return (
<div className="p-3 text-sm"> <div className="p-3 text-sm">
{/* Header */} {/* Header */}
@ -159,8 +175,8 @@ const Templates = () => {
</div> </div>
</td> </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">{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">{formatTimestamp(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.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> <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 */} {/* Dropdown Trigger + Tooltip */}
<td className="px-4 py-2 text-center relative"> <td className="px-4 py-2 text-center relative">
@ -173,7 +189,7 @@ const Templates = () => {
{showMenu === index && ( {showMenu === index && (
<div <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 tabIndex={0} // Make it focusable
onBlur={(e) => { onBlur={(e) => {
// Ensure it only closes if focus truly leaves the container // Ensure it only closes if focus truly leaves the container
@ -183,7 +199,7 @@ const Templates = () => {
}} }}
> >
<button <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)} onClick={() => handleEditClick(item.id)}
> >
Edit Edit

View File

@ -0,0 +1,254 @@
import React, { useState } from "react";
import {
FiX,
FiCheckCircle,
FiXCircle,
FiClock,
FiUser,
FiCalendar,
FiFileText,
FiAlertTriangle,
FiTruck,
FiUserCheck,
FiUserX,
FiPackage,
FiMapPin,
FiEye,
FiRotateCcw
} from "react-icons/fi";
interface AuditModalProps {
closeModal: () => void;
}
const AuditModal: React.FC<AuditModalProps> = ({ closeModal }) => {
const [activeTab, setActiveTab] = useState<'status' | 'audit'>('status');
const statusHistory = [
{
id: 1,
title: "The Accrual status has been successfully changed to Approved.",
author: "Alexander Turner",
role: "Analyst Perspective on Transaction",
time: "34m ago",
fromStatus: "Ready to Review",
toStatus: "Approved",
icon: <FiCheckCircle className="w-4 h-4" />,
statusColor: "text-green-600"
},
{
id: 2,
title: "The Accrual has been updated to reflect a status of Reject.",
author: "Alexander Turner",
time: "3h ago",
fromStatus: "Ready to Review",
toStatus: "Rejected",
icon: <FiXCircle className="w-4 h-4" />,
statusColor: "text-red-600"
},
{
id: 3,
title: "The Accrual has been revised for accuracy.",
author: "Alex Turner",
role: "Manager View Transaction State",
time: "7d ago",
fromStatus: "Rejected",
toStatus: "Reviewed",
icon: <FiRotateCcw className="w-4 h-4" />,
statusColor: "text-yellow-600"
},
{
id: 4,
title: "The Accrual state has been updated from 'ready' to 'under review'.",
author: "Alexander Turner",
role: "Analyst Perspective on Transaction",
time: "34m ago",
fromStatus: "Ready to Review",
toStatus: "Reviewed",
icon: <FiEye className="w-4 h-4" />,
statusColor: "text-blue-600"
},
{
id: 5,
title: "The Accrual has been updated from rejected to reviewed.",
author: "Alex Turner",
role: "Analyst View Transaction",
time: "34m ago",
fromStatus: "Rejected",
toStatus: "Reviewed",
icon: <FiRotateCcw className="w-4 h-4" />,
statusColor: "text-blue-600"
}
];
const auditTrail = [
{
id: 1,
title: "Batch 11317 Pickup Request Created",
author: "Sandra Darlington",
location: "Lagos General Hospital Gbagada",
time: "11 Aug, 2023 09:23 AM",
icon: <FiPackage className="w-4 h-4" />,
type: "created"
},
{
id: 2,
title: "Batch 11317 Pickup Request",
author: "Oladejo Israel",
location: "Institute of Human Virology H3Africa BioRepository",
time: "12 Aug, 2023 11:19 AM",
status: "Approved",
icon: <FiCheckCircle className="w-4 h-4" />,
type: "approved"
},
{
id: 3,
title: "Batch 11317 was Initiated",
author: "Sandra Darlington",
location: "Lagos General Hospital Gbagada",
time: "12 Aug, 2023 11:19 AM",
icon: <FiFileText className="w-4 h-4" />,
type: "initiated"
},
{
id: 4,
title: "Sandra Darlington Assigned batch 11317",
assignee: "Yusuf Ahmed",
time: "12 Aug, 2023 11:20 AM",
icon: <FiUserCheck className="w-4 h-4" />,
type: "assigned"
},
{
id: 5,
title: "Batch 11317 was rejected by Yusuf Ahmed",
reason: "due to faulty vehicle",
time: "12 Aug, 2023 11:20 AM",
icon: <FiXCircle className="w-4 h-4" />,
type: "rejected"
},
{
id: 6,
title: "Sandra Darlington Assigned batch 11317",
assignee: "Kolado Jolade",
time: "12 Aug, 2023 11:20 AM",
icon: <FiUserCheck className="w-4 h-4" />,
type: "assigned"
},
{
id: 7,
title: "Logger Synced",
deviceId: "ID: LGR-0421",
status: "95% remaining",
time: "12 Aug, 2023 11:25 AM",
icon: <FiCheckCircle className="w-4 h-4" />,
type: "synced"
},
{
id: 8,
title: "Logger Disconnected",
deviceId: "ID: LGR-0421",
time: "12 Aug, 2023 11:25 AM",
icon: <FiAlertTriangle className="w-4 h-4" />,
type: "disconnected"
}
];
const getStatusBadge = (status: string) => {
const statusClasses = {
'Approved': 'bg-green-100 text-green-800',
'Rejected': 'bg-red-100 text-red-800',
'Reviewed': 'bg-yellow-100 text-yellow-800',
'Ready to Review': 'bg-blue-100 text-blue-800'
};
return statusClasses[status as keyof typeof statusClasses] || 'bg-gray-100 text-gray-800';
};
const getAuditTypeColor = (type: string) => {
const colors = {
'created': 'text-blue-600',
'approved': 'text-green-600',
'initiated': 'text-purple-600',
'assigned': 'text-indigo-600',
'rejected': 'text-red-600',
'synced': 'text-green-600',
'disconnected': 'text-orange-600'
};
return colors[type as keyof typeof colors] || 'text-gray-600';
};
return (
<div className="fixed inset-0 flex items-center justify-center backdrop-blur-sm bg-white/10 z-50">
<div className="bg-white rounded-md shadow-md w-full max-w-[75vw] max-h-[95vh] min-h-[85vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-[var(--primary)] rounded-t-md">
<div className="flex items-center">
<h2 className="text-xl font-semibold text-white">
Audit Status
</h2>
</div>
<button
onClick={closeModal}
className="text-white cursor-pointer transition-colors"
>
<FiX className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-1.5">
<div className="text-sm text-gray-500 font-medium mb-2">TODAY</div>
{statusHistory.slice(0, 2).map((item) => (
<div key={item.id} className="flex items-start space-x-4 pb-2 border-b border-gray-100 last:border-b-0">
<div className={`${item.statusColor} mt-1`}>
{item.icon}
</div>
<div className="flex-1">
<p className="text-gray-800 font-medium text-sm mb-0.5">{item.title}</p>
<div className="flex items-center space-x-2 text-xs text-gray-600 mb-1">
<span>By {item.author}</span>
{item.role && (
<>
<span>•</span>
<span className="text-blue-600 text-xs">({item.role})</span>
</>
)}
</div>
<div className="flex items-center space-x-3">
<span className="text-xs text-gray-500">{item.time}</span>
<div className="flex items-center space-x-2">
<span className={`px-2 py-0.5 text-xs rounded-full ${getStatusBadge(item.fromStatus)}`}>
{item.fromStatus}
</span>
<span className="text-gray-400">→</span>
<span className={`px-2 py-0.5 text-xs rounded-full ${getStatusBadge(item.toStatus)}`}>
{item.toStatus}
</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Footer */}
<div className="flex justify-end p-2 border-t border-gray-200">
<button
onClick={closeModal}
className="cursor-pointer px-4 py-1.5 bg-[var(--sendForSignBgCol)] text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
>
Close
</button>
</div>
</div>
</div>
);
};
export default AuditModal;

View File

@ -1,5 +1,5 @@
import { AiFillSignature } from 'react-icons/ai'; import { AiFillSignature } from 'react-icons/ai';
import { Link, useNavigate, useParams } from 'react-router-dom'; import { Link, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { LuGitGraph } from 'react-icons/lu'; import { LuGitGraph } from 'react-icons/lu';
import AccountNav from '../accounts/AccountNav'; import AccountNav from '../accounts/AccountNav';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -10,6 +10,7 @@ import { MdEditDocument } from "react-icons/md";
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store/store'; import { RootState } from '../../store/store';
import { SignTypeEnum } from '../../utlity/types'; import { SignTypeEnum } from '../../utlity/types';
import AuditModal from './AuditModal';
// Sample recipient data — you can replace this with actual API data later // Sample recipient data — you can replace this with actual API data later
const recipients = [ const recipients = [
@ -44,6 +45,35 @@ const TransactionView = () => {
const [transactionDetails, setTransactionDetails] = useState(null); const [transactionDetails, setTransactionDetails] = useState(null);
const [signingOrderModal, setSigningOrderModal] = useState({ open: false }); const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
const user = useSelector((state: RootState) => state.user) const user = useSelector((state: RootState) => state.user)
const location = useLocation();
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
// update whenever location changes
setIsModalOpen(location.search.includes("auditStatus"));
}, [location]);
const handleOpenAuditModal = () => {
const { pathname, search } = location;
let newSearch = search
? search.includes("auditStatus")
? search
: `${search}&auditStatus`
: "?auditStatus";
window.history.pushState({}, "", pathname + newSearch);
setIsModalOpen(true); // ✅ update state immediately
};
const closeModal = () => {
let search = location.search
.replace(/[?&]auditStatus\b/, "")
.replace(/^&/, "?")
.replace(/\?$/, "");
window.history.pushState({}, "", location.pathname + search);
setIsModalOpen(false); // ✅ update state immediately
};
console.log(transactionDetails, "line 37"); console.log(transactionDetails, "line 37");
@ -99,9 +129,13 @@ const TransactionView = () => {
More More
</button> </button>
<Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-two/${accountId}/${transactionId}`}> {/* <Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-two/${accountId}/${transactionId}`}>
Edit Edit
</Link> </Link> */}
<button className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' onClick={handleOpenAuditModal}>
Audit
</button>
</div> </div>
<hr className='my-6 border-slate-200' /> <hr className='my-6 border-slate-200' />
@ -207,6 +241,8 @@ const TransactionView = () => {
</div> </div>
</div> </div>
} }
{isModalOpen && <AuditModal closeModal={closeModal} />}
</> </>
} }
</> </>