Compare commits

...

8 Commits

Author SHA1 Message Date
JaiPandey425 0ad679fef7 Your descriptive commit message here 2025-07-15 15:31:44 +05:30
JaiPandey425 a877766da7 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/SME-Web into jai 2025-07-11 17:40:37 +05:30
=bhagwanSingh ba150f7cb2 a 2025-07-11 17:32:42 +05:30
=bhagwanSingh bdd9849422 Route Update 2025-07-11 17:09:00 +05:30
=bhagwanSingh 71ef8913d7 Common Nav 2025-07-11 16:27:40 +05:30
=bhagwanSingh 2f2b4f3aca Merge branch 'merge2' 2025-07-11 13:03:33 +05:30
=bhagwanSingh 5b1d804ec3 template 2025-07-11 12:30:28 +05:30
jaipandey 79151e2cf3 Merge pull request 'Process step two , dashboard , navbar' (#3) from jai into master
Reviewed-on: http://git.hicglobalsolutions.in/BhawanSingh/SME-Web/pulls/3
2025-07-11 06:55:50 +00:00
20 changed files with 1193 additions and 169 deletions

View File

@ -14,18 +14,12 @@ import SignDocument from "./components/client/SignDocument";
import Transactions from "./components/transactions/Transactions";
import ProtectedRoute from "./components/routes_helper/ProtectedRoute";
import { HashRouter } from 'react-router-dom';
import { useDispatch, useSelector } from "react-redux";
import { fetchUserDetails } from "./store/slices/userSlice";
import { AppDispatch, RootState } from "./store/store";
import { useEffect, useLayoutEffect } from "react";
import Setting from "./components/setting/Setting";
import Accounts from "./components/accounts/Accounts";
import Account from "./components/accounts/Account";
import TransactionView from "./components/transactions/TransactionView";
import AccountTransactionWrapper from "./components/AccountTransactionWrapper";
import SigningOrderDiagram from "./SigningOrderDiagram ";
import Templates from "./components/templates/Templates";
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
const App = () => {
return (
@ -41,23 +35,19 @@ const App = () => {
</ProtectedRoute>
}>
<Route path="/" element={<Home />} />
<Route path="/templates" element={<Home />} />
{/* <Route path="/s/settings" element={<Setting />} /> */}
<Route element={<AccountTransactionWrapper />}>
{/* <Route element={<AccountTransactionWrapper />}> */}
<Route path="/accounts" element={<Accounts />} />
<Route path="/accounts/:accountId/transactions" element={<Account />} />
<Route path="/transactions" element={<Account />} />
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
<Route path="/transactions/:filterParam" element={<Transactions />} />
</Route>
<Route path="/templates" element={<Templates />} />
{/* </Route> */}
<Route path="/s/:page1" element={<Setting />} />
</Route>
<Route path="/process-step-one/:accountId/:transactionId" element={
<ProtectedRoute>
<ProcessStepOne />
@ -70,6 +60,12 @@ const App = () => {
</ProtectedRoute>
} />
<Route path="/document-completion-screen/:accountId/:transactionId" element={
<ProtectedRoute>
<DocumentCompletionScreen />
</ProtectedRoute>
} />
{/* Links work wothout Login */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />

View File

@ -1,13 +1,14 @@
import { Outlet } from 'react-router-dom'
import AccountNav from './accounts/AccountNav'
// import AccountNav from './accounts/AccountNav'
import CommonNav from './CommonNav'
const AccountTransactionWrapper = () => {
return (
<>
<AccountNav />
<CommonNav />
<Outlet />
</>
)

View File

@ -0,0 +1,411 @@
import React, { useEffect, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
import api from '../config/api';
interface AccountNavProps {
accountName?: string | null;
transactionName?: string | null;
}
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: number;
name: string;
email: string;
language: string;
account: Account;
}
interface AccountDataArray {
userId: number,
userEmail: string,
name: string,
accountId: number;
accountName: string;
role: string;
status: string;
}
// Helper function to dispatch custom storage event
const dispatchSessionStorageChange = (key: string, newValue: any) => {
window.dispatchEvent(new CustomEvent('sessionStorageChange', {
detail: { key, newValue }
}));
};
const CommonNav: React.FC<AccountNavProps> = () => {
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
console.log("activeAccountId", activeAccountId);
const [open, setOpen] = useState(false);
const [accountsOpen, setAccountsOpen] = useState(false);
const [loadingAccounts, setLoadingAccounts] = useState(false);
const [accounts, setAccounts] = useState<AccountDataArray[]>([]);
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const [user, setUser] = useState<UserData>({
id: 0,
name: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
console.log("user", user)
const getAccountDetails = async () => {
try {
let res = await api.get('/api/user/accounts');
let items = res?.data;
console.log('items', items);
const cleanedData: AccountDataArray[] = items.map((item: any) => ({
userId: item.userId,
userEmail: item.userEmail,
name: item.name,
accountId: item.accountId,
accountName: item.accountName,
role: item.role,
status: item.status,
}));
setAccounts(cleanedData);
} catch (error) {
console.log(error);
}
};
const handleAccountsClick = async () => {
setAccountsOpen(!accountsOpen);
if (!accountsOpen) {
setLoadingAccounts(true);
if (accounts.length === 0 || hasModalBeenClosed) {
await getAccountDetails();
setHasModalBeenClosed(false);
}
setLoadingAccounts(false);
}
};
const handleAccountSwitch = (account: any) => {
console.log("handleAccountSwitch", account);
// Update sessionStorage
sessionStorage.setItem('activeAccountId', JSON.stringify(account.accountId));
// Dispatch custom event
dispatchSessionStorageChange('activeAccountId', account.accountId);
// Update user state
setUser({
id: account.userId,
name: account.name,
email: account.userEmail,
language: "", // set it if available
account: {
id: account.accountId,
isDefault: false, // update as needed
name: account.accountName,
},
});
sessionStorage.setItem('userDetails', JSON.stringify({
id: account.userId,
name: account.name,
email: account.userEmail,
language: "", // set it if available
account: {
id: account.accountId,
isDefault: false, // update as needed
name: account.accountName,
},
}));
setAccountsOpen(false);
setOpen(false);
};
const navigate = useNavigate();
const getUserDetails = async () => {
try {
let res = await api.get(`/api/user/details`);
console.log("getuser", res);
const item = res?.data;
if (item) {
const userData = {
id: item?.id,
name : item?.firstName + " " + item?.lastName ,
email: item?.email,
language: "",
account: {
id: item?.account.id,
isDefault: false,
name: item?.account.name,
},
};
console.log("userData",userData)
setUser(userData);
sessionStorage.setItem('userDetails', JSON.stringify(userData));
sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id));
// Dispatch event for initial load
dispatchSessionStorageChange('activeAccountId', userData.account.id);
console.log("Data is saved in sessionStorage");
}
} catch (error) {
console.error("Error fetching user details:", error);
}
};
useEffect(() => {
if (!activeAccountId) {
getUserDetails();
} else {
setUser(userDetails);
}
}, []);
const location = useLocation();
const rawPathnames = location.pathname
.split("/")
.filter((x) => x && isNaN(Number(x)));
// Special case: if first segment is 's' and next is 'settings', remove 's'
const pathnames =
rawPathnames[0] === "s" && rawPathnames[1] === "settings"
? rawPathnames.slice(1)
: rawPathnames;
const labelMap: Record<string, string> = {
accounts: "Accounts",
transactions: "Transactions",
templates: "Templates"
};
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
if (open) {
setHasModalBeenClosed(true);
}
setOpen(false);
setAccountsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
const handleLogout = async () => {
try {
let res = await api.post('/api/auth/logout');
if (res.status === 200) {
sessionStorage.clear();
setOpen(false);
navigate("/login");
}
} catch (error) {
console.log(error);
}
};
const handleMainDropdownToggle = () => {
if (open) {
setHasModalBeenClosed(true);
}
setOpen(!open);
if (!open) {
setAccountsOpen(false);
}
};
const formatSegment = (segment: string) =>
segment
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
const ShimmerRow = () => (
<div className="animate-pulse px-4 py-3 border-b border-gray-100 last:border-b-0">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gray-200 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
<div className="w-16 h-6 bg-gray-200 rounded"></div>
</div>
</div>
);
return (
<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">
{pathnames.length === 0 ? (
<span className="text-gray-900 font-semibold">Dashboard</span>
) : (
pathnames.map((segment, index) => {
const label = labelMap[segment] || formatSegment(segment);
const to = "/" + pathnames.slice(0, index + 1).join("/");
const isLast = index === pathnames.length - 1;
return (
<span key={to} className="flex items-center">
{index > 0 && <span className="mx-2 text-gray-400">{">"}</span>}
{!isLast ? (
<Link
to={to}
className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
>
{label}
</Link>
) : (
<span className="text-gray-900 font-semibold">{label}</span>
)}
</span>
);
})
)}
</nav>
<div className="flex items-center space-x-2">
<div className="relative" ref={dropdownRef}>
<button
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
onClick={handleMainDropdownToggle}
aria-expanded={open}
aria-haspopup="true"
>
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
{user ? (
<span className="w-full h-full object-cover p-1.5 font-bold">
{(user?.name?.slice(0, 2)).toUpperCase()}
</span>
) : (
<FaUser className="text-lg" />
)}
</div>
</button>
{open && (
<div className="absolute right-0 mt-2 w-72 top-8 rounded-xl shadow-xl bg-white border border-gray-200 z-50 overflow-hidden">
<div className="px-4 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b border-gray-100">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold shadow-md">
{user ? (
<span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold">
{(user?.name?.slice(0, 2)).toUpperCase()}
</span>
) : (
<FaUser className="text-lg" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 truncate">{user?.name || '-'}</div>
<div className="text-sm text-gray-600 truncate">{user?.email || '-'}</div>
<div className="text-xs text-gray-500 mt-1">
Account #{user?.account?.name || '-'}
</div>
</div>
</div>
</div>
<div className="border-b border-gray-100">
<button
onClick={handleAccountsClick}
className="w-full flex items-center justify-between px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer"
>
<div className="flex items-center">
<FaBuilding className="mr-3 text-gray-500" />
<span>Switch Account</span>
</div>
<FaChevronDown className={`text-gray-400 transition-transform ${accountsOpen ? 'rotate-180' : ''}`} />
</button>
{accountsOpen && (
<div className="bg-gray-50 max-h-64 overflow-y-auto">
{loadingAccounts ? (
<div className="space-y-0">
<ShimmerRow />
<ShimmerRow />
</div>
) : (
<div className="divide-y divide-gray-200">
{accounts?.length > 0 ? (
accounts?.map((account) => (
<button
key={account.accountId}
onClick={() => handleAccountSwitch(account)}
className={`w-full px-4 py-3 hover:bg-white transition-colors cursor-pointer text-left`}
>
<div className={`flex items-center justify-between`}>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-gray-200 to-gray-400 flex items-center justify-center text-white font-semibold text-xs">
{account.accountName.slice(0, 2).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<span className="font-medium text-gray-900 text-sm">{account.accountName}</span>
{account.accountId === activeAccountId && (
<FaCheck className="text-green-600 text-xs" />
)}
</div>
</div>
</div>
<div className="text-xs text-gray-500 bg-gray-200 px-2 py-1 rounded">
{account.role}
</div>
</div>
</button>
))
) : (
<span className='text-gray-400 font-medium text-[12px] flex justify-center p-3'>No Account...</span>
)}
</div>
)}
</div>
)}
</div>
<div className="border-t border-gray-100">
<button
onClick={handleLogout}
className="w-full flex items-center px-4 py-3 text-sm text-red-600 hover:bg-red-50 transition-colors cursor-pointer"
>
<FaSignOutAlt className="mr-3" />
<span>Log Out</span>
</button>
</div>
</div>
)}
</div>
</div>
</header>
);
};
export default CommonNav;

View File

@ -67,36 +67,36 @@ const Account = () => {
textColor: 'text-teal-700',
trend: 'up'
},
{
title: 'Waiting for Others',
count: data.waitingForOthers || 0,
icon: <FaClock size={18} 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'
},
{
title: 'declined',
count: data.declined || 0,
icon: <FaClock size={18} className="text-purple-600" />,
gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
shadowColor: 'shadow-purple-200/50',
iconBg: 'bg-purple-100 border-purple-200',
textColor: 'text-purple-700',
trend: 'down'
},
{
title: 'cancelled',
count: data.cancelled || 0,
icon: <FaClock size={18} className="text-green-600" />,
gradient: 'bg-gradient-to-br from-green-50 to-green-100',
shadowColor: 'shadow-green-200/50',
iconBg: 'bg-green-100 border-green-200',
textColor: 'text-green-700',
trend: 'down'
},
// {
// title: 'Waiting for Others',
// count: data.waitingForOthers || 0,
// icon: <FaClock size={18} 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'
// },
// {
// title: 'declined',
// count: data.declined || 0,
// icon: <FaClock size={18} className="text-purple-600" />,
// gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
// shadowColor: 'shadow-purple-200/50',
// iconBg: 'bg-purple-100 border-purple-200',
// textColor: 'text-purple-700',
// trend: 'down'
// },
// {
// title: 'cancelled',
// count: data.cancelled || 0,
// icon: <FaClock size={18} className="text-green-600" />,
// gradient: 'bg-gradient-to-br from-green-50 to-green-100',
// shadowColor: 'shadow-green-200/50',
// iconBg: 'bg-green-100 border-green-200',
// textColor: 'text-green-700',
// trend: 'down'
// },
{
title: 'draft',
count: data.draft || 0,
@ -115,7 +115,9 @@ const Account = () => {
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
console.log("activeAccountId Trans.",activeAccountId)
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
console.log("activeAccountId Trans.", activeAccountId)
// const { accountId } = useParams<{ accountId: string }>();
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
@ -141,32 +143,38 @@ const Account = () => {
// getUserDetails()
// }, [])
useEffect(() => {
const userDetails = JSON.parse(sessionStorage.getItem('userDetails'));
setCurrentEmailId(userDetails?.email)
}, []);
// Listen for sessionStorage changes
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'activeAccountId') {
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
setActiveAccountId(newActiveAccountId);
}
};
// Listen for storage events (works for changes from other tabs)
window.addEventListener('storage', handleStorageChange);
// Custom event listener for same-tab changes
const handleCustomStorageChange = (e: CustomEvent) => {
if (e.detail.key === 'activeAccountId') {
setActiveAccountId(e.detail.newValue);
}
};
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
};
}, []);
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'activeAccountId') {
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
setActiveAccountId(newActiveAccountId);
}
};
// Listen for storage events (works for changes from other tabs)
window.addEventListener('storage', handleStorageChange);
// Custom event listener for same-tab changes
const handleCustomStorageChange = (e: CustomEvent) => {
if (e.detail.key === 'activeAccountId') {
setActiveAccountId(e.detail.newValue);
}
};
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
};
}, []);
const getDashboardData = async () => {
@ -281,7 +289,8 @@ const Account = () => {
<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"></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">
@ -318,6 +327,9 @@ const Account = () => {
<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">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>

View File

@ -17,6 +17,7 @@ import confetti from 'canvas-confetti'
import { useWindowSize } from 'react-use'
import { FaCheckCircle } from 'react-icons/fa'
import { IoClose } from 'react-icons/io5'
import CommonNav from '../CommonNav'
function Dashboard() {
const [showSideBar, setShowSideBar] = React.useState(true)
@ -102,32 +103,32 @@ function Dashboard() {
}, [documentSignType])
useEffect(() => {
if (showConfetti) {
const duration = 3000;
const end = Date.now() + duration;
useEffect(() => {
if (showConfetti) {
const duration = 3000;
const end = Date.now() + duration;
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
confetti({
particleCount: 80, // more particles
angle: Math.random() * 360, // fire in random direction
spread: 220, // wider spread
startVelocity: 15, // more power
scalar: 1.2, // bigger pieces (default is 1)
origin: {
x: Math.random(),
y: Math.random() * 0.6,
},
zIndex: 9999,
});
}, 350); // fire every 150ms
}
}, [showConfetti]);
confetti({
particleCount: 80, // more particles
angle: Math.random() * 360, // fire in random direction
spread: 220, // wider spread
startVelocity: 15, // more power
scalar: 1.2, // bigger pieces (default is 1)
origin: {
x: Math.random(),
y: Math.random() * 0.6,
},
zIndex: 9999,
});
}, 350); // fire every 150ms
}
}, [showConfetti]);
@ -173,6 +174,7 @@ function Dashboard() {
<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`}>
{/* Main Content */}
<div className='px-2 py-1.5'>
<CommonNav />
<Outlet />
</div>
</div>

View File

@ -159,7 +159,6 @@ const Home = () => {
return (
<>
<AccountNav />
<Cards
onDateRangeChange={handleDateRangeChange}
dashboardData={dashboardData}

View File

@ -0,0 +1,354 @@
import React, { useEffect } from "react";
import {
FaCheck,
FaFileAlt,
FaUsers,
FaEnvelope,
FaClock,
FaDownload,
FaShare,
FaShieldAlt,
FaLock,
FaPen,
FaCheckCircle
} from 'react-icons/fa';
import { fetchRecipients } from "../../../store/slices/selectedRecipientsSlice";
import { fetchDocuments } from "../../../store/slices/documentSlice";
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate, useParams } from "react-router-dom";
import Loader from "../../dashboard/Loader";
import confetti from 'canvas-confetti'
import { IoClose } from "react-icons/io5";
import { IoMdHome } from "react-icons/io";
type Signer = {
id: number;
name: string;
email: string;
routingOrder: number;
completed: boolean;
};
// Signers List Component
const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
return (
<div className="bg-white rounded-md shadow-sm border border-gray-200">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<FaUsers className="text-blue-600" />
Signature Status
</h3>
<span className="text-sm text-gray-600">
{signers.filter(s => s.completed).length} of {signers.length} signed
</span>
</div>
<div className="space-y-3 max-h-[29vh] overflow-y-auto no-scrollbar">
{signers.map((signer) => (
<div key={signer.id} className="flex items-center gap-4 p-3 bg-gray-50 rounded-lg">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium">
{signer.name.split(" ").map(n => n[0].toUpperCase()).join("")}
</div>
<div className="flex-1">
<div className="font-medium text-gray-800">{signer.name}</div>
<div className="text-sm text-gray-600">{signer.email}</div>
<div className="text-xs text-gray-500">Order: {signer.routingOrder}</div>
</div>
<div className="flex items-center gap-2">
{signer.completed ? (
<div className="flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm">
<FaCheck className="w-3 h-3" />
Signed
</div>
) : (
<div className="flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-700 rounded-full text-sm">
<FaClock className="w-3 h-3" />
Pending
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
);
};
// Fixed Signing Order Diagram Component
const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
const groupByRoutingOrder = (data: Signer[]) => {
const grouped: Record<number, Signer[]> = {};
data.forEach((signer) => {
if (!grouped[signer.routingOrder]) {
grouped[signer.routingOrder] = [];
}
grouped[signer.routingOrder].push(signer);
});
return grouped;
};
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0].toUpperCase())
.join("");
};
const grouped = groupByRoutingOrder(signers);
const sortedOrders = Object.keys(grouped)
.map(Number)
.sort((a, b) => a - b);
const currentProcessingOrder =
sortedOrders.find((order) =>
grouped[order].some((signer) => !signer.completed)
) || -1;
return (
<div className="bg-white rounded-md shadow-sm border border-gray-200">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Signing Flow</h3>
<div className="overflow-x-auto overflow-y-auto max-h-96 no-scrollbar ">
<div className="flex justify-center items-center gap-4 min-w-max">
{/* Mail icon */}
<div className="flex flex-col items-center">
<div className="bg-blue-100 p-4 rounded-full">
<FaEnvelope className="text-blue-600 text-2xl" />
</div>
<p className="text-sm text-gray-600 mt-2">Sent</p>
</div>
{/* Dotted line */}
<div className="flex items-center">
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
</div>
{/* Signers by routing order */}
<div className="flex items-center justify-center space-x-6">
{sortedOrders.map((order, index) => (
<React.Fragment key={order}>
<div className="flex flex-col items-center space-y-4">
<div className="text-sm font-medium text-gray-600 mb-2">
Step {order}
</div>
<div className="flex flex-col items-center space-y-3">
{grouped[order].map((signer) => {
let bgColor = "";
let icon = null;
if (signer.completed) {
bgColor = "bg-[var(--primary)]";
icon = <FaCheck className="text-green-600" />;
} else if (order === currentProcessingOrder) {
bgColor = "bg-yellow-500";
icon = <FaClock className="text-yellow-800" />;
} else {
bgColor = "bg-gray-400";
icon = <FaLock className="text-gray-500" />;
}
return (
<div key={signer.id} className="relative">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${bgColor}`}
title={signer.name}
>
{getInitials(signer.name)}
<span className="absolute bottom-[-4px] right-[-8px] p-1 rounded-full bg-white shadow">
{icon}
</span>
</div>
<p className="mt-1 text-xs text-gray-600 text-center max-w-16 truncate">
{signer.name.split(" ")[0]}
</p>
</div>
);
})}
</div>
</div>
{index < sortedOrders.length - 1 && (
<div className="flex items-center">
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
</div>
)}
</React.Fragment>
))}
</div>
</div>
</div>
</div>
</div>
);
};
// Main DocumentCompletionScreen Component
const DocumentCompletionScreen = () => {
const documentList = [
{ title: 'Document 1', pages: 12 },
{ title: 'Document 2', pages: 8 },
{ title: 'Document 3', pages: 5 },
{ title: 'Document 4', pages: 22 },
{ title: 'Document 5', pages: 10 },
{ title: 'Document 6', pages: 15 },
{ title: 'Document 7', pages: 7 },
{ title: 'Document 8', pages: 9 },
{ title: 'Document 9', pages: 6 },
{ title: 'Document 10', pages: 11 },
{ title: 'Document 11', pages: 4 }, // Won't be shown due to slice
];
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const [showConfetti, setShowConfetti] = React.useState(true)
if (!accountId || !transactionId) {
window.location.href = "/";
return null;
}
// const transactionId = useSelector((state: RootState) => state.transaction.transactionId)
const navigate = useNavigate();
const dispatch = useDispatch();
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
useEffect(() => {
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
}, [])
useEffect(() => {
if (showConfetti) {
const duration = 3000;
const end = Date.now() + duration;
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
confetti({
particleCount: 80, // more particles
angle: Math.random() * 360, // fire in random direction
spread: 220, // wider spread
startVelocity: 15, // more power
scalar: 1.2, // bigger pieces (default is 1)
origin: {
x: Math.random(),
y: Math.random() * 0.6,
},
zIndex: 9999,
});
}, 350); // fire every 150ms
}
}, [showConfetti]);
return (
<>
<>
{
showConfetti && (
<div className="absolute top-0 left-0 w-full h-full z-[9999] bg-white/80 backdrop-blur-sm flex items-center justify-center">
{/* <Confetti width={width} height={height} /> */}
<div className="relative bg-white bg-opacity-90 backdrop-blur-lg border border-green-200 shadow-xl rounded-md px-8 py-6 w-[90%] max-w-sm text-center animate-fade-in-up">
{/* Close Icon */}
<button
onClick={() => setShowConfetti(false)} // or whatever logic hides the modal
className="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
aria-label="Close"
>
<IoClose className="text-2xl" />
</button>
{/* Success Icon */}
<FaCheckCircle className="text-green-500 text-5xl mb-4 mx-auto drop-shadow-lg" />
{/* Message */}
<h2 className="text-2xl font-semibold text-gray-800 mb-1">Transaction Successful</h2>
<p className="text-gray-600 text-sm">Your transaction has been created and saved successfully.</p>
</div>
</div>
)
}
</>
<div className="min-h-screen bg-[var(--mainViewBg)]">
{/* Top Banner */}
<div className="bg-[var(--primary)] text-white shadow-lg">
<div className="max-w-[97vw] mx-auto px-2 py-2.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link to='/' className="w-5 h-5 rounded-full flex items-center justify-center">
<IoMdHome className="text-xl" />
</Link>
<div className="w-5 h-5 bg-white/20 rounded-full flex items-center justify-center">
<FaCheck className="text-sm" />
</div>
<div>
<h2 className="text-lg font-bold text-white/80">Awesome! Your document is sent for signature</h2>
</div>
</div>
<Link to={`/accounts/${accountId}/transactions/${transactionId}`} className="flex items-center gap-1 cursor-pointer bg-white text-green-600 px-2.5 py-1 rounded-md font-semibold hover:bg-gray-100 transition-colors">
<FaPen className="text-[10px]" />
<span className="text-[13px]">Edit</span>
</Link>
</div>
</div>
</div>
{(selectedRecipients.length > 0 && selectedDocuments.length > 0) ?
<div className="p-3">
<div className="max-w-[97vw] mx-auto">
{/* Header */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
{/* Left Section - Document Details */}
<div className="lg:col-span-5 space-y-3">
{/* Document Preview Card */}
<div className="bg-white rounded-md shadow-sm border border-gray-200 overflow-hidden max-h-[88vh] no-scrollbar overflow-y-auto">
<div className="p-3 ">
{selectedDocuments?.map((item, index) => (
<div key={item.id} className="flex items-start justify-between mb-2 px-2 py-3 bg-gray-50">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
{/* <FaFileAlt className="text-blue-600 text-xl" /> */}
<img src={item.images[0].src} alt="document" className="h-10 w-10" />
</div>
<div>
<h3 className="text-md font-semibold text-gray-800">
Name : {item.name ?? '--'}
</h3>
<div className="text-gray-600 text-sm">
Status : {item.status ?? '--'}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Section - Signer Components */}
<div className="lg:col-span-7 space-y-1.5">
{/* Signing Order Diagram */}
<SigningOrderDiagram signers={selectedRecipients} />
{/* Signers List */}
<SignersList signers={selectedRecipients} />
</div>
</div>
</div>
</div>
: <Loader />}
</div>
</>
);
};
export default DocumentCompletionScreen;

View File

@ -53,10 +53,30 @@ const ProcessStepOne = () => {
}, [])
const handleNextClick = async (isSendNow: boolean) => {
if (!selectedDocuments.length || !selectedRecipients.length) {
let formError = {
documents: false,
recipients: false,
subject: false
}
if (!selectedDocuments.length) {
formError.documents = true
}
if (!selectedRecipients.length) {
formError.recipients = true
}
if (!emailDtls.subject.length) {
formError.subject = true
}
if (formError.documents || formError.recipients || formError.subject) {
console.log(selectedDocuments, 'line 193')
toast.dismiss();
toast.error("Add Documents | Recipients First", {
toast.error(`${formError.documents ? "Add Document" : ""}
${(formError.documents && formError.recipients) ? " | " : ""}
${formError.recipients ? "Add Recipient" : ""}
${(formError.documents || formError.recipients) ? " | " : ""}
${formError.subject ? "Add Subject" : ""}`, {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
@ -218,7 +238,8 @@ const ProcessStepOne = () => {
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
<input
type="text"
value={emailDtls.subject || "Documents for Signature"}
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)]"

View File

@ -32,7 +32,7 @@ const RecipientAddCard = () => {
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (recipient.firstName === "" || recipient.lastName === "" || recipient.email === "" || !recipient.type) return;
if (recipient.lastName === "" || recipient.email === "" || !recipient.type) return;
// Validate Email Format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@ -106,7 +106,7 @@ const RecipientAddCard = () => {
{/* Name Inputs */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input
required
// required
type="text"
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}

View File

@ -14,6 +14,8 @@ import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice';
import { fetchDocuments } from '../../../store/slices/documentSlice';
import { FaQuestionCircle } from 'react-icons/fa';
import typeToIconMap from '../../../helper/iconMap';
import { IoAdd } from 'react-icons/io5';
import Loader from '../../dashboard/Loader';
export interface DroppedItemsObj {
@ -211,7 +213,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
"dimensions": dropTag.dimensions,
"type": dropTag.type.toUpperCase(),
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
// validation: {
"minLength": "minLength" in dropTag ? dropTag.minLength : 0,
"maxLength": "maxLength" in dropTag ? dropTag.maxLength : 0,
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
@ -523,6 +524,11 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
})()
}
const handleAddRadioClick = (key:string) => {
console.log("Handle Radio Clicled", key)
console.log(droppedItems, "line 525")
}
// const handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => {
// // const dataToSet: DroppedItemsObj = {
// // ...droppedItems,
@ -747,21 +753,53 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
{/* Expand Circle - ENDS */}
{
item.type !== "radio" ?
<div
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
// style={{ width: `${item.width}px !important` }}
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
style={{ background: selectedRecipient[0]?.color }}
>
<p
className={`flex justify-center items-center w-full text-xl text-white h-full`}
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
>
{IconComponent}
</p>
</div>
:
<>
<div>
{/* Radio - Container - START */}
<div>
<div
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
// style={{ width: `${item.width}px !important` }}
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
style={{ background: selectedRecipient[0]?.color }}
>
<p
className={`flex justify-center items-center w-full text-xl text-white h-full`}
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
>
{IconComponent}
</p>
</div>
{/* Radio- Item - START */}
<div
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
// style={{ width: `${item.width}px !important` }}
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
style={{ background: selectedRecipient[0]?.color }}
>
<p
className={`flex justify-center items-center w-full text-xl text-white h-full`}
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
>
{IconComponent}
</p>
</div>
{/* Radio- Item - END */}
</div>
{/* Radio - Container - END */}
<IoAdd onClick={() => handleAddRadioClick(key)} className='ml-[50%] translate-x-[-50%] cursor-pointer' />
</div>
</>
}
</div>
)
}

View File

@ -10,7 +10,7 @@ import { MdKeyboardArrowDown } from 'react-icons/md'
import { RiResetLeftLine, RiResetRightLine } from 'react-icons/ri'
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice'
import { fetchDocuments } from '../../../store/slices/documentSlice'
import { useParams } from 'react-router-dom'
import { useNavigate, useParams } from 'react-router-dom'
interface NavTwoProps {
isFields: boolean;
@ -57,6 +57,8 @@ const NavTwo: React.FC<NavTwoProps> = ({ isFields, setIsFields, isSend, setIsSen
const [isFitDd, setIsFitDd] = useState<boolean>(false)
const [selFitIndx, setSelFitIndx] = useState<number>(0);
const navigate = useNavigate()
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
@ -76,6 +78,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ isFields, setIsFields, isSend, setIsSen
setIsSend(false);
}
setIsSend(!isSend)
navigate(`/document-completion-screen/${accountId}/${transactionId}`)
}
const handleSetIsFields = (setTo: boolean) => {

View File

@ -101,7 +101,7 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
<button onClick={() => handleMove('right')} disabled={disabled} className={buttonClass} title="Move right" type="button">
<FaArrowRight size={12} />
</button>
<span className="mt-1">Right: {Math.round(right)}px</span>
{/* <span className="mt-1">Right: {Math.round(right)}px</span> */}
</div>
{/* Bottom Row */}
@ -110,7 +110,7 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
<button onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move down" type="button">
<FaArrowDown size={12} />
</button>
<span className="mt-1">Bottom: {Math.round(bottom)}px</span>
{/* <span className="mt-1">Bottom: {Math.round(bottom)}px</span> */}
</div>
<div></div>
</div>

View File

@ -19,8 +19,7 @@ interface Account {
interface UserData {
id: string;
firstName: string;
lastName: string;
name: string;
email: string;
language: string;
account: Account;
@ -43,32 +42,35 @@ interface AccountOption {
const Setting = () => {
const { page1 } = useParams<{ page1: string }>();
console.log("page1" , page1)
console.log("page1", page1)
const location = useLocation();
console.log("location",location)
console.log("location", location)
const navigate = useNavigate();
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
// Refs for subsections within each main tab
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState("userProfile");
const [activeSubSection, setActiveSubSection] = useState<string>("");
const [user, setUser] = useState<UserData>({
id: "",
firstName: "",
lastName: "",
email: "",
language: "",
const [user, setUser] = useState<UserData>({
id: '',
name: '',
email: '',
language: '',
account: {
id: 0,
isDefault: false,
name: "",
name: '',
},
});
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
// Dropdown data states
const [accountOptions, setAccountOptions] = useState<AccountOption[]>([]);
const [accountOptionsLoading, setAccountOptionsLoading] = useState(false);
@ -203,8 +205,8 @@ const Setting = () => {
return <Language userData={user} />;
case "create and invite":
return (
<CreateAndInvite
onSubmit={handleSubmit}
<CreateAndInvite
onSubmit={handleSubmit}
createAndInviteLoader={createAndInviteLoader}
accountOptions={accountOptions}
accountOptionsLoading={accountOptionsLoading}
@ -240,18 +242,58 @@ const Setting = () => {
{ id: "contactShare", label: "Share Contact", type: "contact" },
];
const getUserDetails = async () => {
try {
let res = await api.get(`/api/user/details`)
console.log("getuser", res)
setUser(res?.data)
} catch (error) {
console.error("Error fetching user details:", error);
// Listen for sessionStorage changes
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'activeAccountId') {
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
setActiveAccountId(newActiveAccountId);
}
};
// Listen for storage events (works for changes from other tabs)
window.addEventListener('storage', handleStorageChange);
// Custom event listener for same-tab changes
const handleCustomStorageChange = (e: CustomEvent) => {
if (e.detail.key === 'activeAccountId') {
setActiveAccountId(e.detail.newValue);
}
};
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
};
}, []);
// Re-fetch dashboard data when activeAccountId changes
useEffect(() => {
const rawUser = JSON.parse(sessionStorage.getItem('userDetails') || 'null');
if (rawUser) {
const userData: UserData = {
id: String(rawUser.id || ''),
name: rawUser.name || '',
email: rawUser.email || '',
language: rawUser.language || '',
account: {
id: rawUser.account?.id || 0,
isDefault: rawUser.account?.isDefault || false,
name: rawUser.account?.name || '',
},
};
setUser(userData);
}
}
}, [activeAccountId]);
useEffect(() => {
getUserDetails()
// Fetch account list when component mounts
getAccountList()
}, [])
@ -259,7 +301,7 @@ const Setting = () => {
// Set initial subsection when component mounts or activeTab changes
useEffect(() => {
const subSections = getSubSections(activeTab);
console.log("subSections",subSections)
console.log("subSections", subSections)
const hash = location.hash.replace('#', '');
if (subSections.length > 0) {
@ -292,7 +334,7 @@ const Setting = () => {
setCreateAndInviteLoader(true)
try {
let res;
let toastMesaage ;
let toastMesaage;
if (action === 'create') {
console.log('Creating account:', data);
const payloadAccount = {
@ -310,10 +352,10 @@ const Setting = () => {
role: data.role
}
res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
console.log("InviteNAme",res)
console.log("InviteNAme", res)
toastMesaage = "Invite Successfull...!"
}
if (res?.data) {
toast.success(toastMesaage, {
position: "top-center",
@ -326,7 +368,7 @@ const Setting = () => {
theme: "light",
transition: Bounce,
});
// Refetch account list after successful submission
await getAccountList();
}
@ -340,7 +382,7 @@ const Setting = () => {
return (
<>
{/* <SettingNav /> */}
<AccountNav/>
{/* <AccountNav/> */}
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
{/* Main Navigation Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5">
@ -364,7 +406,7 @@ const Setting = () => {
</div>
{/* Content with Aside Navigation */}
<div className="flex mt-3 px-8 flex-1 min-h-0">
<div className="flex mt-3 px-8 flex-1 min-h-0">
{/* Fixed Aside Navigation - Shows subsections of active tab */}
{activeTab === 'userProfile' &&
<section className="flex-shrink-0 mr-6 w-50 relative">

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
import { MdCloudUpload } from 'react-icons/md';
import { MdCloudUpload } from 'react-icons/md';
import api from '../../../config/api';
import DrawSignatureCanvas from './DrawSignatureCanvas';
import { FaSpinner } from "react-icons/fa";
@ -54,7 +54,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
getAllSignatures,
editingSignature
}) => {
const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD'>('SELECT');
const [activeTab, setActiveTab] = useState<'TYPE' | 'DRAW' | 'UPLOAD' | 'SELECT'>('TYPE');
const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
@ -102,9 +102,10 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
// Navigation items
const navigationItems = [
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
{ id: 'TYPE', label: 'TYPE', icon: <GrCheckboxSelected className="w-4 h-4" /> },
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
];
// Initialize form with editing signature data
@ -112,7 +113,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
if (editingSignature) {
setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
setActiveTab(editingSignature.signatureType as 'SELECT' | 'DRAW' | 'UPLOAD');
setActiveTab(editingSignature.signatureType as 'TYPE' | 'DRAW' | 'UPLOAD');
// Set font for SELECT tab
const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
@ -262,7 +263,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
const renderContent = () => {
switch (activeTab) {
case 'SELECT':
case 'TYPE':
return (
<div className="h-full flex flex-col ">
<div className="mb-6">
@ -383,6 +384,16 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
</div>
);
case 'SELECT':
return (
<div className="h-full flex flex-col">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Select Signature</h2>
</div>
</div>
);
default:
return null;
}
@ -462,7 +473,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
{navigationItems.map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
onClick={() => setActiveTab(item.id as 'TYPE' | 'DRAW' | 'UPLOAD')}
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
? 'bg-blue-50 text-blue-700 border border-blue-200'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'

View File

@ -175,7 +175,7 @@ const SignatureManager: React.FC = () => {
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
DS
Initials
</div>
{/* Signature Image */}
@ -186,6 +186,10 @@ const SignatureManager: React.FC = () => {
alt="DS Signature"
/>
</div>
{/* Initial ID */}
<div className="text-xs text-gray-500 truncate">
{signature.initialsHash}
</div>
</div>
</div>

View File

@ -6,9 +6,8 @@ interface Account {
}
interface UserData {
id: string;
firstName: string;
lastName: string;
id: number;
name:string
email: string;
language: string;
account: Account;
@ -26,7 +25,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
{/* Profile Picture */}
<div className="flex-shrink-0 text-center">
<div className="w-20 h-20 bg-green-800 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
{getInitials(userData.firstName, userData.lastName)}
{userData.name.slice(0,2).toUpperCase()}
</div>
</div>
@ -36,7 +35,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
<div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
<div className="text-gray-400 font-medium text-[13px]">
{userData.firstName} {userData.lastName}
{userData.name}
</div>
</div>

View File

@ -0,0 +1,128 @@
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import { FaCheck, FaDatabase } from "react-icons/fa";
import AddAccountModal from "../accounts/AddAccountModal";
import { Bounce, toast } from "react-toastify";
import api from "../../config/api";
import { useState } from "react";
const Templates = () => {
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const handleAction = async (accountId: string, action: 'accept' | 'reject') => {
try {
let response = await api.post(`/api/accounts/${accountId}/accept/invite`)
console.log(response)
if (response.data.status === 202) {
toast.success("Login Successful", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
} catch (error) {
console.error('Error posting data:', error);
} finally {
setOpenMenuId(null); // close menu
}
};
return (
<>
<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 w-12"></th>
<th className="px-4 py-3 text-left">Name</th>
<th className="px-4 py-3 text-center">Owner</th>
<th className="px-4 py-3 text-center">Created Date</th>
<th className="px-4 py-3 text-center">Last Change</th>
<th className="px-4 py-3 text-center"></th>
<th className="px-4 py-3 text-center"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr
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">
<div className="flex items-center gap-2">
<span>{"test"}</span>
</div>
</td>
<td className="px-4 py-3 text-center">{"1"}</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded-xl text-xs font-medium`}>
{"role"}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded-xl text-xs font-medium`}>
{"text"}
</span>
</td>
<td>
</td>
<td className="px-4 py-3 text-center relative">
<button
className="mx-auto"
aria-label="Actions menu"
>
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black" />
</button>
{/* Dropdown menu */}
{/* {openMenuId === "1" && (
<div
className="absolute right-13 top-6 mt-1 w-18 bg-white border text-[13px] font-bold border-gray-300 rounded-md shadow-lg z-10"
onClick={(e) => e.stopPropagation()} // prevent row click closing
>
<button
className="w-full px-3 py-1 text-left text-green-700 hover:bg-green-100 rounded-t-md cursor-pointer"
onClick={() => handleAction(1, 'accept')}
>
Accept
</button>
<button
className="w-full px-3 py-1 text-left text-red-700 hover:bg-red-100 rounded-b-md cursor-pointer"
onClick={() => handleAction(1, 'reject')}
>
Reject
</button>
</div>
)} */}
</td>
</tr>
</tbody>
</table>
</div>
</>
);
};
export default Templates;

View File

@ -64,13 +64,14 @@ const TransactionView = () => {
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
{/* Header */}
<h1 className='text-lg font-semibold text-gray-800'>
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails.transaction.name ?? "Untitled"}</span>
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails?.transaction?.subject ?? "Untitled"}</span>
</h1>
<div className='mt-2'>
{/* new Date(transactionDetails?.transaction?.lastModifiedAt * 1000) */}
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
<p className='text-slate-600'>Last Change on <span>12/02/25</span></p>
<p className='text-slate-600'>Sent on <span>11/02/25</span></p>
<p className=''><span className='text-slate-600 font-semibold'>Last Change on</span> <span className='text-slate-400'>{new Date(transactionDetails?.transaction?.lastModifiedAt * 1000).toISOString()}</span></p>
<p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
</div>
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
@ -139,7 +140,7 @@ const TransactionView = () => {
<div>
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
<p className='mt-2 text-slate-600'>No Message Found</p>
<p className='mt-2 text-slate-600'>{ transactionDetails?.transaction?.message ?? "No Message Found...!"}</p>
</div>
</div>

View File

@ -29,7 +29,7 @@ api.interceptors.response.use(
const originalRequest = error.config;
// Prevent multiple refresh attempts
// if (error.response?.status === 401 || error.response?.status === 500) {
if (error.response?.status === 401) {
if (error.response?.status === 401 || error.response?.status === 500) {
originalRequest._retry = true;
try {
if (!isRefreshing) {

View File

@ -11,7 +11,9 @@ export default defineConfig({
'quality-friendly-goldfish.ngrok-free.app',
'officially-powerful-goat.ngrok-free.app',
'89f0-49-36-189-120.ngrok-free.app',
'c8ee-49-36-191-130.ngrok-free.app'
'c8ee-49-36-191-130.ngrok-free.app',
'c76f1251c173.ngrok-free.app',
'enabled-upright-boxer.ngrok-free.app'
],
},
build: {