Compare commits

...

29 Commits

Author SHA1 Message Date
=bhagwanSingh 313217eded d 2025-07-22 14:03:38 +05:30
=bhagwanSingh b6c08123f0 recipientSign 2025-07-22 13:34:28 +05:30
=bhagwanSingh b65614ae69 Merge remote-tracking branch 'origin/master' 2025-07-15 19:54:10 +05:30
=bhagwanSingh 8daf9d9a11 'a' 2025-07-15 18:39:56 +05:30
jaipandey 2cfe1ad364 Merge pull request 'completion-screen-2' (#5) from jai into master
Reviewed-on: http://git.hicglobalsolutions.in/BhawanSingh/SME-Web/pulls/5
2025-07-15 10:43:53 +00:00
JaiPandey425 5c3131f3c3 completion-screen-2 2025-07-15 16:12:17 +05:30
jaipandey 0cae5b1ffb Merge pull request 'jai' (#4) from jai into master
Reviewed-on: http://git.hicglobalsolutions.in/BhawanSingh/SME-Web/pulls/4
2025-07-15 10:08:58 +00:00
JaiPandey425 38370e73f9 completion-screen 2025-07-15 15:34:11 +05:30
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
JaiPandey425 7fffb22e09 session storage 2025-07-11 17:34:26 +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
JaiPandey425 8bb43d6ee5 Process step two , dashboard , navbar 2025-07-11 12:23:11 +05:30
=bhagwanSingh 8ec313e6d8 Merge2 2025-07-08 12:23:33 +05:30
=bhagwanSingh 54ee5e163d mergerd 2025-07-08 12:22:27 +05:30
=bhagwanSingh 1ecbca5fce a 2025-07-08 12:21:27 +05:30
jaipandey 063201751c Merge pull request 'Change the UI of Cards' (#2) from jai into master
Reviewed-on: http://git.hicglobalsolutions.in/BhawanSingh/SME-Web/pulls/2
2025-07-08 06:51:02 +00:00
JaiPandey425 73273865f4 Change the UI of Cards 2025-07-07 18:29:00 +05:30
jaipandey 8296cc8006 Merge pull request 'jai' (#1) from jai into master
Reviewed-on: http://git.hicglobalsolutions.in/BhawanSingh/SME-Web/pulls/1
2025-07-07 06:15:27 +00:00
JaiPandey425 2bb54b2043 Edit Signature and Account Card , Trans 2025-07-04 19:58:03 +05:30
JaiPandey425 da37cbe16a Edit Signature 2025-07-04 14:08:40 +05:30
JaiPandey425 261dc665c6 Update SettingNav.tsx 2025-07-04 12:19:51 +05:30
JaiPandey425 79b121c25b chnages 2025-07-04 12:02:45 +05:30
JaiPandey425 faa4839fef DrawSignature 2025-07-03 11:33:55 +05:30
45 changed files with 6024 additions and 1170 deletions

View File

@ -14,18 +14,12 @@ import SignDocument from "./components/client/SignDocument";
import Transactions from "./components/transactions/Transactions"; import Transactions from "./components/transactions/Transactions";
import ProtectedRoute from "./components/routes_helper/ProtectedRoute"; 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 Setting from "./components/setting/Setting";
import Accounts from "./components/accounts/Accounts"; import Accounts from "./components/accounts/Accounts";
import Account from "./components/accounts/Account"; import Account from "./components/accounts/Account";
import TransactionView from "./components/transactions/TransactionView"; import TransactionView from "./components/transactions/TransactionView";
import AccountTransactionWrapper from "./components/AccountTransactionWrapper"; import Templates from "./components/templates/Templates";
import SigningOrderDiagram from "./SigningOrderDiagram "; import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
const App = () => { const App = () => {
return ( return (
@ -41,33 +35,43 @@ const App = () => {
</ProtectedRoute> </ProtectedRoute>
}> }>
<Route path="/" element={<Home />} /> <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" element={<Accounts />} />
<Route path="/accounts/:accountId/transactions" element={<Account />} /> <Route path="/accounts/:accountId/transactions" element={<Account />} />
<Route path="/transactions" element={<Account />} />
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} /> <Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
<Route path="/transactions/:filterParam" element={<Transactions />} /> <Route path="/transactions/:filterParam" element={<Transactions />} />
</Route> <Route path="/templates" element={<Templates />} />
{/* </Route> */}
<Route path="/s/:page1" element={<Setting />} /> <Route path="/s/:page1" element={<Setting />} />
</Route> </Route>
<Route path="/process-step-one/:accountId/:transactionId" element={ <Route path="/process-step-one/:accountId/:transactionId" element={
<ProtectedRoute> <ProtectedRoute>
<ProcessStepOne /> <ProcessStepOne />
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/process-step-one/:accountId/template/:templateId" element={
<ProtectedRoute>
<ProcessStepOne />
</ProtectedRoute>
} />
<Route path="/process-step-two/:accountId/:transactionId" element={ <Route path="/process-step-two/:accountId/:transactionId" element={
<ProtectedRoute> <ProtectedRoute>
<ProcessStepTwo /> <ProcessStepTwo />
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/document-completion-screen/:accountId/:transactionId" element={
<ProtectedRoute>
<DocumentCompletionScreen />
</ProtectedRoute>
} />
{/* Links work wothout Login */} {/* Links work wothout Login */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />

View File

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

@ -4,6 +4,20 @@ 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 } from "react-icons/fa";
import Cards from "../home/Cards";
import AddInviteModal from "./AddInviteModal";
import { Bounce, toast } from "react-toastify";
import {
FaCheckCircle,
FaClock,
FaHourglassHalf,
FaTasks,
FaChevronDown,
FaPlus,
FaUserPlus,
FaFilter,
FaCalendarAlt
} from 'react-icons/fa';
interface Transaction { interface Transaction {
id: number; id: number;
@ -18,14 +32,173 @@ const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString(); return new Date(timestamp * 1000).toLocaleString();
}; };
const Account = () => { const Account = () => {
const { accountId } = useParams<{ accountId: string }>(); const mapApiDataToCards = (data: any): CardData[] => {
return [
{
title: 'Action Needed',
count: data.actionNeeded || 0,
icon: <FaTasks size={18} className="text-indigo-600" />,
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
shadowColor: 'shadow-indigo-200/50',
iconBg: 'bg-indigo-100 border-indigo-200',
textColor: 'text-indigo-700',
trend: 'up'
},
{
title: 'Expiring Soon',
count: data.expiring || 0,
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
shadowColor: 'shadow-rose-200/50',
iconBg: 'bg-rose-100 border-rose-200',
textColor: 'text-rose-700',
trend: 'up'
},
{
title: 'Completed',
count: data.completed || 0,
icon: <FaCheckCircle size={18} className="text-teal-600" />,
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
shadowColor: 'shadow-teal-200/50',
iconBg: 'bg-teal-100 border-teal-200',
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: 'draft',
count: data.draft || 0,
icon: <FaClock size={18} className="text-stone-600" />,
gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
shadowColor: 'shadow-stone-200/50',
iconBg: 'bg-stone-100 border-stone-200',
textColor: 'text-stone-700',
trend: 'down'
}
// Add more cards here if needed
];
};
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
console.log("activeAccountId Trans.", activeAccountId)
// const { accountId } = useParams<{ accountId: string }>();
const [transactions, setTransactions] = useState<Transaction[]>([]); const [transactions, setTransactions] = useState<Transaction[]>([]);
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]); const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [accountsData, setAccountsData] = useState([])
const [inviteLoading, setInviteLoading] = useState(false);
const [cards, setCards] = useState<CardData[]>([]);
const [accountID, setAccountID] = useState<number>()
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isModalOpen, setIsModalOpen] = useState(false);
// const getUserDetails = async () => {
// try {
// let res = await api.get('/api/user/details')
// let accountIDData = res?.data?.account?.id
// setAccountID(accountIDData)
// } catch (error) {
// console.log(error)
// }
// }
// useEffect(() => {
// 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);
};
}, []);
const getDashboardData = async () => {
setIsLoading(true)
const payload = {
account: activeAccountId
};
try {
const res = await api.get('/api/dashboard', { params: payload });
console.log("Dashboard Data:", res.data);
const cardDataArray = mapApiDataToCards(res.data);
setCards(cardDataArray);
} catch (error) {
console.error("Error fetching dashboard data:", error);
} finally {
setIsLoading(false)
}
};
const fetchTransactions = async () => { const fetchTransactions = async () => {
console.log("fetchTransactions")
try { try {
const response = await api(`/api/accounts/${accountId}/transactions`); const response = await api(`/api/accounts/${activeAccountId}/transactions`);
setTransactions(response.data || []); setTransactions(response.data || []);
} catch (error) { } catch (error) {
console.error("Error fetching transactions:", error); console.error("Error fetching transactions:", error);
@ -33,19 +206,80 @@ const Account = () => {
}; };
useEffect(() => { useEffect(() => {
if (accountId) fetchTransactions(); if (activeAccountId) {
}, [accountId]); getDashboardData()
fetchTransactions();
}
}, [activeAccountId]);
const handleOpenModal = () => {
setIsModalOpen(true);
};
// useEffect(() => {
// if (accountID) fetchTransactions();
// }, [accountID]);
// const getAccountDetails = async () => {
// try {
// let res = await api.get('/api/user/accounts')
// let items = res?.data
// setAccountsData(items)
// } catch (error) {
// console.log(error)
// }
// }
// useEffect(() => {
// getAccountDetails()
// }, [])
const navigate = useNavigate(); const navigate = useNavigate();
const handleTransactionClick = (id: number) => { const handleTransactionClick = (id: number) => {
navigate(`/accounts/${accountId}/transactions/${id}`); navigate(`/accounts/${activeAccountId}/transactions/${id}`);
}; };
const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
setInviteLoading(true);
try {
const payloadInvite = {
email: data.email,
role: data.role
}
let res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
if (res?.data) {
toast.success("Invite Sent Successfully...!", {
position: "top-center",
autoClose: 1500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
await fetchTransactions()
setIsModalOpen(false);
}
} catch (err) {
console.error('Failed to invite', err);
} finally {
setInviteLoading(false);
}
};
return ( return (
<> <>
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
isLoading={isLoading} />
{transactions.length > 0 && ( {transactions.length > 0 && (
<div className="overflow-x-auto py-4"> <div className="overflow-x-auto py-2">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm"> <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"> <thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
<tr> <tr>
@ -55,7 +289,8 @@ const Account = () => {
<th className="px-4 py-3 text-center">Active</th> <th className="px-4 py-3 text-center">Active</th>
<th className="px-4 py-3 text-center">Last Change</th> <th className="px-4 py-3 text-center">Last Change</th>
<th className="px-4 py-3 text-center">Expire After (days)</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> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
@ -92,6 +327,9 @@ const Account = () => {
<td className="px-4 py-3 text-center"> <td className="px-4 py-3 text-center">
{tx.expireAfter ?? "-"} {tx.expireAfter ?? "-"}
</td> </td>
<td className="px-4 py-3 text-center ">
{tx?.createdBy?.email === currentEmailId ? <button className="px-2 py-0.5 rounded-xl text-xs font-medium bg-green-100 text-green-700 cursor-pointer">Sign now</button> : "-"}
</td>
<td className="px-4 py-3 text-center"> <td className="px-4 py-3 text-center">
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" /> <PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
</td> </td>
@ -102,6 +340,8 @@ const Account = () => {
</table> </table>
</div> </div>
)} )}
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}
accountOptions={accountsData} />}
</> </>
); );
}; };

View File

@ -1,139 +1,405 @@
import React, { use, useEffect } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Link, useLocation, useParams } from 'react-router-dom'; import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
import { FaCog } from 'react-icons/fa';
import { IoIosNotifications } from 'react-icons/io';
import { RootState } from '../../store/store';
import { UserState } from '../../store/slices/userSlice';
import api from '../../config/api'; import api from '../../config/api';
// const fetchAccountData = async (accountId: string) => {
// try {
// if(!accountId) return null;
// const response = await api.get(`/api/accounts/${accountId}`);
// return response.data;
// } catch (error) {
// console.error("Error fetching account data:", error);
// throw error;
// }
// }
// const fetchTransactionData = async (accountId: string, transactionId: string) => {
// try {
// if(!accountId || !transactionId) return null;
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
// return response.data;
// } catch (error) {
// console.error("Error fetching account data:", error);
// throw error;
// }
// }
interface AccountNavProps { interface AccountNavProps {
accountName: string | null; accountName?: string | null;
transactionName: 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 AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transactionName = null }) => { const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transactionName = null }) => {
const user: UserState = useSelector((state: RootState) => state.user); const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
console.log("activeAccountId", activeAccountId);
const initials = user.username const [open, setOpen] = useState(false);
? user.username const [accountsOpen, setAccountsOpen] = useState(false);
.split(' ') const [loadingAccounts, setLoadingAccounts] = useState(false);
.map((n) => n[0]) const [accounts, setAccounts] = useState<AccountDataArray[]>([]);
.join('') const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
.slice(0, 2) const dropdownRef = useRef<HTMLDivElement>(null);
.toUpperCase()
: 'U';
// useEffect(() => { const [user, setUser] = useState<UserData>({
// if (accountId) { id: 0,
// api.get(`/api/accounts/${accountId}`) name: "",
// .then(response => { email: "",
// // Handle the account data if needed language: "",
// console.log(response.data, "Line 29"); account: {
// }) id: 0,
// .catch(error => { isDefault: false,
// console.error("Error fetching account data:", error); name: "",
// }); },
// } });
// }, [accountId, transactionId]);
const location = useLocation(); 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 pathnames = location.pathname const cleanedData: AccountDataArray[] = items.map((item: any) => ({
.split("/") userId: item.userId,
.filter((x) => x && isNaN(Number(x))); // remove numeric IDs userEmail: item.userEmail,
name: item.name,
accountId: item.accountId,
accountName: item.accountName,
role: item.role,
status: item.status,
}));
const labelMap = { setAccounts(cleanedData);
accounts: "Accounts", } catch (error) {
transactions: "Transactions", console.log(error);
// Add more routes here if needed }
};
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 ( 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-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
<nav className="text-md font-medium text-gray-800 capitalize"> {pathnames.length === 0 ? (
{pathnames.map((segment, index) => { <span className="text-gray-900 font-semibold">Dashboard</span>
const label = labelMap[segment] || segment; ) : (
pathnames.map((segment, index) => {
const label = labelMap[segment] || formatSegment(segment);
const to = "/" + pathnames.slice(0, index + 1).join("/"); const to = "/" + pathnames.slice(0, index + 1).join("/");
const isLast = index === pathnames.length - 1; const isLast = index === pathnames.length - 1;
return ( return (
<span key={to}> <span key={to} className="flex items-center">
{index > 0 && <span className="mx-2 text-gray-400">{">"}</span>}
{!isLast ? ( {!isLast ? (
<>
<Link <Link
to={to} to={to}
className="text-blue-600 hover:underline" className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
> >
{label} {label}
</Link> </Link>
<span className="mx-1">{">"}</span>
</>
) : ( ) : (
<span>{label}</span> <span className="text-gray-900 font-semibold">{label}</span>
)} )}
</span> </span>
); );
})} })
)}
</nav> </nav>
{/* <h1 className="text-md font-medium text-gray-800 capitalize">Account</h1> */} <div className="flex items-center space-x-2">
<div className="relative" ref={dropdownRef}>
<div className="flex items-center">
{/* Notification + Settings with smaller gap */}
<div className="flex items-center">
<button <button
aria-label="Notifications" className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600" onClick={handleMainDropdownToggle}
aria-expanded={open}
aria-haspopup="true"
> >
<IoIosNotifications className="text-xl" /> <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> </button>
<Link {open && (
to="/s/settings" <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">
aria-label="Settings" <div className="px-4 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b border-gray-100">
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600" <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">
<FaCog className="text-sm" /> {user ? (
</Link> <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>
{/* Avatar with separate spacing */} <div className="border-b border-gray-100">
<div className="w-9 h-9 ms-3 rounded-full overflow-hidden bg-[var(--primary)] flex items-center justify-center text-white font-semibold text-sm"> <button
{user.picture ? ( onClick={handleAccountsClick}
<img 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"
src={user.picture} >
alt="User avatar" <div className="flex items-center">
className="w-full h-full object-cover" <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>
) : ( ) : (
initials <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>
</div> </div>

View File

@ -6,6 +6,10 @@ import AccountNav from "./AccountNav";
import api from "../../config/api"; import api from "../../config/api";
import { AccountData, AccountStatus, UserRole } from "../../utlity/types"; import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Cards from "../home/Cards";
import AddAccountModal from "./AddAccountModal";
import { Bounce, toast } from "react-toastify";
import Loader from "../../helper/Loader";
// import { UserRole, AccountStatus, AccountData } from "../../types/account"; // import { UserRole, AccountStatus, AccountData } from "../../types/account";
@ -30,8 +34,16 @@ const getStatusLabel = (
}; };
const Accounts = () => { const Accounts = () => {
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]); const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]); const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [accounts, setAccounts] = useState<string[]>([]);
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const handleOpenModal = () => {
setIsModalOpen(true);
};
const navigate = useNavigate(); const navigate = useNavigate();
@ -53,8 +65,65 @@ const Accounts = () => {
navigate(`/accounts/${id}/transactions`); navigate(`/accounts/${id}/transactions`);
}; };
const handleAddAccount = async (accountName: string) => {
try {
const payloadAccount = {
name: accountName
}
let res = await api.post(`/api/user/accounts`, payloadAccount)
if (res?.data) {
toast.success("Account Add Successfully...!", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
await fetchAccounts()
setIsModalOpen(false);
}
} catch (error) {
console.log(error)
} finally {
}
};
const handleMenuToggle = (accountId: string) => {
setOpenMenuId(openMenuId === accountId ? null : accountId);
};
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 ( return (
<> <>
{/* <Cards type="account" onOpenModal={handleOpenModal} /> */}
{allAccounts.length > 0 && ( {allAccounts.length > 0 && (
<div className="overflow-x-auto py-2"> <div className="overflow-x-auto py-2">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm"> <table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
@ -101,8 +170,38 @@ const Accounts = () => {
{status.text} {status.text}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-center"> <td className="px-4 py-3 text-center relative">
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" /> <button
onClick={(e) => {
e.stopPropagation();
handleMenuToggle(account.accountId);
}}
className="mx-auto"
aria-label="Actions menu"
>
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black" />
</button>
{/* Dropdown menu */}
{openMenuId === account.accountId && (
<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(account.accountId, '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(account.accountId, 'reject')}
>
Reject
</button>
</div>
)}
</td> </td>
</tr> </tr>
); );
@ -111,6 +210,8 @@ const Accounts = () => {
</table> </table>
</div> </div>
)} )}
{isModalOpen && <AddAccountModal onClose={() => setIsModalOpen(false)}
onAddAccount={handleAddAccount} />}
</> </>
); );
}; };

View File

@ -0,0 +1,52 @@
import React, { useState } from 'react';
interface AddAccountModalProps {
onClose: () => void;
onAddAccount: (accountName: string) => void;
}
const AddAccountModal: React.FC<AddAccountModalProps> = ({ onClose, onAddAccount }) => {
const [inputValue, setInputValue] = useState('');
const handleAddClick = () => {
if (inputValue.trim()) {
onAddAccount(inputValue.trim());
setInputValue(''); // clear input after submit
}
};
return (
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-80">
<h3 className="text-lg font-medium text-gray-800 mb-4">Add New Account</h3>
<input
type="text"
placeholder="Enter account name"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="w-full mb-4 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setInputValue('');
onClose();
}}
className="px-4 py-1.5 text-[13px] cursor-pointer font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
Cancel
</button>
<button
onClick={handleAddClick}
className="px-2 py-1.5 cursor-pointer font-semibold bg-[var(--modalSubmitBG)] text-[13px] text-white hover:bg-[var(--modalSubmitBGHover)] rounded-md transition-colors"
disabled={!inputValue.trim()}
>
Add Account
</button>
</div>
</div>
</div>
);
};
export default AddAccountModal;

View File

@ -0,0 +1,214 @@
import { useRef, useState } from "react";
import { FaSpinner } from "react-icons/fa";
import Loader from "../../helper/Loader";
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
interface AddInviteModalProps {
onClose: () => void;
onInvite: (data: { i_accountName: number; email: string; role: string }) => void;
accountOptions: AccountOption[];
accountOptionsLoading: boolean;
inviteLoading: boolean;
}
const AddInviteModal: React.FC<AddInviteModalProps> = ({
onClose,
onInvite,
accountOptions,
accountOptionsLoading,
inviteLoading,
}) => {
const [inviteFormData, setInviteFormData] = useState({
i_accountName: 0,
email: "",
role: "",
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [accountSearchTerm, setAccountSearchTerm] = useState("");
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(null);
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
const [isRoleDropdownOpen, setIsRoleDropdownOpen] = useState(false);
const accountDropdownRef = useRef<HTMLDivElement>(null);
const roleDropdownRef = useRef<HTMLDivElement>(null);
const roles = ["ADMIN", "EDITOR", "VIEWER"];
// Handlers
const handleInviteChange = (field: keyof typeof inviteFormData, value: string) => {
setInviteFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
}
};
const handleAccountDropdownFocus = () => setIsAccountDropdownOpen(true);
const handleRoleDropdownFocus = () => setIsRoleDropdownOpen(true);
const handleDropdownBlur = (
e: React.FocusEvent<HTMLDivElement>,
ref: React.RefObject<HTMLDivElement>,
setOpen: (open: boolean) => void
) => {
setTimeout(() => {
const active = document.activeElement as HTMLElement;
if (ref.current && !ref.current.contains(active)) {
setOpen(false);
}
}, 100);
};
const handleAccountSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setAccountSearchTerm(val);
setSelectedAccount(null);
setInviteFormData((prev) => ({ ...prev, i_accountName: 0 }));
};
const handleAccountSelect = (option: AccountOption) => {
setSelectedAccount(option);
setAccountSearchTerm(option.accountName);
setInviteFormData((prev) => ({ ...prev, i_accountName: option.accountId }));
setIsAccountDropdownOpen(false);
setErrors((prev) => ({ ...prev, i_accountName: "" }));
};
const handleRoleSelect = (role: string) => {
setInviteFormData((prev) => ({ ...prev, role }));
setIsRoleDropdownOpen(false);
setErrors((prev) => ({ ...prev, role: "" }));
};
const handleSubmit = () => {
const { i_accountName, email, role } = inviteFormData;
const errs: { [key: string]: string } = {};
if (!i_accountName) errs.i_accountName = "Account is required";
if (!email.trim()) errs.email = "Email is required";
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = "Invalid email";
if (!role.trim()) errs.role = "Role is required";
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
onInvite(inviteFormData);
};
const filteredOptions = accountOptions.filter((option) =>
option.accountName.toLowerCase().includes(accountSearchTerm.toLowerCase())
);
return (
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-[26rem]">
<h3 className="text-lg font-medium text-gray-800 mb-4">Send Invite</h3>
{/* Account Name Dropdown */}
<div className="mb-3 relative" onBlur={(e) => handleDropdownBlur(e, accountDropdownRef, setIsAccountDropdownOpen)}>
<label className="text-sm font-medium text-gray-600 mb-1 block">Account</label>
<div ref={accountDropdownRef}>
<input
type="text"
placeholder={accountOptionsLoading ? "Loading..." : "Select or search account"}
value={accountSearchTerm}
onChange={handleAccountSearchChange}
onFocus={handleAccountDropdownFocus}
disabled={accountOptionsLoading}
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-60 disabled:cursor-not-allowed"
/>
{isAccountDropdownOpen && !accountOptionsLoading && (
<ul className="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 shadow-lg max-h-40 overflow-auto">
{filteredOptions.length > 0 ? (
filteredOptions.map((opt) => (
<li
key={opt.accountId}
className="px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
onMouseDown={() => handleAccountSelect(opt)}
>
{opt.accountName}
</li>
))
) : (
<li className="px-3 py-2 text-gray-500 text-sm">No results found</li>
)}
</ul>
)}
</div>
{errors.i_accountName && <p className="text-sm text-red-500 mt-1">{errors.i_accountName}</p>}
</div>
{/* Email Field */}
<div className="mb-3">
<label className="text-sm font-medium text-gray-600 mb-1 block">Email</label>
<input
type="email"
placeholder="Enter email"
value={inviteFormData.email}
onChange={(e) => handleInviteChange("email", e.target.value)}
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-60 disabled:cursor-not-allowed"
/>
{errors.email && <p className="text-sm text-red-500 mt-1">{errors.email}</p>}
</div>
{/* Role Dropdown */}
<div className="mb-4 relative" onBlur={(e) => handleDropdownBlur(e, roleDropdownRef, setIsRoleDropdownOpen)}>
<label className="text-sm font-medium text-gray-600 mb-1 block">Role</label>
<div ref={roleDropdownRef}>
<input
type="text"
placeholder="Select role"
value={inviteFormData.role}
readOnly
onFocus={handleRoleDropdownFocus}
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-60 disabled:cursor-not-allowed"
/>
{isRoleDropdownOpen && (
<div className="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 shadow-lg">
{roles.map((role) => (
<div
key={role}
className="px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
onMouseDown={() => handleRoleSelect(role)}
>
{role}
</div>
))}
</div>
)}
</div>
{errors.role && <p className="text-sm text-red-500 mt-1">{errors.role}</p>}
</div>
{/* Buttons */}
<div className="flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-1.5 text-[13px] font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmit}
className="px-4 py-1.5 text-[13px] font-semibold bg-[var(--modalSubmitBG)] text-white hover:bg-[var(--modalSubmitBGHover)] rounded-md transition-colors flex items-center gap-1"
disabled={inviteLoading}
>
{inviteLoading && <FaSpinner className="animate-spin text-sm" />}
Invite
</button>
</div>
</div>
</div>
);
};
export default AddInviteModal;

View File

@ -0,0 +1,63 @@
import React from 'react'
import { MdKeyboardArrowDown } from 'react-icons/md';
interface DocumentPagesInterface {
selectedDocuments: DocumentType[];
setDropDownTogglePage: React.Dispatch<React.SetStateAction<string>>;
dropDownTogglePage: string;
}
const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownTogglePage }: DocumentPagesInterface) => {
return (
<div className='col-span-2 bg-white h-full p-4 overflow-y-auto'>
<div className="flex flex-col gap-2">
{selectedDocuments && selectedDocuments.length && selectedDocuments.map((document, indx) => {
return (
<div
key={indx}
// tabIndex={1}
// onBlur={() => setDropDownTogglePage('')}
>
<div
onClick={() => (`${(dropDownTogglePage === 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`)}
className={`p-2 ${(dropDownTogglePage === 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
{
document.images != undefined && <img
key={indx}
className='rounded-sm shadow'
src={document.images[0].src}
alt="" />
}
<div
className={`flex items-center justify-between w-full text-xs rounded-sm cursor-pointer ${(dropDownTogglePage === 'pageDropDown' + indx) ? "text-slate-100" : "text-slate-600"}`}>
<p className={`whitespace-nowrap overflow-hidden text-ellipsis max-w-[18ch]`}>{document.name}</p>
<MdKeyboardArrowDown className={`${(dropDownTogglePage === 'pageDropDown' + indx) ? 'rotate-180' : ''}`} />
</div>
</div>
{
dropDownTogglePage === 'pageDropDown' + indx &&
<div className='my-1 flex justify-center items-center gap-1 flex-wrap'>
{document.images != undefined && document.images.map((img, indx) => (
<a
href={"#" + document.documentId + indx}
className='max-w-max px-4 py-2 shadow cursor-pointer border-1 rounded-sm border-slate-200 hover:border-[var(--primary)] transition-all duration-300 bg-slate-300'
>
<p className='text-center text-xs text-slate-600'>{indx + 1}</p>
</a>
))}
</div>
}
</div>
)
})}
</div>
</div>
)
}
export default DocumentPages

View File

@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
import axios from "axios" import axios from "axios"
import { RootState } from "../../store/store"; import { RootState } from "../../store/store";
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { DroppedItem } from "../../utlity/types"; import { DroppedItem, DroppedItemsObj } from "../../utlity/types";
import Loader from "../dashboard/Loader"; import Loader from "../dashboard/Loader";
import SignCon from "../signature/SignCon"; import SignCon from "../signature/SignCon";
@ -11,7 +11,7 @@ import { Bounce, toast } from "react-toastify";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { hideLoader, showLoader } from "../../store/slices/loaderSlice"; import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
import { FaRegImage, FaFileAlt, FaSignature, FaStamp } from 'react-icons/fa' import { FaRegImage, FaFileAlt, FaSignature, FaStamp, FaCheck, FaQuestionCircle } from 'react-icons/fa'
import { IoIosDocument } from 'react-icons/io' import { IoIosDocument } from 'react-icons/io'
import { MdKeyboardArrowDown } from 'react-icons/md' import { MdKeyboardArrowDown } from 'react-icons/md'
import { TbChevronsDownLeft, TbSignature } from "react-icons/tb"; import { TbChevronsDownLeft, TbSignature } from "react-icons/tb";
@ -20,6 +20,10 @@ import dayjs from "dayjs";
import maskUtils from "../../utlity/maskUtils"; import maskUtils from "../../utlity/maskUtils";
import StampUpload from "../stamp/StampUpload"; import StampUpload from "../stamp/StampUpload";
import TermsModal from "./TermsModal"; import TermsModal from "./TermsModal";
import DocumentPages from "./DocumentPages";
import FieldList from "../process/processStepTwo/FieldList";
import typeToIconMap from "../../helper/iconMap";
import { droppedTagsDefaultField } from "../process/processStepTwo/DocumentContainer";
@ -100,6 +104,30 @@ export interface StampTypeObj {
blob: string; blob: string;
} }
interface FIT {
label: string,
value: number | null
}
const fitObj = [
{
label: "Fit",
value: "FIT"
},
{
label: "50%",
value: "50"
},
{
label: "150%",
value: "150"
},
{
label: "200%",
value: "200"
},
]
// Functions - LIST // Functions - LIST
// handleSignClick - Handle when sign || Initial tag got clicked // handleSignClick - Handle when sign || Initial tag got clicked
// fetchData - Fetch document images and tags data // fetchData - Fetch document images and tags data
@ -115,9 +143,12 @@ const SignDocument = () => {
const [tagJson, setTagJson] = useState<DroppedItem[] | []>([]); const [tagJson, setTagJson] = useState<DroppedItem[] | []>([]);
const [transaction, setTransaction] = useState<TransactionType | null>(null); const [transaction, setTransaction] = useState<TransactionType | null>(null);
const [recSignStatus, setRecSignStatus] = useState<string | null>(null); const [recSignStatus, setRecSignStatus] = useState<string | null>(null);
const [droppedItems, setDroppedItems] = useState([]);
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" }); const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
console.log(recipient, "recipient Line 145")
// Handling Date Opacity // Handling Date Opacity
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({}); const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
const [isDateOpen, setIsDateOpen] = useState<string | null>(null); const [isDateOpen, setIsDateOpen] = useState<string | null>(null);
@ -126,6 +157,9 @@ const SignDocument = () => {
// Show is fields - Showing all fields There // Show is fields - Showing all fields There
const [isFields, setIsFields] = useState<boolean>(true); const [isFields, setIsFields] = useState<boolean>(true);
const [isFitDd, setIsFitDd] = useState<boolean>(false)
const [selFitIndx, setSelFitIndx] = useState<number>(0);
const globalLoader = useSelector((state: RootState) => state.loader) const globalLoader = useSelector((state: RootState) => state.loader)
const [openSign, setOpenSign] = useState<boolean>(false); const [openSign, setOpenSign] = useState<boolean>(false);
@ -146,6 +180,21 @@ const SignDocument = () => {
const [signConfObj, setSignConfObj] = useState<SignConfTypeObj | {}>({}); const [signConfObj, setSignConfObj] = useState<SignConfTypeObj | {}>({});
const zoom = (percentage: number, contentClass: string) => {
const content = document.querySelector(`.${contentClass}`);
if (content) {
(content as HTMLElement).style.zoom = `${percentage}%`;
}
}
useEffect(() => {
let indx = fitObj[selFitIndx].value
if (indx === "FIT") {
indx = "100";
}
zoom(Number(indx), 'zoom-con')
}, [selFitIndx])
useEffect(() => { useEffect(() => {
dispatch(showLoader("Loading ...")); dispatch(showLoader("Loading ..."));
fetchData(); fetchData();
@ -153,18 +202,19 @@ const SignDocument = () => {
useEffect(() => { useEffect(() => {
if (!focusElement) { if (!focusElement) {
if (tagJson && tagJson.length) { if (droppedItems && droppedItems.length) {
setFocusElement({ ...tagJson[0], focusElementIndx: 0 }); setFocusElement({ ...droppedItems[0], focusElementIndx: 0 });
} }
} }
}, [tagJson]) }, [droppedItems])
console.log(saved, "Line 162") console.log(saved, "Line 162")
useEffect(() => { useEffect(() => {
if (recSignStatus === "COMPLETE") { if (recSignStatus === "COMPLETED") {
console.log("SETTING HIDE TAGS to TRUE LIne 161") console.log("SETTING HIDE TAGS to TRUE LIne 161")
setHideTags(true); // setHideTags(true);
setTransparentTags(true);
} else { } else {
console.log("SETTING HIDE TAGS to TRUE LIne 161") console.log("SETTING HIDE TAGS to TRUE LIne 161")
setHideTags(false); setHideTags(false);
@ -239,9 +289,12 @@ const SignDocument = () => {
.then((res) => { .then((res) => {
console.dir(res) console.dir(res)
if (res.status === 200) { if (res.status === 200) {
console.log(res, "Line 288")
setSelectedDocuments(res.data.documents) setSelectedDocuments(res.data.documents)
setRecipient(res.data.recipient) setRecipient(res.data.recipient)
setTagJson(res.data.tagJsons) setDroppedItems(res.data.tagJsons)
setTransaction(res.data.transaction) setTransaction(res.data.transaction)
setRecSignStatus(res.data.status) setRecSignStatus(res.data.status)
@ -258,7 +311,7 @@ const SignDocument = () => {
} }
console.log(tagJson, "Line 261") console.log(droppedItems, "Line 261")
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@ -266,7 +319,7 @@ const SignDocument = () => {
tags: [] tags: []
} }
if (!tagJson) return; if (!droppedItems) return;
// const form = e.currentTarget; // const form = e.currentTarget;
// const formData = new FormData(form); // const formData = new FormData(form);
@ -278,8 +331,8 @@ const SignDocument = () => {
// }); // });
let signInitialsCount = 0; let signInitialsCount = 0;
for (let i = 0; i < tagJson.length; i++) { for (let i = 0; i < droppedItems.length; i++) {
if (tagJson[i].type === "SIGNATURE" || tagJson[i].type === "INITIALS") { if (droppedItems[i].type === "SIGNATURE" || droppedItems[i].type === "INITIALS") {
signInitialsCount += 1; signInitialsCount += 1;
} }
} }
@ -301,7 +354,7 @@ const SignDocument = () => {
return; return;
} }
for (const tag of tagJson) { for (const tag of droppedItems) {
console.log(tag, "Tag Data") console.log(tag, "Tag Data")
// if ((tag.type === "SIGNATURE" || tag.subType === "INITIALS") && !tag.value) { // if ((tag.type === "SIGNATURE" || tag.subType === "INITIALS") && !tag.value) {
@ -447,19 +500,37 @@ const SignDocument = () => {
// } // }
// } // }
const fetchSignatureData = async (signatureId: string) => {
try {
const response = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/signatures/${signatureId}`, {
headers: { "ngrok-skip-browser-warning": "69420" }
});
if (response.status === 200) {
return response.data.signature;
}
} catch (error) {
console.error("Error fetching signature data:", error);
}
return null;
}
useEffect(() => { useEffect(() => {
if (tagJson) { (async () => {
for (let i = 0; i < tagJson?.length; i++) { if (droppedItems) {
for (const tag of droppedItems) {
if (tag.type === "SIGNATURE" && "value" in tag) {
const signatureData = await fetchSignatureData(tag.value);
console.log(signatureData);
} }
} }
}, [tagJson]) }
})()
}, [droppedItems])
const handleTagUpdate = (updateIndx: number, updateValue: string) => { const handleTagUpdate = (updateIndx: number, updateValue: string) => {
if (tagJson && tagJson.length) { if (droppedItems && droppedItems.length) {
const updatedTag = tagJson.map((val, indx) => { const updatedTag = droppedItems.map((val, indx) => {
if (indx === updateIndx) { if (indx === updateIndx) {
return { return {
...val, ...val,
@ -468,7 +539,7 @@ const SignDocument = () => {
} }
return val; return val;
}) })
setTagJson(updatedTag); setDroppedItems(updatedTag);
} }
// setTagJson((prev) => { // setTagJson((prev) => {
@ -480,6 +551,198 @@ const SignDocument = () => {
// }); // });
} }
// HANDLE DROP - START
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
console.log("// HANDLE DROP - START")
event.preventDefault();
const data = window.dragElemData;
if (!data) return;
const dragElemData = JSON.parse(data); // Get the dropped label
const field = { ...dragElemData }
if (!field.label) return;
// const field = fields.find((item) => item.label === dragElemData.label); // Find corresponding field
// if (!field) return; // If not found, exit
const dropZone = event.currentTarget.getBoundingClientRect();
const x = event.clientX - dropZone.left - dragElemData.offsetX;
const y = event.clientY - dropZone.top - dragElemData.offsetY;
const key = `${docId}_${pageIndex}`;
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
let prepData = {
properties: data.properties,
icon: typeToIconMap[field.label.toUpperCase()] || <FaQuestionCircle />,
type: dragElemData.label.toUpperCase(),
subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '',
label: dragElemData.label,
dimensions: {
left: x,
top: y,
width: dragElemData.width,
height: dragElemData.height,
},
styles: dragElemData.label === "text" ? {
"font": "Great Vibes",
"italic": false,
"bold": false,
"underline": false,
"color": "black",
"textSize": 14.0
} : null,
documentId: docId,
pageIndex: pageIndex,
recipientId: dragElemData.recipientId,
recipientIndx: dragElemData.recipientIndx,
update: true,
isDeleted: false
}
if ("group" in dragElemData) {
prepData.group = dragElemData.group;
prepData.isGroupExist = false;
// if(uniqGroupIds.includes(dragElemData.group)) {
// prepData.isGroupExist = true;
// } else {
// prepData.isGroupExist = false;
// setUniqGroupIds([...uniqGroupIds, dragElemData.group]);
// }
}
if (prepData.type === "temporal") {
prepData.currentTimeStamp = true;
}
if (prepData.type.toLowerCase() === "number") {
prepData.decimalPlace = 0;
prepData.isCurrency = false;
}
// const dataToSet: DroppedItemsObj = {
// ...droppedItems,
// [key]: [...(droppedItems[key] || []),
// prepData],
// }
// const dataToSet: DroppedItemsObj = {
// ...droppedItems,
// [key]: [...(droppedItems[key] || []),
// {
// ...prepData,
// radioGroups: dragElemData.label === "radio" ?
// [
// {
// ...prepData,
// dimensions: {
// ...prepData.dimensions, top: 8, left: 10
// }
// },
// {
// ...prepData, dimensions: {
// ...prepData.dimensions,
// top: 8,
// left: 10
// }
// }
// ]
// :
// []
// }],
// }
// if (dragElemData.label === "radio") {
// setDroppedItems(dataToSet);
// } else {
// setDroppedItems(dataToSet);
// }
setDroppedItems((prev) => [...prev, prepData]);
// setDroppedItemsCounter((prev) => prev + 1);
// setTimeout(() => {
// const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
// handleTagClick(key, tagIndex)
// }, 0)
};
// HANDLE DROP - END
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Allows the drop event to work
};
// HANDLE TAG MOUSE DOWN - START
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
event.preventDefault();
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
let targetElement = event?.currentTarget;
if (!targetContainer || !targetElement) return;
const containerRect = targetContainer.getBoundingClientRect();
const elementRect = targetElement.getBoundingClientRect();
// Calculate the initial offset (where the mouse was clicked inside the element)
const offsetX = event.clientX - elementRect.left;
const offsetY = event.clientY - elementRect.top;
const conScrollWidth = targetContainer?.scrollWidth;
const conScrollHeight = targetContainer?.scrollHeight;
let isGroupAttr = event.currentTarget.hasAttribute("group");
let groupId = null;
if (isGroupAttr) {
groupId = event.currentTarget.getAttribute("group");
}
let groupElements: NodeListOf<HTMLElement> | null = null
if (isGroupAttr && groupId) {
groupElements = document.querySelectorAll(`div[group="${groupId}"]`);
}
const handleMouseMove = (event: MouseEvent) => {
// Get new X and Y positions relative to the container
let left = event.clientX - containerRect.left - offsetX;
let top = event.clientY - containerRect.top - offsetY;
// **Restrict movement within container bounds**
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
console.log(left, top, "LEFTTOP")
if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
// Apply the movement to the element
setDroppedItems(
droppedItems.map((item, i) =>
i === index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item
)
);
};
const handleMouseUp = () => {
document.removeEventListener("pointermove", handleMouseMove);
document.removeEventListener("pointerup", handleMouseUp);
};
// Add event listeners
document.addEventListener("pointermove", handleMouseMove);
document.addEventListener("pointerup", handleMouseUp);
};
// HANDLE TAG MOUSE DOWN - END
console.log(droppedItems, "DROPPED ITEMS LINE 680")
// const [showTerms, setShowTerms] = useState<boolean>(true); // const [showTerms, setShowTerms] = useState<boolean>(true);
return ( return (
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}> <div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>
@ -498,6 +761,40 @@ const SignDocument = () => {
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'> <div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide font-semibold text-[1rem] text-[var(--primary)]'>Sign Made Easy</p> <p className='tracking-wide font-semibold text-[1rem] text-[var(--primary)]'>Sign Made Easy</p>
</div> </div>
{/* FIT - START */}
<div
tabIndex={1}
onBlur={() => setIsFitDd(false)}
>
<div
onClick={() => setIsFitDd(!isFitDd)}
className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<p className='tracking-wide'>Fit</p>
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
</div>
{isFitDd && <div className='absolute bg-white rounded-sm z-50'>
{fitObj.map((val, indx) => {
return (<div
key={indx}
onClick={() => setSelFitIndx(indx)}
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
<div
className='flex items-center justify-between w-full'>
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
</div>
</div>)
})
}
</div>}
</div>
{/* FIT - END */}
</div> </div>
@ -547,9 +844,6 @@ const SignDocument = () => {
<p>{recipient && recipient.firstName} {recipient && recipient.lastName}</p> <p>{recipient && recipient.firstName} {recipient && recipient.lastName}</p>
</div> </div>
{/* Right Side - ENDS */} {/* Right Side - ENDS */}
</div> </div>
@ -560,65 +854,23 @@ const SignDocument = () => {
{/* Left Side pages start */} {/* Left Side pages start */}
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */} {/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
<div className='col-span-2 bg-white h-full p-4 overflow-y-auto'> <div className='col-span-2 bg-white h-full p-4 overflow-y-auto'>
{recipientId && <FieldList color={"#8f87f1"} recipientId={Number(recipientId)} recipientIndx={0} acntId={"1"} />}
<div className="flex flex-col gap-2">
{selectedDocuments && selectedDocuments.length && selectedDocuments.map((document, indx) => {
return (
<div
key={indx}
// tabIndex={1}
// onBlur={() => setDropDownTogglePage('')}
>
<div
onClick={() => setDropDownTogglePage(`${(dropDownTogglePage === 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`)}
className={`p-2 ${(dropDownTogglePage === 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
{
document.images != undefined && <img
key={indx}
className='rounded-sm shadow'
src={document.images[0].src}
alt="" />
}
<div
className={`flex items-center justify-between w-full text-xs rounded-sm cursor-pointer ${(dropDownTogglePage === 'pageDropDown' + indx) ? "text-slate-100" : "text-slate-600"}`}>
<p className={`whitespace-nowrap overflow-hidden text-ellipsis max-w-[18ch]`}>{document.name}</p>
<MdKeyboardArrowDown className={`${(dropDownTogglePage === 'pageDropDown' + indx) ? 'rotate-180' : ''}`} />
</div>
</div> </div>
{
dropDownTogglePage === 'pageDropDown' + indx &&
<div className='my-1 flex justify-center items-center gap-1 flex-wrap'>
{document.images != undefined && document.images.map((img, indx) => (
<a
href={"#" + document.documentId + indx}
className='max-w-max px-4 py-2 shadow cursor-pointer border-1 rounded-sm border-slate-200 hover:border-[var(--primary)] transition-all duration-300 bg-slate-300'
>
<p className='text-center text-xs text-slate-600'>{indx + 1}</p>
</a>
))}
</div>
}
</div>
)
})}
</div>
</div>
{/* Pages - END */} {/* Pages - END */}
{/* Main container */} {/* Main container */}
<div <div
id="doc-con" id="doc-con"
className="relative col-span-10 px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth" className="zoom-con relative col-span-8 px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth"
> >
{ {
selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => { selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => {
console.log(tagJson, "tagJsontagJsontagJson") console.log(droppedItems, "tagJsontagJsontagJson")
return ( return (
<div key={doc.documentId} className='flex flex-col'> <div key={doc.documentId} className='flex flex-col'>
<div className='py-2 text-sm'> <div className='py-2 text-sm'>
@ -627,16 +879,20 @@ const SignDocument = () => {
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'> <div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'>
{ {
tagJson && tagJson.length > 0 && doc.images && doc.images.length > 0 && doc.images.map((page, indx) => { doc.images && doc.images.length > 0 && doc.images.map((page, indx) => {
const key = `${doc.documentId}_${page.index}`; const key = `${doc.documentId}_${page.index}`;
const currDocTags = tagJson.filter((val) => val.pageIndex === page.index) console.log(droppedItems, "line 820")
console.log(currDocTags, tagJson, page, doc, "innnnnnnnnnnnn") // const tagJsonData = [...droppedItems];
// console.log(tagJsonData, "Line 821")
const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index)
console.log(currDocTags, droppedItems, page, doc, "innnnnnnnnnnnn")
return ( return (
<div <div
key={indx} key={indx}
id={doc.documentId + indx} id={doc.documentId + indx}
// onDrop={(e) => handleDrop(e, doc.documentId, doc.documentId, page.index)} onDrop={(e) => handleDrop(e, doc.documentId, doc.documentId, page.index)}
// onDragOver={handleDragOver} onDragOver={(e) => handleDragOver(e)}
className='drop-page-in shadow-md relative' className='drop-page-in shadow-md relative'
> >
@ -644,13 +900,15 @@ const SignDocument = () => {
{currDocTags.map((item, index) => { {currDocTags.map((item, index) => {
console.log(item, "micCheck 842")
if (item.pageIndex !== page.index) return; if (item.pageIndex !== page.index) return;
return ( return (
<div <div
tabIndex={1} tabIndex={1}
onFocus={() => setFocusElement({ ...item, focusElementIndx: index })} onFocus={() => setFocusElement({ ...item, focusElementIndx: index })}
key={item.id} key={item.id}
draggable="false" // draggable="false"
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
className="absolute rounded dropped-item text-sm" className="absolute rounded dropped-item text-sm"
// onClick={() => handleTagClick(key, index)} // onClick={() => handleTagClick(key, index)}
style={{ style={{
@ -668,7 +926,7 @@ const SignDocument = () => {
<div <div
id={item.id} id={item.id}
tabIndex={1} tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-none" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`} className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
// style={{ height: item.dimensions.height + "px" }} // style={{ height: item.dimensions.height + "px" }}
> >
@ -967,7 +1225,7 @@ const SignDocument = () => {
} }
{/* tag pagination Start */} {/* tag pagination Start */}
{!transparentTags && !hideTags && <div className="sticky rounded-md bottom-2 left-[50%] translate-[-50%] max-w-max flex bg-slate-900 text-white text-sm opacity-70"> {/* {!transparentTags && !hideTags && <div className="sticky rounded-md bottom-2 left-[50%] translate-[-50%] max-w-max flex bg-slate-900 text-white text-sm opacity-70">
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-l-md transition-all duration-150 " <div className="px-3 py-1 cursor-pointer hover:bg-black rounded-l-md transition-all duration-150 "
onClick={() => handleTagNavClick("P")} onClick={() => handleTagNavClick("P")}
>Prev</div> >Prev</div>
@ -975,10 +1233,14 @@ const SignDocument = () => {
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-r-md transition-all duration-150 " <div className="px-3 py-1 cursor-pointer hover:bg-black rounded-r-md transition-all duration-150 "
onClick={() => handleTagNavClick("N")} onClick={() => handleTagNavClick("N")}
>Next</div> >Next</div>
</div>} </div>} */}
</div> </div>
<DocumentPages selectedDocuments={selectedDocuments} setDropDownTogglePage={setDropDownTogglePage} dropDownTogglePage={dropDownTogglePage} />
{/* Right Side - START */} {/* Right Side - START */}
{/* <div className="col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"> {/* <div className="col-span-2 bg-slate-100 h-full p-4 overflow-y-auto">
{ {

View File

@ -17,6 +17,7 @@ import confetti from 'canvas-confetti'
import { useWindowSize } from 'react-use' import { useWindowSize } from 'react-use'
import { FaCheckCircle } from 'react-icons/fa' import { FaCheckCircle } from 'react-icons/fa'
import { IoClose } from 'react-icons/io5' import { IoClose } from 'react-icons/io5'
import CommonNav from '../CommonNav'
function Dashboard() { function Dashboard() {
const [showSideBar, setShowSideBar] = React.useState(true) const [showSideBar, setShowSideBar] = React.useState(true)
@ -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`}> <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 */} {/* Main Content */}
<div className='px-2 py-1.5'> <div className='px-2 py-1.5'>
<CommonNav />
<Outlet /> <Outlet />
</div> </div>
</div> </div>

View File

@ -10,6 +10,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 { GrTransaction } from 'react-icons/gr';
type SideBarProps = { type SideBarProps = {
showSideBar: boolean; showSideBar: boolean;
@ -24,7 +25,8 @@ const menuPathMap: MenuPathMap = {
'/': 'Home', '/': 'Home',
'/templates': 'Templates', '/templates': 'Templates',
'/s/contact': 'Contact', '/s/contact': 'Contact',
'/accounts': 'Accounts', // '/accounts': 'Accounts',
'transactions' : "Account"
}; };
const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => { const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
@ -40,6 +42,7 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
headers: { "ngrok-skip-browser-warning": "69420" } headers: { "ngrok-skip-browser-warning": "69420" }
} }
); );
sessionStorage.clear();
// Redirect to login or refresh the page after logout // Redirect to login or refresh the page after logout
window.location.href = '/login'; window.location.href = '/login';
} catch (err) { } catch (err) {
@ -61,7 +64,8 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
<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} />
</div> </div>
</div> </div>

View File

@ -1,97 +1,328 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { FaCheckCircle, FaClock, FaHourglassHalf, FaTasks } from 'react-icons/fa'; import {
FaCheckCircle,
FaClock,
FaHourglassHalf,
FaTasks,
FaChevronDown,
FaPlus,
FaUserPlus,
FaFilter,
FaCalendarAlt
} from 'react-icons/fa';
const cards = [ // Define a type for card data
interface CardData {
title: string;
count: number;
icon: JSX.Element;
gradient: string;
shadowColor: string;
iconBg: string;
textColor: string;
percentage?: number;
trend?: 'up' | 'down' | 'stable';
}
// interface CardData {
// title: string;
// count: number;
// icon: React.ReactNode;
// gradient: string;
// shadowColor: string;
// iconBg: string;
// textColor: string;
// trend: 'up' | 'down';
// }
interface CardsProps {
type?: 'dashboard' | 'account' | 'transactions';
onOpenModal?: () => void;
dashboardData: any;
fromDate: string;
toDate: string;
onDateRangeChange: (range: { from: string; to: string }) => void;
cards: CardData[];
isLoading: boolean
}
const AccountData: CardData[] = [
{ {
title: 'Action Required', title: 'Active Accounts',
count: 12, count: 12,
icon: <FaTasks size={20} className="text-orange-500" />, icon: <FaTasks size={18} className="text-blue-600" />,
ring: 'ring-orange-400/20', gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
bg: 'bg-orange-50', shadowColor: 'shadow-blue-200/50',
iconBg: 'bg-blue-100 border-blue-200',
textColor: 'text-blue-700',
trend: 'up'
}, },
{ {
title: 'Waiting for Others', title: 'In-Progress',
count: 8, count: 8,
icon: <FaClock size={20} className="text-yellow-500" />, icon: <FaClock size={18} className="text-purple-600" />,
ring: 'ring-yellow-400/20', gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
bg: 'bg-yellow-50', shadowColor: 'shadow-purple-200/50',
}, iconBg: 'bg-purple-100 border-purple-200',
{ textColor: 'text-purple-700',
title: 'Expiring Soon', trend: 'stable'
count: 5,
icon: <FaHourglassHalf size={20} className="text-red-500" />,
ring: 'ring-red-400/20',
bg: 'bg-red-50',
}, },
{ {
title: 'Completed', title: 'Completed',
count: 27, count: 27,
icon: <FaCheckCircle size={20} className="text-green-500" />, icon: <FaCheckCircle size={18} className="text-green-600" />,
ring: 'ring-green-400/20', gradient: 'bg-gradient-to-br from-green-50 to-green-100',
bg: 'bg-green-50', shadowColor: 'shadow-green-200/50',
iconBg: 'bg-green-100 border-green-200',
textColor: 'text-green-700',
trend: 'up'
},
{
title: 'Inactive',
count: 5,
icon: <FaHourglassHalf size={18} className="text-gray-600" />,
gradient: 'bg-gradient-to-br from-gray-50 to-gray-100',
shadowColor: 'shadow-gray-200/50',
iconBg: 'bg-gray-100 border-gray-200',
textColor: 'text-gray-700',
trend: 'down'
}, },
]; ];
const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year']; const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year'];
const accounts = ['All Accounts', 'Account 1', 'Account 2', 'Account 3'];
const Cards = () => { const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRangeChange, cards, isLoading }) => {
const [selectedFilter, setSelectedFilter] = useState('Last 7 Days'); const [selectedFilter, setSelectedFilter] = useState<string>('Last 7 Days');
const [selectedAccount, setSelectedAccount] = useState<string>('All Accounts');
const [showCards, setShowCards] = useState<CardData[]>([]);
// const [isLoading, setIsLoading] = useState<boolean>(true);
// useEffect(() => {
// setIsLoading(true);
// setTimeout(() => {
// if (type === 'account') {
// setShowCards(AccountData);
// } else {
// setShowCards(cards);
// }
// setIsLoading(false);
// }, 500);
// }, [type]);
useEffect(() => {
if (type === 'dashboard') {
const now = new Date();
let fromDate = new Date();
switch (selectedFilter) {
case 'Last 7 Days':
fromDate.setDate(now.getDate() - 7);
break;
case 'Last 30 Days':
fromDate.setDate(now.getDate() - 30);
break;
case 'Last 6 Months':
fromDate.setMonth(now.getMonth() - 6);
break;
case 'This Year':
fromDate = new Date(now.getFullYear(), 0, 1);
break;
default:
fromDate.setDate(now.getDate() - 7);
}
const from = fromDate.toISOString().split('T')[0];
const to = now.toISOString().split('T')[0];
onDateRangeChange({ from, to });
}
}, [selectedFilter]);
const getButtonConfig = () => {
switch (type) {
case 'account':
return {
text: 'Add Account',
icon: <FaPlus size={14} />,
gradient: 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800'
};
case 'transactions':
return {
text: 'Send Invite',
icon: <FaUserPlus size={14} />,
gradient: 'bg-gradient-to-r from-emerald-600 to-emerald-700 hover:from-emerald-700 hover:to-emerald-800'
};
default:
return null;
}
};
const buttonConfig = getButtonConfig();
const renderTrendIndicator = (trend: string | undefined, percentage: number | undefined) => {
if (!trend || !percentage) return null;
const trendColors = {
up: 'text-emerald-500',
down: 'text-red-500',
stable: 'text-gray-500'
};
const trendSymbols = {
up: '↗',
down: '↘',
stable: '→'
};
return ( return (
<div className="w-full px-4 py-3"> <div className={`flex items-center text-xs ${trendColors[trend as keyof typeof trendColors]} mt-1`}>
<span className="mr-1">{trendSymbols[trend as keyof typeof trendSymbols]}</span>
{/* <span>{percentage}%</span> */}
</div>
);
};
// if (isLoading) {
// return (
// <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>
// );
// }
return (
<div className="w-full px-4 py-3 mt-2 rounded-md">
{/* Header */} {/* Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3"> <div className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-2 gap-4">
<h2 className="text-lg font-medium text-gray-700">Overview</h2> <div className="flex items-center gap-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="flex gap-2 w-full md:w-auto"> <FaFilter className="text-white text-lg" />
{/* Account Selector */} </div> */}
<select <div>
value={'All'} <h2 className="text-lg font-semibold text-gray-800">Overview</h2>
onChange={() => {}} {/* <p className="text-gray-600 text-sm">Monitor performance metrics</p> */}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
<option>All</option>
<option>Account 1</option>
<option>Account 2</option>
<option>Account 3</option>
</select>
{/* Date Filter */}
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
{filters.map((f) => (
<option key={f}>{f}</option>
))}
</select>
</div> </div>
</div> </div>
</div>
{/* Cards */} {/* Cards */}
<div className="cursor-pointer grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> {isLoading ? <div className="w-full [h-50vh] px-6 py-8">
{cards.map((card) => ( <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="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
{cards.map((card, index) => (
<div <div
key={card.title} key={card.title}
className={`rounded-md bg-white/60 backdrop-blur-md border border-gray-200 p-5 transition-transform shadow-md hover:scale-[1.02] hover:shadow-lg`} className={`group relative overflow-hidden rounded-xl ${card.gradient} border border-white/40 p-4 transition-all duration-300 hover:scale-105 hover:shadow-lg ${card.shadowColor} cursor-pointer backdrop-blur-sm`}
style={{
animationDelay: `${index * 100}ms`
}}
> >
<div className="flex items-center space-x-4"> {/* Background Pattern */}
<div className={`p-3 rounded-full ${card.bg} ${card.ring}`}> <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 z-10">
<div className="flex items-center justify-between mb-2">
<div className={`p-2 rounded-lg border ${card.iconBg} group-hover:scale-110 transition-transform duration-200`}>
{card.icon} {card.icon}
</div> </div>
<div> {renderTrendIndicator(card.trend, card.percentage)}
<p className="text-sm text-gray-500">{card.title}</p> </div>
<p className="text-xl font-bold text-gray-800">{card.count}</p>
<div className="space-y-1">
<h3 className="text-xs font-medium text-gray-600 group-hover:text-gray-700 transition-colors">
{card.title}
</h3>
<div className="flex items-baseline gap-1">
<p className={`text-xl font-bold ${card.textColor} group-hover:scale-105 transition-transform duration-200`}>
{card.count}
</p>
<span className="text-[10px] text-gray-500">items</span>
</div> </div>
</div> </div>
</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>
); );
}; };
export default Cards; export default Cards;
{/* <div className="flex flex-wrap gap-3 w-full lg:w-auto">
{type === 'dashboard' && <>
Account Selector
<div className="relative">
<select
value={selectedAccount}
onChange={(e) => setSelectedAccount(e.target.value)}
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
>
{accounts.map((account) => (
<option key={account} value={account}>{account}</option>
))}
</select>
<FaChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
</div>
Date Filter
<div className="relative">
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
>
{filters.map((filter) => (
<option key={filter} value={filter}>{filter}</option>
))}
</select>
<FaCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
</div>
</>}
Action Button
{buttonConfig && (
<button
className={`flex items-center gap-1 px-3 py-3 text-sm font-semibold text-white rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95 cursor-pointer ${buttonConfig.gradient}`}
onClick={onOpenModal}
>
{buttonConfig.icon}
{buttonConfig.text}
</button>
)}
</div> */}

View File

@ -1,22 +1,175 @@
import React from 'react' import React, { useEffect, useState, useCallback } from 'react';
import HomeTopNav from './HomeTopNav' import AccountNav from '../accounts/AccountNav';
import UploadDoc from './UploadDoc' import Cards from './Cards';
import FilterSort from './FilterSort' import StartSection from './StartSection';
import ShowDocs from './ShowDocs' import api from '../../config/api';
import Cards from './Cards' import {
import StartSection from './StartSection' FaCheckCircle,
FaClock,
FaHourglassHalf,
FaTasks,
FaChevronDown,
FaPlus,
FaUserPlus,
FaFilter,
FaCalendarAlt
} from 'react-icons/fa';
const Home = () => { interface CardData {
return ( title: string;
<> count: number;
<HomeTopNav /> icon: JSX.Element;
<Cards /> gradient: string;
<StartSection /> shadowColor: string;
{/* <UploadDoc /> */} iconBg: string;
{/* <FilterSort /> */} textColor: string;
{/* <ShowDocs /> */} percentage?: number;
</> trend?: 'up' | 'down' | 'stable';
)
} }
export default Home const Home = () => {
const mapApiDataToCards = (data: any): CardData[] => {
return [
{
title: 'Action Needed',
count: data.actionNeeded || 0,
icon: <FaTasks size={18} className="text-indigo-600" />,
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
shadowColor: 'shadow-indigo-200/50',
iconBg: 'bg-indigo-100 border-indigo-200',
textColor: 'text-indigo-700',
trend: 'up'
},
{
title: 'Expiring Soon',
count: data.expiring || 0,
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
shadowColor: 'shadow-rose-200/50',
iconBg: 'bg-rose-100 border-rose-200',
textColor: 'text-rose-700',
trend: 'up'
},
{
title: 'Completed',
count: data.completed || 0,
icon: <FaCheckCircle size={18} className="text-teal-600" />,
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
shadowColor: 'shadow-teal-200/50',
iconBg: 'bg-teal-100 border-teal-200',
textColor: 'text-teal-700',
trend: 'up'
},
{
title: 'Draft',
count: data.draft || 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'
},
];
};
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const [dashboardData, setDashboardData] = useState<any>(null);
const [fromDate, setFromDate] = useState<string>('');
const [toDate, setToDate] = useState<string>('');
const [cards, setCards] = useState<CardData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
console.log("activeAccountId", activeAccountId);
console.log("cards", cards);
const getDashboardData = async () => {
if (!activeAccountId) return;
setIsLoading(true);
const payload = {
account: activeAccountId
};
try {
const res = await api.get('/api/dashboard', { params: payload });
setDashboardData(res.data);
console.log("Dashboard Data:", res.data);
const cardDataArray = mapApiDataToCards(res.data);
setCards(cardDataArray);
} catch (error) {
console.error("Error fetching dashboard data:", error);
} finally {
setIsLoading(false);
}
};
const handleDateRangeChange = useCallback(async (range: { from: string; to: string }) => {
const isoFrom = new Date(range.from).toISOString();
const isoTo = new Date(range.to).toISOString();
setFromDate(isoFrom);
setToDate(isoTo);
}, []);
// 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(() => {
if (activeAccountId) {
getDashboardData();
}
}, [activeAccountId]);
// Set default date range on mount (last 7 days)
useEffect(() => {
const now = new Date();
const from = new Date();
from.setDate(now.getDate() - 7);
setFromDate(from.toISOString());
setToDate(now.toISOString());
}, []);
return (
<>
<Cards
onDateRangeChange={handleDateRangeChange}
dashboardData={dashboardData}
fromDate={fromDate}
toDate={toDate}
cards={cards}
isLoading={isLoading}
/>
<StartSection />
</>
);
};
export default Home;

View File

@ -33,7 +33,7 @@ const HomeTopNav = () => {
</button> </button>
<Link <Link
to="/s/settings" to="settings"
aria-label="Settings" aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600" className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
> >

View File

@ -77,7 +77,7 @@ const StartSection = () => {
return ( return (
<section className="px-4 py-3 w-full"> <section className="px-4 py-3 w-full">
<h2 className="text-lg font-semibold text-gray-800 mb-6">Get Started</h2> <h2 className="text-lg font-semibold text-gray-800 mb-2">Get Started</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{cards.map((card, index) => ( {cards.map((card, index) => (

View File

@ -0,0 +1,356 @@
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/60 rounded-md shadow-sm border border-gray-100">
<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-white/40 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/60 rounded-md shadow-sm border border-gray-100">
<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 h-[69vh] no-scrollbar flex justify-center items-center">
<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="w-full px-6 py-2 bg-white border-b rounded-sm border-gray-200">
<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 text-green-700 rounded-full flex items-center justify-center">
<IoMdHome className="h-10 w-10" />
</Link>
{/* <div className="w-5 h-5 bg-white text-green-800 rounded-full flex items-center justify-center">
<FaCheck className="text-sm" />
</div> */}
<div>
<h2 className="text-md font-semibold text-gray-900">Awesome! Your document is sent for signature</h2>
</div>
</div>
<Link to={`/accounts/${accountId}/transactions/${transactionId}`} className="flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]">
<FaPen className=" text-white text-md" />
<span className="tracking-wide text-white">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-6 space-y-2">
{/* Document Preview Card */}
<div className="bg-white/60 rounded-md shadow-sm border border-gray-100 overflow-hidden max-h-[88vh] no-scrollbar overflow-y-auto">
<div className="p-3 ">
{selectedDocuments?.map((item, index) => (
<div key={item.id} className="flex items-start justify-between mb-2 px-2 py-3">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
{/* <FaFileAlt className="text-blue-600 text-xl" /> */}
<img src={item.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><SignersList signers={selectedRecipients} /></div>
</div>
{/* Right Section - Signer Components */}
<div className="lg:col-span-6 space-y-1.5">
{/* Signing Order Diagram */}
<SigningOrderDiagram signers={selectedRecipients} />
{/* Signers List */}
</div>
</div>
</div>
</div>
: <Loader />}
</div>
</>
);
};
export default DocumentCompletionScreen;

View File

@ -9,7 +9,7 @@ import { RootState } from "../../../store/store";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { setImages } from "../../../store/slices/selectedDocsSlice"; import { setImages } from "../../../store/slices/selectedDocsSlice";
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice"; import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice"; import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
import { hideLoader, showLoader } from "../../../store/slices/loaderSlice"; import { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
import Loader from "../../dashboard/Loader"; import Loader from "../../dashboard/Loader";
@ -22,15 +22,24 @@ const ProcessStepOne = () => {
message: string; message: string;
} }
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const templateId = useParams<{ templateId: string }>().templateId;
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const navigate = useNavigate();
const dispatch = useDispatch();
if (!transactionId) {
transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
transactionId = templateId;
dispatch(toggleToTemplates());
}
console.log("ProcessStepOne called with accountId:", accountId, "and transactionId:", transactionId);
if (!accountId || !transactionId) { if (!accountId || !transactionId) {
window.location.href = "/"; window.location.href = "/";
return null; return null;
} }
const navigate = useNavigate();
const dispatch = useDispatch();
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
// const { selectedDocuments, signType } = useSelector((state: RootState) => state.selectedDocuments) // const { selectedDocuments, signType } = useSelector((state: RootState) => state.selectedDocuments)
@ -53,10 +62,30 @@ const ProcessStepOne = () => {
}, []) }, [])
const handleNextClick = async (isSendNow: boolean) => { 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') console.log(selectedDocuments, 'line 193')
toast.dismiss(); 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", position: "top-center",
autoClose: 5000, autoClose: 5000,
hideProgressBar: false, hideProgressBar: false,
@ -79,9 +108,9 @@ const ProcessStepOne = () => {
order: index, order: index,
})); }));
await api.patch(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, { "documents": documentsData }); await api.patch(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents`, { "documents": documentsData });
await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`).then(res => { await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents?include_images=true`).then(res => {
if (res.status === 200) { if (res.status === 200) {
dispatch(setDocuments(res.data)); dispatch(setDocuments(res.data));
} }
@ -92,7 +121,7 @@ const ProcessStepOne = () => {
dispatch(showLoader("Processing Recipients...")); dispatch(showLoader("Processing Recipients..."));
const recipientsData = selectedRecipients.map((recipient, index) => ({ const recipientsData = selectedRecipients.map((recipient, index) => ({
// index, id: "id" in recipient ? recipient.id : null,
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1, routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
type: recipient.type, type: recipient.type,
firstName: recipient.firstName, firstName: recipient.firstName,
@ -113,14 +142,12 @@ const ProcessStepOne = () => {
})); }));
dispatch(addRecipientAsync({ recipientsData, accountId, transactionId })); dispatch(addRecipientAsync({ recipientsData, accountId, transactionId }));
dispatch(showLoader("Processing Message...")); dispatch(showLoader("Processing Message..."));
await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, { subject: emailDtls.subject, message: emailDtls.message, status: "CREATED" }).then((res) => { await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { subject: emailDtls.subject, message: emailDtls.message, status: "CREATED" }).then((res) => {
console.log(res); console.log(res);
}) })
dispatch(hideLoader());
if (isSendNow) { if (isSendNow) {
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS)); dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS));
@ -218,7 +245,8 @@ const ProcessStepOne = () => {
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label> <label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
<input <input
type="text" type="text"
value={emailDtls.subject || "Documents for Signature"} required
value={emailDtls.subject}
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })} onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
placeholder="Email Subject" 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)]" 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>) => { const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (recipient.firstName === "" || recipient.lastName === "" || recipient.email === "" || !recipient.type) return; if (recipient.lastName === "" || recipient.email === "" || !recipient.type) return;
// Validate Email Format // Validate Email Format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@ -106,7 +106,7 @@ const RecipientAddCard = () => {
{/* Name Inputs */} {/* Name Inputs */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input <input
required // required
type="text" type="text"
onFocus={() => setOpenContactModal(true)} onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur} onBlur={handleContactModalBlur}

View File

@ -1,5 +1,6 @@
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
removeRecipientAsync,
removeSelectedRecipient, removeSelectedRecipient,
// setSigningOrder, // setSigningOrder,
updateRecipientOrder, // 👈 You need to define this in your slice updateRecipientOrder, // 👈 You need to define this in your slice
@ -11,6 +12,7 @@ import { FaUsersLine } from "react-icons/fa6";
import { useState } from "react"; import { useState } from "react";
import SigningOrderDiagram from "../../../SigningOrderDiagram "; import SigningOrderDiagram from "../../../SigningOrderDiagram ";
import { IoCloseOutline } from "react-icons/io5"; import { IoCloseOutline } from "react-icons/io5";
import { useParams } from "react-router-dom";
const getInitials = (name: string) => { const getInitials = (name: string) => {
if (!name) return ""; if (!name) return "";
@ -22,7 +24,15 @@ const getInitials = (name: string) => {
}; };
const RecipientCards = () => { const RecipientCards = () => {
const dispatch = useDispatch<AppDispatch>(); const templateId = useParams<{ templateId: string }>().templateId;
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const dispatch = useDispatch();
if (!transactionId) {
transactionId = templateId;
}
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients); const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
// const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder); // const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder);
const signType = useSelector((state: RootState) => state.selectedDocuments.signType); const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
@ -34,7 +44,14 @@ const RecipientCards = () => {
}; };
const handleRemoveRecipient = (recipient) => { const handleRemoveRecipient = (recipient) => {
dispatch(removeSelectedRecipient(recipient)) if (!accountId || !transactionId) return;
// dispatch(removeSelectedRecipient(recipient))
dispatch(removeRecipientAsync({
recipientId: recipient.id,
email: recipient.email,
accountId: accountId,
transactionId: transactionId,
}))
if (recipients.length === 2) { if (recipients.length === 2) {
let leftRecipient = recipients.filter((obj) => { let leftRecipient = recipients.filter((obj) => {
if ("id" in obj && "id" in recipient) { if ("id" in obj && "id" in recipient) {

View File

@ -11,15 +11,19 @@ import { Bounce, toast } from "react-toastify";
import api from '../../../config/api'; import api from '../../../config/api';
import DocumentCard from './DocumentCard'; 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';
const SelectedDocuments = () => { const SelectedDocuments = () => {
const dispatch = useDispatch<AppDispatch>(); // let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const documents = useSelector((state: RootState) => state.documents.documents);
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); const documents = useSelector((state: RootState) => state.documents.documents);
if (!accountId || !transactionId) { const templateId = useParams<{ templateId: string }>().templateId;
window.location.href = "/"; let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
return null;
const dispatch = useDispatch();
if (!transactionId) {
transactionId = templateId;
} }
const [isModalOpen, setModalOpen] = useState(false); const [isModalOpen, setModalOpen] = useState(false);

View File

@ -15,6 +15,10 @@ import { fetchDocuments } from '../../../store/slices/documentSlice';
import { FaQuestionCircle } from 'react-icons/fa'; import { FaQuestionCircle } from 'react-icons/fa';
import typeToIconMap from '../../../helper/iconMap'; import typeToIconMap from '../../../helper/iconMap';
import { IoAdd } from 'react-icons/io5';
import Loader from '../../dashboard/Loader';
import { handleGroupBorderPosition } from '../../../utlity/tagGroupConUtils';
export interface DroppedItemsObj { export interface DroppedItemsObj {
[key: string]: DroppedItem[] [key: string]: DroppedItem[]
} }
@ -23,7 +27,7 @@ interface DroppedTagsDefaultFieldType {
[key: string]: string [key: string]: string
} }
const droppedTagsDefaultField: DroppedTagsDefaultFieldType = { export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = {
text: "paragraph", text: "paragraph",
temporal: "date", temporal: "date",
number: "integer", number: "integer",
@ -52,6 +56,8 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents) const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
// const [uniqGroupIds, setUniqGroupIds] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId })); selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
@ -131,7 +137,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// debugger // debugger
for (const [recipientId, tags] of Object.entries(tagsData)) { for (const [recipientId, tags] of Object.entries(tagsData)) {
console.log(recipientId, tags, "line 133") // console.log(recipientId, tags, "line 133")
const updateTags = [] const updateTags = []
for (const tag of tags) { for (const tag of tags) {
if ("update" in tag && tag.update) { if ("update" in tag && tag.update) {
@ -190,7 +196,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
let mask = null let mask = null
console.log(dropTag, "line 132") // console.log(dropTag, "line 132")
// if (dropTag.subType?.toLowerCase() === "password") { // if (dropTag.subType?.toLowerCase() === "password") {
// console.log(dropTag) // console.log(dropTag)
@ -200,7 +206,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
} }
// } // }
console.log(dropTag, "line 204") // console.log(dropTag, "line 204")
const isDeleted = "isDeleted" in dropTag ? dropTag.isDeleted : false const isDeleted = "isDeleted" in dropTag ? dropTag.isDeleted : false
@ -210,23 +216,23 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
"dimensions": dropTag.dimensions, "dimensions": dropTag.dimensions,
"type": dropTag.type.toUpperCase(), "type": dropTag.type.toUpperCase(),
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null), // "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
validation: { "minLength": "minLength" in dropTag ? dropTag.minLength : 0,
"minLength": 0, "maxLength": "maxLength" in dropTag ? dropTag.maxLength : 0,
"maxLength": 0,
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error", "error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
"regex": "regex" in dropTag ? dropTag.regex : [] "regex": "regex" in dropTag ? dropTag.regex : []
}, // }
,
"documentId": dropTag.documentId, "documentId": dropTag.documentId,
"options": ("options" in dropTag) ? dropTag.options : [], "options": ("options" in dropTag) ? dropTag.options : [],
"pageIndex": dropTag.pageIndex, "pageIndex": dropTag.pageIndex,
"recipientId": dropTag.recipientId, "recipientId": dropTag.recipientId,
"styles": "styles" in dropTag ? dropTag.styles : { "styles": "styles" in dropTag ? dropTag.styles : {
"font": "Great Vibes", "font": "",
"italic": false, "italic": false,
"bold": false, "bold": false,
"underline": false, "underline": false,
"color": "black", "color": "black",
"textSize": 14.0 "textSize": 0
}, },
// "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true, // "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true,
"required": "required" in dropTag ? dropTag.required : true, "required": "required" in dropTag ? dropTag.required : true,
@ -275,10 +281,10 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
tagsData = groupByRecipientId(tagsData); tagsData = groupByRecipientId(tagsData);
sendTagsToRecipients(tagsData, accountId, transactionId); sendTagsToRecipients(tagsData, accountId, transactionId);
// (async () => { (async () => {
// const res = await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, {status: "SENT"}) const res = await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, { status: "SENT" })
// console.log(res, "Check123") console.log(res, "Check123")
// })() })()
}, [isSend]) }, [isSend])
// Save Tags - END // Save Tags - END
@ -288,6 +294,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// HANDLE DROP - START // HANDLE DROP - START
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => { const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
console.log("// HANDLE DROP - START")
event.preventDefault(); event.preventDefault();
const data = window.dragElemData; const data = window.dragElemData;
@ -336,6 +343,18 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
isDeleted: false isDeleted: false
} }
if ("group" in dragElemData) {
prepData.group = dragElemData.group;
prepData.isGroupExist = false;
// if(uniqGroupIds.includes(dragElemData.group)) {
// prepData.isGroupExist = true;
// } else {
// prepData.isGroupExist = false;
// setUniqGroupIds([...uniqGroupIds, dragElemData.group]);
// }
}
if (prepData.type === "temporal") { if (prepData.type === "temporal") {
prepData.currentTimeStamp = true; prepData.currentTimeStamp = true;
} }
@ -348,36 +367,44 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
const dataToSet: DroppedItemsObj = { const dataToSet: DroppedItemsObj = {
...droppedItems, ...droppedItems,
[key]: [...(droppedItems[key] || []), [key]: [...(droppedItems[key] || []),
{ prepData],
...prepData,
radioGroups: dragElemData.label === "radio" ?
[
{
...prepData,
dimensions: {
...prepData.dimensions, top: 8, left: 10
}
},
{
...prepData, dimensions: {
...prepData.dimensions,
top: 8,
left: 10
}
}
]
:
[]
}],
} }
if (dragElemData.label === "radio") { // const dataToSet: DroppedItemsObj = {
setDroppedItems(dataToSet); // ...droppedItems,
} else { // [key]: [...(droppedItems[key] || []),
setDroppedItems(dataToSet); // {
} // ...prepData,
// radioGroups: dragElemData.label === "radio" ?
// [
// {
// ...prepData,
// dimensions: {
// ...prepData.dimensions, top: 8, left: 10
// }
// },
// {
// ...prepData, dimensions: {
// ...prepData.dimensions,
// top: 8,
// left: 10
// }
// }
// ]
// :
// []
// }],
// }
// if (dragElemData.label === "radio") {
// setDroppedItems(dataToSet);
// } else {
// setDroppedItems(dataToSet);
// }
setDroppedItems(dataToSet);
setDroppedItemsCounter((prev) => prev + 1); setDroppedItemsCounter((prev) => prev + 1);
setTimeout(() => { setTimeout(() => {
const tagIndex = key in droppedItems ? droppedItems[key].length : 0; const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
handleTagClick(key, tagIndex) handleTagClick(key, tagIndex)
@ -390,6 +417,9 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
event.preventDefault(); event.preventDefault();
let targetContainer = event?.currentTarget?.parentElement?.parentElement; let targetContainer = event?.currentTarget?.parentElement?.parentElement;
console.log(targetContainer, "targetContainer line 421")
let targetElement = event?.currentTarget?.parentElement; let targetElement = event?.currentTarget?.parentElement;
if (!targetContainer || !targetElement) return; if (!targetContainer || !targetElement) return;
@ -404,16 +434,29 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
const conScrollWidth = targetContainer?.scrollWidth; const conScrollWidth = targetContainer?.scrollWidth;
const conScrollHeight = targetContainer?.scrollHeight; const conScrollHeight = targetContainer?.scrollHeight;
let isGroupAttr = event.currentTarget.hasAttribute("group");
let groupId = null;
if (isGroupAttr) {
groupId = event.currentTarget.getAttribute("group");
}
let groupElements: NodeListOf<HTMLElement> | null = null
if (isGroupAttr && groupId) {
groupElements = document.querySelectorAll(`div[group="${groupId}"]`);
}
const handleMouseMove = (event: MouseEvent) => { const handleMouseMove = (event: MouseEvent) => {
// Get new X and Y positions relative to the container // Get new X and Y positions relative to the container
let left = event.clientX - containerRect.left - offsetX; let left = event.clientX - containerRect.left - offsetX;
let top = event.clientY - containerRect.top - offsetY; let top = event.clientY - containerRect.top - offsetY;
// **Restrict movement within container bounds** // **Restrict movement within container bounds**
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width)); left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height)); top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
// Apply the movement to the element // Apply the movement to the element
setDroppedItems({ setDroppedItems({
...droppedItems, ...droppedItems,
@ -439,6 +482,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// HANDLE EXPAND TAG - START // HANDLE EXPAND TAG - START
const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => { const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
console.log("HANDLE EXPAND TAG - START")
event.preventDefault(); // Prevent text selection issues event.preventDefault(); // Prevent text selection issues
const expandingElemIcon = event.currentTarget; const expandingElemIcon = event.currentTarget;
@ -519,6 +563,30 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
})() })()
} }
// const handleAddRadioClick = (key: string) => {
// console.log("Handle Radio Clicled", key)
// console.log(droppedItems, "line 525")
// }
const handleAddGroupItemClicked = (key: string, item) => {
const groupId = item.group;
item = { ...item, dimensions: { ...item.dimensions, top: item.dimensions.top + item.dimensions.height + 20 } }
// dimensions: { ...item.dimensions, left: newLeft, top: newTop }
setTimeout(() => {
handleIGroupBorderPosition(item.group)
}, 0)
setDroppedItems({
...droppedItems,
[key]: [...droppedItems[key], { ...item, isGroupExist: true }],
});
}
// const handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => { // const handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => {
// // const dataToSet: DroppedItemsObj = { // // const dataToSet: DroppedItemsObj = {
// // ...droppedItems, // // ...droppedItems,
@ -620,13 +688,75 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// } // }
// NEW DIRECTIONAL MOVEMENT HANDLER - START
const handleDirectionalMove = (direction: 'up' | 'down' | 'left' | 'right', stepSize: number = 5) => {
if (!selectedTag) return;
const { key, index } = selectedTag;
const currentItem = droppedItems[key]?.[index];
if (!currentItem) return;
let newLeft = currentItem.dimensions.left;
let newTop = currentItem.dimensions.top;
switch (direction) {
case 'up':
newTop = Math.max(0, newTop - stepSize);
break;
case 'down':
newTop = newTop + stepSize;
break;
case 'left':
newLeft = Math.max(0, newLeft - stepSize);
break;
case 'right':
newLeft = newLeft + stepSize;
break;
}
setDroppedItems({
...droppedItems,
[key]: Array.isArray(droppedItems[key])
? droppedItems[key].map((item, i) =>
i === index
? { ...item, dimensions: { ...item.dimensions, left: newLeft, top: newTop }, update: true }
: item
)
: [],
});
};
// Create a wrapper that matches DirectionalControls' expected signature
const directionalControlsHandler = (direction: 'up' | 'down' | 'left' | 'right') => {
handleDirectionalMove(direction, 5); // Use default stepSize of 5
};
function handleIGroupBorderPosition(groupId: string) {
const groupElements: NodeListOf<HTMLElement> = document.querySelectorAll(`div[group="${groupId}"]`);
let targetContainer = groupElements[0].parentElement?.parentElement;
console.log(targetContainer, "targetContainer line 421")
let targetElement = groupElements[0].parentElement;
if (!targetContainer || !targetElement) return;
const containerRect = targetContainer.getBoundingClientRect();
handleGroupBorderPosition(groupElements, containerRect, 40, 40);
}
return ( return (
<> <>
{/* Center Doc Container - START */} {/* Center Doc Container - START */}
<div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'> <div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'>
{ {
selectedDocuments.length > 0 && selectedDocuments.map((doc, indxDoc) => { selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
return ( return (
<div key={indxDoc} className='flex flex-col'> <div key={indxDoc} className='flex flex-col'>
<div className='py-2 text-sm'> <div className='py-2 text-sm'>
@ -650,10 +780,12 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
{/* DROPPED ITEMS - Render only the dropped items for this specific docId and page - STARTS */} {/* DROPPED ITEMS - Render only the dropped items for this specific docId and page - STARTS */}
{droppedItems[key]?.map((item, index) => { {droppedItems[key]?.map((item, index) => {
if (item.isDeleted) return; if (item.isDeleted) return;
const type = item.type.toUpperCase(); const type = item.type.toUpperCase();
const IconComponent = typeToIconMap[type] || <FaQuestionCircle />; const IconComponent = typeToIconMap[type] || <FaQuestionCircle />;
const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id === item.recipientId); const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id === item.recipientId);
return ( return (
<>
<div <div
key={index} key={index}
draggable="false" draggable="false"
@ -697,11 +829,11 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
</div>} </div>}
{/* Expand Circle - ENDS */} {/* Expand Circle - ENDS */}
{/* Tag - START */}
{
<div <div
{...("group" in item && item.group ? { "group": item.group } : {})}
onPointerDown={(e) => handleTagMouseDown(e, key, index)} 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' className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
style={{ background: selectedRecipient[0]?.color }} style={{ background: selectedRecipient[0]?.color }}
> >
@ -712,8 +844,27 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
{IconComponent} {IconComponent}
</p> </p>
</div> </div>
}
{/* Tag - END */}
</div> </div>
{
"group" in item && item.group && "isGroupExist" in item && !item.isGroupExist &&
<>
<div className="group-border" border-group={item.group} border-pos="top"></div>
<div className="group-border" border-group={item.group} border-pos="right"></div>
<div className="group-border bottom-border" border-group={item.group} border-pos="bottom">
<div onClick={() => handleAddGroupItemClicked(key, item)}><IoAdd /></div>
</div>
<div className="group-border" border-group={item.group} border-pos="left"></div>
</>
}
</>
) )
} }
@ -750,13 +901,19 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
</div> </div>
) )
}) }) : <Loader />
} }
</div > </div >
{selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} />} {selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} DirectionalControlsProps={{
onMove: directionalControlsHandler,
disabled: false,
stepSize: 5,
className: ''
}} />
}
{/* Center Doc Container - END */} {/* Center Doc Container - END */}
</> </>
) )

View File

@ -1,10 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import fields from '../../../helper/fieldsListData'; import fields from '../../../helper/fieldsListData';
import api from '../../../config/api'; // import api from '../../../config/api';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import typeToIconMap from '../../../helper/iconMap'; import typeToIconMap from '../../../helper/iconMap';
import { FaQuestionCircle } from 'react-icons/fa'; import { FaQuestionCircle } from 'react-icons/fa';
import { TagsProperties } from '../../../utlity/types'; import { TagsProperties } from '../../../utlity/types';
import { v4 as uuidv4 } from "uuid";
import axios from 'axios';
const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string, recipientId: number, recipientIndx: number, tagsProperties: TagsProperties) => { const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string, recipientId: number, recipientIndx: number, tagsProperties: TagsProperties) => {
const dragElemRect = event.currentTarget.getBoundingClientRect(); const dragElemRect = event.currentTarget.getBoundingClientRect();
@ -31,6 +33,8 @@ const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string
properties: tagsProperties properties: tagsProperties
} }
if (obj.label.toLowerCase() === "radio" || obj.label.toLowerCase() === "checkbox") obj.group = uuidv4();
window.dragElemData = JSON.stringify(obj); window.dragElemData = JSON.stringify(obj);
} }
@ -38,21 +42,31 @@ interface FieldListProps {
color: string; color: string;
recipientId: number; recipientId: number;
recipientIndx: number; recipientIndx: number;
acntId?: string | null;
} }
const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx }) => { const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx, acntId = null }) => {
const [tagsData, setTagsData] = useState<TagsProperties[]>([]); const [tagsData, setTagsData] = useState<TagsProperties[]>([]);
const { accountId } = useParams<{ accountId: string }>(); let { accountId } = useParams<{ accountId: string }>();
if(acntId) {
accountId = acntId;
}
if (!accountId) { if (!accountId) {
window.location.href = "/"; window.location.href = "/";
return null; return null;
} }
useEffect(() => { useEffect(() => {
api axios
.get(`/api/accounts/${accountId}/tags`) .get(`${import.meta.env.VITE_BASE_URI}/api/accounts/${accountId}/tags`, {
headers: {
"ngrok-skip-browser-warning": "69420",
}
})
.then((res) => { .then((res) => {
const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP']; const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP'];

View File

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

View File

@ -0,0 +1,120 @@
import React, { useEffect, useRef } from 'react';
import { FaArrowUp, FaArrowDown, FaArrowLeft, FaArrowRight, FaRegCircle } from 'react-icons/fa';
interface DirectionalControlsProps {
onMove: (direction: 'up' | 'down' | 'left' | 'right', stepSize?: number) => void;
disabled?: boolean;
stepSize?: number;
className?: string;
selectedType: {
left?: number;
top?: number;
width?: number;
height?: number;
};
}
const DirectionalControls: React.FC<DirectionalControlsProps> = ({
onMove,
disabled = false,
stepSize = 1,
className = '',
selectedType,
}) => {
const onMoveRef = useRef(onMove);
useEffect(() => {
onMoveRef.current = onMove;
}, [onMove]);
const handleMove = (direction: 'up' | 'down' | 'left' | 'right') => {
if (!disabled) {
onMoveRef.current(direction, stepSize);
}
};
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (disabled) return;
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
handleMove('up');
break;
case 'ArrowDown':
event.preventDefault();
handleMove('down');
break;
case 'ArrowLeft':
event.preventDefault();
handleMove('left');
break;
case 'ArrowRight':
event.preventDefault();
handleMove('right');
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [disabled, stepSize]);
const buttonClass = `
p-1 rounded-sm transition-all duration-200 border shadow-sm text-[10px]
${disabled
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-white text-gray-600 hover:bg-blue-100 active:bg-blue-200 cursor-pointer'}
flex items-center justify-center w-6 h-6
`;
const { top = 0, left = 0, width = 0, height = 0 } = selectedType || {};
const right = left + width;
const bottom = top + height;
return (
<div className={`grid grid-cols-3 grid-rows-1 text-[10px] text-gray-700 ${className}`}>
{/* Top Row */}
<div></div>
<div className="flex flex-col items-center">
<button onClick={() => handleMove('up')} disabled={disabled} className={buttonClass} title="Move up" type="button">
<FaArrowUp size={12} />
</button>
<span className="mt-1">Top: {Math.round(top)}px</span>
</div>
<div></div>
{/* Middle Row */}
<div className="flex flex-col items-center justify-center">
<button onClick={() => handleMove('left')} disabled={disabled} className={buttonClass} title="Move left" type="button">
<FaArrowLeft size={12} />
</button>
<span className="mt-1">Left: {Math.round(left)}px</span>
</div>
<div className="w-8 h-8 flex items-center justify-center text-xs text-gray-400 ps-5"><FaRegCircle/></div>
<div className="flex flex-col items-center justify-center">
<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> */}
</div>
{/* Bottom Row */}
<div></div>
<div className="flex flex-col items-center">
<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> */}
</div>
<div></div>
</div>
);
};
export default DirectionalControls;

View File

@ -1,29 +1,68 @@
import React from 'react' import React, { useRef } from 'react';
interface MaxMinLenAttrIntr { interface MaxMinLenAttrIntr {
maxLength: number; maxLength: number;
minLength: number; minLength: number;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
} }
const MaxMinLenAttr : React.FC<MaxMinLenAttrIntr> = ({ maxLength, minLength }) => { const MaxMinLenAttr: React.FC<MaxMinLenAttrIntr> = ({ maxLength, minLength, handlePropertyChange }) => {
const firstChangedField = useRef<null | 'minLength' | 'maxLength'>(null);
const handleMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newMax = Number(e.target.value);
if (!firstChangedField.current) {
firstChangedField.current = 'maxLength';
}
if (firstChangedField.current === 'minLength' && newMax < minLength) {
alert(`Max Length must be greater than or equal to Min Length (${minLength})`);
return;
}
handlePropertyChange('maxLength', newMax);
};
const handleMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newMin = Number(e.target.value);
if (!firstChangedField.current) {
firstChangedField.current = 'minLength';
}
if (firstChangedField.current === 'maxLength' && newMin > maxLength) {
alert(`Min Length must be less than or equal to Max Length (${maxLength})`);
return;
}
handlePropertyChange('minLength', newMin);
};
return ( return (
<> <>
<div> <div>
<label htmlFor="">Max Length</label> <label htmlFor="maxLength">Max Length</label>
<input <input
value={minLength ? minLength : 0} id="maxLength"
value={maxLength}
type="number" type="number"
className="outline-none border-1 rounded px-2 py-1 border-slate-200" /> min={0}
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
onChange={handleMaxChange}
/>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<label htmlFor="">Min Length</label> <label htmlFor="minLength">Min Length</label>
<input <input
value={maxLength ? maxLength : 255} id="minLength"
value={minLength}
type="number" type="number"
className="outline-none border-1 rounded px-2 py-1 border-slate-200" /> min={0}
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
onChange={handleMinChange}
/>
</div> </div>
</> </>
) );
} };
export default MaxMinLenAttr export default MaxMinLenAttr;

View File

@ -65,7 +65,7 @@ const RecipientsAttrCon: React.FC<RecipientsAttrConIntr> = ({ selectedRecipientI
key={indx} key={indx}
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-pointer hover:bg-slate-100' className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-pointer hover:bg-slate-100'
onClick={() => { onClick={() => {
// handlePropertyChange() handlePropertyChange('recipientId', recipient.id);
setTimeout(() => { setTimeout(() => {
setDropDownToggle(false); setDropDownToggle(false);
}, 0) }, 0)

View File

@ -16,12 +16,27 @@ import TemporalDateAttr from "./TemporalDateAttr";
import SelectTagAttr from "./SelectTagAttr"; import SelectTagAttr from "./SelectTagAttr";
import MaxMinLenAttr from "./MaxMinLenAttr"; import MaxMinLenAttr from "./MaxMinLenAttr";
import RecipientsAttrCon from "./RecipientsAttrCon"; import RecipientsAttrCon from "./RecipientsAttrCon";
import MovableFieldContainer from "./MovableField";
import DirectionalControls from "./DirectionalControls";
// interface DirectionalControlsProps {
// onMove: (direction: 'up' | 'down' | 'left' | 'right') => void;
// disabled?: boolean;
// stepSize?: number;
// className?: string;
// }
export interface DroppedItemProps { export interface DroppedItemProps {
selectedTag: DroppedItem selectedTag: DroppedItem
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>> setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>
selTagKey: string, selTagKey: string,
selTagIndx: number selTagIndx: number,
DirectionalControlsProps: {
onMove: (direction: 'up' | 'down' | 'left' | 'right', stepSize?: number) => void;
disabled?: boolean;
stepSize?: number;
className?: string;
}
} }
export enum TEXT { export enum TEXT {
@ -77,10 +92,27 @@ export enum DROPDOWNTOGGLE {
DDMMYY_FORMAT DDMMYY_FORMAT
} }
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx }) => { const defaultStyles = {
font: "",
italic: false,
bold: false,
underline: false,
color: "#000000",
textSize: 0
};
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx, DirectionalControlsProps }) => {
const selectedType = selectedTag.type in enumMap && enumMap[selectedTag.type as keyof typeof enumMap]; const selectedType = selectedTag.type in enumMap && enumMap[selectedTag.type as keyof typeof enumMap];
// console.log("selectedTag", selectedTag, selectedType)
const [styles, setStyles] = useState(defaultStyles);
const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null); const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null);
const [decimalPlace, setDecimalPlace] = useState<number>(2) const [decimalPlace, setDecimalPlace] = useState<number>(2)
const [styleOpen, setStyleOpen] = useState(false); const [styleOpen, setStyleOpen] = useState(false);
@ -150,16 +182,23 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
)); ));
} else { } else {
setDroppedItems((prev) => ( setDroppedItems((prev) => {
{ console.log('Previous state:', prev);
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array const updatedItems = Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) => ? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith, update: true } : item i === selTagIndx ? { ...item, [changeTo]: changeWith, update: true } : item
) )
: [], : [];
}// Provide a fallback in case it's undefined or not an array
)); console.log('Updated items:', updatedItems);
return {
...prev,
[selTagKey]: updatedItems,
};
});
} }
setDropDownToggle(null) setDropDownToggle(null)
} }
@ -193,24 +232,67 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
handlePropertyChange("decimalPlace", val) handlePropertyChange("decimalPlace", val)
} }
// useEffect(() => {
// if ("styles" in selectedTag) {
// console.log("styles" in selectedTag)
// const fontSize = selectedTag?.styles?.textSize;
// const tagHeight = selectedTag?.dimensions?.height - 18;
// console.log(fontSize)
// if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.PARAGRAPH && tagHeight > (fontSize + fontSize)) {
// handlePropertyChange("subType", TEXT.PARAGRAPH)
// } else if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.SINGLE_LINE && tagHeight <= (fontSize + fontSize)) {
// console.log("Change subType Called from elseif")
// handlePropertyChange("subType", TEXT.SINGLE_LINE)
// }
// }
// }, [selectedTag])
useEffect(() => { useEffect(() => {
if ("styles" in selectedTag) { if (!selectedTag) return;
const fontSize = selectedTag?.styles?.textSize;
if (!selectedTag.styles) {
handlePropertyChange("styles", defaultStyles);
setStyles(defaultStyles);
} else {
setStyles({ ...defaultStyles, ...selectedTag.styles });
}
// Optional subType logic
const fontSize = selectedTag?.styles?.textSize || defaultStyles.textSize;
const tagHeight = selectedTag?.dimensions?.height - 18; const tagHeight = selectedTag?.dimensions?.height - 18;
if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.PARAGRAPH && tagHeight > (fontSize + fontSize)) { if (
handlePropertyChange("subType", TEXT.PARAGRAPH) selectedTag.subType !== TEXT.PASSWORD &&
} else if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.SINGLE_LINE && tagHeight <= (fontSize + fontSize)) { selectedTag.subType !== TEXT.PARAGRAPH &&
console.log("Change subType Called from elseif") tagHeight > fontSize * 2
handlePropertyChange("subType", TEXT.SINGLE_LINE) ) {
handlePropertyChange("subType", TEXT.PARAGRAPH);
} else if (
selectedTag.subType !== TEXT.PASSWORD &&
selectedTag.subType !== TEXT.SINGLE_LINE &&
tagHeight <= fontSize * 2
) {
handlePropertyChange("subType", TEXT.SINGLE_LINE);
} }
} }, [selectedTag]);
}, [selectedTag])
const handleStyleChange = (key, value) => {
const updatedStyles = {
...(selectedTag.styles ?? defaultStyles),
[key]: value
};
setStyles(updatedStyles);
handlePropertyChange("styles", updatedStyles);
};
return ( return (
<> <>
{/* Right Side Bar Start */} {/* Right Side Bar Start */}
<div className='col-span-2 w-full h-full bg-white text-sm'> <div className='col-span-2 w-full h-full bg-white text-sm overflow-y-auto'>
<p className="p-2 font-semibold text-md">Properties</p> <p className="p-2 font-semibold text-md">Properties</p>
<hr className="text-slate-200" /> <hr className="text-slate-200" />
@ -220,11 +302,20 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
<RecipientsAttrCon selectedRecipientId={selectedTag.recipientId} handlePropertyChange={handlePropertyChange} /> <RecipientsAttrCon selectedRecipientId={selectedTag.recipientId} handlePropertyChange={handlePropertyChange} />
} }
<DirectionalControls {...DirectionalControlsProps} selectedType={selectedTag.dimensions}/>
{selectedTag.label && {selectedTag.label &&
<LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />} <LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />}
{(selectedTag.type !== TagsTypes.SIGNATURE && selectedTag.type !== TagsTypes.INITIALS) && {(selectedTag.type !== TagsTypes.SIGNATURE && selectedTag.type !== TagsTypes.INITIALS) &&
<RequiredAttr required={selectedTag.required ? selectedTag.required : false} handlePropertyChange={handlePropertyChange} />}
<>
<RequiredAttr required={selectedTag.required ? selectedTag.required : false} handlePropertyChange={handlePropertyChange} />
</>
}
{selectedTag.type.toLowerCase() === TagsTypes.TEMPORAL && {selectedTag.type.toLowerCase() === TagsTypes.TEMPORAL &&
<CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />} <CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />}
@ -290,11 +381,13 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
} }
{ {
selectedTag.type === TagsTypes.TEXT && (selectedTag.type.toLowerCase() === TagsTypes.TEXT || selectedTag.type.toLowerCase() === TagsTypes.COMPANY) &&
<MaxMinLenAttr maxLength={selectedTag.maxLength} minLength={selectedTag.minLength} /> <MaxMinLenAttr maxLength={selectedTag.maxLength} minLength={selectedTag.minLength} handlePropertyChange={handlePropertyChange} />
} }
{ {
<div className=""> <div className="">
<div className="flex items-center justify-between mr-2 cursor-pointer" <div className="flex items-center justify-between mr-2 cursor-pointer"
@ -313,6 +406,8 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
type="text" type="text"
placeholder="e.g. Arial" placeholder="e.g. Arial"
className="outline-none border-1 rounded px-2 py-1 border-slate-200" className="outline-none border-1 rounded px-2 py-1 border-slate-200"
value={styles.font}
onChange={(e) => handleStyleChange("font", e.target.value)}
/> />
</div> </div>
@ -323,6 +418,10 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
// value={} // value={}
type="number" type="number"
className="outline-none border-1 rounded px-2 py-1 border-slate-200" className="outline-none border-1 rounded px-2 py-1 border-slate-200"
value={styles.textSize}
onChange={(e) =>
handleStyleChange("textSize", parseInt(e.target.value, 10) || 0)
}
/> />
</div> </div>
@ -332,21 +431,27 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
<input <input
type="color" type="color"
className="outline-none border-1 rounded px-2 py-1 border-slate-200 w-10 h-8" className="outline-none border-1 rounded px-2 py-1 border-slate-200 w-10 h-8"
value={styles.color}
onChange={(e) => handleStyleChange("color", e.target.value)}
/> />
</div> </div>
{/* Toggles for Bold, Italic, Underline */} {/* Toggles for Bold, Italic, Underline */}
<div className="flex gap-4 items-center mt-2"> <div className="flex gap-4 items-center mt-2">
<label className="flex gap-1 items-center"> <label className="flex gap-1 items-center">
<input type="checkbox" /> <input type="checkbox" checked={styles.bold}
onChange={(e) => handleStyleChange("bold", e.target.checked)} />
Bold Bold
</label> </label>
<label className="flex gap-1 items-center"> <label className="flex gap-1 items-center">
<input type="checkbox" /> <input type="checkbox" checked={styles.italic} onChange={(e) => handleStyleChange("italic", e.target.checked)} />
Italic Italic
</label> </label>
<label className="flex gap-1 items-center"> <label className="flex gap-1 items-center">
<input type="checkbox" /> <input type="checkbox" checked={styles.underline}
onChange={(e) =>
handleStyleChange("underline", e.target.checked)
} />
Underline Underline
</label> </label>
</div> </div>

View File

@ -16,21 +16,31 @@ const SelectTagAttr: React.FC<SelectTagAttrInt> = ({ options, selTagKey, selTagI
const handleAddSelectOption = () => { const handleAddSelectOption = () => {
if (selectedOption && selectedOption.length) { if (selectedOption && selectedOption.length) {
setDroppedItems((prev) => ( setDroppedItems((prev) => {
{ console.log("Previous droppedItems:", prev);
const newState = {
...prev, ...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) [selTagKey]: Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) => { ? prev[selTagKey].map((item, i) => {
if (i === selTagIndx) { if (i === selTagIndx) {
return { ...item, options: item.options ? [...item.options, selectedOption] : [selectedOption], update: true }; return {
...item,
options: item.options ? [...item.options, selectedOption] : [selectedOption],
update: true,
};
} }
return item; return item;
}) })
: [], : [],
};
console.log("New droppedItems state:", newState);
return newState;
});
} }
)); };
}
}
const handleSelectOptionDel = (indx: number) => { const handleSelectOptionDel = (indx: number) => {
setDroppedItems((prev) => ( setDroppedItems((prev) => (

View File

@ -1,63 +1,119 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { FaSpinner } from 'react-icons/fa';
export type ActionType = 'create' | 'invite'; export type ActionType = 'create' | 'invite';
type CreateData = { accountName: string }; type CreateData = { accountName: string };
type InviteData = { i_accountName: string; email: string; role: string }; type InviteData = { i_accountName: number; email: string; role: string };
type FormDataType = CreateData | InviteData; export type FormDataType = CreateData | InviteData;
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
interface Props { interface Props {
onSubmit: (data: FormDataType, action: ActionType) => void; onSubmit: (data: FormDataType, action: ActionType) => void;
createAndInviteLoader: boolean;
accountOptions: AccountOption[];
accountOptionsLoading: boolean;
} }
const CreateAndInvite = ({ onSubmit }: Props) => { const CreateAndInvite = ({
const [isOpen, setIsOpen] = useState(false); onSubmit,
const dropdownRef = useRef<HTMLDivElement>(null); createAndInviteLoader,
accountOptions,
accountOptionsLoading
}: Props) => {
const [isRoleDropdownOpen, setIsRoleDropdownOpen] = useState(false);
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
const roleDropdownRef = useRef<HTMLDivElement>(null);
const accountDropdownRef = useRef<HTMLDivElement>(null);
const [createFormData, setCreateFormData] = useState<{ accountName: string }>({ const [createFormData, setCreateFormData] = useState<{ accountName: string }>({
accountName: '', accountName: '',
}); });
const [accountSearchTerm, setAccountSearchTerm] = useState('');
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(null);
const [inviteFormData, setInviteFormData] = useState<{ const [inviteFormData, setInviteFormData] = useState<{
i_accountName: string; i_accountName: number;
email: string; email: string;
role: string; role: string;
}>({ }>({
i_accountName: '', i_accountName: 0,
email: '', email: '',
role: '', role: '',
}); });
const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [errors, setErrors] = useState<{ [key: string]: string }>({});
const roles = ['ADMIN', 'EDITOR', 'VIEWER'];
const roles = ['Owner', 'Client']; // Handle role dropdown
const handleRoleDropdownFocus = () => setIsRoleDropdownOpen(true);
const handleFocus = () => setIsOpen(true); const handleRoleDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setTimeout(() => { setTimeout(() => {
const activeElement = document.activeElement as HTMLElement; const activeElement = document.activeElement as HTMLElement;
if (dropdownRef.current && !dropdownRef.current.contains(activeElement)) { if (roleDropdownRef.current && !roleDropdownRef.current.contains(activeElement)) {
setIsOpen(false); setIsRoleDropdownOpen(false);
} }
}, 100); }, 100);
}; };
const handleSelect = (role: string) => { const handleRoleSelect = (role: string) => {
setInviteFormData((prev) => ({ ...prev, role })); setInviteFormData((prev) => ({ ...prev, role }));
setIsOpen(false); setIsRoleDropdownOpen(false);
}; };
// Handle account dropdown
const handleAccountDropdownFocus = () => setIsAccountDropdownOpen(true);
const handleAccountDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement;
if (accountDropdownRef.current && !accountDropdownRef.current.contains(activeElement)) {
setIsAccountDropdownOpen(false);
}
}, 100);
};
const handleAccountSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setAccountSearchTerm(value);
// Clear selected account if user is typing
if (selectedAccount && value !== selectedAccount.accountName) {
setSelectedAccount(null);
setInviteFormData((prev) => ({ ...prev, i_accountName: 0 }));
}
};
const handleAccountSelect = (option: AccountOption) => {
setSelectedAccount(option);
setAccountSearchTerm(option.accountName);
setInviteFormData((prev) => ({ ...prev, i_accountName: option.accountId }));
setIsAccountDropdownOpen(false);
// Clear any existing account name error
if (errors.i_accountName) {
setErrors(prev => ({ ...prev, i_accountName: '' }));
}
};
// Handle create form
const handleCreateChange = (value: string) => { const handleCreateChange = (value: string) => {
setCreateFormData({ accountName: value }); setCreateFormData({ accountName: value });
}; // Clear error when user starts typing
if (errors.accountName) {
const handleInviteChange = ( setErrors(prev => ({ ...prev, accountName: '' }));
field: keyof typeof inviteFormData, }
value: string
) => {
setInviteFormData((prev) => ({ ...prev, [field]: value }));
}; };
const handleCreateSubmit = () => { const handleCreateSubmit = () => {
@ -69,11 +125,20 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
onSubmit(createFormData, 'create'); onSubmit(createFormData, 'create');
}; };
// Handle invite form
const handleInviteChange = (field: keyof typeof inviteFormData, value: string) => {
setInviteFormData((prev) => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const handleInviteSubmit = () => { const handleInviteSubmit = () => {
const { i_accountName, email, role } = inviteFormData; const { i_accountName, email, role } = inviteFormData;
const errs: { [key: string]: string } = {}; const errs: { [key: string]: string } = {};
if (!i_accountName.trim()) errs.i_accountName = 'Account name is required'; if (!i_accountName) errs.i_accountName = 'Account name is required';
if (!email.trim()) errs.email = 'Email is required'; if (!email.trim()) errs.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email'; else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email';
if (!role.trim()) errs.role = 'Role is required'; if (!role.trim()) errs.role = 'Role is required';
@ -87,9 +152,14 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
onSubmit(inviteFormData, 'invite'); onSubmit(inviteFormData, 'invite');
}; };
// Filter account options based on search term
const filteredAccountOptions = accountOptions.filter((option) =>
option.accountName?.toLowerCase().includes(accountSearchTerm.toLowerCase())
);
return ( return (
<div className="mt-3 space-y-3"> <div className="ms-2 space-y-3">
{/* Create */} {/* Create Section */}
<section className="border-gray-200 border-b"> <section className="border-gray-200 border-b">
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2> <h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
<div className="flex flex-col mb-6"> <div className="flex flex-col mb-6">
@ -107,9 +177,17 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
<button <button
type="button" type="button"
onClick={handleCreateSubmit} onClick={handleCreateSubmit}
className="ms-1 text-[14px] px-3 py-[1.1] bg-gray-300 text-slate-600 font-medium rounded-md border border-gray-300 hover:bg-opacity-90 transition cursor-pointer" className="ms-1 flex items-center text-[14px] px-3 py-[1.1] bg-gray-300 text-slate-600 font-medium rounded-md border border-gray-300 hover:bg-opacity-90 transition cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed"
disabled={createAndInviteLoader}
> >
Create {createAndInviteLoader ? (
<>
<FaSpinner className="animate-spin me-1" />
CREATING...
</>
) : (
'CREATE'
)}
</button> </button>
</div> </div>
{errors.accountName && ( {errors.accountName && (
@ -118,22 +196,62 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
</div> </div>
</section> </section>
{/* Invite */} {/* Invite Section */}
<section className="border-gray-300"> <section className="border-gray-300">
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2> <h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
<div className="grid grid-cols-2 gap-4 mb-6"> <div className="grid grid-cols-2 gap-4 mb-6">
{/* Account Name */} {/* Account Name Dropdown */}
<div className="flex flex-col"> <div className="flex flex-col relative">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1"> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Account Name Account Name
</label> </label>
<div
className="relative w-full"
onBlur={handleAccountDropdownBlur}
ref={accountDropdownRef}
tabIndex={-1}
>
<input <input
type="text" type="text"
placeholder="Enter account name" placeholder={accountOptionsLoading ? "Loading accounts..." : "Select or search account"}
value={inviteFormData.i_accountName} value={accountSearchTerm}
onChange={(e) => handleInviteChange('i_accountName', e.target.value)} onChange={handleAccountSearchChange}
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" onFocus={handleAccountDropdownFocus}
disabled={accountOptionsLoading}
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-60 disabled:cursor-not-allowed"
/> />
{/* Loading indicator */}
{accountOptionsLoading && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<FaSpinner className="animate-spin text-gray-400 text-sm" />
</div>
)}
{/* Account Dropdown */}
{isAccountDropdownOpen && !accountOptionsLoading && (
<div className="absolute w-full bg-white border border-gray-300 rounded-md mt-1 shadow-lg z-10">
<ul className="max-h-40 overflow-y-auto">
{filteredAccountOptions.length > 0 ? (
filteredAccountOptions.map((option) => (
<li
key={option.accountId}
onMouseDown={() => handleAccountSelect(option)}
className="px-3 py-2 text-[13px] text-gray-600 hover:bg-gray-100 cursor-pointer"
>
<div className="flex justify-between items-center">
<span>{option.accountName}</span>
</div>
</li>
))
) : (
<li className="px-3 py-2 text-gray-500">No results found</li>
)}
</ul>
</div>
)}
</div>
{errors.i_accountName && ( {errors.i_accountName && (
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span> <span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
)} )}
@ -163,8 +281,8 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
</label> </label>
<div <div
className="relative w-full" className="relative w-full"
onBlur={handleBlur} onBlur={handleRoleDropdownBlur}
ref={dropdownRef} ref={roleDropdownRef}
tabIndex={-1} tabIndex={-1}
> >
<input <input
@ -172,40 +290,48 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
placeholder="Select role" placeholder="Select role"
value={inviteFormData.role} value={inviteFormData.role}
readOnly readOnly
onFocus={handleFocus} onFocus={handleRoleDropdownFocus}
className="w-full px-3 py-1.5 border text-gray-600 border-gray-400 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] transition" className="w-full px-3 py-1.5 border text-gray-600 border-gray-400 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] transition cursor-pointer"
/> />
<div <div
className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${ className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${
isOpen isRoleDropdownOpen
? 'opacity-100 scale-100' ? 'opacity-100 scale-100'
: 'opacity-0 scale-95 pointer-events-none' : 'opacity-0 scale-95 pointer-events-none'
}`} }`}
> >
{roles.map((r) => ( {roles.map((role) => (
<div <div
key={r} key={role}
onMouseDown={() => handleSelect(r)} onMouseDown={() => handleRoleSelect(role)}
className="px-4 py-2 text-[13px] hover:bg-gray-100 cursor-pointer" className="px-4 py-1 text-[13px] hover:bg-gray-100 cursor-pointer"
> >
{r} {role}
</div> </div>
))} ))}
</div> </div>
</div>
{errors.role && ( {errors.role && (
<span className="text-sm text-red-500 mt-1">{errors.role}</span> <span className="text-sm text-red-500 mt-1">{errors.role}</span>
)} )}
</div> </div>
</div> </div>
</div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
type="button" type="button"
onClick={handleInviteSubmit} onClick={handleInviteSubmit}
className="px-4 py-2 text-sm font-medium rounded-md cursor-pointer hover:bg-opacity-90 transition text-[14px] bg-gray-300 text-slate-600" className="px-4 py-2 text-sm font-medium rounded-md cursor-pointer hover:bg-opacity-90 transition text-[14px] bg-gray-300 text-slate-600 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={createAndInviteLoader}
> >
Invite {createAndInviteLoader ? (
<>
<FaSpinner className="animate-spin me-1" />
INVITING...
</>
) : (
'INVITE'
)}
</button> </button>
</div> </div>
</section> </section>

View File

@ -7,6 +7,8 @@ import Language from "./Language";
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite' import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
import AsideSection from "./AsideSection"; import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature"; import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
import AccountNav from "../accounts/AccountNav";
// Define the shape of state object // Define the shape of state object
interface Account { interface Account {
@ -17,8 +19,7 @@ interface Account {
interface UserData { interface UserData {
id: string; id: string;
firstName: string; name: string;
lastName: string;
email: string; email: string;
language: string; language: string;
account: Account; account: Account;
@ -30,11 +31,26 @@ interface SubSection {
component?: React.ReactNode; component?: React.ReactNode;
} }
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
const Setting = () => { const Setting = () => {
const { page1 } = useParams<{ page1: string }>(); const { page1 } = useParams<{ page1: string }>();
console.log("page1", page1)
const location = useLocation(); const location = useLocation();
console.log("location", location)
const navigate = useNavigate(); const navigate = useNavigate();
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
// Refs for subsections within each main tab // Refs for subsections within each main tab
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -42,18 +58,23 @@ const Setting = () => {
const [activeTab, setActiveTab] = useState("userProfile"); const [activeTab, setActiveTab] = useState("userProfile");
const [activeSubSection, setActiveSubSection] = useState<string>(""); const [activeSubSection, setActiveSubSection] = useState<string>("");
const [user, setUser] = useState<UserData>({ const [user, setUser] = useState<UserData>({
id: "", id: '',
firstName: "", name: '',
lastName: "", email: '',
email: "", language: '',
language: "",
account: { account: {
id: 0, id: 0,
isDefault: false, isDefault: false,
name: "", name: '',
}, },
}); });
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
// Dropdown data states
const [accountOptions, setAccountOptions] = useState<AccountOption[]>([]);
const [accountOptionsLoading, setAccountOptionsLoading] = useState(false);
// Define subsections for each main tab // Define subsections for each main tab
const getSubSections = (tabId: string): SubSection[] => { const getSubSections = (tabId: string): SubSection[] => {
switch (tabId) { switch (tabId) {
@ -62,16 +83,6 @@ const Setting = () => {
{ id: "profile-info", label: "Profile Information" }, { id: "profile-info", label: "Profile Information" },
{ id: "signature", label: "Signature" }, { id: "signature", label: "Signature" },
]; ];
// case "create and invite":
// return [
// { id: "create-account", label: "Create Account" },
// { id: "invite-users", label: "Invite Users" },
// { id: "manage-invitations", label: "Manage Invitations" }
// ];
// return [
// { id: "share-options", label: "Share Options" },
// { id: "privacy-settings", label: "Privacy Settings" }
// ];
default: default:
return []; return [];
} }
@ -143,6 +154,21 @@ const Setting = () => {
} }
}, [location.hash, activeTab]); }, [location.hash, activeTab]);
// Fetch account list for dropdown
const getAccountList = async () => {
setAccountOptionsLoading(true);
try {
const res = await api.get('/api/user/accounts');
if (res?.data) {
setAccountOptions(res.data);
}
} catch (error) {
console.error('Error fetching account options:', error);
} finally {
setAccountOptionsLoading(false);
}
};
// Render subsection content based on active tab and subsection // Render subsection content based on active tab and subsection
const renderSubSectionContent = (subSectionId: string) => { const renderSubSectionContent = (subSectionId: string) => {
switch (activeTab) { switch (activeTab) {
@ -178,7 +204,14 @@ const Setting = () => {
case "language": case "language":
return <Language userData={user} />; return <Language userData={user} />;
case "create and invite": case "create and invite":
return <CreateAndInvite onSubmit={handleSubmit} />; return (
<CreateAndInvite
onSubmit={handleSubmit}
createAndInviteLoader={createAndInviteLoader}
accountOptions={accountOptions}
accountOptionsLoading={accountOptionsLoading}
/>
);
default: default:
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>; return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
} }
@ -209,23 +242,66 @@ const Setting = () => {
{ id: "contactShare", label: "Share Contact", type: "contact" }, { id: "contactShare", label: "Share Contact", type: "contact" },
]; ];
const getUserDetails = async () => {
try { // Listen for sessionStorage changes
let res = await api.get(`/api/user/details`) useEffect(() => {
console.log("getuser", res) const handleStorageChange = (e: StorageEvent) => {
setUser(res?.data) if (e.key === 'activeAccountId') {
} catch (error) { const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
console.error("Error fetching user details:", error); 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(() => { useEffect(() => {
getUserDetails() // Fetch account list when component mounts
getAccountList()
}, []) }, [])
// Set initial subsection when component mounts or activeTab changes // Set initial subsection when component mounts or activeTab changes
useEffect(() => { useEffect(() => {
const subSections = getSubSections(activeTab); const subSections = getSubSections(activeTab);
console.log("subSections", subSections)
const hash = location.hash.replace('#', ''); const hash = location.hash.replace('#', '');
if (subSections.length > 0) { if (subSections.length > 0) {
@ -255,20 +331,58 @@ const Setting = () => {
}, [activeSubSection]); }, [activeSubSection]);
const handleSubmit = async (data: FormDataType, action: ActionType) => { const handleSubmit = async (data: FormDataType, action: ActionType) => {
setCreateAndInviteLoader(true)
try { try {
let res;
let toastMesaage;
if (action === 'create') { if (action === 'create') {
console.log('Creating account:', data); console.log('Creating account:', data);
const payloadAccount = {
name: data.accountName
}
res = await api.post(`/api/user/accounts`, payloadAccount)
console.log("resAccountName", res)
if (res.status === 201) {
toastMesaage = `${action.toUpperCase()} Created Successfull...!`
}
} else if (action === 'invite') { } else if (action === 'invite') {
console.log('Inviting user:', data); console.log('Inviting user:', data);
const payloadInvite = {
email: data.email,
role: data.role
}
res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
console.log("InviteNAme", res)
toastMesaage = "Invite Successfull...!"
}
if (res?.data) {
toast.success(toastMesaage, {
position: "top-center",
autoClose: 1500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// Refetch account list after successful submission
await getAccountList();
} }
} catch (err) { } catch (err) {
console.error('Submission error:', err); console.error('Submission error:', err);
} finally {
setCreateAndInviteLoader(false)
} }
}; };
return ( return (
<> <>
<SettingNav /> {/* <SettingNav /> */}
{/* <AccountNav/> */}
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2"> <div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
{/* Main Navigation Tabs */} {/* Main Navigation Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5"> <div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5">
@ -279,8 +393,7 @@ const Setting = () => {
key={tab.id} key={tab.id}
onClick={() => handleMainTabClick(tab.id)} onClick={() => handleMainTabClick(tab.id)}
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200 className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
${ ${activeTab === tab.id
activeTab === tab.id
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600" ? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
: "text-gray-600 hover:text-blue-500" : "text-gray-600 hover:text-blue-500"
}`} }`}

View File

@ -39,6 +39,7 @@ const SettingNav = () => {
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600" className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
> >
<FaCog className="text-sm" /> <FaCog className="text-sm" />
</Link> </Link>
</div> </div>

View File

@ -1,13 +1,34 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
// import { X, Upload, Edit3, Image, Cloud, HardDrive, Droplets } from 'lucide-react'; import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
import { AiOutlineClose, AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai'; import { MdCloudUpload } from 'react-icons/md';
import { MdImage, MdCloudUpload, MdStorage } from 'react-icons/md';
import api from '../../../config/api'; import api from '../../../config/api';
import DrawSignatureCanvas from './DrawSignatureCanvas';
import { FaSpinner } from "react-icons/fa";
import { GrCheckboxSelected } from "react-icons/gr";
import { Bounce, toast } from "react-toastify";
interface Signature {
id: string;
email: string;
signatureType: string;
lastName: string;
firstName: string
font: string;
initialsBlob: string;
initialsStampedBlob: string;
initialsHash: string;
signatureHash: string;
signatureBlob: string;
signatureStampedBlob: string;
default: boolean
}
interface AddSignatureModalProps { interface AddSignatureModalProps {
setShowAddSignature?: (value: boolean) => void; setShowAddSignature?: (value: boolean) => void;
getAllSignatures: () => Promise<void>;
editingSignature?: Signature | null;
} }
interface Account { interface Account {
id: number; id: number;
isDefault: boolean; isDefault: boolean;
@ -23,12 +44,25 @@ interface UserData {
account: Account; account: Account;
} }
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignature }) => { interface SelectStyle {
const [activeTab, setActiveTab] = useState<'choose' | 'draw' | 'upload'>('choose'); id: number,
style: string
}
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
setShowAddSignature,
getAllSignatures,
editingSignature
}) => {
const [activeTab, setActiveTab] = useState<'TYPE' | 'DRAW' | 'UPLOAD' | 'SELECT'>('TYPE');
const [fullName, setFullName] = useState(''); const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState(''); const [initials, setInitials] = useState('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [signatures, setSignatures] = useState<{
fullSignature: string | null;
initials: string | null;
}>({ fullSignature: null, initials: null });
// Sample signature options for the CHOOSE tab
const signatureOptions = [ const signatureOptions = [
{ id: 1, name: 'print', initials: 'LA', style: 'print' }, { id: 1, name: 'print', initials: 'LA', style: 'print' },
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' }, { id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
@ -38,7 +72,9 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' }, { id: 6, name: 'classic', initials: 'LA', style: 'classic' },
]; ];
const [selectedSignature, setSelectedSignature] = useState<number>(1); const [selectedSignature, setSelectedSignature] = useState<SelectStyle>(
signatureOptions.length > 0 ? { id: signatureOptions[0].id, style: signatureOptions[0].style } : { id: 0, style: "" }
);
// Store the actual file // Store the actual file
const [file1, setFile1] = useState<File | null>(null); const [file1, setFile1] = useState<File | null>(null);
@ -48,7 +84,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
const [preview1, setPreview1] = useState<string | null>(null); const [preview1, setPreview1] = useState<string | null>(null);
const [preview2, setPreview2] = useState<string | null>(null); const [preview2, setPreview2] = useState<string | null>(null);
//user // User data
const [user, setUser] = useState<UserData>({ const [user, setUser] = useState<UserData>({
id: "", id: "",
firstName: "", firstName: "",
@ -62,13 +98,49 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
}, },
}); });
const isEditMode = !!editingSignature;
// Navigation items // Navigation items
const navigationItems = [ const navigationItems = [
{ id: 'choose', label: 'Choose', icon: <MdImage className="w-5 h-5" /> }, { id: 'TYPE', label: 'TYPE', icon: <GrCheckboxSelected className="w-4 h-4" /> },
{ id: 'draw', label: 'Draw', icon: <AiOutlineEdit className="w-5 h-5" /> }, { id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
{ id: 'upload', label: 'Upload', icon: <AiOutlineUpload className="w-5 h-5" /> }, { id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
]; ];
// Initialize form with editing signature data
useEffect(() => {
if (editingSignature) {
setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
setActiveTab(editingSignature.signatureType as 'TYPE' | 'DRAW' | 'UPLOAD');
// Set font for SELECT tab
const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
if (fontOption) {
setSelectedSignature({ id: fontOption.id, style: fontOption.style });
}
// Set signature blobs for all tabs
if (editingSignature.signatureBlob && editingSignature.initialsBlob) {
setSignatures({
fullSignature: `data:image/png;base64,${editingSignature.signatureBlob}`,
initials: `data:image/png;base64,${editingSignature.initialsBlob}`
});
// Set preview URLs for UPLOAD tab
if (editingSignature.signatureType === 'UPLOAD') {
setPreview1(`data:image/png;base64,${editingSignature.signatureBlob}`);
setPreview2(`data:image/png;base64,${editingSignature.initialsBlob}`);
}
}
}
}, [editingSignature]);
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
setSignatures(data);
};
// Cleanup preview URLs when component unmounts or image changes // Cleanup preview URLs when component unmounts or image changes
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -77,23 +149,121 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
}; };
}, [preview1, preview2]); }, [preview1, preview2]);
const handleImageChange = ( const handleImageChange = async (
e: React.ChangeEvent<HTMLInputElement>, e: React.ChangeEvent<HTMLInputElement>,
setFile: React.Dispatch<React.SetStateAction<File | null>>, setFile: React.Dispatch<React.SetStateAction<File | null>>,
setPreview: React.Dispatch<React.SetStateAction<string | null>> setPreview: React.Dispatch<React.SetStateAction<string | null>>,
type: "fullSignature" | "initials"
) => { ) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
setFile(file); setFile(file);
const previewUrl = URL.createObjectURL(file); const previewUrl = URL.createObjectURL(file);
setPreview(previewUrl); setPreview(previewUrl);
// Convert to base64
const base64 = await convertToBase64(file);
// Update the appropriate signature
setSignatures((prev) => ({
...prev,
[type]: base64,
}));
} }
}; };
const convertToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const handleSubmit = async () => {
setIsSubmitting(true);
if (!signatures.fullSignature || !signatures.initials) {
alert('Please provide both signature and initials');
setIsSubmitting(false);
return;
}
try {
const payload = {
signatureType: activeTab,
signatureBase64: signatures.fullSignature,
initialsBase64: signatures.initials,
firstName: fullName.split(' ')[0] || user.firstName,
lastName: fullName.split(' ')[1] || user.lastName,
font: selectedSignature.style
};
let res;
if (isEditMode && editingSignature) {
// Update existing signature
const updatePayload = {
...payload,
signatureId: editingSignature.id
};
res = await api.put(`/api/user/signatures`, updatePayload);
console.log("handleUpdate", res);
toast.success("Signature Updated Successfully", {
position: "top-center",
autoClose: 500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
} else {
// Create new signature
res = await api.post(`/api/user/signatures`, payload);
console.log("handleSubmit", res);
toast.success("Signature Created Successfully", {
position: "top-center",
autoClose: 500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
if (res?.data) {
await getAllSignatures();
setShowAddSignature?.(false);
}
} catch (error) {
console.error("Error submitting signature:", error);
toast.error(isEditMode ? "Failed to update signature" : "Failed to create signature", {
position: "top-center",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
} finally {
setIsSubmitting(false);
}
};
const renderContent = () => { const renderContent = () => {
switch (activeTab) { switch (activeTab) {
case 'choose': case 'TYPE':
return ( return (
<div className="h-full flex flex-col "> <div className="h-full flex flex-col ">
<div className="mb-6"> <div className="mb-6">
@ -109,8 +279,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
type="radio" type="radio"
id={`signature-${option.id}`} id={`signature-${option.id}`}
name="signature" name="signature"
checked={selectedSignature === option.id} checked={selectedSignature.id === option.id}
onChange={() => setSelectedSignature(option.id)} onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
className="w-4 h-4 text-blue-600 focus:ring-blue-500" className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/> />
<div className="flex-1 grid grid-cols-2 gap-2"> <div className="flex-1 grid grid-cols-2 gap-2">
@ -147,56 +317,15 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
</div> </div>
); );
case 'draw': case 'DRAW':
return ( return (
<div className="h-full flex flex-col"> <DrawSignatureCanvas
<div className="mb-6"> onSignatureChange={handleSignatureChange}
<h2 className="text-xl font-semibold text-gray-900 mb-2">Draw Signature</h2> initialSignatures={isEditMode ? signatures : undefined}
<p className="text-sm text-gray-600">Create your signature by drawing directly on the canvas</p> />
</div>
<div className="flex-1 flex flex-col">
<div className="grid grid-cols-12 gap-6 mb-6">
{/* Draw area 1 */}
<div className="col-span-8">
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
<div className="text-center">
{/* <Edit3 className="mx-auto h-12 w-12 text-gray-400 mb-3" /> */}
<p className="text-sm text-gray-500">Click and drag to draw your signature</p>
<p className="text-xs text-gray-400 mt-1">Use mouse or touch to create your signature</p>
</div>
</div>
</div>
{/* Draw area 2 */}
<div className="col-span-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
<div className="text-center">
{/* <Edit3 className="mx-auto h-8 w-8 text-gray-400 mb-2" /> */}
<p className="text-xs text-gray-500">Draw initials</p>
</div>
</div>
</div>
</div>
<div className="flex gap-3">
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors flex items-center gap-2">
{/* <X className="w-4 h-4" /> */}
Clear
</button>
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
Undo
</button>
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
Redo
</button>
</div>
</div>
</div>
); );
case 'upload': case 'UPLOAD':
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="mb-6"> <div className="mb-6">
@ -208,7 +337,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{/* Upload 1 */} {/* Upload 1 */}
<div className="col-span-8"> <div className="col-span-8">
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label> <label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors"> <label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
{preview1 ? ( {preview1 ? (
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" /> <img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
) : ( ) : (
@ -224,19 +353,19 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
type="file" type="file"
accept="image/*" accept="image/*"
className="hidden" className="hidden"
onChange={(e) => handleImageChange(e, setFile1, setPreview1)} onChange={(e) => handleImageChange(e, setFile1, setPreview1, "fullSignature")}
/> />
</div> </div>
{/* Upload 2 */} {/* Upload 2 */}
<div className="col-span-4"> <div className="col-span-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label> <label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors"> <label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
{preview2 ? ( {preview2 ? (
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" /> <img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
) : ( ) : (
<div className="text-center"> <div className="text-center">
<MdImage className="mx-auto h-8 w-8 text-gray-400 mb-2" /> <MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
<p className="text-xs text-gray-500">Upload initials</p> <p className="text-xs text-gray-500">Upload initials</p>
</div> </div>
)} )}
@ -246,7 +375,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
type="file" type="file"
accept="image/*" accept="image/*"
className="hidden" className="hidden"
onChange={(e) => handleImageChange(e, setFile2, setPreview2)} onChange={(e) => handleImageChange(e, setFile2, setPreview2, "initials")}
/> />
</div> </div>
</div> </div>
@ -255,6 +384,16 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
</div> </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: default:
return null; return null;
} }
@ -262,17 +401,24 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
const getUserDetails = async () => { const getUserDetails = async () => {
try { try {
let res = await api.get(`/api/user/details`) let res = await api.get(`/api/user/details`);
console.log("getuser", res) console.log("getuser", res);
setUser(res?.data) let currUser = res?.data;
setUser(currUser);
// Only set default values if not in edit mode
if (!isEditMode) {
setFullName(currUser.firstName + " " + currUser.lastName);
setInitials(currUser.firstName.slice(0, 1) + currUser.lastName.slice(0, 1));
}
} catch (error) { } catch (error) {
console.error("Error fetching user details:", error); console.error("Error fetching user details:", error);
} }
} };
useEffect(() => { useEffect(() => {
getUserDetails() getUserDetails();
}, []) }, []);
return ( return (
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 flex items-center justify-center z-50 p-4">
@ -327,8 +473,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{navigationItems.map((item) => ( {navigationItems.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => setActiveTab(item.id as 'choose' | '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 transition-colors ${activeTab === item.id className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
? 'bg-blue-50 text-blue-700 border border-blue-200' ? 'bg-blue-50 text-blue-700 border border-blue-200'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`} }`}
@ -351,25 +497,37 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{/* Content */} {/* Content */}
<div className="flex-1 py-5 px-5 overflow-hidden "> <div className="flex-1 py-5 px-5 overflow-hidden ">
<button {/* <button
onClick={() => setShowAddSignature?.(false)} onClick={() => setShowAddSignature?.(false)}
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20" className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
> >
<AiOutlineClose className="w-5 h-5 text-gray-500" /> <AiOutlineClose className="w-5 h-5 text-gray-500" />
</button> </button> */}
{renderContent()} {renderContent()}
</div> </div>
{/* Footer Buttons in Main Area */} {/* Footer Buttons in Main Area */}
<div className="p-4 border-t border-gray-200 flex justify-end gap-3"> <div className="p-4 border-t border-gray-200 flex justify-end gap-3">
<button <button
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors" className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
onClick={() => setShowAddSignature?.(false)} onClick={() => setShowAddSignature?.(false)}
disabled={isSubmitting}
> >
CANCEL CANCEL
</button> </button>
<button className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors"> <button
CREATE SIGNATURE className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md bg-[var(--modalSubmitBG)] text-white hover:bg-[var(--modalSubmitBGHover)] transition-colors flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
onClick={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<FaSpinner className="animate-spin" />
{isEditMode ? 'UPDATING...' : 'CREATING...'}
</>
) : (
isEditMode ? 'UPDATE SIGNATURE' : 'CREATE SIGNATURE'
)}
</button> </button>
</div> </div>

View File

@ -0,0 +1,286 @@
import React, { useRef, useState, useEffect } from 'react';
type Props = {
onSignatureChange: (data: { fullSignature: string | null; initials: string | null }) => void;
initialSignatures?: { fullSignature: string | null; initials: string | null };
};
const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignatures }) => {
const canvasRef1 = useRef<HTMLCanvasElement>(null);
const canvasRef2 = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [paths1, setPaths1] = useState<Array<Array<{ x: number; y: number }>>>([]);
const [paths2, setPaths2] = useState<Array<Array<{ x: number; y: number }>>>([]);
const [currentPath, setCurrentPath] = useState<Array<{ x: number; y: number }>>([]);
const [activeCanvas, setActiveCanvas] = useState<'full' | 'initials' | null>(null);
const [initialLoaded, setInitialLoaded] = useState(false);
// Store initial signature images to avoid reloading
const [initialImage1, setInitialImage1] = useState<HTMLImageElement | null>(null);
const [initialImage2, setInitialImage2] = useState<HTMLImageElement | null>(null);
// Setup canvas dimensions when component mounts
useEffect(() => {
const setupCanvas = () => {
if (canvasRef1.current) {
const rect1 = canvasRef1.current.getBoundingClientRect();
canvasRef1.current.width = rect1.width;
canvasRef1.current.height = rect1.height;
}
if (canvasRef2.current) {
const rect2 = canvasRef2.current.getBoundingClientRect();
canvasRef2.current.width = rect2.width;
canvasRef2.current.height = rect2.height;
}
};
setupCanvas();
// Load initial signatures after canvas setup
if (initialSignatures && !initialLoaded) {
setTimeout(() => {
loadInitialSignatures();
}, 100);
}
}, []);
const loadInitialSignatures = () => {
if (!initialSignatures) return;
let loadedCount = 0;
const totalToLoad = (initialSignatures.fullSignature ? 1 : 0) + (initialSignatures.initials ? 1 : 0);
const checkComplete = () => {
loadedCount++;
if (loadedCount === totalToLoad) {
setInitialLoaded(true);
}
};
// Load full signature
if (initialSignatures.fullSignature && canvasRef1.current) {
const img1 = new Image();
img1.onload = () => {
setInitialImage1(img1);
const ctx1 = canvasRef1.current?.getContext('2d');
if (ctx1) {
ctx1.clearRect(0, 0, canvasRef1.current!.width, canvasRef1.current!.height);
ctx1.drawImage(img1, 0, 0);
}
checkComplete();
};
img1.src = initialSignatures.fullSignature;
}
// Load initials
if (initialSignatures.initials && canvasRef2.current) {
const img2 = new Image();
img2.onload = () => {
setInitialImage2(img2);
const ctx2 = canvasRef2.current?.getContext('2d');
if (ctx2) {
ctx2.clearRect(0, 0, canvasRef2.current!.width, canvasRef2.current!.height);
ctx2.drawImage(img2, 0, 0);
}
checkComplete();
};
img2.src = initialSignatures.initials;
}
if (totalToLoad === 0) {
setInitialLoaded(true);
}
};
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
setIsDrawing(true);
const rect = e.currentTarget.getBoundingClientRect();
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
setCurrentPath([point]);
setActiveCanvas(e.currentTarget === canvasRef1.current ? 'full' : 'initials');
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const rect = e.currentTarget.getBoundingClientRect();
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
const newPath = [...currentPath, point];
setCurrentPath(newPath);
// Draw immediately for smooth experience
drawCurrentPath(e.currentTarget as HTMLCanvasElement, newPath);
};
const drawCurrentPath = (canvas: HTMLCanvasElement, path: Array<{ x: number; y: number }>) => {
if (!canvas || path.length < 2) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Only draw the current path segment without clearing the canvas
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
const lastPoint = path[path.length - 2];
const currentPoint = path[path.length - 1];
ctx.beginPath();
ctx.moveTo(lastPoint.x, lastPoint.y);
ctx.lineTo(currentPoint.x, currentPoint.y);
ctx.stroke();
};
const exportCanvasAsImage = (canvas: HTMLCanvasElement | null): string | null => {
if (!canvas) return null;
return canvas.toDataURL('image/png');
};
const handleMouseUp = () => {
setIsDrawing(false);
if (currentPath.length === 0 || !activeCanvas) return;
if (activeCanvas === 'full') {
const newPaths = [...paths1, currentPath];
setPaths1(newPaths);
} else {
const newPaths = [...paths2, currentPath];
setPaths2(newPaths);
}
setCurrentPath([]);
// Export both canvases and notify parent
const fullSignature = exportCanvasAsImage(canvasRef1.current);
const initials = exportCanvasAsImage(canvasRef2.current);
onSignatureChange({ fullSignature, initials });
};
const redrawCanvas = (canvas: HTMLCanvasElement, paths: Array<Array<{ x: number; y: number }>>, initialImage: HTMLImageElement | null) => {
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw initial signature if exists
if (initialImage) {
ctx.drawImage(initialImage, 0, 0);
}
// Draw all paths
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
paths.forEach((path) => {
if (path.length < 2) return;
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
for (let i = 1; i < path.length; i++) {
ctx.lineTo(path[i].x, path[i].y);
}
ctx.stroke();
});
};
const clearCanvas = () => {
if (canvasRef1.current) {
const ctx1 = canvasRef1.current.getContext('2d');
ctx1?.clearRect(0, 0, canvasRef1.current.width, canvasRef1.current.height);
}
if (canvasRef2.current) {
const ctx2 = canvasRef2.current.getContext('2d');
ctx2?.clearRect(0, 0, canvasRef2.current.width, canvasRef2.current.height);
}
setPaths1([]);
setPaths2([]);
setInitialImage1(null);
setInitialImage2(null);
setInitialLoaded(false);
onSignatureChange({ fullSignature: null, initials: null });
};
const undoLastPath = () => {
if (activeCanvas === 'full' && paths1.length > 0) {
const newPaths = paths1.slice(0, -1);
setPaths1(newPaths);
redrawCanvas(canvasRef1.current!, newPaths, initialImage1);
} else if (activeCanvas === 'initials' && paths2.length > 0) {
const newPaths = paths2.slice(0, -1);
setPaths2(newPaths);
redrawCanvas(canvasRef2.current!, newPaths, initialImage2);
}
// Update parent with new signature data
setTimeout(() => {
const fullSignature = exportCanvasAsImage(canvasRef1.current);
const initials = exportCanvasAsImage(canvasRef2.current);
onSignatureChange({ fullSignature, initials });
}, 10);
};
return (
<div className="h-full flex flex-col">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Draw Signature</h2>
<p className="text-sm text-gray-600">Create your signature by drawing directly on the canvas</p>
</div>
<div className="flex-1 flex flex-col">
<div className="grid grid-cols-12 gap-6 mb-2">
{/* Full Signature Canvas */}
<div className="col-span-8">
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors">
<canvas
ref={canvasRef1}
className="w-full h-48 cursor-crosshair"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
/>
</div>
</div>
{/* Initials Canvas */}
<div className="col-span-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors">
<canvas
ref={canvasRef2}
className="w-full h-48 cursor-crosshair"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
/>
</div>
</div>
</div>
{/* Controls */}
<div className="flex gap-4">
<button
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
onClick={clearCanvas}
>
Clear All
</button>
<button
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
onClick={undoLastPath}
disabled={!activeCanvas || (activeCanvas === 'full' ? paths1.length === 0 : paths2.length === 0)}
>
Undo Last
</button>
</div>
</div>
</div>
);
};
export default DrawSignatureCanvas;

View File

@ -1,63 +1,125 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi'; import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
import AddSignatureModal from './AddSignatureModal'; import AddSignatureModal from './AddSignatureModal';
import api from '../../../config/api';
import { Bounce, toast } from "react-toastify";
import { FaSpinner } from 'react-icons/fa';
interface Signature { interface Signature {
id: string; id: string;
signature: string; email: string;
initial: string; signatureType: string;
createdAt: string; lastName: string;
firstName: string
font: string;
initialsBlob: string;
initialsStampedBlob: string;
initialsHash: string;
signatureHash: string;
signatureBlob: string;
signatureStampedBlob: string;
default: boolean;
signatureMimeType:string;
initialsMimeType:string
} }
const SignatureManager: React.FC = () => { const SignatureManager: React.FC = () => {
const [signatures, setSignatures] = useState<Signature[]>([ const [loading, setLoading] = useState<boolean>(false)
{ const [signatures, setSignatures] = useState<Signature[]>([]);
id: '1',
signature: 'https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg',
initial: 'AB',
createdAt: 'dca3e646-62cd-4cc...'
},
{
id: '2',
signature: '∼∼⌒∼∼⌒∼',
initial: 'AB',
createdAt: 'dca3e646-62cd-4cc...'
},
{
id: '3',
signature: 'Digital Signature',
initial: 'DS',
createdAt: 'dca3e646-62cd-4cc...'
}
]);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null); const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const [showAddSignature , setShowAddSignature] = useState<boolean>(false) const [showAddSignature, setShowAddSignature] = useState<boolean>(false);
const [editingSignature, setEditingSignature] = useState<Signature | null>(null);
const getAllSignatures = async () => {
setLoading(true)
try {
let res = await api.get("/api/user/signatures")
console.log("getAllSignatures", res.data)
let signatureData = res?.data
setSignatures(signatureData)
} catch (error) {
console.log("getAllSignatures", error)
} finally {
setLoading(false)
}
}
useEffect(() => {
getAllSignatures()
}, [])
const handleEdit = (id: string) => { const handleEdit = (id: string) => {
console.log('Edit signature:', id); console.log('Edit signature:', id);
let editSignature = signatures.find((item) => item.id === id);
console.log("editSignature", editSignature);
if (editSignature) {
setEditingSignature(editSignature);
setShowAddSignature(true);
}
setActiveDropdown(null); setActiveDropdown(null);
}; };
const handleDelete = (id: string) => { const handleDelete = async (id: string) => {
setSignatures(signatures.filter(sig => sig.id !== id)); if (!id) return;
setLoading(true)
try {
let res = await api.delete(`/api/user/signatures/${id}`)
console.log(res.data)
toast.success("Signature Delete Successfull", {
position: "top-center",
autoClose: 500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
await getAllSignatures();
} catch (error) {
console.log("delete signature", error)
} finally {
setActiveDropdown(null); setActiveDropdown(null);
setLoading(false)
}
}; };
const toggleDropdown = (id: string) => { const toggleDropdown = (id: string) => {
setActiveDropdown(activeDropdown === id ? null : id); setActiveDropdown(activeDropdown === id ? null : id);
}; };
const handleAddNew = () => {
setEditingSignature(null);
setShowAddSignature(true);
};
const handleModalClose = () => {
setShowAddSignature(false);
setEditingSignature(null);
};
return ( return (
<>
{loading ? <div className=' flex items-center justify-center'>
<div className="p-12 text-center mt-8">
<div className="text-gray-400 mb-4">
<FaSpinner className="w-20 h-20 mx-auto animate-spin" />
</div>
<h3 className="text-xl font-semibold text-gray-700 mb-2">Loading...</h3>
</div>
</div> :
<div className="min-h-screen bg-white p-3 rounded-md px-4 mb-6"> <div className="min-h-screen bg-white p-3 rounded-md px-4 mb-6">
<div className="max-w-[84vw] mx-auto"> <div className="max-w-[84vw] mx-auto">
<>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<div> <div>
<h1 className="text-[17px] font-medium text-gray-500 mb-2">Signatures</h1> <h1 className="text-[17px] font-medium text-gray-500 mb-2">Signatures</h1>
<p className="text-gray-300 text-sm">Add or update your name and signature styles.</p> <p className="text-gray-300 text-sm">Add or update your name and signature styles.</p>
</div> </div>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={()=>setShowAddSignature((prev) => !prev)}> <button className="bg-[var(--modalSubmitBG)] hover:bg-[var(--modalSubmitBGHover)] text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={handleAddNew}>
<FiPlus className="" /> <FiPlus className="" />
ADD ADD
</button> </button>
@ -83,7 +145,7 @@ const SignatureManager: React.FC = () => {
{/* Table Body */} {/* Table Body */}
<div className="divide-y divide-gray-100"> <div className="divide-y divide-gray-100">
{signatures.map((signature, index) => ( {signatures.map((signature, index) => (
<div key={signature.id} className="grid grid-cols-12 gap-4 px-6 py-6 hover:bg-gray-50 transition-colors duration-150"> <div key={signature.id} className="grid grid-cols-12 gap-4 px-6 py-9 hover:bg-gray-50 transition-colors duration-150">
{/* Signature Column */} {/* Signature Column */}
<div className="col-span-5"> <div className="col-span-5">
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-blue-600 rounded-bl-2xl rounded-tl-2xl"> <div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-blue-600 rounded-bl-2xl rounded-tl-2xl">
@ -95,7 +157,7 @@ const SignatureManager: React.FC = () => {
{/* Signature Image */} {/* Signature Image */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center "> <div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<img <img
src="https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg" src={`data:${signature.signatureMimeType};base64,${signature.signatureBlob}`}
className="h-[50px] w-[250px] object-contain" className="h-[50px] w-[250px] object-contain"
alt="Signature" alt="Signature"
/> />
@ -103,31 +165,33 @@ const SignatureManager: React.FC = () => {
{/* Signature ID */} {/* Signature ID */}
<div className="text-xs text-gray-500 truncate"> <div className="text-xs text-gray-500 truncate">
{signature.createdAt} {signature.signatureHash}
</div> </div>
</div> </div>
</div> </div>
{/* Initial Column */} {/* Initial Column */}
<div className="col-span-5"> <div className="col-span-5">
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl"> <div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
{/* Label */} {/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1"> <div className="text-sm text-gray-600 font-medium mb-1">
DS Initials
</div> </div>
{/* Signature Image */} {/* Signature Image */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center "> <div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<img <img
src="https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg" src={`data:${signature.initialsMimeType};base64,${signature.initialsBlob}`}
className="h-[50px] object-contain" className="h-[50px] object-contain"
alt="DS Signature" alt="DS Signature"
/> />
</div> </div>
{/* Initial ID */}
<div className="text-xs text-gray-500 truncate">
{signature.initialsHash}
</div>
</div> </div>
</div> </div>
{/* Actions Column */} {/* Actions Column */}
<div className="col-span-2 flex justify-between items-start pt-9 ps-8 "> <div className="col-span-2 flex justify-between items-start pt-9 ps-8 ">
@ -174,12 +238,9 @@ const SignatureManager: React.FC = () => {
</div> </div>
<h3 className="text-xl font-semibold text-gray-700 mb-2">No signatures yet</h3> <h3 className="text-xl font-semibold text-gray-700 mb-2">No signatures yet</h3>
<p className="text-gray-500 mb-6">Create your first signature to get started.</p> <p className="text-gray-500 mb-6">Create your first signature to get started.</p>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold flex items-center gap-2 mx-auto transition-all duration-200">
<FiPlus className="w-5 h-5" />
ADD SIGNATURE
</button>
</div> </div>
)} )}
</>
</div> </div>
{/* Click outside to close dropdown */} {/* Click outside to close dropdown */}
@ -191,9 +252,14 @@ const SignatureManager: React.FC = () => {
)} )}
{showAddSignature && ( {showAddSignature && (
<AddSignatureModal setShowAddSignature={setShowAddSignature}/> <AddSignatureModal
setShowAddSignature={handleModalClose}
getAllSignatures={getAllSignatures}
editingSignature={editingSignature}
/>
)} )}
</div> </div>
}</>
); );
}; };

View File

@ -6,9 +6,8 @@ interface Account {
} }
interface UserData { interface UserData {
id: string; id: number;
firstName: string; name:string
lastName: string;
email: string; email: string;
language: string; language: string;
account: Account; account: Account;
@ -26,7 +25,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
{/* Profile Picture */} {/* Profile Picture */}
<div className="flex-shrink-0 text-center"> <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"> <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>
</div> </div>
@ -36,7 +35,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
<div className="flex py-1"> <div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div> <div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
<div className="text-gray-400 font-medium text-[13px]"> <div className="text-gray-400 font-medium text-[13px]">
{userData.firstName} {userData.lastName} {userData.name}
</div> </div>
</div> </div>

View File

@ -1,107 +1,581 @@
import React, { useEffect, useState } from 'react' // import React from 'react'
import Draw from './Draw' // import { SaveType } from '../client/SignDocument';
import TypedSign from './TypedSign';
import SignUpload from './SignUpload';
import { IoMdClose } from 'react-icons/io';
import axios from 'axios';
import { store } from '../../store/store';
import { SaveType, SaveTypeEnum } from '../client/SignDocument';
interface SignConProps { // interface SignConProps {
// setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
// recipientId: string;
// saved: SaveType;
// setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
// // save: () => void;
// }
// const SignCon: React.FC<SignConProps> = ({ setOpenSign, recipientId, saved, setSaved }) => {
// return (
// <div>SignCon</div>
// )
// }
// export default SignCon
import React, { useEffect, useState } from 'react';
// import api from '../../config/api';
import DrawSignatureCanvas from '../setting/Signature/DrawSignatureCanvas';
import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
import { MdCloudUpload } from 'react-icons/md';
import { FaSpinner } from "react-icons/fa";
import { GrCheckboxSelected } from "react-icons/gr";
import { Bounce, toast } from "react-toastify";
import axios from 'axios';
import { SaveType } from '../client/SignDocument';
import { CiSaveDown1 } from 'react-icons/ci';
interface Signature {
id: string;
email: string;
signatureType: string;
lastName: string;
firstName: string
font: string;
initialsBlob: string;
initialsStampedBlob: string;
initialsHash: string;
signatureHash: string;
signatureBlob: string;
signatureStampedBlob: string;
default: boolean
}
interface AddSignatureModalProps {
// setShowAddSignature?: (value: boolean) => void;
// getAllSignatures: () => Promise<void>;
// editingSignature?: Signature | null;
recipientId?: string;
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>; setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
recipientId: string;
saved: SaveType; saved: SaveType;
setSaved: React.Dispatch<React.SetStateAction<SaveType>>; setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
// save: () => void;
} }
const SignCon: React.FC<SignConProps> = ({ setOpenSign, recipientId, saved, setSaved }) => { interface Account {
id: number;
const [activeSaveType, setActiveSaveType] = React.useState<SaveTypeEnum>(SaveTypeEnum.DRAW); isDefault: boolean;
name: string;
const handleSaveSignature = () => {
if (!saved.save) return
console.log("signature:", saved.initial, "initials:", saved.signature, "subType:", saved.saveType, "MIC CHECK")
// return
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/signature?recipientId=${recipientId}`,
{
signature: saved.signature,
initials: saved.initial,
subType: saved.saveType
},
{ headers: { "ngrok-skip-browser-warning": "69420", } })
.then((res) => {
console.log(res, "Signature saved response")
if (res.status === 200 || res.status === 201) {
setOpenSign(false)
setActiveSaveType(saved.saveType as SaveTypeEnum)
} }
})
.catch((err) => { interface UserData {
console.error('Error saving signature:', err); id: string;
}) firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
interface SelectStyle {
id: number,
style: string
}
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
setOpenSign,
recipientId,
saved,
setSaved
}) => {
const [activeTab, setActiveTab] = useState<'TYPED' | 'DRAW' | 'UPLOAD'>('TYPED');
const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [signatures, setSignatures] = useState<{
fullSignature: string | null;
initials: string | null;
}>({ fullSignature: null, initials: null });
const signatureOptions = [
{ id: 1, name: 'print', initials: 'LA', style: 'print' },
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
{ id: 3, name: 'script', initials: 'LA', style: 'script' },
{ id: 4, name: 'elegant', initials: 'LA', style: 'elegant' },
{ id: 5, name: 'modern', initials: 'LA', style: 'modern' },
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' },
];
const [selectedSignature, setSelectedSignature] = useState<SelectStyle>(
signatureOptions.length > 0 ? { id: signatureOptions[0].id, style: signatureOptions[0].style } : { id: 0, style: "" }
);
// Store the actual file
const [file1, setFile1] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
// Store the preview URL
const [preview1, setPreview1] = useState<string | null>(null);
const [preview2, setPreview2] = useState<string | null>(null);
// User data
// const [user, setUser] = useState<UserData>({
// id: "",
// firstName: "",
// lastName: "",
// email: "",
// language: "",
// account: {
// id: 0,
// isDefault: false,
// name: "",
// },
// });
// const isEditMode = !!editingSignature;
// Navigation items
const navigationItems = [
{ id: 'TYPED', label: 'TYPED', 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: 'SAVED', label: 'SAVED', icon: <CiSaveDown1 className="w-5 h-5" /> },
];
// Initialize form with editing signature data
// useEffect(() => {
// if (editingSignature) {
// setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
// setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
// setActiveTab(editingSignature.signatureType as 'TYPED' | 'DRAW' | 'UPLOAD');
// // Set font for SELECT tab
// const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
// if (fontOption) {
// setSelectedSignature({ id: fontOption.id, style: fontOption.style });
// }
// // Set signature blobs for all tabs
// if (editingSignature.signatureBlob && editingSignature.initialsBlob) {
// setSignatures({
// fullSignature: `data:image/png;base64,${editingSignature.signatureBlob}`,
// initials: `data:image/png;base64,${editingSignature.initialsBlob}`
// });
// // Set preview URLs for UPLOAD tab
// if (editingSignature.signatureType === 'UPLOAD') {
// setPreview1(`data:image/png;base64,${editingSignature.signatureBlob}`);
// setPreview2(`data:image/png;base64,${editingSignature.initialsBlob}`);
// }
// }
// }
// }, [editingSignature]);
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
setSignatures(data);
}; };
// Getting Signatures - START
useEffect(() => { useEffect(() => {
handleSaveSignature(); (async () => {
}, [saved]) const res = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/signatures?recipientId=${recipientId}`, {
headers: {
"ngrok-skip-browser-warning": "69420"
}
})
return ( console.log(res, "Geting Signatures")
<div className='absolute top-0 left-0 w-full h-full flex items-center justify-center '> })()
<div className='absolute top-0 left-0 w-full h-full bg-slate-100 opacity-60'></div> }, [])
// Getting Signatures - END
<div className='relative lg:min-w-[650px] max-w-max mx-auto shadow-md rounded-sm bg-white pt-3 pb-1 px-3'> // Cleanup preview URLs when component unmounts or image changes
<IoMdClose useEffect(() => {
onClick={() => setOpenSign(false)} return () => {
className='absolute right-3 text-slate-400 text-lg hover:text-[var(--primary)] cursor-pointer transition-all' /> if (preview1) URL.revokeObjectURL(preview1);
{/* Signature Save types Nav */} if (preview2) URL.revokeObjectURL(preview2);
<div className='flex text-sm'> };
<div className={`px-4 pb-1 cursor-pointer border-b-2 font-medium ${activeSaveType === SaveTypeEnum.DRAW ? "border-[var(--primary)] text-[var(--primary)]" : "border-slate-100"}`} }, [preview1, preview2]);
onClick={() => setActiveSaveType(SaveTypeEnum.DRAW)}>
<p className='tracking-wide'>Draw</p>
</div>
<div className={`px-4 pb-1 cursor-pointer border-b-2 font-medium ${activeSaveType === SaveTypeEnum.TYPE ? "border-[var(--primary)] text-[var(--primary)]" : "border-slate-100"}`}
onClick={() => setActiveSaveType(SaveTypeEnum.TYPE)}>
<p className='tracking-wide'>Type</p>
</div>
<div className={`px-4 pb-1 cursor-pointer border-b-2 font-medium ${activeSaveType === SaveTypeEnum.UPLOAD ? "border-[var(--primary)] text-[var(--primary)]" : "border-slate-100"}`}
onClick={() => setActiveSaveType(SaveTypeEnum.UPLOAD)}>
<p className='tracking-wide'>Upload</p>
</div>
</div>
{activeSaveType === SaveTypeEnum.DRAW && <Draw const handleImageChange = async (
saved={saved} e: React.ChangeEvent<HTMLInputElement>,
setSaved={setSaved} setFile: React.Dispatch<React.SetStateAction<File | null>>,
/>} setPreview: React.Dispatch<React.SetStateAction<string | null>>,
{activeSaveType === SaveTypeEnum.TYPE && <TypedSign type: "fullSignature" | "initials"
saved={saved} ) => {
setSaved={setSaved} const file = e.target.files?.[0];
/>} if (file) {
{activeSaveType === SaveTypeEnum.UPLOAD && <SignUpload setFile(file);
saved={saved} const previewUrl = URL.createObjectURL(file);
setSaved={setSaved} setPreview(previewUrl);
/>}
{/* <div className="mt-3 flex justify-end gap-2"> // Convert to base64
<button const base64 = await convertToBase64(file);
// onClick={close}
className="border-1 border-[var(--primary)] text-[var(--primary)] px-4 text-sm py-1 rounded shadow opacity-70 hover:opacity-100 transition-all cursor-pointer" // Update the appropriate signature
> setSignatures((prev) => ({
Cancel ...prev,
</button> [type]: base64,
<button }));
// onClick={save} }
className="bg-[var(--primary)] opacity-90 hover:opacity-100 text-white px-4 text-sm py-1 rounded shadow disabled:opacity-50 transition-all cursor-pointer" };
>
Save const convertToBase64 = (file: File): Promise<string> => {
</button> return new Promise((resolve, reject) => {
</div> */} const reader = new FileReader();
</div> reader.readAsDataURL(file);
</div> reader.onload = () => resolve(reader.result as string);
) reader.onerror = (error) => reject(error);
});
};
const handleSubmit = async () => {
setIsSubmitting(true);
console.log("handleSubmit Called")
if (!signatures.fullSignature || !signatures.initials) {
alert('Please provide both signature and initials');
setIsSubmitting(false);
return;
} }
export default SignCon; try {
const payload = {
signatureType: activeTab,
signatureBase64: signatures.fullSignature,
initialsBase64: signatures.initials,
firstName: fullName.split(' ')[0] || user.firstName,
lastName: fullName.split(' ')[1] || user.lastName,
font: selectedSignature.style
};
let res;
// if (isEditMode && editingSignature) {
// // Update existing signature
// const updatePayload = {
// ...payload,
// signatureId: editingSignature.id
// };
// res = await axios.put(`${import.meta.env.VITE_BASE_URI}/api/user/signatures`, updatePayload);
// console.log("handleUpdate", res);
// toast.success("Signature Updated Successfully", {
// position: "top-center",
// autoClose: 500,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
// } else {
// Create new signature
res = await axios.post(`${import.meta.env.VITE_BASE_URI}/api/user/signatures`,
payload,
{
headers: {
"ngrok-skip-browser-warning": "69420"
}
});
console.log("handleSubmit", res);
toast.success("Signature Created Successfully", {
position: "top-center",
autoClose: 500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// }
if (res?.data) {
await getAllSignatures();
setShowAddSignature?.(false);
}
} catch (error) {
console.error("Error submitting signature:", error);
// toast.error(isEditMode ? "Failed to update signature" : "Failed to create signature", {
// position: "top-center",
// autoClose: 2000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
} finally {
setIsSubmitting(false);
}
};
const renderContent = () => {
switch (activeTab) {
case 'TYPED':
return (
<div className="h-full flex flex-col ">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Choose Signature Style</h2>
<p className="text-sm text-gray-600">Select from pre-designed signature styles</p>
</div>
<div className="flex-1 overflow-y-auto">
<div className="space-y-4">
{signatureOptions.map((option) => (
<div key={option.id} className="flex items-center space-x-4 px-2 py-1 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<input
type="radio"
id={`signature-${option.id}`}
name="signature"
checked={selectedSignature.id === option.id}
onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1 grid grid-cols-2 gap-2">
<div className=" border-gray-300 rounded-lg px-2 py-1 bg-white ">
<div className="text-xs text-gray-500 mb-2">Signed by:</div>
<div className={`text-lg ${option.style === 'cursive' ? 'font-serif italic' :
option.style === 'script' ? 'font-mono' :
option.style === 'elegant' ? 'font-serif' :
option.style === 'modern' ? 'font-sans tracking-wide' :
'font-sans'
}`}>
{fullName || option.name}
</div>
<div className="text-xs text-gray-400 mt-2">
dca3e646-62cd-4cc...
</div>
</div>
<div className=" rounded-lg px-2 py-2 bg-white ">
<div className="text-xs text-gray-500 mb-2">DS</div>
<div className={`text-2xl ${option.style === 'cursive' ? 'font-serif italic' :
option.style === 'script' ? 'font-mono' :
option.style === 'elegant' ? 'font-serif' :
option.style === 'modern' ? 'font-sans tracking-wide' :
'font-sans'
}`}>
{initials || option.initials}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
case 'DRAW':
return (
<DrawSignatureCanvas
onSignatureChange={handleSignatureChange}
// initialSignatures={isEditMode ? signatures : undefined}
/>
);
case 'UPLOAD':
return (
<div className="h-full flex flex-col">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Upload Signature</h2>
</div>
<div className="flex-1 overflow-y-auto">
<div>
<div className="grid grid-cols-12 gap-6">
{/* Upload 1 */}
<div className="col-span-8">
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
{preview1 ? (
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
) : (
<div className="text-center">
<MdCloudUpload className="mx-auto h-12 w-12 text-gray-400 mb-3" />
<p className="text-sm text-gray-500 mb-1">Click to upload or drag and drop</p>
<p className="text-xs text-gray-400">PNG, JPG, GIF up to 10MB</p>
</div>
)}
</label>
<input
id="upload1"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleImageChange(e, setFile1, setPreview1, "fullSignature")}
/>
</div>
{/* Upload 2 */}
<div className="col-span-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
{preview2 ? (
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
) : (
<div className="text-center">
<MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
<p className="text-xs text-gray-500">Upload initials</p>
</div>
)}
</label>
<input
id="upload2"
type="file"
accept="image/*"
className="hidden"
onChange={(e) => handleImageChange(e, setFile2, setPreview2, "initials")}
/>
</div>
</div>
</div>
</div>
</div>
);
default:
return null;
}
};
// const getUserDetails = async () => {
// try {
// let res = await api.get(`/api/user/details`);
// console.log("getuser", res);
// let currUser = res?.data;
// setUser(currUser);
// // Only set default values if not in edit mode
// if (!isEditMode) {
// setFullName(currUser.firstName + " " + currUser.lastName);
// setInitials(currUser.firstName.slice(0, 1) + currUser.lastName.slice(0, 1));
// }
// } catch (error) {
// console.error("Error fetching user details:", error);
// }
// };
// useEffect(() => {
// getUserDetails();
// }, []);
return (
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-md shadow-xl w-full max-w-[80vw] h-[80vh] flex relative">
{/* Sidebar Navigation */}
<div className="w-64 bg-gray-50 rounded-l-lg border-r border-gray-200 flex flex-col">
{/* Header */}
<div className="px-4 py-4 border-b border-gray-200">
<div className="flex items-center space-x-3">
<div className='px-1 py-1'>
<h3 className="text-md font-semibold text-gray-900">Add Signature</h3>
<p className="text-xs text-gray-500">Create your digital signature</p>
</div>
</div>
</div>
{/* Form Inputs */}
<div className="px-6 py-2.5 border-b border-gray-200">
<div className="space-y-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<input
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
placeholder="Enter full name"
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Initials
</label>
<input
type="text"
value={initials}
onChange={(e) => setInitials(e.target.value)}
placeholder="Enter initials"
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
</div>
{/* Navigation Menu */}
<div className="flex-1 px-6 py-2.5">
<nav className="space-y-2">
{navigationItems.map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id as 'TYPED' | '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'
}`}
>
<span className={activeTab === item.id ? 'text-blue-600' : 'text-gray-400'}>
{item.icon}
</span>
<span>{item.label}</span>
</button>
))}
</nav>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex flex-col ">
{/* Header with close button */}
{/* Content */}
<div className="flex-1 py-5 px-5 overflow-hidden ">
{/* <button
onClick={() => setShowAddSignature?.(false)}
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
>
<AiOutlineClose className="w-5 h-5 text-gray-500" />
</button> */}
{renderContent()}
</div>
{/* Footer Buttons in Main Area */}
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
<button
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
onClick={() => setOpenSign?.(false)}
disabled={isSubmitting}
>
CANCEL
</button>
<button
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md bg-[var(--modalSubmitBG)] text-white hover:bg-[var(--modalSubmitBGHover)] transition-colors flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
onClick={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<FaSpinner className="animate-spin" />
{/* {isEditMode ? 'UPDATING...' : 'CREATING...'} */}
</>
) : (
<>
CREATE SIGNATURE
{/* isEditMode ? 'UPDATE SIGNATURE' : 'CREATE SIGNATURE' */}
</>
)}
</button>
</div>
</div>
</div>
</div>
);
};
export default AddSignatureModal;

View File

@ -0,0 +1,97 @@
import { useState } from "react";
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import { Bounce, toast } from "react-toastify";
import api from "../../config/api";
import { IoMdAdd } from "react-icons/io";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
const Templates = () => {
const [activeFilter, setActiveFilter] = useState("All");
const accountDetails = useSelector((state: any) => state.user.account);
const accountId = accountDetails.id || "";
const navigate = useNavigate();
const filters = ["All", "My Templates", "Shared"];
const handleAddTemplate = async () => {
console.log("handleAddTemplate called");
const res = await api.post(`/api/accounts/${accountId}/templates`);
navigate(`/process-step-one/${accountId}/template/${res.data}`)
}
return (
<div className="p-3 text-sm">
{/* Header */}
<div className="flex items-center justify-between mb-3">
{/* Filter as pill buttons */}
<div className="flex gap-2">
{filters.map((filter) => (
<button
key={filter}
onClick={() => setActiveFilter(filter)}
className={`px-3 py-1 rounded-full text-xs border transition
${activeFilter === filter
? "bg-[var(--primary)] text-white"
: "border-gray-300 text-gray-600 hover:bg-gray-100"
}`}
>
{filter}
</button>
))}
</div>
<div
onClick={handleAddTemplate}
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm cursor-pointer hover:opacity-90">
<IoMdAdd className="text-white" />
<button
className="text-white rounded text-sm hover:opacity-90">
Add Template
</button>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-200 bg-white shadow rounded">
<thead className="bg-gray-100 text-xs text-gray-700 uppercase tracking-wider">
<tr>
<th className="px-4 py-2 w-10"></th>
<th className="px-4 py-2 text-left">Name</th>
<th className="px-4 py-2 text-center">Owner</th>
<th className="px-4 py-2 text-center">Created</th>
<th className="px-4 py-2 text-center">Last Updated</th>
<th className="px-4 py-2 text-center"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr className="hover:bg-gray-50">
<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">Test Template</td>
<td className="px-4 py-2 text-center text-sm">User 1</td>
<td className="px-4 py-2 text-center text-sm">2025-07-14</td>
<td className="px-4 py-2 text-center text-sm">Just now</td>
<td className="px-4 py-2 text-center relative">
<button
className="text-slate-400 hover:text-black"
>
<PiDotsThreeOutlineVerticalFill className="text-lg" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</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"> <div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
{/* Header */} {/* Header */}
<h1 className='text-lg font-semibold text-gray-800'> <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> </h1>
<div className='mt-2'> <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'>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=''><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'>Sent on <span>11/02/25</span></p> <p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
</div> </div>
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p> <p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
@ -139,7 +140,7 @@ const TransactionView = () => {
<div> <div>
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2> <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>
</div> </div>

View File

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

View File

@ -43,6 +43,9 @@
--hideModalBg: #2d2d2d; --hideModalBg: #2d2d2d;
--processStepOneTextHead: #000000; --processStepOneTextHead: #000000;
--modalSubmitBG: #1919d7;
--modalSubmitBGHover: #395ee1;
} }
@ -59,30 +62,70 @@
canvas { canvas {
touch-action: none; touch-action: none;
-ms-touch-action: none; /* For older IE/Edge */ -ms-touch-action: none;
/* For older IE/Edge */
pointer-events: auto; pointer-events: auto;
} }
@keyframes textColorChange { @keyframes textColorChange {
0%, 100% {
0%,
100% {
color: white; color: white;
} }
50% { 50% {
color: #41644A; /* Tailwind blue-500 */ color: #41644A;
/* Tailwind blue-500 */
} }
} }
.animate-text { .animate-text {
animation: textColorChange 2s infinite ease-in-out; animation: textColorChange 2s infinite ease-in-out;
} }
@layer utilities { @layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
/* Hide scrollbar for IE, Edge and Firefox */ /* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar { .no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none;
scrollbar-width: none; /* Firefox */ /* IE and Edge */
scrollbar-width: none;
/* Firefox */
} }
} }
/* Group Border - START */
.group-border {
position: absolute;
border: 1px dashed var(--primary);
/* pointer-events: none; */
}
.bottom-border>div {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
z-index: 50;
background-color: var(--primary);
color: var(--primaryLight);
padding: 2px 8px;
border-radius: 5px;
}
.bottom-border>div:hover {
background-color: var(--primaryLight2) ;
opacity: 0.9;
transition: all 0.3s ease-in-out;
cursor: pointer;
}
/* Group Border - END */
/* Loader CSS */

View File

@ -1,27 +1,188 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; // import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
// import { v4 as uuidv4 } from "uuid"; // // import { v4 as uuidv4 } from "uuid";
import { hideLoader, showLoader } from "./loaderSlice"; // import { hideLoader, showLoader } from "./loaderSlice";
// import { showModal } from "./modalSlice"; // // import { showModal } from "./modalSlice";
// import { updateSelectedDocuments } from "./selectedDocsSlice"; // // import { updateSelectedDocuments } from "./selectedDocsSlice";
import api from "../../config/api"; // import api from "../../config/api";
import { Bounce, toast } from "react-toastify"; // import { Bounce, toast } from "react-toastify";
// import { createAxiosInstance } from "../../config/axiosInstance"; // // import { createAxiosInstance } from "../../config/axiosInstance";
// const axiosInstance = createAxiosInstance(); // // const axiosInstance = createAxiosInstance();
// export interface Document {
// id: string;
// name: string;
// extension: string;
// original?: boolean;
// corrupted?: boolean;
// isCompleted: boolean;
// status: string;
// }
// export interface DocumentState {
// documents: Document[];
// loading: boolean;
// error: string | null;
// }
// const initialState: DocumentState = {
// documents: [],
// loading: false,
// error: null,
// };
// interface uploadArgs {
// formData: FormData;
// accountId: string;
// transactionId: string;
// }
// // Fetch documents from API
// export const fetchDocuments = createAsyncThunk<Document[], { accountId: string, transactionId: string }>(
// "documents/fetchDocuments",
// async ({ accountId, transactionId }, { rejectWithValue }) => {
// try {
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`);
// return response.data as Document[];
// } catch (error: any) {
// // if(error.status === 401) {}
// return rejectWithValue(error.response?.data || "Failed to fetch documents");
// }
// }
// );
// // Upload multiple documents using FormData (Key: "document")
// export const uploadDocuments = createAsyncThunk<Document[], uploadArgs>(
// "documents/uploadDocuments",
// async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => {
// try {
// dispatch(showLoader("Uploading documents..."));
// console.log("Form Data", formData.getAll("document"))
// const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, {
// headers: {
// "Content-Type": "multipart/form-data"
// },
// });
// console.log("Form Uploaded Response DocumentSlice Line 185", response.data)
// dispatch(hideLoader());
// return [response.data];
// } catch (error: any) {
// dispatch(hideLoader());
// toast.error("Error Uploading Document", {
// position: "bottom-right",
// autoClose: 3000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: false,
// draggable: false,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
// console.log("Failed Uploading Documents")
// throw new Error("Failed Uploading Documents")
// // Mock data in case of failure
// // const failedDocuments = formData.getAll("document").map((file: any) => ({
// // id: uuidv4(),
// // name: file.name,
// // extension: file.name.split(".").pop() || "unknown",
// // original: Math.random() < 0.5,
// // corrupted: Math.random() < 0.5,
// // }));
// // dispatch(updateSelectedDocuments({ documents: failedDocuments }));
// // return failedDocuments; // Instead of rejecting, return mock data
// }
// }
// );
// const documentSlice = createSlice({
// name: "documents",
// initialState,
// reducers: {
// setDocuments: (state, action: PayloadAction<Document[]>) => {
// state.documents = action.payload;
// },
// setDocProgComplete: (state, action: PayloadAction<string>) => {
// const docIndex = state.documents.findIndex(doc => doc.id === action.payload);
// if (docIndex !== -1) {
// state.documents[docIndex].isCompleted = true;
// }
// },
// resetDocuments: (state) => {
// state.documents = [];
// },
// deleteDocument: (state, action: PayloadAction<string>) => {
// state.documents = state.documents.filter(doc => doc.id !== action.payload);
// },
// },
// extraReducers: (builder) => {
// builder
// // Fetch documents
// .addCase(fetchDocuments.pending, (state) => {
// state.loading = true;
// state.error = null;
// })
// .addCase(fetchDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
// state.loading = false;
// state.documents = action.payload;
// })
// .addCase(fetchDocuments.rejected, (state, action) => {
// state.loading = false;
// state.error = action.payload as string;
// })
// // Upload multiple documents
// .addCase(uploadDocuments.pending, (state) => {
// state.loading = true;
// state.error = null;
// })
// .addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
// state.loading = false;
// state.documents = [...action.payload, ...state.documents];
// })
// .addCase(uploadDocuments.rejected, (state, action) => {
// state.loading = false;
// state.error = action.payload as string;
// });
// },
// });
// export const { resetDocuments, setDocuments, setDocProgComplete, deleteDocument } = documentSlice.actions;
// export default documentSlice.reducer;
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { toast, Bounce } from 'react-toastify';
import api from '../../config/api';
import { hideLoader, showLoader } from './loaderSlice';
// import { showLoader, hideLoader } from '../loader/loaderSlice';
// -----------------------
// Document Type
// -----------------------
export interface Document { export interface Document {
id: string; id?: string;
name: string; name: string;
extension: string; url: string;
original?: boolean; type: string;
corrupted?: boolean; pageCount?: number;
isCompleted: boolean; [key: string]: any;
status: string;
} }
export interface DocumentState { // -----------------------
// Initial State
// -----------------------
interface DocumentState {
documents: Document[]; documents: Document[];
loading: boolean; loading: boolean;
error: string | null; error: string | null;
@ -33,80 +194,81 @@ const initialState: DocumentState = {
error: null, error: null,
}; };
interface uploadArgs { // -----------------------
formData: FormData; // Thunk: Fetch Documents
accountId: string; // -----------------------
transactionId: string; export const fetchDocuments = createAsyncThunk<
} Document[],
{ accountId: string; transactionId: string }
>(
// Fetch documents from API 'documents/fetchDocuments',
export const fetchDocuments = createAsyncThunk<Document[], { accountId: string, transactionId: string }>( async ({ accountId, transactionId }, { rejectWithValue, getState }) => {
"documents/fetchDocuments",
async ({ accountId, transactionId }, { rejectWithValue }) => {
try { try {
const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`); const state = getState() as {
selectedRecipients: { transactionTemplateToggle: string };
};
const toggle = state.selectedRecipients.transactionTemplateToggle;
const response = await api.get(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents?include_images=true`
);
return response.data as Document[]; return response.data as Document[];
} catch (error: any) { } catch (error: any) {
// if(error.status === 401) {} return rejectWithValue(error.response?.data || 'Failed to fetch documents');
return rejectWithValue(error.response?.data || "Failed to fetch documents");
} }
} }
); );
// Upload multiple documents using FormData (Key: "document") // -----------------------
export const uploadDocuments = createAsyncThunk<Document[], uploadArgs>( // Thunk: Upload Documents
"documents/uploadDocuments", // -----------------------
async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => { export const uploadDocuments = createAsyncThunk<
Document[],
{ formData: FormData; accountId: string; transactionId: string }
>(
'documents/uploadDocuments',
async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue, getState }) => {
try { try {
dispatch(showLoader("Uploading documents...")); dispatch(showLoader('Uploading documents...'));
console.log("Form Data", formData.getAll("document"))
const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, { const state = getState() as {
selectedRecipients: { transactionTemplateToggle: string };
};
const toggle = state.selectedRecipients.transactionTemplateToggle;
const response = await api.post(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`,
formData,
{
headers: { headers: {
"Content-Type": "multipart/form-data" 'Content-Type': 'multipart/form-data',
}, },
}); }
);
console.log("Form Uploaded Response DocumentSlice Line 185", response.data)
dispatch(hideLoader()); dispatch(hideLoader());
return [response.data]; return [response.data];
} catch (error: any) { } catch (error: any) {
dispatch(hideLoader()); dispatch(hideLoader());
toast.error("Error Uploading Document", {
position: "bottom-right", toast.error('Error Uploading Document', {
position: 'bottom-right',
autoClose: 3000, autoClose: 3000,
hideProgressBar: false, theme: 'light',
closeOnClick: false,
pauseOnHover: false,
draggable: false,
progress: undefined,
theme: "light",
transition: Bounce, transition: Bounce,
}); });
console.log("Failed Uploading Documents") return rejectWithValue('Failed Uploading Documents');
throw new Error("Failed Uploading Documents")
// Mock data in case of failure
// const failedDocuments = formData.getAll("document").map((file: any) => ({
// id: uuidv4(),
// name: file.name,
// extension: file.name.split(".").pop() || "unknown",
// original: Math.random() < 0.5,
// corrupted: Math.random() < 0.5,
// }));
// dispatch(updateSelectedDocuments({ documents: failedDocuments }));
// return failedDocuments; // Instead of rejecting, return mock data
} }
} }
); );
const documentSlice = createSlice({ // -----------------------
name: "documents", // Slice
// -----------------------
const documentsSlice = createSlice({
name: 'documents',
initialState, initialState,
reducers: { reducers: {
setDocuments: (state, action: PayloadAction<Document[]>) => { setDocuments: (state, action: PayloadAction<Document[]>) => {
@ -127,7 +289,7 @@ const documentSlice = createSlice({
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
// Fetch documents // fetchDocuments
.addCase(fetchDocuments.pending, (state) => { .addCase(fetchDocuments.pending, (state) => {
state.loading = true; state.loading = true;
state.error = null; state.error = null;
@ -139,16 +301,22 @@ const documentSlice = createSlice({
.addCase(fetchDocuments.rejected, (state, action) => { .addCase(fetchDocuments.rejected, (state, action) => {
state.loading = false; state.loading = false;
state.error = action.payload as string; state.error = action.payload as string;
toast.error('Failed to fetch documents', {
position: 'bottom-right',
autoClose: 3000,
theme: 'light',
transition: Bounce,
});
}) })
// Upload multiple documents // uploadDocuments
.addCase(uploadDocuments.pending, (state) => { .addCase(uploadDocuments.pending, (state) => {
state.loading = true; state.loading = true;
state.error = null; state.error = null;
}) })
.addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => { .addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
state.loading = false; state.loading = false;
state.documents = [...action.payload, ...state.documents]; state.documents.push(...action.payload);
}) })
.addCase(uploadDocuments.rejected, (state, action) => { .addCase(uploadDocuments.rejected, (state, action) => {
state.loading = false; state.loading = false;
@ -157,5 +325,8 @@ const documentSlice = createSlice({
}, },
}); });
export const { resetDocuments, setDocuments, setDocProgComplete, deleteDocument } = documentSlice.actions; // -----------------------
export default documentSlice.reducer; // Exports
// -----------------------
export const { deleteDocument, resetDocuments, setDocuments, setDocProgComplete } = documentsSlice.actions;
export default documentsSlice.reducer;

View File

@ -1,11 +1,340 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; // import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createAsyncThunk } from '@reduxjs/toolkit'; // import { createAsyncThunk } from '@reduxjs/toolkit';
// import { toast } from 'react-toastify';
// import colors from '../../utlity/colorsArr';
// import api from '../../config/api';
// export enum recipientType {
// SIGNER = "Signer",
// INPERSONSIGNER = "InPersonSigner",
// CC = "CC"
// }
// export interface Recipient {
// id?: string;
// firstName: string;
// lastName: string;
// routingOrder: number;
// email: string;
// hostName: string;
// hostEmail: string;
// type: recipientType;
// color?: string;
// }
// export interface SelectedRecipientInt {
// recipients: Recipient[],
// signingOrder: boolean,
// transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE
// }
// interface AddRecipientArgs {
// recipientsData: Recipient[];
// accountId: string;
// transactionId: string;
// }
// export enum TRANSACTION_TEMPLATE_TOGGLE {
// TRANSACTIONS = "transactions",
// TEMPLATES = "templates"
// }
// const initialState: SelectedRecipientInt = {
// recipients: [],
// signingOrder: false,
// transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS
// };
// export const addRecipientAsync = createAsyncThunk<Recipient[], AddRecipientArgs>(
// 'selectedRecipients/addRecipientAsync',
// async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => {
// console.log(recipientsData, accountId, transactionId, "recipientsData Line 46");
// try {
// console.log(recipientsData, "recipientsData --- Line 48");
// const updateRecipientsData = recipientsData.filter((recipient, idx) => recipient.id !== null);
// const createRecipientsData = recipientsData.filter((recipient, idx) => recipient.id == null);
// console.log(updateRecipientsData, "updateRecipientsData Line 52");
// console.log(createRecipientsData, "createRecipientsData Line 53");
// const resUpdate = await api.put(
// `/api/accounts/${accountId}/transactions/${transactionId}/recipients`,
// updateRecipientsData
// );
// const resCreate = await api.post(
// `/api/accounts/${accountId}/transactions/${transactionId}/recipients`,
// createRecipientsData
// );
// if (createRecipientsData.length === 0) resCreate.data = [];
// if (updateRecipientsData.length === 0) resUpdate.data = [];
// console.log(resCreate, "resCreate");
// console.log(resUpdate, "resUpdate");
// return [...resCreate.data, ...resUpdate.data] as Recipient[];
// } catch (err: any) {
// return rejectWithValue(err.response?.data || "Failed to add recipient");
// }
// }
// );
// interface RemoveRecipientAsyncInterfaceObj {
// id?: string;
// email?: string;
// }
// interface RemoveRecipientAsyncInterface {
// recipientId: number | null;
// accountId: string;
// transactionId: string;
// email: string;
// }
// export const removeRecipientAsync = createAsyncThunk<string | number, RemoveRecipientAsyncInterface>(
// 'selectedRecipients/removeRecipientAsync',
// async (obj, { rejectWithValue }) => {
// try {
// if (obj.recipientId) {
// const res = await api.delete(`/api/accounts/${obj.accountId}/transactions/${obj.transactionId}/recipients/${obj.recipientId}`);
// console.log(res, "Line 77")
// return obj.recipientId;
// }
// return obj.email
// } catch (err: any) {
// return rejectWithValue("Failed to remove recipient");
// }
// }
// );
// export const fetchRecipients = createAsyncThunk<Recipient[], { accountId: string, transactionId: string }>(
// 'selectedRecipients/fetchRecipients',
// async ({ accountId, transactionId }, { rejectWithValue }) => {
// try {
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`);
// return response.data as Recipient[];
// } catch (err: any) {
// return rejectWithValue(err.response?.data || "Failed to fetch recipients");
// }
// }
// );
// const selectedRecipientsSlice = createSlice({
// name: 'selectedRecipients',
// initialState,
// reducers: {
// // Add a single recipient
// addRecipient: (state, action: PayloadAction<Recipient>) => {
// for (const recipient of state.recipients) {
// if (recipient.email === action.payload.email) {
// toast.dismiss();
// toast.error("Email already exists !", {
// position: "bottom-right",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// });
// return state;
// }
// }
// // Adding Recipient on top if type is inPersonSigner
// console.log("Getting Type ", action.payload.type)
// if (action.payload.type === recipientType.INPERSONSIGNER) {
// state.signingOrder = true
// state.recipients = [action.payload, ...state.recipients]
// } else {
// state.recipients.push(action.payload);
// }
// },
// resetRecipients: (state) => {
// state.recipients = [];
// },
// toggleToTemplates: (state) => {
// state.transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
// },
// // Replace the entire selectedRecipients array
// setSelectedRecipients: (state, action: PayloadAction<Recipient[]>) => {
// for (const [index, recipient] of action.payload.entries()) {
// if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) {
// return state;
// }
// }
// state.recipients = action.payload;
// },
// setSigningOrder: (state, action: PayloadAction<boolean>) => {
// for (const recipient of state.recipients) {
// if (recipient.type === recipientType.INPERSONSIGNER) {
// if (state.signingOrder) return state;
// state.signingOrder = true;
// return state
// }
// }
// state.signingOrder = action.payload;
// },
// // Remove Recipients
// removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => {
// console.log("removeSelectedRecipient Called")
// state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email);
// },
// // Add Color in Recipient
// addColorsToRecipient: (state) => {
// // const colors = generateUniqueColors(state.recipients.length);
// for (const [index, recipient] of state.recipients.entries()) {
// console.log(state)
// recipient.color = colors[index]
// }
// return state;
// },
// updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
// console.log("recipient order called Line 159")
// const { id, routingOrder } = action.payload;
// const recipient = state.recipients.find((r) => r.id === id || r.email === id);
// if (recipient) {
// recipient.routingOrder = routingOrder;
// }
// },
// updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => {
// console.log("updateRecipientIds", console.table(action.payload));
// for (const recObj of state.recipients) {
// const email = recObj.email
// for (const recObj2 of action.payload) {
// recObj.id = recObj2.id;
// }
// }
// },
// resetSelRecipient: (state) => {
// state = initialState;
// return state;
// }
// },
// extraReducers: (builder) => {
// builder
// // Fetch Recipient - START
// .addCase(fetchRecipients.pending, (state) => {
// })
// .addCase(fetchRecipients.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
// // console.log("Fetched recipients successfully:", action.payload);
// // Assign colors to recipients
// const assignedRecipients = action.payload.map((recipient, index) => ({
// ...recipient,
// color: colors[index], // Cycle if more recipients than colors
// }));
// state.recipients = assignedRecipients;
// })
// .addCase(fetchRecipients.rejected, (state, action) => {
// console.error("Failed to fetch recipients:", action.payload);
// toast.error("Failed to fetch recipients", {
// position: "bottom-right",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// });
// })
// // Fetch Recipient - END
// // Add Recipient - START
// .addCase(addRecipientAsync.pending, (state) => {
// })
// .addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
// // Optionally avoid duplicates if needed
// console.log("addRecipientAsync Called Line 258", action.payload);
// // const newRecipients = action.payload.filter(newRec => {
// // return !state.recipients.some(existing => existing existing.id === newRec.id);
// // });
// // const newRecipients = action.payload.filter(newRec => {
// // return !state.recipients.some(existing => "id" in existing && existing.id === newRec.id);
// // });
// // // Add new recipients and assign colors
// // const startIndex = state.recipients.length;
// // newRecipients.forEach((rec, index) => {
// // rec.color = colors[startIndex + index];
// // state.recipients.push(rec);
// // });
// state.recipients = action.payload.map((recipient, index) => ({
// ...recipient,
// color: colors[index % colors.length], // Cycle through colors
// }));
// })
// .addCase(addRecipientAsync.rejected, (state, action) => {
// console.error("Failed to fetch recipients:", action.payload);
// toast.error("Failed to fetch recipients", {
// position: "bottom-right",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// });
// })
// // Add Recipient - END
// // Fetch Recipient - START
// .addCase(removeRecipientAsync.pending, (state) => {
// })
// .addCase(removeRecipientAsync.fulfilled, (state, action: PayloadAction<string | number>) => {
// state.recipients = state.recipients.filter(
// (recipient) => recipient.id !== action.payload && recipient.email !== action.payload
// );
// })
// .addCase(removeRecipientAsync.rejected, (state, action) => {
// console.error("Failed to Delete recipients:", action.payload);
// toast.error("Failed to Delete recipients", {
// position: "bottom-right",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// });
// })
// // Fetch Recipient - END
// }
// });
// export const { addRecipient, setSelectedRecipients, toggleToTemplates, removeSelectedRecipient, setSigningOrder, addColorsToRecipient, updateRecipientIds, resetSelRecipient, updateRecipientOrder, resetRecipients } = selectedRecipientsSlice.actions;
// export default selectedRecipientsSlice.reducer;
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import colors from '../../utlity/colorsArr'; import colors from '../../utlity/colorsArr';
import api from '../../config/api'; import api from '../../config/api';
export enum recipientType { export enum recipientType {
SIGNER = "Signer", SIGNER = "Signer",
INPERSONSIGNER = "InPersonSigner", INPERSONSIGNER = "InPersonSigner",
@ -26,54 +355,106 @@ export interface Recipient {
export interface SelectedRecipientInt { export interface SelectedRecipientInt {
recipients: Recipient[], recipients: Recipient[],
signingOrder: boolean signingOrder: boolean,
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE
} }
interface AddRecipientArgs { interface AddRecipientArgs {
recipientsData: Recipient[]; recipientsData: Recipient[];
accountId: string; accountId: string;
transactionId: string; transactionId: string;
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE;
}
interface RemoveRecipientAsyncInterface {
recipientId: number | null;
accountId: string;
transactionId: string;
email: string;
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE;
}
export enum TRANSACTION_TEMPLATE_TOGGLE {
TRANSACTIONS = "transactions",
TEMPLATES = "templates"
} }
const initialState: SelectedRecipientInt = { const initialState: SelectedRecipientInt = {
recipients: [], recipients: [],
signingOrder: false signingOrder: false,
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS
}; };
export const addRecipientAsync = createAsyncThunk<Recipient[], AddRecipientArgs>( // 1. Add Recipient Async
export const addRecipientAsync = createAsyncThunk<
Recipient[],
{ recipientsData: Recipient[]; accountId: string; transactionId: string }
>(
'selectedRecipients/addRecipientAsync', 'selectedRecipients/addRecipientAsync',
async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => { async ({ recipientsData, accountId, transactionId }, { rejectWithValue, getState }) => {
console.log(recipientsData, accountId, transactionId, "recipientsData Line 46");
try { try {
const response = await api.post( const state = getState() as { selectedRecipients: SelectedRecipientInt };
`/api/accounts/${accountId}/transactions/${transactionId}/recipients`, const toggle = state.selectedRecipients.transactionTemplateToggle;
recipientsData
); const updateRecipientsData = recipientsData.filter(recipient => recipient.id !== null);
console.log(response) const createRecipientsData = recipientsData.filter(recipient => recipient.id == null);
return response.data as Recipient[];
const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`;
const resUpdate = updateRecipientsData.length
? await api.put(basePath, updateRecipientsData)
: { data: [] };
const resCreate = createRecipientsData.length
? await api.post(basePath, createRecipientsData)
: { data: [] };
return [...resCreate.data, ...resUpdate.data] as Recipient[];
} catch (err: any) { } catch (err: any) {
return rejectWithValue(err.response?.data || "Failed to add recipient"); return rejectWithValue(err.response?.data || "Failed to add recipient");
} }
} }
); );
export const removeRecipientAsync = createAsyncThunk<string, string>( // 2. Remove Recipient Async
export const removeRecipientAsync = createAsyncThunk<
string | number,
{ recipientId: number | null; accountId: string; transactionId: string; email: string }
>(
'selectedRecipients/removeRecipientAsync', 'selectedRecipients/removeRecipientAsync',
async (email, { rejectWithValue }) => { async ({ recipientId, accountId, transactionId, email }, { rejectWithValue, getState }) => {
try { try {
await api.delete(`/api/recipients`, { data: { email } }); const state = getState() as { selectedRecipients: SelectedRecipientInt };
return email; // return email so we can filter from state const toggle = state.selectedRecipients.transactionTemplateToggle;
const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`;
if (recipientId) {
await api.delete(`${basePath}/${recipientId}`);
return recipientId;
}
return email;
} catch (err: any) { } catch (err: any) {
return rejectWithValue(err.response?.data || "Failed to remove recipient"); return rejectWithValue("Failed to remove recipient");
} }
} }
); );
export const fetchRecipients = createAsyncThunk<Recipient[], { accountId: string, transactionId: string }>( // 3. Fetch Recipients Async
export const fetchRecipients = createAsyncThunk<
Recipient[],
{ accountId: string; transactionId: string }
>(
'selectedRecipients/fetchRecipients', 'selectedRecipients/fetchRecipients',
async ({ accountId, transactionId }, { rejectWithValue }) => { async ({ accountId, transactionId }, { rejectWithValue, getState }) => {
try { try {
const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`); const state = getState() as { selectedRecipients: SelectedRecipientInt };
const toggle = state.selectedRecipients.transactionTemplateToggle;
const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`;
const response = await api.get(basePath);
return response.data as Recipient[]; return response.data as Recipient[];
} catch (err: any) { } catch (err: any) {
return rejectWithValue(err.response?.data || "Failed to fetch recipients"); return rejectWithValue(err.response?.data || "Failed to fetch recipients");
@ -85,7 +466,6 @@ const selectedRecipientsSlice = createSlice({
name: 'selectedRecipients', name: 'selectedRecipients',
initialState, initialState,
reducers: { reducers: {
// Add a single recipient
addRecipient: (state, action: PayloadAction<Recipient>) => { addRecipient: (state, action: PayloadAction<Recipient>) => {
for (const recipient of state.recipients) { for (const recipient of state.recipients) {
if (recipient.email === action.payload.email) { if (recipient.email === action.payload.email) {
@ -93,34 +473,29 @@ const selectedRecipientsSlice = createSlice({
toast.error("Email already exists !", { toast.error("Email already exists !", {
position: "bottom-right", position: "bottom-right",
autoClose: 5000, autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light", theme: "light",
}); });
return state; return state;
} }
} }
// Adding Recipient on top if type is inPersonSigner
console.log("Getting Type ", action.payload.type)
if (action.payload.type === recipientType.INPERSONSIGNER) { if (action.payload.type === recipientType.INPERSONSIGNER) {
state.signingOrder = true state.signingOrder = true;
state.recipients = [action.payload, ...state.recipients] state.recipients = [action.payload, ...state.recipients];
} else { } else {
state.recipients.push(action.payload); state.recipients.push(action.payload);
} }
}, },
resetRecipients: (state) => { resetRecipients: (state) => {
state.recipients = []; state.recipients = [];
}, },
// Replace the entire selectedRecipients array toggleToTemplates: (state) => {
state.transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
},
setSelectedRecipients: (state, action: PayloadAction<Recipient[]>) => { setSelectedRecipients: (state, action: PayloadAction<Recipient[]>) => {
for (const [index, recipient] of action.payload.entries()) { for (const [index, recipient] of action.payload.entries()) {
if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) { if ((recipient.type === recipientType.INPERSONSIGNER && index > 0)
&& action.payload[index - 1].type !== recipientType.INPERSONSIGNER) {
return state; return state;
} }
} }
@ -128,35 +503,26 @@ const selectedRecipientsSlice = createSlice({
state.recipients = action.payload; state.recipients = action.payload;
}, },
setSigningOrder: (state, action: PayloadAction<boolean>) => { setSigningOrder: (state, action: PayloadAction<boolean>) => {
for (const recipient of state.recipients) { for (const recipient of state.recipients) {
if (recipient.type === recipientType.INPERSONSIGNER) { if (recipient.type === recipientType.INPERSONSIGNER) {
if (state.signingOrder) return state; if (state.signingOrder) return state;
state.signingOrder = true; state.signingOrder = true;
return state return state;
} }
} }
state.signingOrder = action.payload; state.signingOrder = action.payload;
}, },
// Remove Recipients
removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => { removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => {
console.log("removeSelectedRecipient Called")
state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email); state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email);
}, },
// Add Color in Recipient
addColorsToRecipient: (state) => { addColorsToRecipient: (state) => {
// const colors = generateUniqueColors(state.recipients.length);
for (const [index, recipient] of state.recipients.entries()) { for (const [index, recipient] of state.recipients.entries()) {
console.log(state) recipient.color = colors[index];
debugger
recipient.color = colors[index]
} }
return state; return state;
}, },
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => { updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
console.log("recipient order called Line 159")
const { id, routingOrder } = action.payload; const { id, routingOrder } = action.payload;
const recipient = state.recipients.find((r) => r.id === id || r.email === id); const recipient = state.recipients.find((r) => r.id === id || r.email === id);
if (recipient) { if (recipient) {
@ -164,32 +530,22 @@ const selectedRecipientsSlice = createSlice({
} }
}, },
updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => { updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => {
console.log("updateRecipientIds", console.table(action.payload));
for (const recObj of state.recipients) { for (const recObj of state.recipients) {
const email = recObj.email
for (const recObj2 of action.payload) { for (const recObj2 of action.payload) {
recObj.id = recObj2.id; recObj.id = recObj2.id;
} }
} }
}, },
resetSelRecipient: (state) => { resetSelRecipient: () => initialState
state = initialState;
return state;
}
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
.addCase(fetchRecipients.pending, (state) => { .addCase(fetchRecipients.pending, () => { })
})
.addCase(fetchRecipients.fulfilled, (state, action: PayloadAction<Recipient[]>) => { .addCase(fetchRecipients.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
// console.log("Fetched recipients successfully:", action.payload);
// Assign colors to recipients
const assignedRecipients = action.payload.map((recipient, index) => ({ const assignedRecipients = action.payload.map((recipient, index) => ({
...recipient, ...recipient,
color: colors[index], // Cycle if more recipients than colors color: colors[index],
})); }));
state.recipients = assignedRecipients; state.recipients = assignedRecipients;
}) })
.addCase(fetchRecipients.rejected, (state, action) => { .addCase(fetchRecipients.rejected, (state, action) => {
@ -197,39 +553,52 @@ const selectedRecipientsSlice = createSlice({
toast.error("Failed to fetch recipients", { toast.error("Failed to fetch recipients", {
position: "bottom-right", position: "bottom-right",
autoClose: 5000, autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light", theme: "light",
}); });
}) })
.addCase(addRecipientAsync.pending, () => { })
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
.addCase(addRecipientAsync.pending, (state) => { state.recipients = action.payload.map((recipient, index) => ({
...recipient,
}) color: colors[index % colors.length],
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient>) => { }));
console.log("Added recipient successfully:", action.payload);
// state.recipients.push(action.payload);
}) })
.addCase(addRecipientAsync.rejected, (state, action) => { .addCase(addRecipientAsync.rejected, (state, action) => {
console.error("Failed to fetch recipients:", action.payload); console.error("Failed to add recipients:", action.payload);
toast.error("Failed to fetch recipients", { toast.error("Failed to add recipients", {
position: "bottom-right",
autoClose: 5000,
theme: "light",
});
})
.addCase(removeRecipientAsync.pending, () => { })
.addCase(removeRecipientAsync.fulfilled, (state, action: PayloadAction<string | number>) => {
state.recipients = state.recipients.filter(
(recipient) => recipient.id !== action.payload && recipient.email !== action.payload
);
})
.addCase(removeRecipientAsync.rejected, (state, action) => {
console.error("Failed to delete recipient:", action.payload);
toast.error("Failed to delete recipient", {
position: "bottom-right", position: "bottom-right",
autoClose: 5000, autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light", theme: "light",
}); });
}); });
} }
}); });
export const { addRecipient, setSelectedRecipients, removeSelectedRecipient, setSigningOrder, addColorsToRecipient, updateRecipientIds, resetSelRecipient, updateRecipientOrder, resetRecipients } = selectedRecipientsSlice.actions; export const {
addRecipient,
setSelectedRecipients,
toggleToTemplates,
removeSelectedRecipient,
setSigningOrder,
addColorsToRecipient,
updateRecipientIds,
resetSelRecipient,
updateRecipientOrder,
resetRecipients
} = selectedRecipientsSlice.actions;
export default selectedRecipientsSlice.reducer; export default selectedRecipientsSlice.reducer;

View File

@ -0,0 +1,91 @@
interface Dimension {
maxTop: number | null;
minTop: number | null;
maxLeft: number | null;
minLeft: number | null;
}
const getGroupDimension = (
group: NodeListOf<HTMLElement>,
containerRect: DOMRect,
offsetX: number,
offsetY: number
) => {
let dimension: Dimension = { maxTop: null, minTop: null, maxLeft: null, minLeft: null };
group.forEach((element: HTMLElement) => {
const rect = element.getBoundingClientRect();
dimension.minTop = Math.min(dimension.minTop ?? Infinity, rect.top);
dimension.maxTop = Math.max(dimension.maxTop ?? 0, rect.bottom);
dimension.minLeft = Math.min(dimension.minLeft ?? Infinity, rect.left);
dimension.maxLeft = Math.max(dimension.maxLeft ?? 0, rect.right);
});
dimension.maxTop = dimension.maxTop !== null ? dimension.maxTop - containerRect.top + (offsetY - 10) : null;
dimension.minTop = dimension.minTop !== null ? dimension.minTop - containerRect.top - (offsetY - 10) : null;
dimension.maxLeft = dimension.maxLeft !== null ? dimension.maxLeft - containerRect.left + (offsetX - 10) : null;
dimension.minLeft = dimension.minLeft !== null ? dimension.minLeft - containerRect.left - (offsetX - 10) : null;
return dimension;
}
const handleGroupBorderPosition = (groupElements: NodeListOf<HTMLElement>, containerRect: DOMRect, offsetX: number, offsetY: number) => {
const dimension = getGroupDimension(groupElements, containerRect, offsetX, offsetY);
const borderTop: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='top']");
const borderRight: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='right']");
const borderBottom: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='bottom']");
const borderLeft: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='left']");
if (borderTop !== null && dimension.maxLeft !== null && dimension.minLeft !== null) {
borderTop.style.width = (dimension.maxLeft - dimension.minLeft) + "px";
borderTop.style.height = "1px";
borderTop.style.top = dimension.minTop + "px";
borderTop.style.left = dimension.minLeft + "px";
}
if (borderRight !== null && dimension.maxTop !== null && dimension.minTop !== null) {
borderRight.style.width = "1px";
borderRight.style.height = (dimension.maxTop - dimension.minTop) + "px";
borderRight.style.top = dimension.minTop + "px";
borderRight.style.left = dimension.maxLeft + "px";
}
if (borderBottom !== null && dimension.maxLeft !== null && dimension.minLeft !== null) {
borderBottom.style.width = (dimension.maxLeft - dimension.minLeft) + "px";
borderBottom.style.height = "1px";
borderBottom.style.top = dimension.maxTop + "px";
borderBottom.style.left = dimension.minLeft + "px";
}
if (borderLeft !== null && dimension.maxTop !== null && dimension.minTop !== null) {
borderLeft.style.width = "1px";
borderLeft.style.height = (dimension.maxTop - dimension.minTop) + "px";
borderLeft.style.top = dimension.minTop + "px";
borderLeft.style.left = dimension.minLeft + "px";
}
}
// const handleTargetBorderEventListener = (groupId: string, containerRect: DOMRect) => {
// console.log("handleTargetBorderEventListener called for groupId:", groupId);
// if (groupId) {
// const groupElements: NodeListOf<HTMLElement> = document.querySelectorAll(`div[group="${groupId}"]`);
// handleGroupBorderPosition(groupElements, containerRect);
// }
// }
const handleInitialGroupBorderPosition = (groupElements: NodeListOf<HTMLElement>, containerRect: DOMRect, offsetX: number, offsetY: number) => {
setTimeout(() => {
handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY)
}, 0)
}
export {
getGroupDimension,
handleGroupBorderPosition,
handleInitialGroupBorderPosition
// handleTargetBorderEventListener
}

View File

@ -11,7 +11,9 @@ export default defineConfig({
'quality-friendly-goldfish.ngrok-free.app', 'quality-friendly-goldfish.ngrok-free.app',
'officially-powerful-goat.ngrok-free.app', 'officially-powerful-goat.ngrok-free.app',
'89f0-49-36-189-120.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: { build: {