Compare commits

..

No commits in common. "3f859a42de7e9ef0783e9d21e0c7d264ba652f31" and "bdaa3dc26304a58d30c6f7bc89c1cf5dc866e470" have entirely different histories.

17 changed files with 400 additions and 766 deletions

View File

@ -41,7 +41,7 @@ const App = () => {
<Route path="/accounts/:accountId/transactions" element={<Account />} /> <Route path="/accounts/:accountId/transactions" element={<Account />} />
<Route path="/transactions" element={<Account />} /> <Route path="/transactions" element={<Account />} />
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} /> <Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
{/* <Route path="/transactions/:filterParam" element={<Transactions />} /> */} <Route path="/transactions/:filterParam" element={<Transactions />} />
<Route path="/templates" element={<Templates />} /> <Route path="/templates" element={<Templates />} />
{/* </Route> */} {/* </Route> */}

View File

@ -266,7 +266,7 @@ const CommonNav: React.FC<AccountNavProps> = () => {
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200"> <header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap"> <nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
{pathnames.length === 0 ? ( {pathnames.length === 0 ? (
<span className="text-gray-900 font-semibold">Home</span> <span className="text-gray-900 font-semibold">Dashboard</span>
) : ( ) : (
pathnames.map((segment, index) => { pathnames.map((segment, index) => {
const label = labelMap[segment] || formatSegment(segment); const label = labelMap[segment] || formatSegment(segment);

View File

@ -18,7 +18,6 @@ import {
FaFilter, FaFilter,
FaCalendarAlt FaCalendarAlt
} from 'react-icons/fa'; } from 'react-icons/fa';
import TransactionDataTable from "./TransactionDataTable";
interface Transaction { interface Transaction {
id: number; id: number;
@ -29,6 +28,12 @@ interface Transaction {
expireAfter: number | null; expireAfter: number | null;
} }
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
const Account = () => { const Account = () => {
const mapApiDataToCards = (data: any): CardData[] => { const mapApiDataToCards = (data: any): CardData[] => {
return [ return [
@ -110,9 +115,12 @@ const Account = () => {
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null') JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
); );
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
console.log("activeAccountId Trans.", activeAccountId) console.log("activeAccountId Trans.", activeAccountId)
// const { accountId } = useParams<{ accountId: string }>(); // const { accountId } = useParams<{ accountId: string }>();
const [transactions, setTransactions] = useState<Transaction[]>([]); const [transactions, setTransactions] = useState<Transaction[]>([]);
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [accountsData, setAccountsData] = useState([]) const [accountsData, setAccountsData] = useState([])
const [inviteLoading, setInviteLoading] = useState(false); const [inviteLoading, setInviteLoading] = useState(false);
const [cards, setCards] = useState<CardData[]>([]); const [cards, setCards] = useState<CardData[]>([]);
@ -135,6 +143,12 @@ const Account = () => {
// getUserDetails() // getUserDetails()
// }, []) // }, [])
useEffect(() => {
const userDetails = JSON.parse(sessionStorage.getItem('userDetails'));
setCurrentEmailId(userDetails?.email)
}, []);
// Listen for sessionStorage changes // Listen for sessionStorage changes
useEffect(() => { useEffect(() => {
const handleStorageChange = (e: StorageEvent) => { const handleStorageChange = (e: StorageEvent) => {
@ -181,9 +195,20 @@ const Account = () => {
} }
}; };
const fetchTransactions = async () => {
console.log("fetchTransactions")
try {
const response = await api(`/api/accounts/${activeAccountId}/transactions`);
setTransactions(response.data || []);
} catch (error) {
console.error("Error fetching transactions:", error);
}
};
useEffect(() => { useEffect(() => {
if (activeAccountId) { if (activeAccountId) {
getDashboardData() getDashboardData()
fetchTransactions();
} }
}, [activeAccountId]); }, [activeAccountId]);
@ -251,9 +276,68 @@ const Account = () => {
<> <>
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards} <Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
isLoading={isLoading} /> isLoading={isLoading} />
{transactions.length > 0 && (
<div className="overflow-x-auto py-2">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
<tr>
<th className="px-4 py-3 text-left"></th>
<th className="px-4 py-3 text-center">Name</th>
<th className="px-4 py-3 text-center">Status</th>
<th className="px-4 py-3 text-center">Active</th>
<th className="px-4 py-3 text-center">Last Change</th>
<th className="px-4 py-3 text-center">Expire After (days)</th>
<th className="px-4 py-3 text-center">Sign</th>
<th className="px-4 py-3 text-center">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{transactions.map((tx) => {
const lastModifiedAt = new Date(tx.lastModifiedAt, "DD/MM/YYYY");
return (
<tr
key={tx.id}
onClick={() => handleTransactionClick(tx.id)}
className="hover:bg-gray-50 cursor-pointer"
>
<td className="px-4 py-3">
<div className="w-5 h-5 rounded border border-slate-300 flex justify-center items-center">
{selectedTransactions.includes(tx.id) && (
<FaCheck className="text-purple-600 text-sm" />
)}
</div>
</td>
<td className="px-4 py-3 text-center">
{tx.subject || "Untitled"}
</td>
<td className="px-4 py-3 text-center">
<span className="px-2 py-0.5 rounded-xl text-xs font-medium bg-blue-100 text-blue-700">
{tx.status}
</span>
</td>
<td className="px-4 py-3 text-center">
{tx.active ? "Yes" : "No"}
</td>
<TransactionDataTable /> <td className="px-4 py-3 text-center">
{formatDate(tx.lastModifiedAt)}
</td>
<td className="px-4 py-3 text-center">
{tx.expireAfter ?? "-"}
</td>
<td className="px-4 py-3 text-center ">
{tx?.createdBy?.email === currentEmailId ? <button className="px-2 py-0.5 rounded-xl text-xs font-medium bg-green-100 text-green-700 cursor-pointer">Sign now</button> : "-"}
</td>
<td className="px-4 py-3 text-center">
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading} {isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}
accountOptions={accountsData} />} accountOptions={accountsData} />}
</> </>

View File

@ -1,110 +0,0 @@
import React, { useEffect, useState } from 'react'
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import api from '../../config/api';
import { Transaction } from '../transactions/Transactions';
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
const TransactionDataTable = () => {
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const fetchTransactions = async () => {
console.log("fetchTransactions")
try {
const response = await api(`/api/accounts/${activeAccountId}/transactions`);
setTransactions(response.data || []);
} catch (error) {
console.error("Error fetching transactions:", error);
}
};
const [transactions, setTransactions] = useState<Transaction[]>([]);
useEffect(() => {
if (activeAccountId) {
fetchTransactions();
}
}, [activeAccountId]);
useEffect(() => {
const userDetails = JSON.parse(sessionStorage.getItem('userDetails'));
setCurrentEmailId(userDetails?.email)
}, []);
return (
<>
{transactions.length > 0 && (
<div className="overflow-x-auto py-2">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
<tr>
<th className="px-4 py-3 text-left"></th>
<th className="px-4 py-3 text-center">Name</th>
<th className="px-4 py-3 text-center">Status</th>
<th className="px-4 py-3 text-center">Active</th>
<th className="px-4 py-3 text-center">Last Change</th>
<th className="px-4 py-3 text-center">Expire After (days)</th>
<th className="px-4 py-3 text-center">Sign</th>
<th className="px-4 py-3 text-center">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{transactions.map((tx) => {
const lastModifiedAt = new Date(tx.lastModifiedAt, "DD/MM/YYYY");
return (
<tr
key={tx.id}
onClick={() => handleTransactionClick(tx.id)}
className="hover:bg-gray-50 cursor-pointer"
>
<td className="px-4 py-3">
<div className="w-5 h-5 rounded border border-slate-300 flex justify-center items-center">
{selectedTransactions.includes(tx.id) && (
<FaCheck className="text-purple-600 text-sm" />
)}
</div>
</td>
<td className="px-4 py-3 text-center">
{tx.subject || "Untitled"}
</td>
<td className="px-4 py-3 text-center">
<span className="px-2 py-0.5 rounded-xl text-xs font-medium bg-blue-100 text-blue-700">
{tx.status}
</span>
</td>
<td className="px-4 py-3 text-center">
{tx.active ? "Yes" : "No"}
</td>
<td className="px-4 py-3 text-center">
{formatDate(tx.lastModifiedAt)}
</td>
<td className="px-4 py-3 text-center">
{tx.expireAfter ?? "-"}
</td>
<td className="px-4 py-3 text-center ">
{tx?.createdBy?.email === currentEmailId ? <button className="px-2 py-0.5 rounded-xl text-xs font-medium bg-green-100 text-green-700 cursor-pointer">Sign now</button> : "-"}
</td>
<td className="px-4 py-3 text-center">
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</>
)
}
export default TransactionDataTable

View File

@ -144,7 +144,7 @@ const SignDocument = () => {
const [transaction, setTransaction] = useState<TransactionType | null>(null); const [transaction, setTransaction] = useState<TransactionType | null>(null);
const [recSignStatus, setRecSignStatus] = useState<string | null>(null); const [recSignStatus, setRecSignStatus] = useState<string | null>(null);
const [droppedItems, setDroppedItems] = useState([]); const [droppedItems, setDroppedItems] = useState([]);
const [signTagId, setSignTagId] = useState<number>(0) const[signTagId , setSignTagId] = useState<number>(0)
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" }); const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
@ -183,7 +183,7 @@ const SignDocument = () => {
const [signConfObj, setSignConfObj] = useState<SignConfTypeObj | {}>({}); const [signConfObj, setSignConfObj] = useState<SignConfTypeObj | {}>({});
console.log("signConfObj", signConfObj) console.log("signConfObj",signConfObj)
const zoom = (percentage: number, contentClass: string) => { const zoom = (percentage: number, contentClass: string) => {
const content = document.querySelector(`.${contentClass}`); const content = document.querySelector(`.${contentClass}`);
@ -244,8 +244,8 @@ const SignDocument = () => {
} }
}, [saved]) }, [saved])
const handleSignClick = (index: number, id: number) => { const handleSignClick = (index: number , id:number) => {
console.log("IDNumber", id) console.log("IDNumber" , id)
setSignTagId(id) setSignTagId(id)
if (saved.save) { if (saved.save) {
if (index in signConfObj) { if (index in signConfObj) {
@ -303,7 +303,7 @@ const SignDocument = () => {
console.log(res, "Line 288") console.log(res, "Line 288")
setSelectedDocuments(res.data.documents) setSelectedDocuments(res.data.documents)
setRecipient(res.data.details) setRecipient(res.data.recipient)
setDroppedItems(res.data.tagJsons) setDroppedItems(res.data.tagJsons)
setTransaction(res.data.transaction) setTransaction(res.data.transaction)
setRecSignStatus(res.data.status) setRecSignStatus(res.data.status)
@ -516,7 +516,7 @@ const SignDocument = () => {
headers: { "ngrok-skip-browser-warning": "69420" } headers: { "ngrok-skip-browser-warning": "69420" }
}); });
if (response.status === 200) { if (response.status === 200) {
console.log("line 516", response.data) console.log("line 516" , response.data)
return response.data; return response.data;
} }
} catch (error) { } catch (error) {
@ -692,7 +692,7 @@ const SignDocument = () => {
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => { const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
event.preventDefault(); event.preventDefault();
let targetContainer = event?.currentTarget?.parentElement?.parentElement; let targetContainer = event?.currentTarget?.parentElement?.parentElement;
let targetElement = event?.currentTarget; let targetElement = event?.currentTarget;
if (!targetContainer || !targetElement) return; if (!targetContainer || !targetElement) return;
@ -810,10 +810,6 @@ const SignDocument = () => {
</div> </div>
<p className="text-xs text-slate-600">Hi! {recipient && recipient.firstName} {recipient && recipient.lastName}</p>
{/* Right Side - STARTS */} {/* Right Side - STARTS */}
<div className='flex justify-center items-center'> <div className='flex justify-center items-center'>
@ -850,15 +846,15 @@ const SignDocument = () => {
{(!transparentTags && !hideTags) ? <button {(!transparentTags && !hideTags) ? <button
// onClick={() => handleFormSubmitBtnClick()} // onClick={() => handleFormSubmitBtnClick()}
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)] outline-none'> className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)] outline-none'>
<p className='tracking-wide text-white'> <p
Sign className='tracking-wide text-white'>Sign</p>
</p>
<FaSignature className='text-white text-md' /> <FaSignature className='text-white text-md' />
</button> </button>
: :
<p className="text-xs">Signed Successfully</p> <p className="text-xs">Signed Successfully</p>
} }
<p>{recipient && recipient.firstName} {recipient && recipient.lastName}</p>
</div> </div>
{/* Right Side - ENDS */} {/* Right Side - ENDS */}
@ -1028,7 +1024,7 @@ const SignDocument = () => {
<div <div
tabIndex={1} tabIndex={1}
id={item.id} id={item.id}
onClick={() => handleSignClick(index, item.id)} onClick={() => handleSignClick(index , item.id)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none bg-transparent" : "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-transparent" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }} // style={{ height: item.dimensions.height + "px" }}
> >
@ -1045,8 +1041,8 @@ const SignDocument = () => {
height: `${item.dimensions.height}px`, height: `${item.dimensions.height}px`,
}} }}
// src={saved.signature as string} /> // src={saved.signature as string} />
src={`data:image/png;base64,${saved.signature}`} src={`data:image/png;base64,${saved.signature}`}
/> />
} }
</div>} </div>}
{/* SIGNATURE FIELD - END */} {/* SIGNATURE FIELD - END */}
@ -1057,7 +1053,7 @@ const SignDocument = () => {
<div <div
tabIndex={1} tabIndex={1}
id={item.id} id={item.id}
onClick={() => handleSignClick(index, item.id)} onClick={() => handleSignClick(index , item.id)}
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" }}
> >
@ -1071,8 +1067,8 @@ const SignDocument = () => {
}} }}
className="w-[100%] h-full" className="w-[100%] h-full"
// src={saved.initial as string} // src={saved.initial as string}
src={`data:image/png;base64,${saved.initial}`} src={`data:image/png;base64,${saved.initial}`}
/> />
} }
</div>} </div>}
{/* INITIALS FIELD - END */} {/* INITIALS FIELD - END */}
@ -1278,13 +1274,7 @@ const SignDocument = () => {
<div style={{ display: `${openSign ? 'block' : 'none'}` }}> <div style={{ display: `${openSign ? 'block' : 'none'}` }}>
{recipient && <SignCon recipientId={recipientId as string} setOpenSign={setOpenSign} saved={saved} setSaved={setSaved} signTagId={signTagId}/>
<SignCon
recipient={recipient}
recipientId={recipientId as string}
setOpenSign={setOpenSign} saved={saved}
setSaved={setSaved}
signTagId={signTagId} />}
</div> </div>
<div style={{ display: `${stamp.open ? 'block' : 'none'}` }}> <div style={{ display: `${stamp.open ? 'block' : 'none'}` }}>

View File

@ -42,10 +42,10 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
.then(res => { .then(res => {
if (res.status === 200 || res.status === 201) { if (res.status === 200 || res.status === 201) {
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED)); dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED));
dispatch(setTransactionId(res.data.id)); dispatch(setTransactionId(res.data));
let navigateUrl = `/process-step-one/${defaultAccountId}/${res.data.id}`; let navigateUrl = `/process-step-one/${defaultAccountId}/${res.data}`;
if (signType === SignTypeEnum.selfSign) navigateUrl = `/process-step-one/${defaultAccountId}/${res.data.id}/self_sign`; if (signType === SignTypeEnum.selfSign) navigateUrl = `/process-step-one/${defaultAccountId}/${res.data}/self_sign`;
setTimeout(() => navigate(navigateUrl), 0) setTimeout(() => navigate(navigateUrl), 0)
} }
}) })
.catch(err => console.log(err)) .catch(err => console.log(err))
@ -81,40 +81,31 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
ref={dropDownRef} ref={dropDownRef}
onBlur={() => setSendForSignDropdown(false)} onBlur={() => setSendForSignDropdown(false)}
> >
<div <div className={`flex justify-between items-center gap-2 cursor-pointer bg-[var(--primary)] rounded text-[var(--sendForSignTextCol)] ${showSideBar ? "" : "pt-[0.4rem] pr-[0.6rem] pb-[0.4rem] pl-[0.2rem]"}`}
className={`flex justify-between items-center gap-2 cursor-pointer bg-[var(--primary)] rounded text-[var(--sendForSignTextCol)] ${showSideBar ? "" : "pt-[0.4rem] pr-[0.6rem] pb-[0.4rem] pl-[0.2rem]"}`}> onClick={() => setSendForSignDropdown(!sendForSignDropdown)}>
<div <div className={`w-full`}>
onClick={() => handleStartSectionClick(SignTypeEnum.multipleSign)}
className={`w-full`}>
{ {
showSideBar ? showSideBar ?
<p <p className='text-center text-sm'>Send for signature</p>
className='text-center text-sm'>Send for signature</p>
: :
<IoIosSend className={`pl-2 text-3xl`} /> <IoIosSend className={`pl-2 text-3xl`} />
} }
</div> </div>
{showSideBar && {showSideBar && <p className='border-l-1 border-[var(--sendForSignTextCol)] px-2 py-2'>
<p <IoMdArrowDropdown className={`text-2xl ${sendForSignDropdown ? 'rotate-180' : ''} transition-all duration-150`} />
className='border-l-1 border-[var(--sendForSignTextCol)] px-2 py-2'> </p>}
<IoMdArrowDropdown
onClick={() => setSendForSignDropdown(!sendForSignDropdown)}
className={`text-2xl ${sendForSignDropdown ? 'rotate-180' : ''} transition-all duration-150`} />
</p>}
</div> </div>
{/* Drop Down Box */} {/* Drop Down Box */}
{ {
sendForSignDropdown && sendForSignDropdown &&
<div className={`w-[300px] rounded absolute z-40 bg-[var(--sendForSignDropDownBg)] shadow-lg top-[54px] text-left outline-none`}> <div className={`w-[300px] rounded absolute z-40 bg-[var(--sendForSignDropDownBg)] shadow-lg top-[54px] text-left outline-none`}>
<div className='py-3 px-8 leading-6 rounded-t-sm hover:bg-[var(--sendForSignDropDownHover)] cursor-pointer' <div className='py-3 px-8 leading-6 rounded-t-sm hover:bg-[var(--sendForSignDropDownHover)] cursor-pointer' onClick={() => handleStartSectionClick(SignTypeEnum.selfSign)}>
onClick={() => handleStartSectionClick(SignTypeEnum.selfSign)}>
<p>Self Sign</p> <p>Self Sign</p>
<p className='text-[var(--greyTextCol)] text-sm'>You're the only signer.</p> <p className='text-[var(--greyTextCol)] text-sm'>You're the only signer.</p>
</div> </div>
<div className='w-full h-[1px] opacity-30 bg-[var(--greyTextCol)]'></div> <div className='w-full h-[1px] opacity-30 bg-[var(--greyTextCol)]'></div>
<div className='py-3 px-8 leading-6 rounded-b-sm hover:bg-[var(--sendForSignDropDownHover)] cursor-pointer' <div className='py-3 px-8 leading-6 rounded-b-sm hover:bg-[var(--sendForSignDropDownHover)] cursor-pointer' onClick={() => handleStartSectionClick(SignTypeEnum.multipleSign)}>
onClick={() => handleStartSectionClick(SignTypeEnum.multipleSign)}>
<p>Send for Sign</p> <p>Send for Sign</p>
<p className='text-[var(--greyTextCol)] text-sm'>You and other need to sign.</p> <p className='text-[var(--greyTextCol)] text-sm'>You and other need to sign.</p>
</div> </div>

View File

@ -1,87 +0,0 @@
import { useRef, useState } from 'react';
import { FiUpload } from 'react-icons/fi'
import { uploadDocuments } from '../../store/slices/documentSlice';
import { useDispatch, useSelector } from 'react-redux';
import { toast } from 'react-toastify';
import { useNavigate } from 'react-router-dom';
import { RootState } from '../../store/store';
import api from '../../config/api';
const StartNow = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<File[]>([]);
const userData = useSelector((state: RootState) => state.user);
const dispatch = useDispatch();
const navigate = useNavigate();
const handleUpload = async (filesToUpload: File[]) => {
const accountId = userData.account?.id;
const transactionRes = await api.post(`/api/accounts/${userData.account?.id}/transactions`);
const transactionId = transactionRes.data.id
const formData = new FormData();
formData.append("file", filesToUpload[0]);
dispatch(uploadDocuments({ formData, accountId, transactionId }))
.unwrap()
.then((res) => {
// toast.dismiss();
setFiles([]);
navigate(`/process-step-one/${accountId}/${transactionId}`);
})
.catch((err) => {
console.error("Upload failed", err);
});
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = e.target.files ? Array.from(e.target.files) : [];
if (selectedFiles.length > 0) {
setFiles((prev) => [...prev, ...selectedFiles]);
handleUpload(selectedFiles);
}
};
return (
<>
<div
onClick={() => fileInputRef.current?.click()}
className="group cursor-pointer border-slate-300 rounded-lg bg-white/60 transition-all duration-200 p-4 flex flex-col items-center justify-center gap-2 text-center shadow-sm hover:shadow-md">
{/* Upload icon with subtle animation */}
<div className="p-2 text-blue-600 group-hover:text-blue-700 transition-colors">
<FiUpload className="text-lg" />
</div>
{/* Compact text */}
<p className="text-xs text-gray-600">
<span className="text-blue-600 font-medium">Click to upload</span> or drag and drop
</p>
<button
className="mt-1 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded shadow-xs transition-colors duration-150 focus:outline-none focus:ring-1 focus:ring-blue-400"
>
Start Now
</button>
<p className="text-[0.65rem] text-gray-400 mt-0.5">
PDF, JPG, PNG (max. 10MB)
</p>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept=".pdf,.doc,.docx,.jpg,.png,.jpeg,.txt"
multiple
/>
</div>
</>
)
}
export default StartNow

View File

@ -1,15 +1,11 @@
import axios from 'axios'; import axios from 'axios';
import React, { ReactNode, useEffect, useState } from 'react'; import React, { ReactNode, useEffect } from 'react';
import { FiUpload, FiEdit, FiFolder, FiArrowRight } from 'react-icons/fi'; import { FiUpload, FiEdit, FiFolder, FiArrowRight } from 'react-icons/fi';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AppDispatch, RootState } from '../../store/store'; import { AppDispatch, RootState } from '../../store/store';
import api from '../../config/api'; import api from '../../config/api';
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice'; import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice';
import { Transaction, TransactionResponse } from '../transactions/Transactions';
import TransactionsTable from '../transactions/TransactionsTable';
import TransactionDataTable from '../accounts/TransactionDataTable';
import StartNow from './StartNow';
export interface StartCard { export interface StartCard {
@ -21,13 +17,13 @@ export interface StartCard {
} }
const cards = [ const cards = [
// { {
// icon: <FiUpload className="w-5 h-5 text-blue-700" />, icon: <FiUpload className="w-5 h-5 text-blue-700" />,
// title: 'Start a Workflow', title: 'Start a Workflow',
// description: 'Begin by uploading a document and tagging fields.', description: 'Begin by uploading a document and tagging fields.',
// buttonText: 'Start Now', buttonText: 'Start Now',
// bg: 'bg-white/60', bg: 'bg-white/60',
// }, },
{ {
icon: <FiEdit className="w-5 h-5 text-green-700" />, icon: <FiEdit className="w-5 h-5 text-green-700" />,
title: 'Sign an Agreement', title: 'Sign an Agreement',
@ -80,56 +76,41 @@ const StartSection = () => {
// }, []) // }, [])
return ( return (
<section className="px-4 pt-2 pb-3 w-full"> <section className="px-4 py-3 w-full">
<h2 className="text-lg font-semibold text-gray-800 mb-2">Get Started</h2> <h2 className="text-lg font-semibold text-gray-800 mb-2">Get Started</h2>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{cards.map((card, index) => (
<StartNow /> <div
onClick={() => handleStartSectionClick(card)}
<div className='col-span-2 grid grid-cols-2 justify-between gap-4'> key={index}
{cards.map((card, index) => ( className={`${card.bg} cursor-pointer relative border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all h-full flex flex-col justify-between min-h-[220px]`}
<div >
onClick={() => handleStartSectionClick(card)} {/* Icon + Text */}
key={index} <div className="flex flex-col gap-3">
className={`${card.bg} w-full max-h-max cursor-pointer relative border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all flex flex-col justify-between`} <div className="w-9 h-9 flex items-center justify-center border border-gray-200 rounded-md bg-white">
> {card.icon}
{/* Icon + Text */}
<div className="flex flex-col gap-3">
<div className="w-9 h-9 flex items-center justify-center border border-gray-200 rounded-md bg-white">
{card.icon}
</div>
<h3 className="text-base font-medium text-gray-900">{card.title}</h3>
<p className="text-sm text-gray-700">{card.description}</p>
</div> </div>
{/* Bottom Area */} <h3 className="text-base font-medium text-gray-900">{card.title}</h3>
<div className="flex items-center justify-between mt-2"> <p className="text-sm text-gray-700">{card.description}</p>
{card.buttonText ? (
<button className="text-sm cursor-pointer font-medium text-white bg-gradient-to-r from-blue-600 to-blue-500 px-4 py-2 rounded-md shadow hover:opacity-90 transition">
{card.buttonText}
</button>
) : (
<div />
)}
<FiArrowRight className="w-4 h-4 text-gray-500" />
</div>
</div> </div>
))}
</div>
{/* Bottom Area */}
<div className="flex items-center justify-between mt-2">
{card.buttonText ? (
<button className="text-sm cursor-pointer font-medium text-white bg-gradient-to-r from-blue-600 to-blue-500 px-4 py-2 rounded-md shadow hover:opacity-90 transition">
{card.buttonText}
</button>
) : (
<div />
)}
<FiArrowRight className="w-4 h-4 text-gray-500" />
</div>
</div>
))}
</div> </div>
<section className='pt-2 pb-3'>
<h2 className="mt-3 text-lg font-semibold text-gray-800 mb-2">Recent Activities</h2>
<section>
<TransactionDataTable />
</section>
</section>
</section> </section>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react'
import { ContactTypeObj } from './RecipientAddCard'; import { ContactTypeObj } from './RecipientAddCard'
import { Recipient } from '../../../store/slices/selectedRecipientsSlice'; import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
interface ContactDataProps { interface ContactDataProps {
@ -10,127 +10,66 @@ interface ContactDataProps {
openContactModal: boolean; openContactModal: boolean;
setOpenContactModal: React.Dispatch<React.SetStateAction<boolean>>; setOpenContactModal: React.Dispatch<React.SetStateAction<boolean>>;
setRecipients: React.Dispatch<React.SetStateAction<Recipient>>; setRecipients: React.Dispatch<React.SetStateAction<Recipient>>;
topRef?: React.RefObject<HTMLDivElement>;
setManualFocusTriggered: React.Dispatch<React.SetStateAction<boolean>>
} }
// Helper function to highlight matching text const ContactModal: React.FC<ContactDataProps> = ({ contactData, inpFirstName, inpLastName, inpEmail, openContactModal, setRecipients, setOpenContactModal }) => {
const highlightText = (text: string, searchTerm: string, shouldHighlight: boolean) => {
if (!shouldHighlight || !searchTerm.trim()) {
return <span>{text}</span>;
}
const searchTermLower = searchTerm.toLowerCase();
const textLower = text.toLowerCase();
const index = textLower.indexOf(searchTermLower);
if (index === -1) {
return <span>{text}</span>;
}
const beforeMatch = text.slice(0, index);
const match = text.slice(index, index + searchTerm.length);
const afterMatch = text.slice(index + searchTerm.length);
return (
<span>
{beforeMatch}
<span className="bg-yellow-200 font-medium">{match}</span>
{afterMatch}
</span>
);
};
const ContactModal: React.FC<ContactDataProps> = ({
contactData,
inpFirstName,
inpLastName,
inpEmail,
openContactModal,
setRecipients,
setOpenContactModal ,
setManualFocusTriggered
}) => {
const handleContactClick = (indx: number) => { const handleContactClick = (indx: number) => {
if (!contactData.contacts.length) return; if (!contactData.contacts.length) return;
setRecipients((prev) => ({ setRecipients((prev) => {
...prev, // Preserve other properties like uuid, newOne, etc. return {
firstName: contactData.contacts[indx].firstName, firstName: contactData.contacts[indx].firstName,
lastName: contactData.contacts[indx].lastName, lastName: contactData.contacts[indx].lastName,
email: contactData.contacts[indx].email, email: contactData.contacts[indx].email,
})); type: prev.type
}
});
setOpenContactModal(false); setOpenContactModal(false);
setManualFocusTriggered(false)
};
const handleKeyDown = (event: React.KeyboardEvent, indx: number) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleContactClick(indx);
}
};
if (!openContactModal || !contactData.contacts.length) {
return null;
} }
return ( return (
<div className="max-h-60 overflow-y-auto"> <>
{contactData.contacts.map((contact, indx) => ( {
<div contactData.contacts.map((recipient, indx) => {
key={contact.id || indx} return <div
tabIndex={0} tabIndex={1}
onMouseDown={() => handleContactClick(indx)} onClick={() => handleContactClick(indx)}
onKeyDown={(e) => handleKeyDown(e, indx)} style={{display: `${openContactModal ? "flex" : "none"}`}}
className="flex items-center px-3 py-2 cursor-pointer hover:bg-gray-50 transition-colors duration-150 focus:bg-gray-100 focus:outline-none rounded-md" className='flex items-center px-3 py-2 cursor-pointer hover:bg-slate-100 transiton-all w-full'>
role="button" <div
aria-label={`Select contact ${contact.firstName} ${contact.lastName}`} key={indx}
> className='w-8 h-8 bg-gray-200 rounded-full'></div>
{/* Avatar */} <div className='ml-2'>
<div className="w-8 h-8 bg-[var(--primaryLight2)] rounded-full flex items-center justify-center text-white text-sm font-medium"> <div className='flex gap-1.5'>
{(contact.firstName?.[0] || '') + (contact.lastName?.[0] || '')} <p className='text-sm font-semibold'
</div> style={{ background: `${contactData.type === "firstName" ? `linear-gradient(to right, yellow 0ch, yellow ${inpFirstName.length}ch, white ${inpFirstName.length}ch, white 100%)` : "white"}` }}
>
{/* Contact Info */} {recipient.firstName}
<div className="ml-3 flex-1 min-w-0"> </p>
<div className="flex gap-1.5 items-center"> <p
<p className="text-sm font-semibold text-gray-900 truncate"> style={{ background: `${contactData.type === "lastName" ? `linear-gradient(to right, yellow 0ch, yellow ${inpLastName.length}ch, white ${inpLastName.length}ch, white 100%)` : "white"}` }}
{highlightText( className='text-sm font-semibold'>
contact.firstName, {recipient.lastName}
inpFirstName, </p>
contactData.type === "firstName" </div>
)} <p className='text-xs text-gray-500'
</p> style={{ background: `${contactData.type === "email" ? `linear-gradient(to right, yellow 0ch, yellow ${inpEmail.length}ch, white ${inpEmail.length}ch, white 100%)` : "white"}` }}
<p className="text-sm font-semibold text-gray-900 truncate"> >
{highlightText( {recipient.email}
contact.lastName,
inpLastName,
contactData.type === "lastName"
)}
</p> </p>
</div> </div>
<p className="text-xs text-gray-500 truncate">
{highlightText(
contact.email,
inpEmail,
contactData.type === "email"
)}
</p>
</div> </div>
</div> })
))} }
</>
{/* No results message */} )
{contactData.contacts.length === 0 && ( }
<div className="px-3 py-4 text-center text-gray-500 text-sm">
No contacts found
</div>
)}
</div>
);
};
export default ContactModal; // return <p
// key={indx}
// className='w-full hover:bg-slate-100 cursor-pointer px-2 py-1'>
// {val.firstName}
// </p>
export default ContactModal

View File

@ -6,7 +6,7 @@ import SelectedDocuments from "./SelectedDocuments";
import api from "../../../config/api"; import api from "../../../config/api";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../store/store"; import { RootState } from "../../../store/store";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { setImages } from "../../../store/slices/selectedDocsSlice"; import { setImages } from "../../../store/slices/selectedDocsSlice";
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice"; import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
@ -39,10 +39,10 @@ const ProcessStepOne = () => {
dispatch(toggleToTemplates()); dispatch(toggleToTemplates());
} }
const [editCase, setEditCase] = useState<string>("") const [editCase , setEditCase] = useState<string>("")
const [editRecipient, setEditRecipient] = useState({}) const [editRecipient , setEditRecipient] = useState({})
// if (!accountId || !transactionId) { // if (!accountId || !transactionId) {
// window.location.href = "/"; // window.location.href = "/";
@ -55,12 +55,6 @@ 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 userData = useSelector((state: RootState) => state.user) const userData = useSelector((state: RootState) => state.user)
// ProcessStepOne.tsx
const topRef = useRef<HTMLDivElement | null>(null);
// const userData = useSelector((state: RootState) => state.user)
const [transactionDetails, setTransactionDetails] = useState<any>([]);
console.log("selectedRecipients", selectedRecipients) console.log("selectedRecipients", selectedRecipients)
@ -102,7 +96,7 @@ const ProcessStepOne = () => {
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) {
console.log(selectedDocuments, 'line 193') console.log(selectedDocuments, 'line 193')
@ -166,12 +160,11 @@ const ProcessStepOne = () => {
language: "en", language: "en",
name: `${recipient.firstName} ${recipient.lastName}`, name: `${recipient.firstName} ${recipient.lastName}`,
roleName: recipient.roleName, roleName: recipient.roleName,
allowSenderToEdit: recipient.allowSenderToEdit, allowSenderToEdit: recipient.allowSenderToEdit ,
allowSenderToDelete: recipient.allowSenderToDelete, allowSenderToDelete: recipient.allowSenderToDelete,
})); }));
let recipientsDataTemp = recipientsData; let recipientsDataTemp = recipientsData;
let recipientId = null;
if (signType === SignTypeEnum.selfSign) { if (signType === SignTypeEnum.selfSign) {
recipientsDataTemp = [{ recipientsDataTemp = [{
@ -196,18 +189,9 @@ const ProcessStepOne = () => {
name: `${userData.firstName} ${userData.lastName}`, name: `${userData.firstName} ${userData.lastName}`,
roleName: "" roleName: ""
}] }]
try {
const basePath = `/api/accounts/${accountId}/transactions/${transactionId}/recipients`;
const res = await api.post(basePath, recipientsDataTemp)
recipientId = res.data[0].id;
} catch (error) {
console.log("Error Creating Self Sign Recipient", error.message)
}
} else {
dispatch(addRecipientAsync({ recipientsData: recipientsDataTemp, accountId, transactionId }));
} }
dispatch(addRecipientAsync({ recipientsData: recipientsDataTemp, accountId, transactionId }));
dispatch(showLoader("Processing Message...")); dispatch(showLoader("Processing Message..."));
console.log(`1/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`) console.log(`1/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`)
@ -229,23 +213,11 @@ const ProcessStepOne = () => {
dispatch(resetDocuments()) dispatch(resetDocuments())
dispatch(resetRecipients()) dispatch(resetRecipients())
dispatch(hideLoader()); dispatch(hideLoader());
if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) { if (transactionTemplateToggle === 'templates') {
console.log(`/process-step-two/${accountId}/template/${transactionId}`) console.log(`/process-step-two/${accountId}/template/${transactionId}`)
navigate(`/process-step-two/${accountId}/template/${transactionId}`) navigate(`/process-step-two/${accountId}/template/${transactionId}`)
} else { } else {
if (signType === SignTypeEnum.selfSign) { navigate(`/process-step-two/${accountId}/${transactionId}`);
(async () => {
try {
const res = await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" })
console.log(res, "Check123");
navigate(`/sign/${recipientId}`);
} catch (err) {
console.log("Error Transaction Sent", err.message);
}
})()
} else {
navigate(`/process-step-two/${accountId}/${transactionId}`);
}
} }
}, 1000); }, 1000);
} }
@ -298,6 +270,15 @@ const ProcessStepOne = () => {
</div> </div>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<div className="flex items-center" >
<input
checked={signType === SignTypeEnum.selfSign}
onChange={(e) => handleSelfSignCb(e)}
id="default-checkbox" type="checkbox" value="" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm outline-none"
/>
<label htmlFor="default-checkbox" className="ms-2 text-sm text-slate-600">SELF SIGN</label>
</div>
<button <button
onClick={() => handleNextClick(false)} onClick={() => handleNextClick(false)}
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition" className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition"
@ -317,40 +298,17 @@ const ProcessStepOne = () => {
</section> </section>
{/* Recipients */} {/* Recipients */}
<section className="mt-8"> {
signType !== SignTypeEnum.selfSign &&
{ < section className="mt-8">
transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <h2 className="text-lg font-medium mb-4 pt-6 border-t text-[var(--processStepOneTextHead)]">Signers</h2>
<div className="flex justify-between items-center">
<h2 className="text-lg font-medium mb-4 pt-6 border-t text-[var(--processStepOneTextHead)]">Signers</h2>
<div className="flex items-center mr-2">
<input
checked={signType === SignTypeEnum.selfSign}
onChange={(e) => handleSelfSignCb(e)}
id="default-checkbox" type="checkbox" value="" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm outline-none"
/>
<label htmlFor="default-checkbox" className="ms-2 text-sm text-slate-600">SELF SIGN</label>
</div>
</div>
}
{
signType !== SignTypeEnum.selfSign &&
<div className="flex flex-col gap-4 align-start"> <div className="flex flex-col gap-4 align-start">
<RecipientCards transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} /> <RecipientCards transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase}/>
{/* {signType !== "self-sign" && <RecipientAddCard />} */} {/* {signType !== "self-sign" && <RecipientAddCard />} */}
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase}/>
<div
ref={topRef}
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} />
</div>
</div> </div>
} </section>
</section> }
{/* Email */} {/* Email */}
<section className="mt-8"> <section className="mt-8">

View File

@ -24,11 +24,7 @@ export interface ContactTypeObj {
} }
const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase, setEditRecipient, setEditCase, topRef }) => { const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,setEditRecipient, setEditCase }) => {
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] }); const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] });
const [openContactModal, setOpenContactModal] = useState(false); const [openContactModal, setOpenContactModal] = useState(false);
@ -36,31 +32,11 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
const formRef = useRef<HTMLFormElement | null>(null); const formRef = useRef<HTMLFormElement | null>(null);
const [recipient, setRecipients] = useState<Recipient>({ uuid: null, newOne: false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: false, allowSenderToDelete: false }); const [recipient, setRecipients] = useState<Recipient>({ uuid:null, newOne:false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: false, allowSenderToDelete: false });
const [showAdvanceSettings, setShowAdvanceSettings] = useState(false) const [showAdvanceSettings, setShowAdvanceSettings] = useState(false)
const [cachedContacts, setCachedContacts] = useState<ContactType[]>([]);
const [filteredContacts, setFilteredContacts] = useState<ContactType[]>([]);
const [activeField, setActiveField] = useState<"firstName" | "lastName" | "email" | null>(null);
const [hasFetchedInitial, setHasFetchedInitial] = useState(false);
const [lastQuery, setLastQuery] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [manualFocusTriggered, setManualFocusTriggered] = useState(false);
console.log(recipient) console.log(recipient)
useEffect(() => {
if (openContactModal) {
if (topRef?.current) {
topRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
} else {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
}, [openContactModal]);
useEffect(() => { useEffect(() => {
if (editCase === "Edit" && editRecipient) { if (editCase === "Edit" && editRecipient) {
setRecipients({ setRecipients({
@ -75,55 +51,56 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
allowSenderToEdit: editRecipient.allowSenderToEdit ?? false, allowSenderToEdit: editRecipient.allowSenderToEdit ?? false,
allowSenderToDelete: editRecipient.allowSenderToDelete ?? false, allowSenderToDelete: editRecipient.allowSenderToDelete ?? false,
uuid: String(editRecipient?.id ?? editRecipient?.uuid ?? ''), uuid: String(editRecipient?.id ?? editRecipient?.uuid ?? ''),
newOne: editRecipient.newOne ?? false newOne:editRecipient.newOne ?? false
}); });
} }
}, [editCase, editRecipient]); }, [editCase, editRecipient]);
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => { const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
console.log("recipientrecipient", recipient) console.log("recipientrecipient",recipient)
if (!recipient.lastName || !recipient.email || !recipient.type) return; if (!recipient.lastName || !recipient.email || !recipient.type) return;
// ✅ Email format validation // ✅ Email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(recipient.email)) { if (!emailRegex.test(recipient.email)) {
console.log("Invalid email format"); console.log("Invalid email format");
return; return;
} }
if (editCase === 'Edit') { if (editCase === 'Edit') {
dispatch(updateRecipient(recipient)); dispatch(updateRecipient(recipient));
} else { } else {
dispatch(addRecipient({ dispatch(addRecipient({
...recipient, ...recipient,
uuid: uuidv4(), uuid:uuidv4(),
newOne: true newOne:true
})); }));
} }
setRecipients({ // ✅ Reset state and form
firstName: "", setRecipients({
lastName: "", firstName: "",
email: "", lastName: "",
hostName: "", email: "",
hostEmail: "", hostName: "",
roleName: "", hostEmail: "",
type: recipientType.SIGNER, roleName: "",
allowSenderToEdit: false, type: recipientType.SIGNER,
allowSenderToDelete: false, allowSenderToEdit: false,
uuid: "", allowSenderToDelete: false,
newOne: false uuid:"",
}); newOne:false
});
// setRecipients(null), // setRecipients(null),
setEditCase("") setEditCase("")
setManualFocusTriggered(false)
formRef.current?.reset(); formRef.current?.reset();
}; };
@ -178,97 +155,7 @@ setManualFocusTriggered(false)
}, 350) }, 350)
} }
const handleFocus = async (field: "firstName" | "lastName" | "email") => {
setManualFocusTriggered(true);
setOpenContactModal(true);
setActiveField(field);
setLastQuery(null); // Reset to allow fresh input fetch if needed
if (!hasFetchedInitial) {
try {
const res = await api.get(`/api/accounts/${activeAccountId}/contacts?pageSize=10`);
const contacts = res.data?.content || [];
setCachedContacts(contacts);
setFilteredContacts(contacts);
setContactData({ type: field, contacts });
setHasFetchedInitial(true);
} catch (err) {
console.error("Initial contact fetch failed:", err);
}
} else {
setFilteredContacts(cachedContacts);
setContactData({ type: field, contacts: cachedContacts });
}
};
useDebouncedEffect(() => {
console.log("useDebouncedEffect")
if (!activeField || !manualFocusTriggered) return;
const inputValue = recipient[activeField].trim().toLowerCase();
if (inputValue.length === 0) {
setFilteredContacts(cachedContacts);
setContactData({ type: activeField, contacts: cachedContacts });
setOpenContactModal(true); // Keep showing modal with all options
return;
}
// Local match
const localMatches = cachedContacts.filter(contact =>
contact[activeField]?.toLowerCase().includes(inputValue)
);
if (localMatches.length > 0) {
setFilteredContacts(localMatches);
setContactData({ type: activeField, contacts: localMatches });
setOpenContactModal(true);
return;
}
if (lastQuery !== inputValue) {
setLastQuery(inputValue);
api
.get(`/api/accounts/${activeAccountId}/contacts?pageSize=5&${activeField}=${inputValue}`)
.then((res) => {
const fetched = res.data?.content || [];
setFilteredContacts(fetched);
setContactData({ type: activeField, contacts: fetched });
setOpenContactModal(true);
})
.catch(console.error);
}
}, [recipient.firstName, recipient.lastName, recipient.email], 300);
useEffect(() => {
const handleClickOutside = (e: MouseEvent | FocusEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpenContactModal(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("focusin", handleClickOutside); // Also handles tabbing out
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("focusin", handleClickOutside);
};
}, []);
return ( return (
<div ref={containerRef}>
<form <form
ref={formRef} ref={formRef}
onSubmit={handleRecipientAdd} onSubmit={handleRecipientAdd}
@ -404,7 +291,7 @@ useEffect(() => {
<div className="grid grid-cols-12 gap-4 mt-3"> <div className="grid grid-cols-12 gap-4 mt-3">
<input <input
type="text" type="text"
onFocus={() => handleFocus("firstName")} onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur} onBlur={handleContactModalBlur}
value={recipient.firstName} value={recipient.firstName}
placeholder="First Name" placeholder="First Name"
@ -414,7 +301,7 @@ useEffect(() => {
<input <input
required required
type="text" type="text"
onFocus={() => handleFocus("lastName")} onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur} onBlur={handleContactModalBlur}
value={recipient.lastName} value={recipient.lastName}
placeholder="Last Name" placeholder="Last Name"
@ -424,7 +311,7 @@ useEffect(() => {
<input <input
required required
type="email" type="email"
onFocus={() => handleFocus("email")} onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur} onBlur={handleContactModalBlur}
value={recipient.email} value={recipient.email}
placeholder="Recipient Email" placeholder="Recipient Email"
@ -458,8 +345,8 @@ useEffect(() => {
</div>} </div>}
{/* Contact Modal */} {/* Contact Modal */}
{openContactModal && contactData.contacts.length > 0 && ( {contactData.contacts.length > 0 && openContactModal && (
<div className="absolute z-50 mt-1 w-2/3 max-h-[50vh] bg-white shadow-lg rounded-md p-3 border border-gray-100 overflow-y-auto no-scrollbar"> <div className="absolute z-50 mt-1 w-full max-w-2xl bg-white shadow-lg rounded-md p-3 border border-gray-100">
<ContactModal <ContactModal
contactData={contactData} contactData={contactData}
inpFirstName={recipient.firstName} inpFirstName={recipient.firstName}
@ -468,14 +355,10 @@ useEffect(() => {
openContactModal={openContactModal} openContactModal={openContactModal}
setRecipients={setRecipients} setRecipients={setRecipients}
setOpenContactModal={setOpenContactModal} setOpenContactModal={setOpenContactModal}
topRef={topRef}
setManualFocusTriggered={setManualFocusTriggered}
/> />
</div> </div>
)} )}
{/* Signing options + Add button in one line */} {/* Signing options + Add button in one line */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex flex-wrap gap-6"> <div className="flex flex-wrap gap-6">
@ -502,11 +385,11 @@ useEffect(() => {
type="submit" type="submit"
className="inline-flex items-center gap-2 bg-[var(--primary)] hover:bg-[var(--primaryLight2)] text-white text-sm font-semibold px-5 py-2 rounded-md transition" className="inline-flex items-center gap-2 bg-[var(--primary)] hover:bg-[var(--primaryLight2)] text-white text-sm font-semibold px-5 py-2 rounded-md transition"
> >
<FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"} <FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
</button> </button>
</div> </div>
</form> </form>
</div>
); );
}; };

View File

@ -49,24 +49,6 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
{ name: 'Dropbox', icon: <FaDropbox /> }, { name: 'Dropbox', icon: <FaDropbox /> },
]; ];
const handleUpload = (filesToUpload: File[]) => {
const formData = new FormData();
formData.append("file", filesToUpload[0])
console.log(formData, "Line 59")
// filesToUpload.forEach((file) => formData.append("document", file));
dispatch(uploadDocuments({ formData, accountId, transactionId }))
.unwrap()
.then((res) => {
toast.dismiss();
setFiles([]);
setModalOpen(false);
})
.catch((err) => {
console.error("Upload failed", err);
});
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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) {
@ -84,6 +66,40 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
} }
}; };
const handleUpload = (filesToUpload: File[]) => {
const formData = new FormData();
formData.append("file", filesToUpload[0])
console.log(formData, "Line 59")
// filesToUpload.forEach((file) => formData.append("document", file));
dispatch(uploadDocuments({ formData, accountId, transactionId }))
.unwrap()
.then((res) => {
toast.dismiss();
// toast.info("📄 Documents uploaded!", {
// position: "bottom-right",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
// Update selected documents after upload
// dispatch(setSelectedDocuments([...documents, ...res]));
// debugger;
setFiles([]);
setModalOpen(false);
})
.catch((err) => {
console.error("Upload failed", err);
});
};
return ( return (
<> <>
{/* Upload Card First */} {/* Upload Card First */}

View File

@ -79,7 +79,6 @@ interface SelectStyle {
} }
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
recipient,
setOpenSign, setOpenSign,
recipientId, recipientId,
saved, saved,
@ -87,7 +86,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
signTagId signTagId
}) => { }) => {
console.log("savedModal", recipient) console.log("savedModal", saved)
const [activeTab, setActiveTab] = useState<'TYPED' | 'DRAW' | 'UPLOAD' | 'SAVED'>('TYPED'); const [activeTab, setActiveTab] = useState<'TYPED' | 'DRAW' | 'UPLOAD' | 'SAVED'>('TYPED');
const [fullName, setFullName] = useState(''); const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState(''); const [initials, setInitials] = useState('');
@ -275,8 +274,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
if (res?.data) { if (res?.data) {
setSignatures({ setSignatures({
fullSignature: "", fullSignature:"",
initials: "", initials:"",
}) })
setSaved({ setSaved({
saveType: res.data.signatureType ?? null, saveType: res.data.signatureType ?? null,
@ -507,16 +506,17 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
const getUserDetails = async () => { const getUserDetails = async () => {
try { try {
// let res = await axios.get(`/api/user/details`, { let res = await axios.get(`/api/user/details`, {
// headers: { headers: {
// "ngrok-skip-browser-warning": "69420", "ngrok-skip-browser-warning": "69420",
// } }
// }); });
// console.log("getuser", res); console.log("getuser", res);
// let currUser = res?.data; let currUser = res?.data;
setUser(recipient); setUser(currUser);
setFullName(recipient.firstName + " " + recipient.lastName);
setInitials(recipient.firstName.slice(0, 1) + recipient.lastName.slice(0, 1)); setFullName(currUser.firstName + " " + currUser.lastName);
setInitials(currUser.firstName.slice(0, 1) + currUser.lastName.slice(0, 1));
} catch (error) { } catch (error) {
console.error("Error fetching user details:", error); console.error("Error fetching user details:", error);
} }

View File

@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import TransactionTopNav from './TransactionTopNav'; import TransactionTopNav from './TransactionTopNav';
import api from '../../config/api'; import api from '../../config/api';
import TransactionsTable from './TransactionsTable';
interface Document { interface Document {
documentId: string; documentId: string;
@ -20,7 +19,7 @@ interface Recipient {
signedDateTime?: string; signedDateTime?: string;
} }
export interface Transaction { interface Transaction {
transactionId: string; transactionId: string;
name: string; name: string;
createAt: string; createAt: string;
@ -31,7 +30,7 @@ export interface Transaction {
recipients: Recipient[]; recipients: Recipient[];
} }
export interface TransactionResponse { interface TransactionResponse {
content: Transaction[]; content: Transaction[];
pageNumber: number; pageNumber: number;
totalPages: number; totalPages: number;
@ -54,7 +53,70 @@ const Transactions = () => {
<div> <div>
{filterParam && <TransactionTopNav filterType={filterParam} />} {filterParam && <TransactionTopNav filterType={filterParam} />}
{/* <h2 className="text-cl font-semibold text-gray-800 mb-2.5">Transactions</h2> */} {/* <h2 className="text-cl font-semibold text-gray-800 mb-2.5">Transactions</h2> */}
{/* <TransactionsTable transactionData={transactionData} /> */}
{
transactionData.length > 0 ?
<div className="overflow-x-auto bg-white shadow-md rounded-lg border border-gray-200">
<table className="min-w-full text-sm text-gray-700">
<thead className="bg-gray-100 text-left text-xs uppercase tracking-wider text-gray-600">
<tr>
<th className="px-6 py-3">Name</th>
<th className="px-6 py-3">Created At</th>
<th className="px-6 py-3">Status</th>
<th className="px-6 py-3">Documents</th>
<th className="px-6 py-3">Recipients</th>
</tr>
</thead>
<tbody>
{transactionData.map((tx) => (
<tr key={tx.transactionId} className="border-t hover:bg-gray-50 transition duration-150">
<td className="px-6 py-4 font-medium text-gray-800">
{tx.name?.trim() || 'N/A'}
</td>
<td className="px-6 py-4">
{new Date(tx.createAt).toLocaleString()}
</td>
<td className="px-6 py-4">
<span className="inline-block px-2 py-1 text-xs font-semibold rounded bg-blue-100 text-blue-800">
{tx.status}
</span>
</td>
<td className="px-6 py-4">
<ul className="list-disc list-inside space-y-1">
{tx.documents.map((doc) => (
<li key={doc.documentId}>{doc.name}</li>
))}
</ul>
</td>
<td className="px-6 py-4 space-y-2">
{tx.recipients.map((rec) => (
<div key={rec.id} className="text-sm">
<div className="font-semibold">
{rec.firstName} {rec.lastName}
</div>
<div className="text-gray-600">Status: {rec.status}</div>
{rec.signedDateTime && (
<div className="text-green-600">
Signed: {new Date(rec.signedDateTime).toLocaleString()}
</div>
)}
</div>
))}
</td>
</tr>
))}
</tbody>
</table>
</div> :
<div className='flex justify-center items-center mt-6 text-slate-400 gap-2'>
<hr className='w-full border-dashed' />
<p className='whitespace-nowrap text-sm'>No Data Found</p>
<hr className='w-full border-dashed' />
</div>
}
</div> </div>
); );
}; };

View File

@ -1,75 +0,0 @@
import { Transaction } from './Transactions'
interface TransactionsTableInterface {
transactionData: Transaction[]
}
const TransactionsTable : React.FC<TransactionsTableInterface>= ({transactionData}) => {
return (
<>
{
transactionData.length > 0 ?
<div className="overflow-x-auto bg-white shadow-md rounded-lg border border-gray-200">
<table className="min-w-full text-sm text-gray-700">
<thead className="bg-gray-100 text-left text-xs uppercase tracking-wider text-gray-600">
<tr>
<th className="px-6 py-3">Name</th>
<th className="px-6 py-3">Created At</th>
<th className="px-6 py-3">Status</th>
<th className="px-6 py-3">Documents</th>
<th className="px-6 py-3">Recipients</th>
</tr>
</thead>
<tbody>
{transactionData.map((tx) => (
<tr key={tx.transactionId} className="border-t hover:bg-gray-50 transition duration-150">
<td className="px-6 py-4 font-medium text-gray-800">
{tx.name?.trim() || 'N/A'}
</td>
<td className="px-6 py-4">
{new Date(tx.createAt).toLocaleString()}
</td>
<td className="px-6 py-4">
<span className="inline-block px-2 py-1 text-xs font-semibold rounded bg-blue-100 text-blue-800">
{tx.status}
</span>
</td>
<td className="px-6 py-4">
<ul className="list-disc list-inside space-y-1">
{tx.documents.map((doc) => (
<li key={doc.documentId}>{doc.name}</li>
))}
</ul>
</td>
<td className="px-6 py-4 space-y-2">
{tx.recipients.map((rec) => (
<div key={rec.id} className="text-sm">
<div className="font-semibold">
{rec.firstName} {rec.lastName}
</div>
<div className="text-gray-600">Status: {rec.status}</div>
{rec.signedDateTime && (
<div className="text-green-600">
Signed: {new Date(rec.signedDateTime).toLocaleString()}
</div>
)}
</div>
))}
</td>
</tr>
))}
</tbody>
</table>
</div> :
<div className='flex justify-center items-center mt-6 text-slate-400 gap-2'>
<hr className='w-full border-dashed' />
<p className='whitespace-nowrap text-sm'>No Data Found</p>
<hr className='w-full border-dashed' />
</div>
}
</>
)
}
export default TransactionsTable

View File

@ -1,15 +1,15 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--primary: #3D74B6; --primary: #41644A;
--primaryLight: #ffffff; --primaryLight: #ffffff;
--primaryLight2: #6a92c2; --primaryLight2: #4b7455;
--primaryLight3: #dae9de; --primaryLight3: #dae9de;
--dashBoardBg: #ffffff; --dashBoardBg: #ffffff;
--mainViewBg: #edeaeb; --mainViewBg: #edeaeb;
--highlightBg1: #edeaeb; --highlightBg1: #edeaeb;
--menuTextCol: #98a0a8; --menuTextCol: #98a0a8;
--menuTextColActive: #3D74B6; --menuTextColActive: #41644A;
--menuBgColActive: #edeaeb; --menuBgColActive: #edeaeb;
--menuTextHoverCol: #e7e7e7; --menuTextHoverCol: #e7e7e7;
@ -17,7 +17,7 @@
--lightGreyCol: #e1e1e1; --lightGreyCol: #e1e1e1;
--compBgCol: #ffffff; --compBgCol: #ffffff;
--avatarBgCol: #3D74B6; --avatarBgCol: #41644A;
--avatarTextCol: #ffffff; --avatarTextCol: #ffffff;

View File

@ -240,6 +240,8 @@ export const uploadDocuments = createAsyncThunk<
console.log(`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`) console.log(`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`)
const response = await api.post( const response = await api.post(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`, `/api/accounts/${accountId}/${toggle}/${transactionId}/documents`,
formData, formData,