jai #18

Merged
jaipandey merged 2 commits from jai into main 2025-08-14 12:06:06 +00:00
24 changed files with 970 additions and 763 deletions

BIN
src/assets/SMELogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -117,7 +117,7 @@ const CommonNav: React.FC<AccountNavProps> = () => {
id: account.userId, id: account.userId,
name: account.name, name: account.name,
email: account.userEmail, email: account.userEmail,
language: "", // set it if available language: account.language, // set it if available
account: { account: {
id: account.accountId, id: account.accountId,
isDefault: false, // update as needed isDefault: false, // update as needed
@ -129,7 +129,7 @@ const CommonNav: React.FC<AccountNavProps> = () => {
id: account.userId, id: account.userId,
name: account.name, name: account.name,
email: account.userEmail, email: account.userEmail,
language: "", // set it if available language: account.language, // set it if available
account: { account: {
id: account.accountId, id: account.accountId,
isDefault: false, // update as needed isDefault: false, // update as needed
@ -154,7 +154,7 @@ const CommonNav: React.FC<AccountNavProps> = () => {
id: item?.id, id: item?.id,
name: item?.firstName + " " + item?.lastName, name: item?.firstName + " " + item?.lastName,
email: item?.email, email: item?.email,
language: "", language: item?.language,
account: { account: {
id: item?.account.id, id: item?.account.id,
isDefault: false, isDefault: false,
@ -263,7 +263,7 @@ const CommonNav: React.FC<AccountNavProps> = () => {
); );
return ( return (
<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-1.5 flex justify-between items-center bg-white border-b 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">Home</span>

View File

@ -3,21 +3,11 @@ import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import AccountNav from "./AccountNav"; import AccountNav from "./AccountNav";
import api from "../../config/api"; import api from "../../config/api";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { FaCheck } from "react-icons/fa"; import { FaCheck, FaClock } from "react-icons/fa";
import Cards from "../home/Cards"; import Cards from "../home/Cards";
import AddInviteModal from "./AddInviteModal"; import AddInviteModal from "./AddInviteModal";
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
import { import { FiFileText, FiClock, FiAlertTriangle, FiCheckCircle } from "react-icons/fi";
FaCheckCircle,
FaClock,
FaHourglassHalf,
FaTasks,
FaChevronDown,
FaPlus,
FaUserPlus,
FaFilter,
FaCalendarAlt
} from 'react-icons/fa';
import TransactionDataTable from "./TransactionDataTable"; import TransactionDataTable from "./TransactionDataTable";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../store/store"; import { RootState } from "../../store/store";
@ -35,34 +25,40 @@ const Account = () => {
const mapApiDataToCards = (data: any): CardData[] => { const mapApiDataToCards = (data: any): CardData[] => {
return [ return [
{ {
title: 'Action Needed', title: "Action Required",
count: data.actionNeeded || 0, count: data.actionNeeded || 0,
icon: <FaTasks size={18} className="text-indigo-600" />, icon: <FiFileText className="w-5 h-5 text-sky-500" />,
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100', iconBg: "bg-sky-100 border-sky-200",
shadowColor: 'shadow-indigo-200/50', textColor: "text-sky-600",
iconBg: 'bg-indigo-100 border-indigo-200', gradient: "bg-white",
textColor: 'text-indigo-700', shadowColor: "shadow-sky-100",
trend: 'up'
}, },
{ {
title: 'Expiring Soon', title: "Waiting for Response",
count: data.waitingForOthers || 0,
icon: <FiClock className="w-5 h-5 text-yellow-500" />,
iconBg: "bg-yellow-100 border-yellow-200",
textColor: "text-yellow-600",
gradient: "bg-white",
shadowColor: "shadow-yellow-100",
},
{
title: "Expiring Soon",
count: data.expiring || 0, count: data.expiring || 0,
icon: <FaHourglassHalf size={18} className="text-rose-600" />, icon: <FiAlertTriangle className="w-5 h-5 text-red-500" />,
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100', iconBg: "bg-red-100 border-red-200",
shadowColor: 'shadow-rose-200/50', textColor: "text-red-600",
iconBg: 'bg-rose-100 border-rose-200', gradient: "bg-white",
textColor: 'text-rose-700', shadowColor: "shadow-red-100",
trend: 'up'
}, },
{ {
title: 'Completed', title: "Completed",
count: data.completed || 0, count: data.completed || 0,
icon: <FaCheckCircle size={18} className="text-teal-600" />, icon: <FiCheckCircle className="w-5 h-5 text-green-500" />,
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100', iconBg: "bg-green-100 border-green-200",
shadowColor: 'shadow-teal-200/50', textColor: "text-green-600",
iconBg: 'bg-teal-100 border-teal-200', gradient: "bg-white",
textColor: 'text-teal-700', shadowColor: "shadow-green-100",
trend: 'up'
}, },
// { // {
// title: 'Waiting for Others', // title: 'Waiting for Others',
@ -94,16 +90,16 @@ const Account = () => {
// textColor: 'text-green-700', // textColor: 'text-green-700',
// trend: 'down' // trend: 'down'
// }, // },
{ // {
title: 'draft', // title: 'draft',
count: data.draft || 0, // count: data.draft || 0,
icon: <FaClock size={18} className="text-stone-600" />, // icon: <FaClock size={18} className="text-stone-600" />,
gradient: 'bg-gradient-to-br from-stone-50 to-stone-100', // gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
shadowColor: 'shadow-stone-200/50', // shadowColor: 'shadow-stone-200/50',
iconBg: 'bg-stone-100 border-stone-200', // iconBg: 'bg-stone-100 border-stone-200',
textColor: 'text-stone-700', // textColor: 'text-stone-700',
trend: 'down' // trend: 'down'
} // }
// Add more cards here if needed // Add more cards here if needed
]; ];
}; };
@ -252,8 +248,9 @@ const Account = () => {
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards} <Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
isLoading={isLoading} /> isLoading={isLoading} />
<section className="my-4 mx-3">
<TransactionDataTable /> <TransactionDataTable />
</section>
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading} {isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}
accountOptions={accountsData} />} accountOptions={accountsData} />}

View File

@ -2,8 +2,10 @@ import React, { useEffect, useState } from 'react'
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import api from '../../config/api'; import api from '../../config/api';
import { Transaction } from '../transactions/Transactions'; import { Transaction } from '../transactions/Transactions';
import { useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { FaCheck } from 'react-icons/fa'; import { FaCheck } from 'react-icons/fa';
import LoaderUi from '../../helper/LoaderUi';
import logoRecord from '../../assets/record.svg';
@ -12,21 +14,36 @@ const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString(); return new Date(timestamp * 1000).toLocaleString();
}; };
const statusColorMap = {
CREATED: "bg-blue-100 text-blue-600",
SENT: "bg-green-100 text-green-600",
COMPLETED: "bg-purple-100 text-purple-600",
EXPIRED: "bg-red-100 text-red-600",
};
const TransactionDataTable = () => { const TransactionDataTable = () => {
const navigate = useNavigate() const navigate = useNavigate()
const loaction = useLocation()
console.log("loaction", loaction.pathname);
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null'); const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]); const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [activeAccountId, setActiveAccountId] = useState<string | null>( const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null') JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
); );
const [loading, setLoading] = useState(false)
const fetchTransactions = async () => { const fetchTransactions = async () => {
console.log("fetchTransactions") console.log("fetchTransactions")
setLoading(true)
try { try {
const response = await api.get(`/api/accounts/${activeAccountId}/transactions?sortBy=lastModifiedAt desc`); const response = await api.get(`/api/accounts/${activeAccountId}/transactions?sortBy=lastModifiedAt desc`);
setTransactions(response.data || []); setTransactions(response.data || []);
} catch (error) { } catch (error) {
console.error("Error fetching transactions:", error); console.error("Error fetching transactions:", error);
} finally {
setLoading(false)
} }
}; };
@ -64,72 +81,141 @@ const TransactionDataTable = () => {
} }
} }
function getStatusColorClass(status) {
return statusColorMap[status] || "bg-gray-100 text-gray-600";
}
return ( return (
<> <>
{transactions.length > 0 && (
<div className="overflow-x-auto py-2"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm"> {/* Header */}
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider"> <div className="flex justify-between items-center px-6 py-4 border-b border-gray-100">
<tr> <h2 className="text-sm font-semibold text-gray-800">Recent Transactions</h2>
<th className="px-4 py-3 text-left"></th> </div>
<th className="px-4 py-3 text-center">Name</th>
<th className="px-4 py-3 text-center">Status</th> {/* Scrollable Table Container */}
<th className="px-4 py-3 text-center">Active</th> <div className="overflow-auto max-h-[67vh] no-scrollbar">
<th className="px-4 py-3 text-center">Last Change</th> <table className="w-full text-sm">
<th className="px-4 py-3 text-center">Expire After (days)</th> {/* Sticky Header */}
<th className="px-4 py-3 text-center">Sign</th> <thead className="bg-gray-50 sticky top-0 z-10">
<th className="px-4 py-3 text-center">Action</th> <tr className="text-gray-600 text-xs uppercase tracking-wider border-b border-gray-200">
<th className="px-6 py-4 text-left font-semibold">Document</th>
<th className="px-6 py-4 text-center font-semibold">Status</th>
{location.pathname === '/transactions' && (
<>
<th className="px-6 py-4 text-center font-semibold">Signed By</th>
<th className="px-6 py-4 text-center font-semibold">Last Change</th>
<th className="px-6 py-4 text-center font-semibold">Expire After (Days)</th>
<th className="px-6 py-4 text-center font-semibold">Actions</th>
</>
)}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> {loading ? (
{transactions.map((tx) => { <tbody>
const lastModifiedAt = new Date(tx.lastModifiedAt, "DD/MM/YYYY"); <tr>
return ( <td colSpan="6" className="relative h-100">
<tr <LoaderUi />
key={tx.id} </td>
onClick={() => handleTransactionClick(tx.id)} </tr>
className="hover:bg-gray-50 cursor-pointer" </tbody>
> ) : transactions.length > 0 ? (
<td className="px-4 py-3"> <>
<div className="w-5 h-5 rounded border border-slate-300 flex justify-center items-center"> {/* Scrollable Body */}
{selectedTransactions.includes(tx.id) && ( <tbody className="bg-white divide-y divide-gray-100">
<FaCheck className="text-purple-600 text-sm" /> {transactions.map((tx) => (
)} <tr
</div> key={tx.id}
</td> onClick={() => handleTransactionClick(tx.id)}
<td className="px-4 py-3 text-center"> className="hover:bg-gray-50 transition-colors duration-150 cursor-pointer"
{tx.subject || "Untitled"} >
</td> <td className="px-6 py-4 text-left">
<td className="px-4 py-3 text-center"> <div className="text-gray-800 font-medium truncate max-w-xs">
<span className="px-2 py-0.5 rounded-xl text-xs font-medium bg-blue-100 text-blue-700"> {tx?.subject || "Untitled"}
{tx.status} </div>
</span> </td>
</td> <td className="px-6 py-4 text-center">
<td className="px-4 py-3 text-center"> <span
{tx.active ? "Yes" : "No"} className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${getStatusColorClass(
</td> tx.status
)}`}
>
{tx.status}
</span>
</td>
<td className="px-4 py-3 text-center"> {location.pathname === '/transactions' && (
{formatTimestamp(tx.lastModifiedAt)} <>
</td> <td className="px-6 py-4">
<td className="px-4 py-3 text-center"> <div className="flex items-center justify-center">
{tx.expireAfter ?? "-"} {tx.signedBy ? (
</td> <div className="flex items-center gap-2">
<td className="px-4 py-3 text-center "> <div
{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> : "-"} className="w-7 h-7 rounded-full flex items-center justify-center text-xs font-medium text-white flex-shrink-0"
</td> style={{ backgroundColor: "#d8b4fe" }}
<td className="px-4 py-3 text-center"> >
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" /> {tx.signedByInitials}
</td> </div>
</tr> <span className="text-gray-700 truncate max-w-[120px]">
) {tx.signedBy}
})} </span>
</tbody> </div>
) : (
<span className="text-gray-400">-</span>
)}
</div>
</td>
<td className="px-6 py-4 text-center">
<span className="text-gray-600 whitespace-nowrap">
{formatTimestamp(tx.lastModifiedAt)}
</span>
</td>
<td className="px-6 py-4 text-center">
<span className="text-gray-700">
{tx.expireAfter ?? "-"}
</span>
</td>
<td className="px-6 py-4 text-center">
<button
className="text-gray-400 hover:text-gray-600 transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
onClick={(e) => {
e.stopPropagation();
// Handle actions menu
}}
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button>
</td>
</>
)}
</tr>
))}
</tbody>
</>) : (
<tbody>
<tr>
<td colSpan="6" className="text-center py-10 text-gray-500">
<div className="flex flex-col items-center justify-center">
<img
src={logoRecord}
alt="Record Not Found"
className="w-70 h-70 mb-4 opacity-70"
/>
</div>
</td>
</tr>
</tbody>
)}
</table> </table>
</div> </div>
)} </div>
</> </>
) )
} }

View File

@ -126,7 +126,7 @@ const Contacts = () => {
setShowModal((prev) => !prev); setShowModal((prev) => !prev);
setActionStatus("Add") setActionStatus("Add")
}} }}
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 "> className="flex items-center gap-2 bg-[var(--mainPrimaryBtn)] px-3 py-1.5 rounded-sm hover:opacity-90 ">
<IoMdAdd className="text-white" /> <IoMdAdd className="text-white" />
<button <button
className="text-white rounded text-sm hover:opacity-90 cursor-pointer"> className="text-white rounded text-sm hover:opacity-90 cursor-pointer">
@ -134,20 +134,22 @@ 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 mx-2 max-h-[79vh] rounded-xl shadow-sm border border-gray-100 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="text-gray-500 uppercase text-xs tracking-wider sticky top-0 z-10">
<tr> <tr>
<th className="px-4 py-3 text-left">First Name</th> <th className="px-6 py-3 font-medium text-left">First Name</th>
<th className="px-4 py-3 text-left">Last Name</th> <th className="px-6 py-3 font-medium text-left">Last Name</th>
<th className="px-4 py-3 text-left">Email</th> <th className="px-6 py-3 font-medium text-left">Email</th>
<th className="px-4 py-3 text-center">Actions</th> <th className="px-6 py-3 font-medium text-center">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-100">
{loading ? ( {loading ? (
<tr> <tr>
<td colSpan="4" className="relative h-100"><LoaderUi /></td> <td colSpan="4" className="relative h-100">
<LoaderUi />
</td>
</tr> </tr>
) : contacts.length === 0 ? ( ) : contacts.length === 0 ? (
<tr> <tr>
@ -156,32 +158,40 @@ const Contacts = () => {
<img <img
src={logoRecord} src={logoRecord}
alt="Record Not Found" alt="Record Not Found"
className="w-70 h-70 mb-4 opacity-70" // adjust size and appearance className="w-70 h-70 mb-4 opacity-70"
/> />
</div> </div>
</td> </td>
</tr> </tr>
) : ( ) : (
contacts.map((contact, index) => ( contacts.map((contact, index) => (
<tr key={contact.id} className="hover:bg-gray-50"> <tr
<td className="px-4 py-3">{contact.firstName}</td> key={contact.id}
<td className="px-4 py-3">{contact.lastName}</td> className="hover:bg-gray-50 transition-colors"
<td className="px-4 py-3">{contact.email}</td> >
<td className="px-4 py-3 text-center"> <td className="px-6 py-4 text-gray-800">{contact.firstName}</td>
<td className="px-6 py-4 text-gray-800">{contact.lastName}</td>
<td className="px-6 py-4 text-gray-800">{contact.email}</td>
<td className="px-6 py-4 text-center">
<div <div
className="relative inline-block" className="relative inline-block"
tabIndex={0} tabIndex={0}
onBlur={handleBlur} onBlur={handleBlur}
> >
<button <button
className="text-gray-600 hover:text-black p-1 focus:outline-none" className="text-gray-400 hover:text-gray-600 p-1 focus:outline-none"
onClick={() => toggleMenu(contact.id)} onClick={() => toggleMenu(contact.id)}
> >
<FaEllipsisV /> <FaEllipsisV />
</button> </button>
{openMenuId === contact.id && ( {openMenuId === contact.id && (
<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"}`}> <div
className={`absolute mt-1 bg-white border border-gray-200 rounded-lg 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"
@ -203,11 +213,18 @@ const Contacts = () => {
)} )}
</tbody> </tbody>
</table> </table>
{showModal &&
<ContactFormModal onSubmit={handleContactSubmit} {showModal && (
loading={createAndInviteLoader} setShowModal={setShowModal} actionStatus={actionStatus} editJsonData={editJsonData} /> <ContactFormModal
} onSubmit={handleContactSubmit}
loading={createAndInviteLoader}
setShowModal={setShowModal}
actionStatus={actionStatus}
editJsonData={editJsonData}
/>
)}
</div> </div>
</div> </div>
</> </>
); );

View File

@ -171,9 +171,9 @@ function Dashboard() {
<SideBar showSideBar={showSideBar} setShowSideBar={setShowSideBar} /> <SideBar showSideBar={showSideBar} setShowSideBar={setShowSideBar} />
{/* On Top Main Content - START */} {/* On Top Main Content - START */}
<div className={`overflow-y-auto absolute ${showSideBar ? 'w-[calc(100%-250px)]' : 'w-[calc(100%-60px)]'} h-full right-0 bottom-0 overflow-hidden bg-[var(--mainViewBg)] transition-all duration-300`}> <div className={`overflow-y-auto absolute no-scrollbar ${showSideBar ? 'w-[calc(100%-250px)]' : 'w-[calc(100%-60px)]'} h-full right-0 bottom-0 overflow-hidden bg-[var(--mainViewBg)] transition-all duration-300`}>
{/* Main Content */} {/* Main Content */}
<div className='px-2 py-1.5'> <div className=''>
<CommonNav /> <CommonNav />
<Outlet /> <Outlet />
</div> </div>

View File

@ -22,8 +22,10 @@ interface NavLinkProps {
// } // }
const NavLink: React.FC<NavLinkProps> = ({ to, icon, label, showSideBar, menuPathMap }) => { const NavLink: React.FC<NavLinkProps> = ({ to, icon, label, showSideBar, menuPathMap }) => {
const location = useLocation(); const location = useLocation();
const isActive = menuPathMap[location.pathname] === label; const isActive = menuPathMap[location.pathname] === label;
console.log('Line 26' , to, icon, label, showSideBar, menuPathMap , "Check" , menuPathMap[location.pathname] === label)
// const [transactionFilter, setTransationFilter] = useState<TransactionFilterEnum>(TransactionFilterEnum.ALL) // const [transactionFilter, setTransationFilter] = useState<TransactionFilterEnum>(TransactionFilterEnum.ALL)
@ -32,7 +34,7 @@ const NavLink: React.FC<NavLinkProps> = ({ to, icon, label, showSideBar, menuPat
<Link <Link
to={to} to={to}
className={`${isActive ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} className={`${isActive ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--sidebarMenuCol)]'}
p-2 rounded-md flex justify-left gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`} p-2 rounded-md flex justify-left gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
> >
{icon} {icon}

View File

@ -12,6 +12,7 @@ import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } f
import { SignTypeEnum } from '../../utlity/types'; import { SignTypeEnum } from '../../utlity/types';
// import { IoMdArrowDropright } from "react-icons/io"; // import { IoMdArrowDropright } from "react-icons/io";
interface Props { interface Props {
showSideBar: boolean; showSideBar: boolean;
} }
@ -84,7 +85,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
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(--sidebarDropDownBg)] rounded text-[var(--sendForSignTextCol)] ${showSideBar ? "" : "pt-[0.4rem] pr-[0.6rem] pb-[0.4rem] pl-[0.2rem]"}`}>
<div <div
onClick={() => handleStartSectionClick(SignTypeEnum.multipleSign)} onClick={() => handleStartSectionClick(SignTypeEnum.multipleSign)}
className={`w-full`}> className={`w-full`}>
@ -98,7 +99,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
</div> </div>
{showSideBar && {showSideBar &&
<p <p
className='border-l-1 border-[var(--sendForSignTextCol)] px-2 py-2'> className=' border-[var(--sendForSignTextCol)] px-2 py-2'>
<IoMdArrowDropdown <IoMdArrowDropdown
onClick={() => setSendForSignDropdown(!sendForSignDropdown)} onClick={() => setSendForSignDropdown(!sendForSignDropdown)}
className={`text-2xl ${sendForSignDropdown ? 'rotate-180' : ''} transition-all duration-150`} /> className={`text-2xl ${sendForSignDropdown ? 'rotate-180' : ''} transition-all duration-150`} />

View File

@ -11,6 +11,7 @@ import { IoSettingsOutline } from "react-icons/io5";
import axios from 'axios'; import axios from 'axios';
import { MdManageAccounts } from 'react-icons/md'; import { MdManageAccounts } from 'react-icons/md';
import { GrContactInfo, GrTransaction } from 'react-icons/gr'; import { GrContactInfo, GrTransaction } from 'react-icons/gr';
import logoSME from '../../assets/SMELogo.png'
type SideBarProps = { type SideBarProps = {
showSideBar: boolean; showSideBar: boolean;
@ -22,11 +23,11 @@ type MenuPathMap = {
} }
const menuPathMap: MenuPathMap = { const menuPathMap: MenuPathMap = {
'/': 'Home', '/': 'Home',
'/templates': 'Templates', '/templates': 'Templates',
'/s/contact': 'Contact', // '/accounts': 'Accounts',
// '/accounts': 'Accounts', '/transactions': 'Transactions',
'transactions' : "Account" '/contacts': 'Contacts'
}; };
const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => { const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
@ -53,21 +54,21 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
return ( return (
<> <>
{/* Side Bar */} {/* Side Bar */}
<div className={`relative h-screen ${showSideBar ? 'w-[250px]' : 'w-[60px]'} transition-all duration-300 bg-white shadow-md`}> <div className={`relative h-screen ${showSideBar ? 'w-[250px]' : 'w-[60px]'} transition-all duration-300 bg-[var(--sidebarBg)] shadow-md`}>
{/* Menu */} {/* Menu */}
<div className={`absolute top-0 ${showSideBar ? 'p-4' : 'py-4'} w-full max-h-max ${showSideBar ? '' : 'text-center'}`}> <div className={`absolute top-0 ${showSideBar ? 'p-4' : 'py-4'} w-full max-h-max ${showSideBar ? '' : 'text-center'}`}>
<h1 className='w-full text-lg text-center tracking-widest font-medium text-gray-800'> <h1 className='w-full text-lg text-center tracking-widest font-medium text-gray-800'>
{showSideBar ? 'Sign Made Easy' : 'SME'} {showSideBar ? <img src={logoSME} loading='lazy' className='h-7 ms-6' /> : <p className='text-white'>SME</p>}
</h1> </h1>
<div <div
id="nav-link-dash" id="nav-link-dash"
className={`flex flex-col mt-4 ${showSideBar ? '' : 'items-center'}`}> className={`flex flex-col mt-4 ${showSideBar ? '' : 'items-center'}`}>
<SendForSign showSideBar={showSideBar} /> <SendForSign showSideBar={showSideBar} />
<NavLink to="/" icon={<FaHome />} label="Home" showSideBar={showSideBar} menuPathMap={menuPathMap} /> <NavLink to="/" icon={<FaHome />} label="Home" showSideBar={showSideBar} menuPathMap={menuPathMap} />
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} /> <NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
{/* <NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */} {/* <NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */}
<NavLink to="transactions" icon={<GrTransaction />} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} /> <NavLink to="/transactions" icon={<GrTransaction />} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} />
<NavLink to="/contacts" icon={<GrContactInfo />} label="Contacts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> <NavLink to="/contacts" icon={<GrContactInfo />} label="Contacts" showSideBar={showSideBar} menuPathMap={menuPathMap} />
</div> </div>
</div> </div>
@ -75,10 +76,10 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
{/* SideBar Toggle Button */} {/* SideBar Toggle Button */}
<div <div
onClick={() => setShowSideBar(!showSideBar)} onClick={() => setShowSideBar(!showSideBar)}
className='absolute top-[100%] translate-y-[-100%] right-0 bg-[var(--highlightBg1)] p-2 rounded-tl-sm rounded-bl-sm cursor-pointer transition-colors' className='absolute top-[100%] translate-y-[-100%] right-0 bg-[var(--dashBoardBg)] p-2 rounded-tl-sm rounded-bl-sm cursor-pointer transition-colors'
> >
<div className='absolute top-[-14px] right-0 w-[14px] h-[14px] rounded-full bg-white shadow-[5px_5px_0px_0px_rgba(237,234,235,1)]'></div> <div className='absolute top-[-14px] right-0 w-[14px] h-[14px] rounded-full shadow-[5px_5px_0px_0px_rgba(237,234,235,1)]'></div>
<div className='absolute bottom-[-14px] right-0 w-[14px] h-[14px] rounded-full bg-white shadow-[5px_-5px_0px_0px_rgba(237,234,235,1)]'></div> <div className='absolute bottom-[-14px] right-0 w-[14px] h-[14px] rounded-full shadow-[5px_-5px_0px_0px_rgba(237,234,235,1)]'></div>
{showSideBar ? ( {showSideBar ? (
<BsLayoutSidebarInsetReverse className='fill-[var(--primary)] hover:text-[var(--primary)] transition-all text-2xl' /> <BsLayoutSidebarInsetReverse className='fill-[var(--primary)] hover:text-[var(--primary)] transition-all text-2xl' />
) : ( ) : (
@ -90,7 +91,7 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
<div className='absolute bottom-14 right-0 flex flex-col gap-1'> <div className='absolute bottom-14 right-0 flex flex-col gap-1'>
<button <button
onClick={() => setShowLogoutModal(true)} onClick={() => setShowLogoutModal(true)}
className='flex items-center gap-2 px-3 py-2 text-gray-600 hover:bg-red-50 hover:text-red-600 rounded-l-sm transition-all duration-200 group' className='flex items-center gap-2 px-3 py-2 text-[var(--sidebarMenuCol)] hover:bg-red-50 hover:text-red-600 rounded-l-sm transition-all duration-200 group'
title="Log out" title="Log out"
> >
<RiLogoutBoxLine className="text-xl group-hover:scale-110 transition-transform" /> <RiLogoutBoxLine className="text-xl group-hover:scale-110 transition-transform" />
@ -99,7 +100,7 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
<Link <Link
to="/s/settings" to="/s/settings"
className='flex items-center gap-2 px-3 py-2 text-gray-600 hover:bg-blue-50 hover:text-blue-600 rounded-l-sm transition-all duration-200 group' className='flex items-center gap-2 px-3 py-2 text-[var(--sidebarMenuCol)] hover:bg-blue-50 hover:text-blue-600 rounded-l-sm transition-all duration-200 group'
title="Settings" title="Settings"
> >
<IoSettingsOutline className="text-xl group-hover:scale-110 transition-transform" /> <IoSettingsOutline className="text-xl group-hover:scale-110 transition-transform" />

View File

@ -204,74 +204,47 @@ const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRa
// } // }
return ( return (
<div className="w-full px-3 rounded-md"> <div className="w-full">
{/* Header */} {/* Cards */}
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-2 gap-4"> {isLoading ? (
<div className="flex items-center gap-3"> <div className="w-full h-[10vh] px-6 py-3">
{/* <div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg"> <div className="animate-pulse">
<FaFilter className="text-white text-lg" /> {/* <div className="h-8 bg-gray-200 rounded w-32 mb-8"></div> */}
</div> */} <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<div> {[...Array(4)].map((_, i) => (
{/* <h2 className="text-lg font-semibold text-gray-800">Overview</h2> */} <div key={i} className="h-10 bg-gray-200 border border-gray-100"></div>
{/* <p className="text-gray-600 text-sm">Monitor performance metrics</p> */}
</div>
</div>
</div>
{/* Cards */}
{isLoading ? <div className="w-full [h-50vh] px-6 py-8">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-32 mb-8"></div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(7)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
))}
</div>
</div>
</div> :
<div className="flex flex-wrap gap-2 justify-end">
{cards.map((card, index) => (
<div
key={card.title}
className={`group relative overflow-hidden rounded-lg ${card.gradient} border border-white/40 px-3 py-1 transition-all duration-300 hover:scale-105 hover:shadow-lg ${card.shadowColor} cursor-pointer backdrop-blur-sm min-w-[200px] max-w-[220px]`}
style={{
animationDelay: `${index * 100}ms`
}}
>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute -top-6 -right-6 w-12 h-12 rounded-full bg-current"></div>
<div className="absolute -bottom-6 -left-6 w-10 h-10 rounded-full bg-current"></div>
</div>
{/* Content */}
<div className="relative flex justify-between items-center z-10">
<div className="flex items-center gap-2">
<div className={`p-2 rounded-lg border ${card.iconBg} group-hover:scale-110 transition-transform duration-200`}>
{card.icon}
</div>
<h3 className="text-sm font-medium text-gray-600 group-hover:text-gray-700 transition-colors">
{card.title}
</h3>
</div>
<div className="flex items-center">
<p className={`text-xl font-bold ${card.textColor} group-hover:scale-105 transition-transform duration-200 leading-none mb-1`}>
{card.count}
</p>
</div>
</div>
{/* Hover Effect */}
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</div>
))} ))}
</div> </div>
</div>
}
</div> </div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{cards.map((card, index) => (
<div
key={card.title}
className="flex items-center justify-between border border-gray-200 bg-white px-4 py-1.5 hover:shadow-md transition-all duration-300 cursor-pointer"
style={{
animationDelay: `${index * 100}ms`,
}}
>
{/* Left side: Icon + Title */}
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${card.iconBg}`}>
{card.icon}
</div>
<h3 className="text-xs font-medium text-gray-600">{card.title}</h3>
</div>
{/* Count */}
<p className={`text-sm font-bold ${card.textColor}`}>
{card.count}
</p>
</div>
))}
</div>
)}
</div>
); );
}; };

View File

@ -3,17 +3,7 @@ import AccountNav from '../accounts/AccountNav';
import Cards from './Cards'; import Cards from './Cards';
import StartSection from './StartSection'; import StartSection from './StartSection';
import api from '../../config/api'; import api from '../../config/api';
import { import { FiFileText, FiClock, FiAlertTriangle, FiCheckCircle } from "react-icons/fi";
FaCheckCircle,
FaClock,
FaHourglassHalf,
FaTasks,
FaChevronDown,
FaPlus,
FaUserPlus,
FaFilter,
FaCalendarAlt
} from 'react-icons/fa';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store/store'; import { RootState } from '../../store/store';
@ -33,44 +23,40 @@ const Home = () => {
const mapApiDataToCards = (data: any): CardData[] => { const mapApiDataToCards = (data: any): CardData[] => {
return [ return [
{ {
title: 'Action Needed', title: "Action Required",
count: data.actionNeeded || 0, count: data.actionNeeded || 0,
icon: <FaTasks size={15} className="text-indigo-600" />, icon: <FiFileText className="w-5 h-5 text-sky-500" />,
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100', iconBg: "bg-sky-100 border-sky-200",
shadowColor: 'shadow-indigo-200/50', textColor: "text-sky-600",
iconBg: 'bg-indigo-100 border-indigo-200', gradient: "bg-white",
textColor: 'text-indigo-700', shadowColor: "shadow-sky-100",
trend: 'up'
}, },
{ {
title: 'Expiring Soon', title: "Waiting for Response",
count: data.waitingForOthers || 0,
icon: <FiClock className="w-5 h-5 text-yellow-500" />,
iconBg: "bg-yellow-100 border-yellow-200",
textColor: "text-yellow-600",
gradient: "bg-white",
shadowColor: "shadow-yellow-100",
},
{
title: "Expiring Soon",
count: data.expiring || 0, count: data.expiring || 0,
icon: <FaHourglassHalf size={15} className="text-rose-600" />, icon: <FiAlertTriangle className="w-5 h-5 text-red-500" />,
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100', iconBg: "bg-red-100 border-red-200",
shadowColor: 'shadow-rose-200/50', textColor: "text-red-600",
iconBg: 'bg-rose-100 border-rose-200', gradient: "bg-white",
textColor: 'text-rose-700', shadowColor: "shadow-red-100",
trend: 'up'
}, },
{ {
title: 'Completed', title: "Completed",
count: data.completed || 0, count: data.completed || 0,
icon: <FaCheckCircle size={15} className="text-teal-600" />, icon: <FiCheckCircle className="w-5 h-5 text-green-500" />,
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100', iconBg: "bg-green-100 border-green-200",
shadowColor: 'shadow-teal-200/50', textColor: "text-green-600",
iconBg: 'bg-teal-100 border-teal-200', gradient: "bg-white",
textColor: 'text-teal-700', shadowColor: "shadow-green-100",
trend: 'up'
},
{
title: 'Draft',
count: data.draft || 0,
icon: <FaClock size={15} className="text-stone-600" />,
gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
shadowColor: 'shadow-blue-200/50',
iconBg: 'bg-blue-100 border-blue-200',
textColor: 'text-blue-700',
trend: 'down'
}, },
]; ];
}; };
@ -84,14 +70,14 @@ const Home = () => {
const [cards, setCards] = useState<CardData[]>([]); const [cards, setCards] = useState<CardData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const userData = useSelector((state : RootState) => state.user); const userData = useSelector((state: RootState) => state.user);
console.log("activeAccountId", activeAccountId); console.log("activeAccountId", activeAccountId);
console.log("cards", cards); console.log("cards", cards);
const getDashboardData = async () => { const getDashboardData = async () => {
if (!activeAccountId) return; if (!activeAccountId) return;
setIsLoading(true); setIsLoading(true);
const payload = { const payload = {
account: activeAccountId account: activeAccountId

View File

@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { RootState } from '../../store/store'; import { RootState } from '../../store/store';
import api from '../../config/api'; import api from '../../config/api';
const StartNow = () => { const StartNow = () => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
@ -51,7 +52,7 @@ const StartNow = () => {
<div <div
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
id='start-now-upload-con' id='start-now-upload-con'
className="group col-span-2 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"> className="group col-span-1 cursor-pointer border-2 border-dashed border-slate-300 rounded-lg bg-[var(--documentUploadBg)] 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 */} {/* Upload icon with subtle animation */}
<div className="p-2 text-blue-600 group-hover:text-blue-700 transition-colors"> <div className="p-2 text-blue-600 group-hover:text-blue-700 transition-colors">
<FiUpload className="text-lg" /> <FiUpload className="text-lg" />

View File

@ -10,6 +10,7 @@ import { Transaction, TransactionResponse } from '../transactions/Transactions';
import TransactionsTable from '../transactions/TransactionsTable'; import TransactionsTable from '../transactions/TransactionsTable';
import TransactionDataTable from '../accounts/TransactionDataTable'; import TransactionDataTable from '../accounts/TransactionDataTable';
import StartNow from './StartNow'; import StartNow from './StartNow';
import TemplateTable from '../templates/TemplateTable';
export interface StartCard { export interface StartCard {
@ -81,31 +82,30 @@ const StartSection = () => {
// }, []) // }, [])
return ( return (
<section className="px-4 pt-2 pb-3 w-full"> <section className="px-3 py-2 w-full mt-2">
<h2 className="text-lg font-semibold text-gray-800 mb-2">Get Started</h2> <h2 className="text-sm font-semibold text-gray-800 px-1 pb-2">Upload Document</h2>
<div className="grid grid-cols-3 gap-4 h-[12vw]"> <div className="grid grid-cols-1 gap-4 h-[12vw] mx-1">
<StartNow /> <StartNow />
<div className='col-span-1 grid grid-cols-1 justify-between gap-4'> {/* <div className='col-span-1 grid grid-cols-1 justify-between gap-4 bg-white rounded-xl'>
{cards.map((card, index) => ( {cards.map((card, index) => (
<div <div
onClick={() => handleStartSectionClick(card.link)} onClick={() => handleStartSectionClick(card.link)}
key={index} key={index}
className={`${card.bg} w-full cursor-pointer relative border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all flex flex-col justify-between`} className={`${card.bg} w-full cursor-pointer relative border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all flex flex-col justify-between`}
> >
{/* Icon + Text */}
<div className="flex flex-col gap-3"> <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"> <div className="w-9 h-9 flex items-center justify-center border border-gray-200 rounded-md bg-white">
{card.icon} {card.icon}
</div> </div>
<h3 className="text-base font-medium text-gray-900">{card.title}</h3> <h3 className="text-sm font-medium text-gray-500">{card.title}</h3>
<p className="text-sm text-gray-700">{card.description}</p> <p className="text-xs text-gray-700">{card.description}</p>
</div> </div>
{/* Bottom Area */}
<div className="flex items-center justify-between mt-2"> <div className="flex items-center justify-between mt-2">
{card.buttonText ? ( {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"> <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">
@ -119,16 +119,21 @@ const StartSection = () => {
</div> </div>
</div> </div>
))} ))}
</div> </div> */}
</div> </div>
<section className='pt-2 pb-3'> {/* <section className='pt-2 pb-3'> */}
<h2 className="mt-6 text-lg font-semibold text-gray-800 mb-2">Recent Activities</h2> {/* <h2 className="mt-6 text-sm font-semibold text-gray-800 mb-2">Recent Activities</h2> */}
<section> <div className='grid grid-cols-12 gap-2 mt-5'>
<section className=' col-span-6'>
<TransactionDataTable /> <TransactionDataTable />
</section> </section>
</section> <section className=' col-span-6'>
<TemplateTable />
</section>
</div>
{/* </section> */}
</section> </section>

View File

@ -448,7 +448,7 @@ const DocumentSubmissionOptions = () => {
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 "> <div className="grid grid-cols-1 lg:grid-cols-2 gap-3 max-w-5xl mx-auto">
{/* Left Panel - Configuration */} {/* Left Panel - Configuration */}
<div className="space-y-6 "> <div className="space-y-6 ">

View File

@ -10,6 +10,7 @@ import api from '../../../config/api'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import axios from 'axios' import axios from 'axios'
interface DocumentCardProps { interface DocumentCardProps {
id: string; id: string;
name: string; name: string;
@ -157,7 +158,7 @@ const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted , tra
</div> </div>
</div> </div>
) : ( ) : (
<FaFileAlt className="text-gray-600 text-4xl mb-1" /> <FaFileAlt className="text-red-600 text-5xl mb-1" />
)} )}
</div> </div>

View File

@ -144,7 +144,7 @@ const ProcessStepOne = () => {
documents: false, documents: false,
recipients: false, recipients: false,
subject: false, subject: false,
recipientEmailAndLastName:false recipientEmailAndLastName: false
} }
if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) { if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
@ -161,7 +161,7 @@ const ProcessStepOne = () => {
if (!isValid) { if (!isValid) {
// alert('All recipients must have both Email and Last Name filled.'); // alert('All recipients must have both Email and Last Name filled.');
formError.recipientEmailAndLastName = true; formError.recipientEmailAndLastName = true;
} }
} }
if (!emailDtls.subject.length) { if (!emailDtls.subject.length) {
@ -370,10 +370,10 @@ const ProcessStepOne = () => {
return ( return (
<> <>
<div className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none h-screen overflow-hidden' : ''}`}> <div className={`relative w-full h-screen bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none overflow-hidden' : ''}`}>
{/* Top Navigation */} {/* Top Navigation - Fixed */}
<div className='fixed top-0 w-full flex items-center justify-between bg-white shadow z-50 px-4 py-3'> <div className='fixed top-0 left-0 right-0 w-full flex items-center justify-between bg-white shadow z-50 px-4 py-3'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<IoMdClose <IoMdClose
onClick={handleClose} onClick={handleClose}
@ -400,7 +400,6 @@ const ProcessStepOne = () => {
id="add-tags-btn-step-one" id="add-tags-btn-step-one"
className='flex items-center gap-3'> className='flex items-center gap-3'>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <button {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <button
onClick={() => handleNextClick(false, true)} onClick={() => handleNextClick(false, true)}
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer" className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
@ -418,87 +417,88 @@ const ProcessStepOne = () => {
</div> </div>
</div> </div>
{/* Main Content */} {/* Main Content - Scrollable */}
<div className="relative max-w-4xl mx-auto pt-[80px] px-4 pb-10"> <div className="h-screen pt-[80px] overflow-y-auto no-scrollbar">
<div className="max-w-4xl rounded-xl shadow-md mx-auto px-6 pb-10 bg-[var(--documentUploadBg)] mb-5">
{/* Documents */} {/* Documents */}
<section> <section>
<h2 className="text-lg font-medium mb-4 text-[var(--processStepOneTextHead)]">Documents</h2> <h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2>
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} /> <SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} />
</section> </section>
{/* Recipients */} {/* Recipients */}
<section className="mt-8"> <section className="mt-8">
<p className="border-t border-gray-200"></p>
{!isTamplate && {!isTamplate &&
transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES &&
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-lg font-medium mb-4 pt-6 border-t text-[var(--processStepOneTextHead)]">Signers</h2> <h2 className=" text-lg font-medium mb-4 pt-6 text-[var(--processStepOneTextHead)]">Signers</h2>
<div className="flex items-center mr-2"> <div className="flex items-center mr-3">
<input <input
checked={signType === SignTypeEnum.selfSign} checked={signType === SignTypeEnum.selfSign}
onChange={(e) => handleSelfSignCb(e)} 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" 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> <label htmlFor="default-checkbox" className="ms-2 text-sm flex gap-1 text-slate-600">
<span>SELF </span><span>SIGN</span></label>
</div>
</div> </div>
</div> }
}
{ {
(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 transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} editRecipient={editRecipient} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} editRecipientCurrentEmail={editRecipientCurrentEmail} /> <RecipientCards transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} editRecipient={editRecipient} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} editRecipientCurrentEmail={editRecipientCurrentEmail} />
{/* {signType !== "self-sign" && <RecipientAddCard />} */}
<div <div
ref={topRef} ref={topRef}
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' : ''}`}
> >
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
</div>
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
</div> </div>
}
</section>
{/* Email */}
<section className="mt-8">
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2>
{/* Subject */}
<div className='flex flex-col mb-4'>
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
<input
type="text"
required
value={emailDtls.subject}
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
placeholder="Email Subject"
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
</div> </div>
}
</section>
{/* Email */} {/* Message */}
<section className="mt-8"> <div className='flex flex-col'>
<h2 className="text-lg font-medium mb-4 pt-6 border-t text-[var(--processStepOneTextHead)]">Email</h2> <label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Message for Signers</label>
<textarea
{/* Subject */} rows={4}
<div className='flex flex-col mb-4'> value={emailDtls.message}
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label> onChange={(e) => setEmailDtls({ ...emailDtls, message: e.target.value })}
<input placeholder="Message"
type="text" className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
required />
value={emailDtls.subject} </div>
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })} </section>
placeholder="Email Subject"
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
</div>
{/* Message */}
<div className='flex flex-col'>
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Message for Signers</label>
<textarea
rows={4}
value={emailDtls.message}
onChange={(e) => setEmailDtls({ ...emailDtls, message: e.target.value })}
placeholder="Message"
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
</div>
</section>
</div>
</div> </div>
{globalLoader.isLoading && <Loader />} {globalLoader.isLoading && <Loader />}
{showSelfSignWarnModal && <SelfSignWarnModal setShowSelfSignWarnModal={setShowSelfSignWarnModal} />} {showSelfSignWarnModal && <SelfSignWarnModal setShowSelfSignWarnModal={setShowSelfSignWarnModal} />}
</div > </div>
</> </>
); );
} }

View File

@ -362,7 +362,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
ref={formRef} ref={formRef}
onSubmit={handleRecipientAdd} onSubmit={handleRecipientAdd}
id="add-recipient-step-one" id="add-recipient-step-one"
className="w-full bg-white border border-gray-200 rounded-lg shadow-md px-6 py-4 space-y-3 relative" className="w-full bg-white border border-gray-200 rounded-lg shadow-sm px-6 py-4 space-y-3 relative"
> >
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && ( {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="space-y-1"> <div className="space-y-1">

View File

@ -13,6 +13,7 @@ import DocumentCard from './DocumentCard';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'; import { toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice';
const SelectedDocuments = ({transactionTemplateToggle}) => { const SelectedDocuments = ({transactionTemplateToggle}) => {
// let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; // let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
@ -91,6 +92,15 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
id='upload-doc-step-one' id='upload-doc-step-one'
className="flex flex-wrap gap-3"> className="flex flex-wrap gap-3">
{/* Uploaded documents */} {/* Uploaded documents */}
<div
onClick={() => setModalOpen(true)}
className="cursor-pointer w-[160px] h-[170px] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-white hover:bg-gray-50 transition shadow-sm hover:shadow-md"
>
<FaPlus className="text-[var(--primary)] text-3xl mb-2" />
<p className="text-sm text-gray-600 text-center px-2">Upload Document</p>
</div>
{/* Document Show */}
<ReactSortable <ReactSortable
className="flex flex-wrap gap-3" className="flex flex-wrap gap-3"
list={documents.map(doc => ({ ...doc }))} list={documents.map(doc => ({ ...doc }))}
@ -105,13 +115,7 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
))} ))}
</ReactSortable> </ReactSortable>
<div
onClick={() => setModalOpen(true)}
className="cursor-pointer w-[160px] h-[170px] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-white hover:bg-gray-50 transition shadow-sm hover:shadow-md"
>
<FaPlus className="text-[var(--primary)] text-3xl mb-2" />
<p className="text-sm text-gray-600 text-center px-2">Upload Document</p>
</div>
</div> </div>
{/* Modal */} {/* Modal */}

View File

@ -22,7 +22,7 @@ const AsideSection = ({
} }
return ( return (
<div className="w-[16vw] p-4"> <div className="">
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-semibold text-gray-800">{mainTabTitle}</h3> <h3 className="text-lg font-semibold text-gray-800">{mainTabTitle}</h3>
<p className="text-sm text-gray-500 mt-1">Navigate through sections</p> <p className="text-sm text-gray-500 mt-1">Navigate through sections</p>

View File

@ -0,0 +1,219 @@
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom';
import logoRecord from '../../assets/record.svg';
import LoaderUi from '../../helper/LoaderUi';
import api from '../../config/api';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
const TemplateTable = () => {
const loaction = useLocation()
const [loading, setLoading] = useState(false)
const [templates, setTemplates] = useState([])
const accountDetails = useSelector((state: any) => state.user.account);
const accountId = accountDetails.id || "";
const [showMenu, setShowMenu] = useState(null);
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const navigate = useNavigate();
const getAllTemplates = async () => {
setLoading(true)
try {
let res = await api.get(`/api/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc`)
let templateData = res?.data
setTemplates(templateData)
} catch (error) {
console.log(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (activeAccountId) {
getAllTemplates()
}
}, [activeAccountId])
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
const handleUseClick = async (item: any) => {
try {
const payload = {
name: item?.name ?? "",
subject: item.subject,
message: item.message,
description: item.description,
expireAfter: item.expireAfter,
enableIdStamping: item.enableIdStamping,
allowViewAudit: item.allowViewAudit,
allowReassign: item.allowReassign,
enableSignatureIdStamping: item.enableSignatureIdStamping,
templateId: item.id,
};
let res = await api.post(`/api/accounts/${activeAccountId}/transactions`, payload)
console.log("handleUseClick", res.data)
// /process-step-one/:accountId/:transactionId
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) {
console.log(error)
}
}
const handleEditClick = (templateId: string) => {
setShowMenu(null);
navigate(`/process-step-one/${accountId}/template/${templateId}`);
}
function formatTimestamp(isoString, options = { timeZone: 'local' }) {
// Parse the ISO string to a Date object
const date = new Date(isoString);
if (isNaN(date)) {
return 'Invalid date';
}
// Return formatted date string
if (options.timeZone === 'UTC') {
return date.toUTCString(); // e.g. "Mon, 04 Aug 2025 13:46:46 GMT"
} else {
return date.toLocaleString(); // e.g. "8/4/2025, 9:46:46 AM" (based on local time zone)
}
}
return (
<>
<div className="overflow-y-auto max-h-[79vh] rounded-xl shadow-sm border border-gray-100 no-scrollbar">
<div className="flex justify-between items-center px-6 py-4 border-b border-gray-100 bg-white">
<h2 className="text-sm font-semibold text-gray-800 ">Recent Template</h2>
</div>
<table className="min-w-full text-sm bg-white">
<thead className=" sticky top-0 z-10 bg-gray-100 text-gray-600 text-xs uppercase tracking-wider border-b border-gray-200">
<tr>
{/* <th className="px-4 py-3 w-10 text-left font-medium">
<div className="w-4 h-4 border border-slate-300 rounded flex items-center justify-center">
<FaCheck className="text-purple-600 text-xs" />
</div>
</th> */}
<th className="px-6 py-4 font-semibold text-left">Name</th>
{loaction.pathname === '/templates' && (<>
<th className="px-6 py-4 font-semibold text-left">Created</th>
<th className="px-6 py-4 font-semibold text-left">Last Updated</th>
</>)}
<th className="px-6 py-4 font-semibold text-left">Use</th>
<th className="px-6 py-4 font-semibold text-left">Action</th>
</tr>
</thead>
{loading ? (
<tbody>
<tr>
<td colSpan="6" className="relative h-100">
<LoaderUi />
</td>
</tr>
</tbody>
) : templates.length > 0 ? (
<tbody className="divide-y divide-gray-100">
{templates.map((item, index) => (
<tr key={index} className="hover:bg-gray-50 transition-colors">
{/* <td className="px-4 py-4">
<div className="w-4 h-4 border border-slate-300 rounded flex items-center justify-center">
<FaCheck className="text-purple-600 text-xs" />
</div>
</td> */}
<td className="px-4 py-4 text-gray-800 text-left">
{item.name || 'Untitled'}
</td>
{loaction.pathname === '/templates' && (<>
<td className="px-4 py-4 text-gray-800 text-left">
{formatTimestamp(item.createdAt)}
</td>
<td className="px-4 py-4 text-gray-800 text-left">
{formatTimestamp(item.lastModifiedAt)}
</td>
</>)}
<td
className="px-4 py-4 text-left"
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>
{/* Action Menu */}
<td className="px-4 py-4 text-left">
<div
className="relative inline-block"
tabIndex={0}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setShowMenu(null);
}
}}
>
<button
className="text-gray-400 hover:text-gray-600 p-1 focus:outline-none"
onClick={() =>
setShowMenu(showMenu === index ? null : index)
}
>
<PiDotsThreeOutlineVerticalFill className="text-lg ms-4" />
</button>
{showMenu === index && (
<div
className="absolute -top-9 right-4 bg-white border border-gray-200 rounded-lg shadow-lg z-20 min-w-[100px]"
>
<button
onMouseDown={() => handleEditClick(item.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
>
Edit
</button>
<button
onMouseDown={() => {
setShowMenu(null);
console.log('Delete clicked', item);
}}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600"
>
Delete
</button>
</div>
)}
</div>
</td>
</tr>
))}
</tbody>
) : (
<tbody>
<tr>
<td colSpan="6" className="text-center py-10 text-gray-500">
<div className="flex flex-col items-center justify-center">
<img
src={logoRecord}
alt="Record Not Found"
className="w-70 h-70 mb-4 opacity-70"
/>
</div>
</td>
</tr>
</tbody>
)}
</table>
</div>
</>
)
}
export default TemplateTable

View File

@ -7,7 +7,8 @@ import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { FaSpinner } from "react-icons/fa"; import { FaSpinner } from "react-icons/fa";
import LoaderUi from "../../helper/LoaderUi"; import LoaderUi from "../../helper/LoaderUi";
import logoRecord from '../../assets/record.svg';
import TemplateTable from "./TemplateTable";
@ -38,73 +39,10 @@ const Templates = () => {
} }
const getAllTemplates = async () => {
setLoading(true)
try {
let res = await api.get(`/api/accounts/${activeAccountId}/templates?sortBy=lastModifiedAt desc`)
let templateData = res?.data
setTemplates(templateData)
} catch (error) {
console.log(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (activeAccountId) {
getAllTemplates()
}
}, [activeAccountId])
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
const handleUseClick = async (item: any) => {
try {
const payload = {
name: item?.name ?? "",
subject: item.subject,
message: item.message,
description: item.description,
expireAfter: item.expireAfter,
enableIdStamping: item.enableIdStamping,
allowViewAudit: item.allowViewAudit,
allowReassign: item.allowReassign,
enableSignatureIdStamping: item.enableSignatureIdStamping,
templateId: item.id,
};
let res = await api.post(`/api/accounts/${activeAccountId}/transactions`, payload)
console.log("handleUseClick", res.data)
// /process-step-one/:accountId/:transactionId
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) {
console.log(error)
}
}
const handleEditClick = (templateId: string) => {
setShowMenu(null);
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">
@ -129,7 +67,7 @@ const Templates = () => {
<div <div
onClick={handleAddTemplate} onClick={handleAddTemplate}
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm cursor-pointer hover:opacity-90"> className="flex items-center gap-2 bg-[var(--mainPrimaryBtn)] px-3 py-1.5 rounded-sm cursor-pointer hover:opacity-90">
<IoMdAdd className="text-white" /> <IoMdAdd className="text-white" />
<button <button
className="text-white rounded text-sm hover:opacity-90"> className="text-white rounded text-sm hover:opacity-90">
@ -139,110 +77,8 @@ const Templates = () => {
</div> </div>
{/* Table */} {/* Table */}
<div className="max-h-[77vh] pb-16 overflow-y-auto relative no-scrollbar"> <section className="my-4 mx-3"></section>
<table className="min-w-full border border-gray-200 bg-white shadow rounded table-fixed"> <TemplateTable/>
<thead className="bg-gray-100 text-xs text-gray-700 uppercase tracking-wider sticky top-0 z-10">
<tr>
<th className="px-4 py-3 w-10 bg-gray-100">
<div className="w-4 h-4 border border-slate-300 rounded flex items-center justify-center">
{/* <FaCheck className="text-purple-600 text-xs" /> */}
</div>
</th>
<th className="px-4 py-2 text-center bg-gray-100">Name</th>
<th className="px-4 py-2 text-center bg-gray-100">Created</th>
<th className="px-4 py-2 text-center bg-gray-100">Last Updated</th>
<th className="px-4 py-2 text-center bg-gray-100">Use</th>
<th className="px-4 py-2 text-center bg-gray-100">Action</th>
</tr>
</thead>
{loading ? (
<tbody>
<tr>
<td colSpan="6" className="relative h-100">
<LoaderUi />
</td>
</tr>
</tbody>
) : templates.length > 0 ? (
<tbody className="divide-y divide-gray-200">
{templates.map((item, index) => {
return (
<tr key={index} className="hover:bg-gray-50 relative">
<td className="px-4 py-2">
<div className="w-4 h-4 border border-slate-300 rounded flex items-center justify-center">
{/* <FaCheck className="text-purple-600 text-xs" /> */}
</div>
</td>
<td className="px-4 py-2 text-sm text-center">{item.name || 'Untitled'}</td>
<td className="px-4 py-2 text-sm text-center">{formatTimestamp(item.createdAt)}</td>
<td className="px-4 py-2 text-sm text-center">{formatTimestamp(item.lastModifiedAt)}</td>
<td className="px-4 py-2 text-sm text-center" onClick={() => handleUseClick(item)}><span className="bg-[var(--lightGreyCol)] cursor-pointer border-gray-300 text-gray-600 hover:bg-gray-100 px-3 py-1 rounded-lg font-semibold text-[12px]">Use</span></td>
{/* Dropdown Trigger + Tooltip */}
<td className="px-4 py-2 text-center relative">
<button
onClick={() => setShowMenu(showMenu === index ? null : index)}
className="text-slate-400 hover:text-black focus:outline-none"
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button>
{showMenu === index && (
<div
className="absolute right-21 top-0 mt-2 w-20 bg-white border border-gray-200 rounded shadow-lg z-10 transition-opacity duration-300 ease-in-out"
tabIndex={0} // Make it focusable
onBlur={(e) => {
// Ensure it only closes if focus truly leaves the container
if (!e.currentTarget.contains(e.relatedTarget)) {
setShowMenu(null);
}
}}
>
<button
className="block w-full text-left px-4 py-1 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => handleEditClick(item.id)}
>
Edit
</button>
<button
className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-100"
onClick={() => {
setShowMenu(null);
console.log('Delete clicked', item);
}}
>
Delete
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
) : (
<tbody>
<tr>
<td colSpan="6" className="text-center py-10 text-gray-500">
<div className="flex flex-col items-center justify-center">
<img
src={logoRecord}
alt="Record Not Found"
className="w-70 h-70 mb-4 opacity-70" // adjust size and appearance
/>
</div>
</td>
</tr>
</tbody>
)}
</table>
</div>
</div> </div>
); );
}; };

View File

@ -1,254 +1,321 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
FiX, FiX,
FiCheckCircle, FiCheckCircle,
FiXCircle, FiXCircle,
FiClock, FiClock,
FiUser, FiUser,
FiCalendar, FiCalendar,
FiFileText, FiFileText,
FiAlertTriangle, FiAlertTriangle,
FiTruck, FiTruck,
FiUserCheck, FiUserCheck,
FiUserX, FiUserX,
FiPackage, FiPackage,
FiMapPin, FiMapPin,
FiEye, FiEye,
FiRotateCcw FiRotateCcw,
FiSend,
FiMail,
FiActivity,
FiRefreshCw,
FiLock,
FiUnlock,
FiUserPlus,
FiEdit
} from "react-icons/fi"; } from "react-icons/fi";
import api from "../../config/api";
import { useParams } from "react-router-dom";
import logoRecord from '../../assets/record.svg'
import LoaderUi from "../../helper/LoaderUi";
import { FaRegEyeSlash } from "react-icons/fa";
import { MdAccessTime, MdCancel } from "react-icons/md";
interface AuditModalProps { interface AuditModalProps {
closeModal: () => void; closeModal: () => void;
} }
const AuditModal: React.FC<AuditModalProps> = ({ closeModal }) => { const AuditModal: React.FC<AuditModalProps> = ({ closeModal }) => {
const [activeTab, setActiveTab] = useState<'status' | 'audit'>('status'); const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
const statusHistory = [ if (!accountId || !transactionId) {
{ window.location.href = "/";
id: 1, return null;
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 = [ let [auditStatus, setAuditStatus] = useState([])
{ let [loading, setLoading] = useState(false)
id: 1, const getAuditDetails = async () => {
title: "Batch 11317 Pickup Request Created", setLoading(true)
author: "Sandra Darlington", try {
location: "Lagos General Hospital Gbagada", let res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/audit?lang=${userDetails.language || ''}`)
time: "11 Aug, 2023 09:23 AM", if (res?.status === 200) {
icon: <FiPackage className="w-4 h-4" />, // console.log(res?.data?.events)
type: "created" let auditResponse = res?.data?.events
}, setAuditStatus(auditResponse)
{ }
id: 2, } catch (error) {
title: "Batch 11317 Pickup Request", console.log(error)
author: "Oladejo Israel", } finally {
location: "Institute of Human Virology H3Africa BioRepository", setLoading(false)
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) => { useEffect(() => {
const statusClasses = { if (accountId && transactionId) {
'Approved': 'bg-green-100 text-green-800', getAuditDetails()
'Rejected': 'bg-red-100 text-red-800', }
'Reviewed': 'bg-yellow-100 text-yellow-800', }, [accountId, transactionId])
'Ready to Review': 'bg-blue-100 text-blue-800'
const statusHistory = [
{
"action": "SENT",
"message": "Transaction Sent by John Doe <john@email.com>",
"status": "SENT",
"eventData": {
"ip": "192.168.1.1",
"longitude": "3.3792",
"latitude": "6.5244",
"timezone": "WAT"
},
"timestamp": "2025-01-15T10:30:00Z",
"language": "en"
},
{
"action": "INVITED",
"message": "Invitation Sent to Jane Smith <jane@email.com> to sign documents",
"status": "SENT",
"eventData": {
"ip": "0.0.0.0",
"longitude": "11",
"latitude": "12",
"timezone": ""
},
"timestamp": "2025-01-15T11:45:00Z",
"language": "es"
},
{
"action": "VIEWED",
"message": "Document viewed by recipient Alice Johnson <alice@email.com>",
"status": "VIEWED",
"eventData": {
"ip": "203.0.113.5",
"longitude": "-74.0059",
"latitude": "40.7128",
"timezone": "EST"
},
"timestamp": "2025-01-15T14:20:00Z",
"language": "en"
},
{
"action": "SIGNED",
"message": "Document signed by Bob Wilson <bob@email.com>",
"status": "COMPLETED",
"eventData": {
"ip": "198.51.100.10",
"longitude": "-122.4194",
"latitude": "37.7749",
"timezone": "PST"
},
"timestamp": "2025-01-15T16:10:00Z",
"language": "en"
}
];
const getActionIcon = (action: string) => {
const icons = {
'CREATED': <FiFileText className="w-4 h-4 text-sky-500" />,
'SENT': <FiSend className="w-4 h-4" />,
'INVITED': <FiMail className="w-4 h-4" />,
'COMPLETED': <FiCheckCircle className="w-4 h-4" />,
'RE_SENT_INVITATION': <FiRefreshCw className="w-4 h-4" />,
'RE_SENT_EXPIRED_INVITATION': <FiRefreshCw className="w-4 h-4" />,
'VIEWED': <FiEye className="w-4 h-4" />,
'VIEWED_IN_PERSON': <FaRegEyeSlash className="w-4 h-4" />, // Slightly different eye icon
'AUTH_FAILED': <FiXCircle className="w-4 h-4 text-red-500" />,
'ACCESS_CODE_FAILED': <FiLock className="w-4 h-4 text-red-500" />,
'ACCESS_CODE_VALIDATED': <FiUnlock className="w-4 h-4 text-green-500" />,
'AUTH_VALIDATED': <FiCheckCircle className="w-4 h-4 text-green-500" />,
'SIGNED': <FiCheckCircle className="w-4 h-4" />,
'EXPIRED': <MdAccessTime className="w-4 h-4 text-gray-500" />,
'CANCELLED': <MdCancel className="w-4 h-4 text-red-500" />,
'DECLINED': <FiXCircle className="w-4 h-4 text-red-500" />,
'PENDING': <FiClock className="w-4 h-4" />,
'ASSIGNED': <FiUserCheck className="w-4 h-4" />,
'REASSIGNED': <FiUserPlus className="w-4 h-4" />,
'EDITED': <FiEdit className="w-4 h-4" />,
};
return icons[action as keyof typeof icons] || <FiActivity className="w-4 h-4" />;
}; };
return statusClasses[status as keyof typeof statusClasses] || 'bg-gray-100 text-gray-800';
};
const getAuditTypeColor = (type: string) => { const getActionColor = (action: string, status: string) => {
const colors = { if (status === 'COMPLETED') return 'text-green-600';
'created': 'text-blue-600', if (status === 'FAILED' || status === 'REJECTED') return 'text-red-600';
'approved': 'text-green-600', if (status === 'PENDING') return 'text-yellow-600';
'initiated': 'text-purple-600',
'assigned': 'text-indigo-600', const colors = {
'rejected': 'text-red-600', 'SENT': 'text-blue-600',
'synced': 'text-green-600', 'INVITED': 'text-indigo-600',
'disconnected': 'text-orange-600' 'VIEWED': 'text-purple-600',
'SIGNED': 'text-green-600',
'REJECTED': 'text-red-600'
};
return colors[action as keyof typeof colors] || 'text-gray-600';
}; };
return colors[type as keyof typeof colors] || 'text-gray-600';
};
return ( const getStatusBadge = (status: string) => {
<div className="fixed inset-0 flex items-center justify-center backdrop-blur-sm bg-white/10 z-50"> const statusClasses = {
<div className="bg-white rounded-md shadow-md w-full max-w-[75vw] max-h-[95vh] min-h-[85vh] flex flex-col"> 'SENT': 'bg-blue-100 text-blue-800',
{/* Header */} 'COMPLETED': 'bg-green-100 text-green-800',
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-[var(--primary)] rounded-t-md"> 'FAILED': 'bg-red-100 text-red-800',
<div className="flex items-center"> 'PENDING': 'bg-yellow-100 text-yellow-800',
<h2 className="text-xl font-semibold text-white"> 'VIEWED': 'bg-purple-100 text-purple-800',
Audit Status 'REJECTED': 'bg-red-100 text-red-800'
</h2> };
</div> return statusClasses[status as keyof typeof statusClasses] || 'bg-gray-100 text-gray-800';
<button };
onClick={closeModal}
className="text-white cursor-pointer transition-colors"
>
<FiX className="w-5 h-5" />
</button>
</div>
{/* Content */} const formatTimestamp = (timestamp: string) => {
<div className="flex-1 overflow-y-auto p-4"> try {
const date = new Date(timestamp);
<div className="space-y-1.5"> if (isNaN(date.getTime())) {
<div className="text-sm text-gray-500 font-medium mb-2">TODAY</div> return timestamp;
}
{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"> const now = new Date();
<div className={`${item.statusColor} mt-1`}> const diffMs = now.getTime() - date.getTime();
{item.icon} const diffMins = Math.floor(diffMs / (1000 * 60));
</div> const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
<div className="flex-1"> const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
<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"> if (diffMins < 60) {
<span>By {item.author}</span> return `${diffMins}m ago`;
{item.role && ( } else if (diffHours < 24) {
<> return `${diffHours}h ago`;
<span>•</span> } else if (diffDays < 7) {
<span className="text-blue-600 text-xs">({item.role})</span> return `${diffDays}d ago`;
</> } else {
)} return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
} catch (error) {
return timestamp;
}
};
const extractEmailFromMessage = (message: string) => {
const emailMatch = message.match(/<([^>]+)>/);
return emailMatch ? emailMatch[1] : '';
};
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> </div>
<div className="flex items-center space-x-3"> <button
<span className="text-xs text-gray-500">{item.time}</span> onClick={closeModal}
<div className="flex items-center space-x-2"> className="text-white cursor-pointer transition-colors hover:text-gray-200"
<span className={`px-2 py-0.5 text-xs rounded-full ${getStatusBadge(item.fromStatus)}`}> >
{item.fromStatus} <FiX className="w-5 h-5" />
</span> </button>
<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>
))}
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 no-scrollbar">
{/* {loading ? <LoaderUi /> : ""} */}
{auditStatus.length ?
<div className="space-y-1.5">
{/* <div className="text-sm text-gray-500 font-medium mb-2">TODAY</div> */}
{auditStatus.map((item, index) => (
<div key={index} className="flex items-start space-x-4 pb-3 border-b border-gray-100 last:border-b-0">
<div className={`${getActionColor(item.action)} mt-1`}>
{getActionIcon(item.action)}
</div>
<div className="flex-1">
<p className="text-gray-600 font-medium text-sm mb-1">{item.message}</p>
<div className="flex items-center space-x-2 text-xs text-gray-600 mb-1">
<span>Action: {item.action}</span>
{extractEmailFromMessage(item.message) && (
<>
<span>•</span>
<span className="text-blue-600">{extractEmailFromMessage(item.message)}</span>
</>
)}
</div>
<div className="flex items-center space-x-2 mb-2">
<span className="text-xs text-gray-600">Status:</span> <span className={`px-2 py-0.5 text-xs rounded-full ${getStatusBadge(item.status)}`}>
{item.status}
</span>
<span className="text-xs text-gray-500">{formatTimestamp(item.timestamp)}</span>
{item.language && (
<span className="text-xs text-gray-400 uppercase">{item.language}</span>
)}
</div>
{/* Event Data Details */}
{/* <div className="grid grid-cols-2 gap-4 text-xs text-gray-600 bg-gray-50 p-2 rounded">
{item.eventData.ip && (
<div>
<span className="font-medium text-gray-700">IP:</span> {item.eventData.ip}
</div>
)}
{item.eventData.timezone && (
<div>
<span className="font-medium text-gray-700">Timezone:</span> {item.eventData.timezone}
</div>
)}
{item.eventData.latitude && item.eventData.longitude && (
<div className="col-span-2">
<span className="font-medium text-gray-700">Location:</span>
<span className="ml-1">Lat: {item.eventData.latitude}, Lng: {item.eventData.longitude}</span>
</div>
)}
</div> */}
</div>
</div>
))}
</div> :
<div className="flex justify-center items-center">
<img
src={logoRecord}
alt="Record Not Found"
className="w-80 h-80 opacity-70" // adjust size and appearance
/>
</div>
}
</div>
{/* Footer */}
<div className="flex justify-between items-center p-3 border-t border-gray-200 bg-gray-50">
<div className="text-sm text-gray-600">
Showing {auditStatus.length} event{auditStatus.length !== 1 ? 's' : ''}
</div>
<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>
</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; export default AuditModal;

View File

@ -62,7 +62,7 @@ const TransactionView = () => {
: "?auditStatus"; : "?auditStatus";
window.history.pushState({}, "", pathname + newSearch); window.history.pushState({}, "", pathname + newSearch);
setIsModalOpen(true); // ✅ update state immediately setIsModalOpen(true); // update state immediately
}; };
const closeModal = () => { const closeModal = () => {
@ -72,7 +72,7 @@ const TransactionView = () => {
.replace(/\?$/, ""); .replace(/\?$/, "");
window.history.pushState({}, "", location.pathname + search); window.history.pushState({}, "", location.pathname + search);
setIsModalOpen(false); // ✅ update state immediately setIsModalOpen(false); // update state immediately
}; };
console.log(transactionDetails, "line 37"); console.log(transactionDetails, "line 37");

View File

@ -1,16 +1,16 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--primary: #3D74B6; --primary: #005AB5;
--primaryLight: #ffffff; --primaryLight: #ffffff;
--primaryLight2: #6a92c2; --primaryLight2: #6a92c2;
--primaryLight3: #dae9de; --primaryLight3: #dae9de;
--dashBoardBg: #ffffff; --dashBoardBg: #ffffff;
--mainViewBg: #edeaeb; --mainViewBg: #F8F9FC;
--highlightBg1: #edeaeb; --highlightBg1: #edeaeb;
--menuTextCol: #98a0a8; --menuTextCol: #98a0a8;
--menuTextColActive: #3D74B6; --menuTextColActive: #005AB5;
--menuBgColActive: #edeaeb; --menuBgColActive: #FFFFFF;
--menuTextHoverCol: #e7e7e7; --menuTextHoverCol: #e7e7e7;
--greyTextCol: #98a0a8; --greyTextCol: #98a0a8;
@ -46,6 +46,17 @@
--modalSubmitBG: #1919d7; --modalSubmitBG: #1919d7;
--modalSubmitBGHover: #395ee1; --modalSubmitBGHover: #395ee1;
/* Figma Design */
--sidebarBg: #005AB5;
--sidebarDropDownBg : #003C78;
--sidebarMenuCol : #FFFFFF;
--documentUploadBg :#FFFFFF;
--mainPrimaryBtn : #077DEC
} }