Compare commits

...

30 Commits

Author SHA1 Message Date
=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
=bhagwanSingh 3d72931007 Attributes Update 2025-07-08 12:14:52 +05:30
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
= e2369a4548 Merge branch 'jai' of http://git.hicglobalsolutions.in/BhawanSingh/SME-Web 2025-07-02 17:34:05 +05:30
Jai Pandey 1215433e16 a 2025-07-02 17:32:16 +05:30
57 changed files with 8231 additions and 1999 deletions

1728
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -69,7 +69,7 @@ const SigningOrderDiagram : React.FC<Signer[]> = (signers) => {
) || -1;
return (
<div className="relative p-8 bg-white max-w-max">
<div className="relative p-8 bg-white rounded-b-lg max-w-max max-h-[60vh] overflow-auto">
{/* Single mail icon on top */}
<div className="flex justify-center items-center gap-4">
@ -85,7 +85,7 @@ const SigningOrderDiagram : React.FC<Signer[]> = (signers) => {
<div className="w-16 border-1 border-dotted border-slate-200 "></div>
</div>
<div className="flex items-center justify-center space-x-6">
<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">

View File

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

View File

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

View File

@ -4,6 +4,20 @@ import AccountNav from "./AccountNav";
import api from "../../config/api";
import { useNavigate, useParams } from "react-router-dom";
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 {
id: number;
@ -18,14 +32,173 @@ const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString();
};
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 [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 () => {
console.log("fetchTransactions")
try {
const response = await api(`/api/accounts/${accountId}/transactions`);
const response = await api(`/api/accounts/${activeAccountId}/transactions`);
setTransactions(response.data || []);
} catch (error) {
console.error("Error fetching transactions:", error);
@ -33,19 +206,80 @@ const Account = () => {
};
useEffect(() => {
if (accountId) fetchTransactions();
}, [accountId]);
if (activeAccountId) {
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 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 (
<>
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
isLoading={isLoading} />
{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">
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
<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">Last Change</th>
<th className="px-4 py-3 text-center">Expire After (days)</th>
<th className="px-4 py-3 text-center"></th>
<th className="px-4 py-3 text-center">Sign</th>
<th className="px-4 py-3 text-center">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
@ -92,6 +327,9 @@ const Account = () => {
<td className="px-4 py-3 text-center">
{tx.expireAfter ?? "-"}
</td>
<td className="px-4 py-3 text-center ">
{tx?.createdBy?.email === currentEmailId ? <button className="px-2 py-0.5 rounded-xl text-xs font-medium bg-green-100 text-green-700 cursor-pointer">Sign now</button> : "-"}
</td>
<td className="px-4 py-3 text-center">
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
</td>
@ -102,6 +340,8 @@ const Account = () => {
</table>
</div>
)}
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}
accountOptions={accountsData} />}
</>
);
};

View File

@ -1,105 +1,405 @@
import React, { use, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import { FaCog } from 'react-icons/fa';
import { IoIosNotifications } from 'react-icons/io';
import { RootState } from '../../store/store';
import { UserState } from '../../store/slices/userSlice';
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';
// 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 {
accountName: string | null;
transactionName: string | null;
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 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
? user.username
.split(' ')
.map((n) => n[0])
.join('')
.slice(0, 2)
.toUpperCase()
: 'U';
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);
// useEffect(() => {
// if (accountId) {
// api.get(`/api/accounts/${accountId}`)
// .then(response => {
// // Handle the account data if needed
// console.log(response.data, "Line 29");
// })
// .catch(error => {
// console.error("Error fetching account data:", error);
// });
// }
// }, [accountId, transactionId]);
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;
<h1 className="text-md font-medium text-gray-800 capitalize">
Account
{accountName ? ` > ${accountName}` : ''}
{transactionName ? ` > ${transactionName}` : ''}
</h1>
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>
{/* <h1 className="text-md font-medium text-gray-800 capitalize">Account</h1> */}
<div className="flex items-center">
{/* Notification + Settings with smaller gap */}
<div className="flex items-center">
<div className="flex items-center space-x-2">
<div className="relative" ref={dropdownRef}>
<button
aria-label="Notifications"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
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>
<Link
to="/settings"
aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
>
<FaCog className="text-sm" />
</Link>
</div>
{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>
{/* Avatar with separate spacing */}
<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">
{user.picture ? (
<img
src={user.picture}
alt="User avatar"
className="w-full h-full object-cover"
/>
) : (
initials
<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>
@ -107,4 +407,4 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
);
};
export default AccountNav;
export default AccountNav;

View File

@ -6,6 +6,10 @@ import AccountNav from "./AccountNav";
import api from "../../config/api";
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
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";
@ -30,8 +34,16 @@ const getStatusLabel = (
};
const Accounts = () => {
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
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();
@ -53,8 +65,65 @@ const Accounts = () => {
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 (
<>
{/* <Cards type="account" onOpenModal={handleOpenModal} /> */}
{allAccounts.length > 0 && (
<div className="overflow-x-auto py-2">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
@ -101,8 +170,38 @@ const Accounts = () => {
{status.text}
</span>
</td>
<td className="px-4 py-3 text-center">
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
<td className="px-4 py-3 text-center relative">
<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>
</tr>
);
@ -111,6 +210,8 @@ const Accounts = () => {
</table>
</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

@ -11,7 +11,7 @@ import { Bounce, toast } from "react-toastify";
import { useDispatch, useSelector } from "react-redux";
import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
import { FaRegImage, FaFileAlt, FaSignature, FaStamp } from 'react-icons/fa'
import { FaRegImage, FaFileAlt, FaSignature, FaStamp, FaCheck } from 'react-icons/fa'
import { IoIosDocument } from 'react-icons/io'
import { MdKeyboardArrowDown } from 'react-icons/md'
import { TbChevronsDownLeft, TbSignature } from "react-icons/tb";
@ -100,6 +100,30 @@ export interface StampTypeObj {
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
// handleSignClick - Handle when sign || Initial tag got clicked
// fetchData - Fetch document images and tags data
@ -126,6 +150,9 @@ const SignDocument = () => {
// Show is fields - Showing all fields There
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 [openSign, setOpenSign] = useState<boolean>(false);
@ -146,6 +173,21 @@ const SignDocument = () => {
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(() => {
dispatch(showLoader("Loading ..."));
fetchData();
@ -162,9 +204,10 @@ const SignDocument = () => {
console.log(saved, "Line 162")
useEffect(() => {
if (recSignStatus === "COMPLETE") {
if (recSignStatus === "COMPLETED") {
console.log("SETTING HIDE TAGS to TRUE LIne 161")
setHideTags(true);
// setHideTags(true);
setTransparentTags(true);
} else {
console.log("SETTING HIDE TAGS to TRUE LIne 161")
setHideTags(false);
@ -447,13 +490,31 @@ 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(() => {
if (tagJson) {
for (let i = 0; i < tagJson?.length; i++) {
(async () => {
if (tagJson) {
for (const tag of tagJson) {
if (tag.type === "SIGNATURE" && "value" in tag) {
const signatureData = await fetchSignatureData(tag.value);
console.log(signatureData);
}
}
}
}
})()
}, [tagJson])
@ -498,6 +559,40 @@ const SignDocument = () => {
<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>
</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>
@ -613,7 +708,7 @@ const SignDocument = () => {
{/* Main container */}
<div
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-10 px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth"
>
{
@ -668,7 +763,7 @@ const SignDocument = () => {
<div
id={item.id}
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" }}
>
@ -967,7 +1062,7 @@ const SignDocument = () => {
}
{/* 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 "
onClick={() => handleTagNavClick("P")}
>Prev</div>
@ -975,7 +1070,7 @@ const SignDocument = () => {
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-r-md transition-all duration-150 "
onClick={() => handleTagNavClick("N")}
>Next</div>
</div>}
</div>} */}
</div>

View File

@ -17,6 +17,7 @@ import confetti from 'canvas-confetti'
import { useWindowSize } from 'react-use'
import { FaCheckCircle } from 'react-icons/fa'
import { IoClose } from 'react-icons/io5'
import CommonNav from '../CommonNav'
function Dashboard() {
const [showSideBar, setShowSideBar] = React.useState(true)
@ -102,32 +103,32 @@ function Dashboard() {
}, [documentSignType])
useEffect(() => {
if (showConfetti) {
const duration = 3000;
const end = Date.now() + duration;
useEffect(() => {
if (showConfetti) {
const duration = 3000;
const end = Date.now() + duration;
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
confetti({
particleCount: 80, // more particles
angle: Math.random() * 360, // fire in random direction
spread: 220, // wider spread
startVelocity: 15, // more power
scalar: 1.2, // bigger pieces (default is 1)
origin: {
x: Math.random(),
y: Math.random() * 0.6,
},
zIndex: 9999,
});
}, 350); // fire every 150ms
}
}, [showConfetti]);
confetti({
particleCount: 80, // more particles
angle: Math.random() * 360, // fire in random direction
spread: 220, // wider spread
startVelocity: 15, // more power
scalar: 1.2, // bigger pieces (default is 1)
origin: {
x: Math.random(),
y: Math.random() * 0.6,
},
zIndex: 9999,
});
}, 350); // fire every 150ms
}
}, [showConfetti]);
@ -173,6 +174,7 @@ function Dashboard() {
<div className={`overflow-y-auto absolute ${showSideBar ? 'w-[calc(100%-250px)]' : 'w-[calc(100%-60px)]'} h-full right-0 bottom-0 overflow-hidden bg-[var(--mainViewBg)] transition-all duration-300`}>
{/* Main Content */}
<div className='px-2 py-1.5'>
<CommonNav />
<Outlet />
</div>
</div>

View File

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

View File

@ -1,97 +1,328 @@
import React, { useState } from 'react';
import { FaCheckCircle, FaClock, FaHourglassHalf, FaTasks } from 'react-icons/fa';
import React, { useEffect, useState } from 'react';
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,
icon: <FaTasks size={20} className="text-orange-500" />,
ring: 'ring-orange-400/20',
bg: 'bg-orange-50',
icon: <FaTasks size={18} className="text-blue-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: 'up'
},
{
title: 'Waiting for Others',
title: 'In-Progress',
count: 8,
icon: <FaClock size={20} className="text-yellow-500" />,
ring: 'ring-yellow-400/20',
bg: 'bg-yellow-50',
},
{
title: 'Expiring Soon',
count: 5,
icon: <FaHourglassHalf size={20} className="text-red-500" />,
ring: 'ring-red-400/20',
bg: 'bg-red-50',
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: 'stable'
},
{
title: 'Completed',
count: 27,
icon: <FaCheckCircle size={20} className="text-green-500" />,
ring: 'ring-green-400/20',
bg: 'bg-green-50',
icon: <FaCheckCircle 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: '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 accounts = ['All Accounts', 'Account 1', 'Account 2', 'Account 3'];
const Cards = () => {
const [selectedFilter, setSelectedFilter] = useState('Last 7 Days');
const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRangeChange, cards, isLoading }) => {
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 (
<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">
<div className="w-full px-4 py-3 mt-2 rounded-md">
{/* Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3">
<h2 className="text-lg font-medium text-gray-700">Overview</h2>
<div className="flex gap-2 w-full md:w-auto">
{/* Account Selector */}
<select
value={'All'}
onChange={() => {}}
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 className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-2 gap-4">
<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">
<FaFilter className="text-white text-lg" />
</div> */}
<div>
<h2 className="text-lg font-semibold text-gray-800">Overview</h2>
{/* <p className="text-gray-600 text-sm">Monitor performance metrics</p> */}
</div>
</div>
</div>
{/* Cards */}
<div className="cursor-pointer grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{cards.map((card) => (
<div
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`}
>
<div className="flex items-center space-x-4">
<div className={`p-3 rounded-full ${card.bg} ${card.ring}`}>
{card.icon}
</div>
<div>
<p className="text-sm text-gray-500">{card.title}</p>
<p className="text-xl font-bold text-gray-800">{card.count}</p>
</div>
</div>
{isLoading ? <div className="w-full [h-50vh] px-6 py-8">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-32 mb-8"></div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(7)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
))}
</div>
))}
</div>
</div>
</div> :
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
{cards.map((card, index) => (
<div
key={card.title}
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`
}}
>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute -top-6 -right-6 w-12 h-12 rounded-full bg-current"></div>
<div className="absolute -bottom-6 -left-6 w-10 h-10 rounded-full bg-current"></div>
</div>
{/* Content */}
<div className="relative 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}
</div>
{renderTrendIndicator(card.trend, card.percentage)}
</div>
<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>
{/* 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>
);
};
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 HomeTopNav from './HomeTopNav'
import UploadDoc from './UploadDoc'
import FilterSort from './FilterSort'
import ShowDocs from './ShowDocs'
import Cards from './Cards'
import StartSection from './StartSection'
import React, { useEffect, useState, useCallback } from 'react';
import AccountNav from '../accounts/AccountNav';
import Cards from './Cards';
import StartSection from './StartSection';
import api from '../../config/api';
import {
FaCheckCircle,
FaClock,
FaHourglassHalf,
FaTasks,
FaChevronDown,
FaPlus,
FaUserPlus,
FaFilter,
FaCalendarAlt
} from 'react-icons/fa';
const Home = () => {
return (
<>
<HomeTopNav />
<Cards />
<StartSection />
{/* <UploadDoc /> */}
{/* <FilterSort /> */}
{/* <ShowDocs /> */}
</>
)
interface CardData {
title: string;
count: number;
icon: JSX.Element;
gradient: string;
shadowColor: string;
iconBg: string;
textColor: string;
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>
<Link
to="/settings"
to="settings"
aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
>

View File

@ -77,7 +77,7 @@ const StartSection = () => {
return (
<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">
{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 { useNavigate, useParams } from "react-router-dom";
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 { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
import Loader from "../../dashboard/Loader";
@ -22,15 +22,24 @@ const ProcessStepOne = () => {
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) {
window.location.href = "/";
return null;
}
const navigate = useNavigate();
const dispatch = useDispatch();
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
// const { selectedDocuments, signType } = useSelector((state: RootState) => state.selectedDocuments)
@ -45,7 +54,7 @@ const ProcessStepOne = () => {
// const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder)
// const user = useSelector((state: RootState) => state.user)
const [emailDtls, setEmailDtls] = useState<EmailDtls>({ subject: "Documents for Signature", message: "" });
useEffect(() => {
@ -53,10 +62,30 @@ const ProcessStepOne = () => {
}, [])
const handleNextClick = async (isSendNow: boolean) => {
if (!selectedDocuments.length || !selectedRecipients.length) {
let formError = {
documents: false,
recipients: false,
subject: false
}
if (!selectedDocuments.length) {
formError.documents = true
}
if (!selectedRecipients.length) {
formError.recipients = true
}
if (!emailDtls.subject.length) {
formError.subject = true
}
if (formError.documents || formError.recipients || formError.subject) {
console.log(selectedDocuments, 'line 193')
toast.dismiss();
toast.error("Add Documents | Recipients First", {
toast.error(`${formError.documents ? "Add Document" : ""}
${(formError.documents && formError.recipients) ? " | " : ""}
${formError.recipients ? "Add Recipient" : ""}
${(formError.documents || formError.recipients) ? " | " : ""}
${formError.subject ? "Add Subject" : ""}`, {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
@ -79,9 +108,9 @@ const ProcessStepOne = () => {
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) {
dispatch(setDocuments(res.data));
}
@ -92,7 +121,7 @@ const ProcessStepOne = () => {
dispatch(showLoader("Processing Recipients..."));
const recipientsData = selectedRecipients.map((recipient, index) => ({
// index,
id: "id" in recipient ? recipient.id : null,
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
type: recipient.type,
firstName: recipient.firstName,
@ -112,12 +141,18 @@ const ProcessStepOne = () => {
name: `${recipient.firstName} ${recipient.lastName}`
}));
dispatch(addRecipientAsync({recipientsData, accountId, transactionId}));
dispatch(addRecipientAsync({ recipientsData, accountId, transactionId }));
dispatch(showLoader("Processing Message..."));
await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { subject: emailDtls.subject, message: emailDtls.message, status: "CREATED" }).then((res) => {
console.log(res);
})
if (isSendNow) {
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS));
setTimeout(() => {
navigate("/");
// navigate("/");
}, 1000);
} else {
setTimeout(() => {
@ -210,7 +245,8 @@ const ProcessStepOne = () => {
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
<input
type="text"
value={emailDtls.subject || "Documents for Signature"}
required
value={emailDtls.subject}
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
placeholder="Email Subject"
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"

View File

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

View File

@ -1,5 +1,6 @@
import { useDispatch, useSelector } from "react-redux";
import {
removeRecipientAsync,
removeSelectedRecipient,
// setSigningOrder,
updateRecipientOrder, // 👈 You need to define this in your slice
@ -11,6 +12,7 @@ import { FaUsersLine } from "react-icons/fa6";
import { useState } from "react";
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
import { IoCloseOutline } from "react-icons/io5";
import { useParams } from "react-router-dom";
const getInitials = (name: string) => {
if (!name) return "";
@ -22,7 +24,15 @@ const getInitials = (name: string) => {
};
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 signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder);
const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
@ -34,7 +44,14 @@ const RecipientCards = () => {
};
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) {
let leftRecipient = recipients.filter((obj) => {
if ("id" in obj && "id" in recipient) {
@ -152,8 +169,8 @@ const RecipientCards = () => {
<div className="pb-2 text-white flex justify-between items-center">
<p className="font-semibold">Signing Order</p>
<div
onClick={() => setSigningOrderModal({...signingOrderModal, open: false})}
className="text-xl cursor-pointer hover:text-gray-200 transition-all">
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
className="text-xl cursor-pointer hover:text-gray-200 transition-all">
<IoCloseOutline />
</div>
</div>

View File

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

View File

@ -4,18 +4,20 @@ import { RootState } from '../../../store/store'
import api from '../../../config/api';
import RightSideBar from './RightSideBar';
import RightSideBar from './rightSideBar/RightSideBar';
import { DroppedItem } from '../../../utlity/types';
import { useNavigate, useParams } from 'react-router-dom';
import { TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../../store/slices/transactionSlice';
import { hideLoader, showLoader } from '../../../store/slices/loaderSlice';
import { IoMdClose } from 'react-icons/io';
import fields from '../../../helper/fieldsListData';
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice';
import { fetchDocuments } from '../../../store/slices/documentSlice';
import { FaQuestionCircle } from 'react-icons/fa';
import typeToIconMap from '../../../helper/iconMap';
import { IoAdd } from 'react-icons/io5';
import Loader from '../../dashboard/Loader';
export interface DroppedItemsObj {
[key: string]: DroppedItem[]
}
@ -186,7 +188,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// const remove = []
// console.log(updatedTags, "updatedTags");
for (const dropPageIds of Object.keys(updatedTags)) {
for (const dropTag of updatedTags[dropPageIds]) {
let mask = null
@ -202,6 +203,8 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
}
// }
console.log(dropTag, "line 204")
const isDeleted = "isDeleted" in dropTag ? dropTag.isDeleted : false
const updatedTagData = {
@ -210,26 +213,26 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
"dimensions": dropTag.dimensions,
"type": dropTag.type.toUpperCase(),
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
validation: {
"minLength": 0,
"maxLength": 0,
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
"regex": "regex" in dropTag ? dropTag.regex : []
},
"minLength": "minLength" in dropTag ? dropTag.minLength : 0,
"maxLength": "maxLength" in dropTag ? dropTag.maxLength : 0,
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
"regex": "regex" in dropTag ? dropTag.regex : []
// }
,
"documentId": dropTag.documentId,
"options": ("options" in dropTag) ? dropTag.options : [],
"pageIndex": dropTag.pageIndex,
"recipientId": dropTag.recipientId,
"styles": "styles" in dropTag ? dropTag.styles : {
"font": "Great Vibes",
"font": "",
"italic": false,
"bold": false,
"underline": false,
"color": "black",
"textSize": 14.0
"textSize": 0
},
// "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true,
"required": false,
"required": "required" in dropTag ? dropTag.required : true,
"update": "update" in dropTag ? dropTag.update : false,
"readOnly": false,
}
@ -272,9 +275,13 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// for(const recipient of selectedRecipients) {
// const recrecipient
// }
tagsData = groupByRecipientId(tagsData);
sendTagsToRecipients(tagsData, accountId, transactionId);
(async () => {
const res = await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, {status: "SENT"})
console.log(res, "Check123")
})()
}, [isSend])
// Save Tags - END
@ -284,6 +291,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// 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;
@ -305,6 +313,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
let prepData = {
properties: data.properties,
icon: typeToIconMap[field.label.toUpperCase()] || <FaQuestionCircle />,
type: dragElemData.label,
subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '',
@ -434,6 +443,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
// HANDLE EXPAND TAG - START
const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
console.log("HANDLE EXPAND TAG - START")
event.preventDefault(); // Prevent text selection issues
const expandingElemIcon = event.currentTarget;
@ -514,6 +524,10 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
})()
}
const handleAddRadioClick = (key:string) => {
console.log("Handle Radio Clicled", key)
console.log(droppedItems, "line 525")
}
// const handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => {
// // const dataToSet: DroppedItemsObj = {
@ -616,13 +630,58 @@ 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
};
return (
<>
{/* 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'>
{
selectedDocuments.length > 0 && selectedDocuments.map((doc, indxDoc) => {
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
return (
<div key={indxDoc} className='flex flex-col'>
<div className='py-2 text-sm'>
@ -694,21 +753,53 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
{/* Expand Circle - ENDS */}
{
item.type !== "radio" ?
<div
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
// style={{ width: `${item.width}px !important` }}
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
style={{ background: selectedRecipient[0]?.color }}
>
<p
className={`flex justify-center items-center w-full text-xl text-white h-full`}
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
>
{IconComponent}
</p>
</div>
:
<>
<div>
{/* Radio - Container - START */}
<div>
<div
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
// style={{ width: `${item.width}px !important` }}
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
style={{ background: selectedRecipient[0]?.color }}
>
<p
className={`flex justify-center items-center w-full text-xl text-white h-full`}
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
>
{IconComponent}
</p>
</div>
{/* Radio- Item - START */}
<div
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
// style={{ width: `${item.width}px !important` }}
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
style={{ background: selectedRecipient[0]?.color }}
>
<p
className={`flex justify-center items-center w-full text-xl text-white h-full`}
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
>
{IconComponent}
</p>
</div>
{/* Radio- Item - END */}
</div>
{/* Radio - Container - END */}
<IoAdd onClick={() => handleAddRadioClick(key)} className='ml-[50%] translate-x-[-50%] cursor-pointer' />
</div>
</>
}
</div>
)
}
@ -746,13 +837,18 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
</div>
)
})
}) : <Loader />
}
</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 */}
</>
)

View File

@ -4,9 +4,9 @@ import api from '../../../config/api';
import { useParams } from 'react-router-dom';
import typeToIconMap from '../../../helper/iconMap';
import { FaQuestionCircle } from 'react-icons/fa';
import { TagsProperties } from '../../../utlity/types';
const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string, recipientId: number, recipientIndx: number) => {
const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string, recipientId: number, recipientIndx: number, tagsProperties: TagsProperties) => {
const dragElemRect = event.currentTarget.getBoundingClientRect();
let width = dragElemRect.width
let height = dragElemRect.height
@ -27,7 +27,8 @@ const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string
recipientId: recipientId,
recipientIndx: recipientIndx,
width,
height
height,
properties: tagsProperties
}
window.dragElemData = JSON.stringify(obj);
@ -41,7 +42,7 @@ interface FieldListProps {
const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx }) => {
const [tagsData, setTagsData] = useState([]);
const [tagsData, setTagsData] = useState<TagsProperties[]>([]);
const { accountId } = useParams<{ accountId: string }>();
if (!accountId) {
@ -91,7 +92,7 @@ const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx
<div
draggable={true}
key={index}
onPointerDown={(e) => handlePointerDown(e, field.type.toLowerCase(), recipientId, recipientIndx)}
onPointerDown={(e) => handlePointerDown(e, field.type.toLowerCase(), recipientId, recipientIndx, field)}
className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300">
<div
className={`p-1 text-sm rounded-sm text-white`}

View File

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

View File

@ -1,10 +1,9 @@
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import NavTwo from './NavTwo';
import LeftSideBar from './LeftSideBar';
import DocumentContainer from './DocumentContainer';
import { IoMdClose } from 'react-icons/io'
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../../../store/store';
import Loader from '../../dashboard/Loader';
@ -25,28 +24,6 @@ const ProcessStepTwo = () => {
return (
<div className={`min-w-full relative h-screen bg-slate-100 overflow-hidden ${(globalLoader.isLoading) ? 'pointer-events-none' : ''}`}>
{/* Top Header One */}
{/* <div className='flex items-center shadow-sm pb-3 bg-slate-100'>
<div className='flex w-full justify-center items-center px-4'>
<div className='cursor-default flex justify-center items-center gap-4 border-b-3 relative border-[var(--primary)] py-1 px-4'>
<p className="text-xs tracking-wider">Setup your Document</p>
<div className='absolute bottom-[-7px] w-3 h-3 rounded-full right-[-4px] bg-[var(--primary)] z-50' ></div>
</div>
<div className='cursor-default flex justify-center items-center gap-4 border-b-3 relative border-[var(--primary)] py-1 px-4'>
<p className="text-xs tracking-wider">Prepare</p>
<div className='absolute bottom-[-7px] w-3 h-3 rounded-full right-[-4px] bg-[var(--primary)] z-50' ></div>
</div>
<div className='cursor-default flex justify-center items-center gap-4 border-b-3 relative border-[var(--primaryLight)] py-1 px-4'>
<p className="text-xs tracking-wider">Hooray !</p>
<div className='absolute bottom-[-7px] w-3 h-3 rounded-full right-[-4px] bg-[var(--primaryLight)] z-50' ></div>
</div>
</div>
</div> */}
{/* Top Header One */}
<NavTwo isFields={isFields} setIsFields={setIsFields} isSend={isSend} setIsSend={setIsSend} />
<div className='grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-46px)] overflow-hidden'>

View File

@ -1,653 +0,0 @@
import { MdKeyboardArrowDown } from "react-icons/md";
import { DroppedItemsObj } from "./DocumentContainer";
import React, { ReactHTMLElement, useEffect, useState } from "react";
import { RiAsterisk } from "react-icons/ri";
import { GoDotFill } from "react-icons/go";
import { IoClose } from "react-icons/io5";
import { LuAsterisk } from "react-icons/lu";
import { dateFormats, dateTimeFormats, monthFormats, timeFormats, weekFormats, yearFormats } from "../../../utlity/dateTimeUtils";
import { DroppedItem } from "../../../utlity/types";
import { IoMdClose } from "react-icons/io";
import dayjs from "dayjs";
interface DroppedItemProps {
selectedTag: DroppedItem
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>
selTagKey: string,
selTagIndx: number
}
enum TEXT {
PARAGRAPH = "paragraph",
SINGLE_LINE = "single_line",
PASSWORD = "password",
}
// enum SIGNATURE {
// TYPED = "typed",
// DRAW = "draw",
// UPLOAD = "upload",
// }
// enum INITIALS {
// TYPED = "typed",
// DRAW = "draw",
// UPLOAD = "upload",
// }
export enum TEMPORAL {
DATE = "date",
TIME = "time",
DATE_TIME = "date_time",
// YEAR = "year",
// MONTH = "month",
// WEEK = "week",
}
enum NUMBER {
INTEGER = "integer",
DECIMAL = "decimal",
CURRENCY = "currency"
}
// enum IMAGE {
// STAMP = "stamp",
// LOGO = "logo"
// }
enum SELECT {
SINGLE_SELECT = "single select",
MULTI_SELECT = "multi select"
}
enum MASKTYPE {
ALL = "all",
LAST_FOUR = "last_four",
CREDIT_CARD = "credit_Card",
NATIONAL_INSURANCE = "national_insurance",
SOCIAL_SECURITY = "social_security",
SOCIAL_INSURANCE = "social_insurance"
}
enum MASKCHARACTER {
ASTERISK = "ASTERISK",
DOT = "DOT",
X = "CROSS",
}
const enumMap = {
text: TEXT,
temporal: TEMPORAL,
number: NUMBER,
image: String,
select: SELECT
}
enum DROPDOWNTOGGLE {
TYPE,
MASK_TYPE,
MASK_CHARACTER,
DDMMYY_FORMAT
}
const dateTimeFormatMap = {
"date": dateFormats,
"time": timeFormats,
"date_time": dateTimeFormats,
"year": yearFormats,
"month": monthFormats,
"week": weekFormats
}
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx }) => {
console.log(selectedTag, "selectedTag Line 102")
const selectedType = selectedTag.type in enumMap && enumMap[selectedTag.type as keyof typeof enumMap];
const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null);
// const [showFormatEg, setShowFormatEg] = useState<boolean>(false);
const [is24Hrs, setIs24Hrs] = useState<boolean>(true);
const [isCurrentTimeStamp, setIsCurrentTimeStamp] = useState<boolean>(true);
// const [selectedOptions, setSelectedOptions] = useState<string[]>([])
const [decimalPlace, setDecimalPlace] = useState<number>(2)
const [selectedOption, setSelectedOption] = useState<string>()
const handlePropertyChange = (changeTo: string, changeWith: string | boolean | number) => {
console.log("Handle Property Change Called", "changeTo", changeTo, "changeWith", changeWith)
if (changeTo === '' || changeWith === '') return
if (changeTo === "subType" && (changeWith === TEMPORAL.DATE || changeWith === TEMPORAL.DATE_TIME || changeWith === TEMPORAL.TIME)) {
// let change: string = changeWith;
// if (change === "DATE TIME") {
// change = "DATE_TIME"
// }
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith || "", format: "", update: "id" in item ? true : false } : item
)
: [],
}// Provide a fallback in case it's undefined or not an array
));
} else if (changeTo === "subType" && typeof changeWith === "string" && changeWith.toLowerCase() === "password") {
console.log("Line 161")
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith, maskType: "All", maskCharacter: "ASTERISK", update: "id" in item ? true : false } : item
)
: [],
}
));
}
else if (changeTo === "subType" && (changeWith === NUMBER.CURRENCY || changeWith === NUMBER.INTEGER || changeWith === NUMBER.DECIMAL)) {
let decPlace = changeWith !== NUMBER.DECIMAL ? 0 : decimalPlace;
const isCurrency = changeWith === NUMBER.CURRENCY;
// const currencyType = null
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith, decimalPlace: decPlace, isCurrency, update: "id" in item ? true : false } : item
)
: [],
}// Provide a fallback in case it's undefined or not an array
));
} else if (changeTo === "format" && selectedTag.subType === TEMPORAL.DATE_TIME) {
let val = timeFormats[0].format;
if (!is24Hrs) {
val = timeFormats[1].format
}
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith + " " + val, update: "id" in item ? true : false } : item
)
: [],
}// Provide a fallback in case it's undefined or not an array
));
} else {
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith, update: "id" in item ? true : false } : item
)
: [],
}// Provide a fallback in case it's undefined or not an array
));
}
setDropDownToggle(null)
}
// const handleSelectTagOptions = () => {
// if (!selectedOptions.length) return
// setDroppedItems((prev) => (
// {
// ...prev,
// [selTagKey]: Array.isArray(prev[selTagKey])
// ? prev[selTagKey].map((item, i) => {
// if (i === selTagIndx) {
// return { ...item, options: [...selectedOptions] };
// }
// return item;
// })
// : [],
// }
// ));
// }
// useEffect(() => {
// handleSelectTagOptions();
// }, [selectedOptions])
useEffect(() => {
if (selectedTag.subType === TEMPORAL.DATE || selectedTag.subType === TEMPORAL.DATE_TIME || selectedTag.subType === TEMPORAL.TIME) {
if (selectedTag.subType === TEMPORAL.DATE) {
handlePropertyChange("format", dateFormats[0].format)
}
else if (selectedTag.subType === TEMPORAL.DATE_TIME) {
handlePropertyChange("format", dateTimeFormats[0].format)
}
else if (selectedTag.subType === TEMPORAL.TIME) {
handlePropertyChange("format", timeFormats[0].format)
}
}
// else if(selectedTag.subType === TEXT.PASSWORD) {
// handlePropertyChange("format", timeFormats[0].format)
// }
}, [selectedTag.subType])
useEffect(() => {
if (selectedTag.format === timeFormats[1].format && is24Hrs) {
setIs24Hrs(false);
} else if (selectedTag.format === timeFormats[0].format && !is24Hrs) {
setIs24Hrs(true);
}
}, [selectedTag.format])
useEffect(() => {
if (selectedTag.label === "temporal") {
handlePropertyChange("subType", TEMPORAL.DATE);
handlePropertyChange("format", "DD/MM/YY");
}
}, [])
const handleIs24HrsClick = (checked: boolean) => {
if (selectedTag.subType === TEMPORAL.TIME) {
if (!checked) {
handlePropertyChange("format", timeFormats[1].format)
} else {
handlePropertyChange("format", timeFormats[0].format)
}
}
setIs24Hrs(checked)
}
const handleIsCurrentTimeStamp = (checked: boolean) => {
if (selectedTag.type.toLowerCase() === "temporal") {
setIsCurrentTimeStamp(checked)
handlePropertyChange("currentTimeStamp", checked);
}
}
const handleAddSelectOption = () => {
if (selectedOption && selectedOption.length) {
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) => {
if (i === selTagIndx) {
return { ...item, options: item.options ? [...item.options, selectedOption] : [selectedOption], update: "id" in item ? true : false };
}
return item;
})
: [],
}
));
}
}
const handleSelectOptionDel = (indx: number) => {
// setSelectedOptions(prev =>
// prev.filter((_, i) => i !== indx)
// )
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) => {
if (i === selTagIndx) {
return { ...item, options: item.options ? item.options.filter((_, i) => i !== indx) : [] };
}
return item;
})
: [],
}
));
}
const handleDecimalPlace = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = Number(e.target.value)
setDecimalPlace(val)
handlePropertyChange("decimalPlace", val)
}
useEffect(() => {
if ("styles" in selectedTag) {
const fontSize = selectedTag?.styles?.textSize;
const tagHeight = selectedTag?.dimensions?.height - 18;
console.log(fontSize, tagHeight, "Line 323", selectedTag?.styles)
if (selectedTag.subType !== "password" && selectedTag.subType !== "paragraph" && tagHeight > (fontSize + fontSize)) {
console.log("Change subType Called from if")
handlePropertyChange("subType", "paragraph")
} else if (selectedTag.subType !== "password" && selectedTag.subType !== "single_line" && tagHeight <= (fontSize + fontSize)) {
console.log("Change subType Called from elseif")
handlePropertyChange("subType", "single_line")
}
}
}, [selectedTag])
console.log("RightSideBar - selectedTag", selectedTag)
return (
<>
{/* Right Side Bar Start */}
<div className='col-span-2 w-full h-full bg-white text-sm'>
<p className="p-2 font-semibold text-md">Properties</p>
<hr className="text-slate-200" />
<div className="p-3 text-sm text-slate-600 flex flex-col gap-2">
{/* label */}
<div className="flex flex-col">
<label htmlFor="" className="">Label</label>
<input
value={selectedTag.label}
onChange={(e) => handlePropertyChange('label', e.target.value)}
type="text"
className="outline-none border-1 rounded px-2 py-1 border-slate-200" />
</div>
{/* Required */}
{(selectedTag.type !== "signature" && selectedTag.type !== "initials") && <div className="flex justify-between items-center">
<label htmlFor="required" className="text-xs text-slate-600 transitions-all duration-150">Required</label>
<input
id="required"
checked={selectedTag.required ? selectedTag.required : false}
onChange={(e) => handlePropertyChange('required', e.target.checked)}
type="checkbox"
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
/>
</div>}
{/* Use Current TimeStamp */}
{selectedTag.type.toLowerCase() === "temporal" && <div className="flex justify-left items-center gap-1.5 mt-0.5">
<label
className={`text-xs ${isCurrentTimeStamp ? "text-slate-600" : "text-slate-400"} transitions-all duration-150`}
htmlFor="use_current_timestamp"
>
Use Current Timestamp
</label>
<input
checked={isCurrentTimeStamp ? isCurrentTimeStamp : false}
onChange={(e) => handleIsCurrentTimeStamp(e.target.checked)}
id="use_current_timestamp"
type="checkbox"
/>
</div>}
{/* Type | SubType */}
{selectedType && Object.keys(selectedType).length > 0 &&
<div className="flex flex-col">
<label htmlFor="" className="">Type</label>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.TYPE ? null : DROPDOWNTOGGLE.TYPE)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="capitalize cursor-pointer">{selectedTag?.subType ? (selectedTag.subType === "date_time" ? "Date Time" : selectedTag.subType) : selectedType[Object.keys(selectedType)[0] as keyof typeof selectedType]}</p>
<MdKeyboardArrowDown />
</div>
{dropDownToggle === DROPDOWNTOGGLE.TYPE &&
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
{Object.keys(selectedType).map((val, indx) => (
<div
key={indx}
onClick={() => handlePropertyChange("subType", selectedType[val as keyof typeof selectedType])}
className={`capitalize px-2 py-1 hover:bg-slate-200 cursor-pointer`}
>
{selectedType[val as keyof typeof selectedType] === "date_time" ? "Date Time" : selectedType[val as keyof typeof selectedType]}
</div>
))}
</div>
}
</div>
}
{/* Mask Type */}
{
selectedTag?.subType === TEXT.PASSWORD &&
<div>
<label htmlFor="" className="">Mask Type</label>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.MASK_TYPE ? null : DROPDOWNTOGGLE.MASK_TYPE)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="capitalize cursor-pointer">{(selectedTag.maskType && selectedTag.maskType.replace("_", " ")) ? selectedTag.maskType : MASKTYPE.ALL}</p>
<MdKeyboardArrowDown />
</div>
{
dropDownToggle === DROPDOWNTOGGLE.MASK_TYPE &&
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
{(Object.keys(MASKTYPE) as (keyof typeof MASKTYPE)[]).map((val, indx) =>
(
<div
key={indx}
onClick={() => handlePropertyChange("maskType", MASKTYPE[val])}
className="capitalize px-2 py-1 hover:bg-slate-200 cursor-pointer"
>
{(MASKTYPE[val]).replace("_", " ")}
</div>
)
)}
</div>
}
</div>
}
{/* Mask Character - START */}
{
selectedTag?.subType === TEXT.PASSWORD &&
<div>
<label htmlFor="" className="">Mask Character</label>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.MASK_CHARACTER ? null : DROPDOWNTOGGLE.MASK_CHARACTER)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="capitalize cursor-pointer py-1">
{selectedTag?.maskCharacter
?
selectedTag.maskCharacter === MASKCHARACTER.ASTERISK && <LuAsterisk /> ||
selectedTag.maskCharacter === MASKCHARACTER.DOT && <GoDotFill /> ||
selectedTag.maskCharacter === MASKCHARACTER.X && <IoClose />
:
<LuAsterisk />
}
</p>
<MdKeyboardArrowDown />
</div>
{
dropDownToggle === DROPDOWNTOGGLE.MASK_CHARACTER &&
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
{(Object.keys(MASKCHARACTER) as (keyof typeof MASKCHARACTER)[]).map((val, indx) =>
(
<div
key={indx}
onClick={() => handlePropertyChange("maskCharacter", MASKCHARACTER[val])}
className="capitalize p-2 hover:bg-slate-200 cursor-pointer"
>
{
MASKCHARACTER[val] === MASKCHARACTER.ASTERISK &&
<LuAsterisk />
}
{
MASKCHARACTER[val] === MASKCHARACTER.DOT &&
<GoDotFill />
}
{
MASKCHARACTER[val] === MASKCHARACTER.X &&
<IoClose />
}
</div>
)
)}
</div>
}
</div>
}
{/* Mask Character - END */}
{/* TEMPORAL DATE FORMAT - START */}
{
(selectedTag?.subType === TEMPORAL.DATE ||
selectedTag?.subType === TEMPORAL.TIME ||
selectedTag?.subType === TEMPORAL.DATE_TIME) &&
<div>
{/* <div className="flex justify-between">
<label htmlFor="" className="">Formats</label>
<div className="flex justify-center items-center gap-1.5">
<input
checked={showFormatEg ? showFormatEg : false}
onChange={(e) => setShowFormatEg(e.target.checked)}
id="data_time_format"
type="checkbox" />
<label className="text-xs text-slate-400" htmlFor="data_time_format">
Show Example
</label>
</div>
</div> */}
<div className="flex justify-between">
<label htmlFor="" className="">Formats</label>
{(selectedTag.subType === TEMPORAL.DATE_TIME || selectedTag.subType === TEMPORAL.TIME) &&
<div className="flex justify-center items-center gap-1.5">
<input
checked={is24Hrs ? is24Hrs : false}
onChange={(e) => handleIs24HrsClick(e.target.checked)}
id="data_time_format"
type="checkbox" />
<label className={`text-xs ${is24Hrs ? "text-slate-600" : "text-slate-400"} transitions-all duration-150`} htmlFor="data_time_format">
24 Hrs
</label>
</div>
}
</div>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.DDMMYY_FORMAT ? null : DROPDOWNTOGGLE.DDMMYY_FORMAT)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="cursor-pointer py-1">
{selectedTag.format ? selectedTag.format : dateTimeFormatMap['date'][0].format}
{/* {(selectedTag.subType !== TEMPORAL.DATE && selectedTag.subType !== TEMPORAL.TIME) ? (is24Hrs ? " " + timeFormats[0].format : " " + timeFormats[1].format) : ""} */}
</p>
<MdKeyboardArrowDown />
</div>
{
dropDownToggle === DROPDOWNTOGGLE.DDMMYY_FORMAT &&
<div className="shadow bg-slate-100 mt-1 border-[1px] max-h-[200px] overflow-y-auto border-slate-100 rounded">
{dateTimeFormatMap[selectedTag.subType].map((valObj, indx) => {
// let val = showFormatEg ? valObj["example"] : valObj["format"]
let val = valObj["format"]
if (selectedTag.subType === TEMPORAL.DATE_TIME) {
if (is24Hrs) {
val = val + " " + timeFormats[0].format;
} else {
val = val + " " + timeFormats[1].format;
}
}
return (
<div
key={indx}
onClick={() => handlePropertyChange("format", valObj["format"])}
className="px-2 py-1 hover:bg-slate-200 cursor-pointer"
>
{val}
</div>
)
}
)}
</div>
}
{selectedTag.format && <div className="pt-4">
{/* <p>
Example
</p> */}
<p className="text-center">{dayjs().format(selectedTag.format)}</p>
</div>}
</div>
}
{/* TEMPORAL DATE FORMAT - END */}
{/* NUMBER - DECIMAL PLACES */}
{
selectedTag?.subType === NUMBER.DECIMAL &&
<div>
<label htmlFor="" className="">Decimal Places</label>
<input
type="number"
onChange={(e) => handleDecimalPlace(e)}
value={decimalPlace}
className="bg-slate-100 w-full outline-none p-2 rounded" />
</div>
}
{(selectedTag?.subType === "single select" || selectedTag?.subType === "multi select") &&
<>
<div>
<label htmlFor="" className="">Add Options</label>
<input
type="text"
onChange={(e) => setSelectedOption(e.target.value)}
value={selectedOption}
onKeyUp={(e) => {
if (e.code === "Enter") {
// if (selectedOption) {
// setSelectedOptions((prev) => [...prev, selectedOption]);
// setSelectedOption("");
// }
handleAddSelectOption()
}
}}
className="bg-slate-100 w-full outline-none p-2 rounded" />
<div className="h-[150px] mt-1 overflow-hidden overflow-y-auto">
{
selectedTag.options && selectedTag.options.map((val, indx) => (
<div
key={indx}
className="flex justify-between items-center px-2 py-1 my-1 rounded bg-slate-100">
<p>{val}</p>
<IoMdClose
onClick={() =>
// setSelectedOptions(prev =>
// prev.filter((_, i) => i !== indx)
// )
handleSelectOptionDel(indx)
}
className="cursor-pointer text-slate-500 hover:text-slate-600" />
</div>
))
}
</div>
</div>
</>
}
</div>
</div>
{/* Right Side Bar End */}
</>
)
}
export default RightSideBar

View File

@ -0,0 +1,42 @@
import { MdKeyboardArrowDown } from 'react-icons/md';
import { DROPDOWNTOGGLE } from './RightSideBar';
interface CommonAttrDropDownInt {
selectedType: any;
subType: string | undefined;
dropDownToggle: DROPDOWNTOGGLE | null;
setDropDownToggle: React.Dispatch<React.SetStateAction<DROPDOWNTOGGLE | null>>;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
const CommonAttrDropDown: React.FC<CommonAttrDropDownInt> = ({selectedType, subType, dropDownToggle, setDropDownToggle, handlePropertyChange}) => {
return (
<div className="flex flex-col">
<label htmlFor="" className="">Type</label>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.TYPE ? null : DROPDOWNTOGGLE.TYPE)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="capitalize cursor-pointer">{subType ? (subType === "date_time" ? "Date Time" : subType) : selectedType[Object.keys(selectedType)[0] as keyof typeof selectedType]}</p>
<MdKeyboardArrowDown />
</div>
{dropDownToggle === DROPDOWNTOGGLE.TYPE &&
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
{Object.keys(selectedType).map((val, indx) => (
<div
key={indx}
onClick={() => handlePropertyChange("subType", selectedType[val as keyof typeof selectedType])}
className={`capitalize px-2 py-1 hover:bg-slate-200 cursor-pointer`}
>
{selectedType[val as keyof typeof selectedType] === "date_time" ? "Date Time" : selectedType[val as keyof typeof selectedType]}
</div>
))}
</div>
}
</div>
)
}
export default CommonAttrDropDown

View File

@ -0,0 +1,37 @@
import React, { useState } from 'react'
interface CurrTimeStampAttrInt {
type: string;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
const CurrTimeStampAttr: React.FC<CurrTimeStampAttrInt> = ({ type, handlePropertyChange}) => {
const [isCurrentTimeStamp, setIsCurrentTimeStamp] = useState<boolean>(true);
const handleIsCurrentTimeStamp = (checked: boolean) => {
if (type.toLowerCase() === "temporal") {
setIsCurrentTimeStamp(checked)
handlePropertyChange("currentTimeStamp", checked);
}
}
return (
<div className="flex justify-left items-center gap-1.5 mt-0.5">
<label
className={`text-xs ${isCurrentTimeStamp ? "text-slate-600" : "text-slate-400"} transitions-all duration-150`}
htmlFor="use_current_timestamp"
>
Use Current Timestamp
</label>
<input
checked={isCurrentTimeStamp ? isCurrentTimeStamp : false}
onChange={(e) => handleIsCurrentTimeStamp(e.target.checked)}
id="use_current_timestamp"
type="checkbox"
/>
</div>
)
}
export default CurrTimeStampAttr

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

@ -0,0 +1,21 @@
import React from 'react'
interface LabelAttrInt {
label: string;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
const LabelAttr: React.FC<LabelAttrInt> = ({ label, handlePropertyChange }) => {
return (
<div className="flex flex-col">
<label htmlFor="" className="">Label</label>
<input
value={label}
onChange={(e) => handlePropertyChange('label', e.target.value)}
type="text"
className="outline-none border-1 rounded px-2 py-1 border-slate-200" />
</div>
)
}
export default LabelAttr

View File

@ -0,0 +1,100 @@
import { MdKeyboardArrowDown } from "react-icons/md";
import { DROPDOWNTOGGLE, MASKCHARACTER, MASKTYPE } from "./RightSideBar";
import { LuAsterisk } from "react-icons/lu";
import { GoDotFill } from "react-icons/go";
import { IoClose } from "react-icons/io5";
interface MaskTypePassAttrInt {
maskType: string;
maskCharacter: string;
dropDownToggle: DROPDOWNTOGGLE | null;
setDropDownToggle: React.Dispatch<React.SetStateAction<DROPDOWNTOGGLE | null>>;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
const MaskTypePassAttr: React.FC<MaskTypePassAttrInt> = ({ maskType, maskCharacter, dropDownToggle, setDropDownToggle, handlePropertyChange }) => {
return (
<>
{/* Mask Type */}
<div>
<label htmlFor="" className="">Mask Type</label>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.MASK_TYPE ? null : DROPDOWNTOGGLE.MASK_TYPE)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="capitalize cursor-pointer">{(maskType && maskType.replace("_", " ")) ? maskType : MASKTYPE.ALL}</p>
<MdKeyboardArrowDown />
</div>
{
dropDownToggle === DROPDOWNTOGGLE.MASK_TYPE &&
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
{(Object.keys(MASKTYPE) as (keyof typeof MASKTYPE)[]).map((val, indx) =>
(
<div
key={indx}
onClick={() => handlePropertyChange("maskType", MASKTYPE[val])}
className="capitalize px-2 py-1 hover:bg-slate-200 cursor-pointer"
>
{(MASKTYPE[val]).replace("_", " ")}
</div>
)
)}
</div>
}
</div>
{/* Mask Character */}
<div>
<label htmlFor="" className="">Mask Character</label>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.MASK_CHARACTER ? null : DROPDOWNTOGGLE.MASK_CHARACTER)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="capitalize cursor-pointer py-1">
{maskCharacter
?
maskCharacter === MASKCHARACTER.ASTERISK && <LuAsterisk /> ||
maskCharacter === MASKCHARACTER.DOT && <GoDotFill /> ||
maskCharacter === MASKCHARACTER.X && <IoClose />
:
<LuAsterisk />
}
</p>
<MdKeyboardArrowDown />
</div>
{
dropDownToggle === DROPDOWNTOGGLE.MASK_CHARACTER &&
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
{(Object.keys(MASKCHARACTER) as (keyof typeof MASKCHARACTER)[]).map((val, indx) =>
(
<div
key={indx}
onClick={() => handlePropertyChange("maskCharacter", MASKCHARACTER[val])}
className="capitalize p-2 hover:bg-slate-200 cursor-pointer"
>
{
MASKCHARACTER[val] === MASKCHARACTER.ASTERISK &&
<LuAsterisk />
}
{
MASKCHARACTER[val] === MASKCHARACTER.DOT &&
<GoDotFill />
}
{
MASKCHARACTER[val] === MASKCHARACTER.X &&
<IoClose />
}
</div>
)
)}
</div>
}
</div>
</>
)
}
export default MaskTypePassAttr

View File

@ -0,0 +1,68 @@
import React, { useRef } from 'react';
interface MaxMinLenAttrIntr {
maxLength: number;
minLength: number;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
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 (
<>
<div>
<label htmlFor="maxLength">Max Length</label>
<input
id="maxLength"
value={maxLength}
type="number"
min={0}
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
onChange={handleMaxChange}
/>
</div>
<div className="mt-2">
<label htmlFor="minLength">Min Length</label>
<input
id="minLength"
value={minLength}
type="number"
min={0}
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
onChange={handleMinChange}
/>
</div>
</>
);
};
export default MaxMinLenAttr;

View File

@ -0,0 +1,88 @@
import React, { useEffect, useState } from 'react'
import { IoCheckmark } from 'react-icons/io5';
import { MdKeyboardArrowDown } from 'react-icons/md';
import { useSelector } from 'react-redux';
import { RootState } from '../../../../store/store';
interface SelectedRecipientsIntr {
id: number;
firstName: string;
lastName: string;
color: string;
}
interface RecipientsAttrConIntr {
selectedRecipientId: number;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
const RecipientsAttrCon: React.FC<RecipientsAttrConIntr> = ({ selectedRecipientId, handlePropertyChange }) => {
const [selectedRecipient, setSelectedRecipient] = useState(null);
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
const [dropDownToggle, setDropDownToggle] = useState(false);
useEffect(() => {
if (selectedRecipients.length) {
const filter = selectedRecipients.filter((obj, indx) => obj.id == selectedRecipientId)
setSelectedRecipient(filter[0]);
}
}, [selectedRecipientId])
if (!selectedRecipient) {
return (
<>
</>
)
}
return (
<>
<div className='relative'
tabIndex={0}
onBlur={() => setDropDownToggle(false)}>
{/* Selected Recipient */}
<div
onClick={() => setDropDownToggle(!dropDownToggle)}
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm border-1 border-slate-200 cursor-pointer'>
<div className='flex items-center gap-1.5'>
<p className='w-3 h-3 rounded-sm' style={{ background: selectedRecipient.color }}></p>
{/* <FaUserTie className='w-3 h-3 text-2xl mt-[1px]' style={{ color: selectedRecipient.color }} /> */}
<p>{"firstName" in selectedRecipient && selectedRecipient.firstName} {"lastName" in selectedRecipient && selectedRecipient.lastName}</p>
</div>
<MdKeyboardArrowDown />
</div>
<div
className={`absolute w-full shadow-md rounded-md mt-1 px-2 py-1 z-20 bg-white ${dropDownToggle ? '' : 'hidden'}`}>
{selectedRecipients.length && selectedRecipients.map((recipient, indx) => {
return (<div
key={indx}
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-pointer hover:bg-slate-100'
onClick={() => {
handlePropertyChange('recipientId', recipient.id);
setTimeout(() => {
setDropDownToggle(false);
}, 0)
}}
>
<div className='flex items-center gap-1.5'>
<p className='w-3 h-3 rounded-sm' style={{ background: recipient.color }}></p>
{/* <FaUser className='w-3 h-3 text-2xl mt-[1px]' style={{ color: recipient.color }} /> */}
<p>{recipient.firstName} {recipient.lastName} </p>
</div>
{selectedRecipientId === recipient.id && <IoCheckmark className='text-[var(--primary)]' />}
</div>)
})}
</div>
</div>
</>
)
}
export default RecipientsAttrCon

View File

@ -0,0 +1,23 @@
import React from 'react'
interface RequiredAttrInt {
required: boolean;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
const RequiredAttr : React.FC<RequiredAttrInt> = ({required, handlePropertyChange}) => {
return (
<div className="flex justify-between items-center">
<label htmlFor="required" className="text-xs text-slate-600 transitions-all duration-150">Required</label>
<input
id="required"
checked={required ? required : false}
onChange={(e) => handlePropertyChange('required', e.target.checked)}
type="checkbox"
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
/>
</div>
)
}
export default RequiredAttr

View File

@ -0,0 +1,472 @@
import { DroppedItemsObj } from "../DocumentContainer";
import React, { useEffect, useState } from "react";
import { dateFormats, dateTimeFormats, timeFormats } from "../../../../utlity/dateTimeFormats";
import { DroppedItem } from "../../../../utlity/types";
import { IoIosArrowDown } from "react-icons/io";
import { TagsTypes } from "../../../../utlity/types";
import LabelAttr from "./LabelAttr";
import RequiredAttr from "./RequiredAttr";
import CurrTimeStampAttr from "./currTimeStampAttr";
import CommonAttrDropDown from "./CommonAttrDropDown";
import MaskTypePassAttr from "./MaskTypePassAttr";
import TemporalDateAttr from "./TemporalDateAttr";
import SelectTagAttr from "./SelectTagAttr";
import MaxMinLenAttr from "./MaxMinLenAttr";
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 {
selectedTag: DroppedItem
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>
selTagKey: string,
selTagIndx: number,
DirectionalControlsProps: {
onMove: (direction: 'up' | 'down' | 'left' | 'right', stepSize?: number) => void;
disabled?: boolean;
stepSize?: number;
className?: string;
}
}
export enum TEXT {
PARAGRAPH = "paragraph",
SINGLE_LINE = "single_line",
PASSWORD = "password",
}
export enum TEMPORAL {
DATE = "date",
TIME = "time",
DATE_TIME = "date_time",
}
export enum NUMBER {
INTEGER = "integer",
DECIMAL = "decimal",
CURRENCY = "currency"
}
export enum SELECT {
SINGLE_SELECT = "single select",
MULTI_SELECT = "multi select"
}
export enum MASKTYPE {
ALL = "all",
LAST_FOUR = "last_four",
CREDIT_CARD = "credit_Card",
NATIONAL_INSURANCE = "national_insurance",
SOCIAL_SECURITY = "social_security",
SOCIAL_INSURANCE = "social_insurance"
}
export enum MASKCHARACTER {
ASTERISK = "ASTERISK",
DOT = "DOT",
X = "CROSS",
}
export const enumMap = {
text: TEXT,
temporal: TEMPORAL,
number: NUMBER,
image: String,
select: SELECT
}
export enum DROPDOWNTOGGLE {
TYPE,
MASK_TYPE,
MASK_CHARACTER,
DDMMYY_FORMAT
}
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];
console.log("selectedTag", selectedTag, selectedType)
const [styles, setStyles] = useState(defaultStyles);
const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null);
const [decimalPlace, setDecimalPlace] = useState<number>(2)
const [styleOpen, setStyleOpen] = useState(false);
const handlePropertyChange = (changeTo: string, changeWith: string | boolean | number, is24Hrs = null) => {
console.log("Handle Property Change Called", "changeTo", changeTo, "changeWith", changeWith)
if (changeTo === '' || changeWith === '') return
if (changeTo === "subType" && (changeWith === TEMPORAL.DATE || changeWith === TEMPORAL.DATE_TIME || changeWith === TEMPORAL.TIME)) {
// let change: string = changeWith;
// if (change === "DATE TIME") {
// change = "DATE_TIME"
// }
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith || "", format: "", update: true } : item
)
: [],
}// Provide a fallback in case it's undefined or not an array
));
} else if (changeTo === "subType" && typeof changeWith === "string" && changeWith.toLowerCase() === "password") {
console.log("Line 161")
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith, maskType: "All", maskCharacter: "ASTERISK", update: true } : item
)
: [],
}
));
}
else if (changeTo === "subType" && (changeWith === NUMBER.CURRENCY || changeWith === NUMBER.INTEGER || changeWith === NUMBER.DECIMAL)) {
let decPlace = changeWith !== NUMBER.DECIMAL ? 0 : decimalPlace;
const isCurrency = changeWith === NUMBER.CURRENCY;
// const currencyType = null
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith, decimalPlace: decPlace, isCurrency, update: true } : item
)
: [],
}// Provide a fallback in case it's undefined or not an array
));
} else if (changeTo === "format" && selectedTag.subType === TEMPORAL.DATE_TIME) {
let val = timeFormats[0].format;
if (!is24Hrs) {
val = timeFormats[1].format
}
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith + " " + val, update: true } : item
)
: [],
}// Provide a fallback in case it's undefined or not an array
));
} else {
setDroppedItems((prev) => {
console.log('Previous state:', prev);
const updatedItems = Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) =>
i === selTagIndx ? { ...item, [changeTo]: changeWith, update: true } : item
)
: [];
console.log('Updated items:', updatedItems);
return {
...prev,
[selTagKey]: updatedItems,
};
});
}
setDropDownToggle(null)
}
useEffect(() => {
if (selectedTag.subType === TEMPORAL.DATE || selectedTag.subType === TEMPORAL.DATE_TIME || selectedTag.subType === TEMPORAL.TIME) {
if (selectedTag.subType === TEMPORAL.DATE) {
handlePropertyChange("format", dateFormats[0].format)
}
else if (selectedTag.subType === TEMPORAL.DATE_TIME) {
handlePropertyChange("format", dateTimeFormats[0].format)
}
else if (selectedTag.subType === TEMPORAL.TIME) {
handlePropertyChange("format", timeFormats[0].format)
}
}
}, [selectedTag.subType])
useEffect(() => {
if (selectedTag.label === "temporal") {
handlePropertyChange("subType", TEMPORAL.DATE);
handlePropertyChange("format", "DD/MM/YY");
}
}, [])
const handleDecimalPlace = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = Number(e.target.value)
setDecimalPlace(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(() => {
if (!selectedTag) return;
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;
if (
selectedTag.subType !== TEXT.PASSWORD &&
selectedTag.subType !== TEXT.PARAGRAPH &&
tagHeight > fontSize * 2
) {
handlePropertyChange("subType", TEXT.PARAGRAPH);
} else if (
selectedTag.subType !== TEXT.PASSWORD &&
selectedTag.subType !== TEXT.SINGLE_LINE &&
tagHeight <= fontSize * 2
) {
handlePropertyChange("subType", TEXT.SINGLE_LINE);
}
}, [selectedTag]);
const handleStyleChange = (key, value) => {
const updatedStyles = {
...(selectedTag.styles ?? defaultStyles),
[key]: value
};
setStyles(updatedStyles);
handlePropertyChange("styles", updatedStyles);
};
return (
<>
{/* Right Side Bar Start */}
<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>
<hr className="text-slate-200" />
<div className="p-3 text-sm text-slate-600 flex flex-col gap-2">
{
<RecipientsAttrCon selectedRecipientId={selectedTag.recipientId} handlePropertyChange={handlePropertyChange} />
}
<DirectionalControls {...DirectionalControlsProps} selectedType={selectedTag.dimensions}/>
{selectedTag.label &&
<LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />}
{(selectedTag.type !== TagsTypes.SIGNATURE && selectedTag.type !== TagsTypes.INITIALS) &&
<>
<RequiredAttr required={selectedTag.required ? selectedTag.required : false} handlePropertyChange={handlePropertyChange} />
</>
}
{selectedTag.type.toLowerCase() === TagsTypes.TEMPORAL &&
<CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />}
{
selectedType && Object.keys(selectedType).length > 0 &&
<CommonAttrDropDown
selectedType={selectedType}
subType={selectedTag.subType}
dropDownToggle={dropDownToggle}
setDropDownToggle={setDropDownToggle}
handlePropertyChange={handlePropertyChange} />
}
{/* Mask Type */}
{
selectedTag?.subType === TEXT.PASSWORD &&
<MaskTypePassAttr
maskType={selectedTag.maskType ? selectedTag.maskType : MASKTYPE.ALL}
maskCharacter={selectedTag.maskCharacter ? selectedTag.maskCharacter : MASKCHARACTER.ASTERISK}
dropDownToggle={dropDownToggle}
setDropDownToggle={setDropDownToggle}
handlePropertyChange={handlePropertyChange} />
}
{/* TEMPORAL DATE FORMAT - START */}
{
(selectedTag?.subType === TEMPORAL.DATE ||
selectedTag?.subType === TEMPORAL.TIME ||
selectedTag?.subType === TEMPORAL.DATE_TIME) &&
<TemporalDateAttr
subType={selectedTag.subType}
format={selectedTag.format}
dropDownToggle={dropDownToggle}
setDropDownToggle={setDropDownToggle}
handlePropertyChange={handlePropertyChange} />
}
{/* TEMPORAL DATE FORMAT - END */}
{/* NUMBER - DECIMAL PLACES */}
{
selectedTag?.subType === NUMBER.DECIMAL &&
<div>
<label htmlFor="" className="">Decimal Places</label>
<input
type="number"
onChange={(e) => handleDecimalPlace(e)}
value={decimalPlace}
className="bg-slate-100 w-full outline-none p-2 rounded" />
</div>
}
{
(selectedTag?.subType === SELECT.SINGLE_SELECT || selectedTag?.subType === SELECT.MULTI_SELECT) &&
<SelectTagAttr
options={selectedTag.options}
selTagKey={selTagKey}
selTagIndx={selTagIndx}
setDroppedItems={setDroppedItems}
handlePropertyChange={handlePropertyChange} />
}
{
(selectedTag.type.toLowerCase() === TagsTypes.TEXT || selectedTag.type.toLowerCase() === TagsTypes.COMPANY) &&
<MaxMinLenAttr maxLength={selectedTag.maxLength} minLength={selectedTag.minLength} handlePropertyChange={handlePropertyChange} />
}
{
<div className="">
<div className="flex items-center justify-between mr-2 cursor-pointer"
onClick={() => setStyleOpen(!styleOpen)}
>
<label
className="block mb-2">Styles</label>
<IoIosArrowDown className={`${styleOpen ? "rotate-[-90deg]" : ""} transition-all`} />
</div>
{
styleOpen && <>
{/* Font */}
<div className="mb-2">
<label className="mr-2">Font</label>
<input
type="text"
placeholder="e.g. Arial"
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
value={styles.font}
onChange={(e) => handleStyleChange("font", e.target.value)}
/>
</div>
{/* Text Size */}
<div className="mb-2">
<label className="mr-2">Text Size</label>
<input
// value={}
type="number"
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>
{/* Color */}
<div className="mb-2">
<label className="mr-2">Color</label>
<input
type="color"
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>
{/* Toggles for Bold, Italic, Underline */}
<div className="flex gap-4 items-center mt-2">
<label className="flex gap-1 items-center">
<input type="checkbox" checked={styles.bold}
onChange={(e) => handleStyleChange("bold", e.target.checked)} />
Bold
</label>
<label className="flex gap-1 items-center">
<input type="checkbox" checked={styles.italic} onChange={(e) => handleStyleChange("italic", e.target.checked)} />
Italic
</label>
<label className="flex gap-1 items-center">
<input type="checkbox" checked={styles.underline}
onChange={(e) =>
handleStyleChange("underline", e.target.checked)
} />
Underline
</label>
</div>
</>
}
</div>
}
</div>
</div>
{/* Right Side Bar End */}
</>
)
}
export default RightSideBar

View File

@ -0,0 +1,105 @@
import { useState } from "react";
import { DROPDOWNTOGGLE } from "./RightSideBar";
import { DroppedItemsObj } from "../DocumentContainer";
import { IoMdClose } from "react-icons/io";
interface SelectTagAttrInt {
options: string[] | undefined;
selTagKey: string;
selTagIndx: number;
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
}
const SelectTagAttr: React.FC<SelectTagAttrInt> = ({ options, selTagKey, selTagIndx, setDroppedItems, handlePropertyChange }) => {
const [selectedOption, setSelectedOption] = useState<string>()
const handleAddSelectOption = () => {
if (selectedOption && selectedOption.length) {
setDroppedItems((prev) => {
console.log("Previous droppedItems:", prev);
const newState = {
...prev,
[selTagKey]: Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) => {
if (i === selTagIndx) {
return {
...item,
options: item.options ? [...item.options, selectedOption] : [selectedOption],
update: true,
};
}
return item;
})
: [],
};
console.log("New droppedItems state:", newState);
return newState;
});
}
};
const handleSelectOptionDel = (indx: number) => {
setDroppedItems((prev) => (
{
...prev,
[selTagKey]: Array.isArray(prev[selTagKey])
? prev[selTagKey].map((item, i) => {
if (i === selTagIndx) {
return { ...item, options: item.options ? item.options.filter((_, i) => i !== indx) : [] };
}
return item;
})
: [],
}
));
}
return (
<>
<div>
<label htmlFor="" className="">Add Options</label>
<input
type="text"
onChange={(e) => setSelectedOption(e.target.value)}
value={selectedOption}
onKeyUp={(e) => {
if (e.code === "Enter") {
// if (selectedOption) {
// setSelectedOptions((prev) => [...prev, selectedOption]);
// setSelectedOption("");
// }
handleAddSelectOption()
}
}}
className="bg-slate-100 w-full outline-none p-2 rounded" />
<div className="h-[150px] mt-1 overflow-hidden overflow-y-auto">
{
options && options.map((val, indx) => (
<div
key={indx}
className="flex justify-between items-center px-2 py-1 my-1 rounded bg-slate-100">
<p>{val}</p>
<IoMdClose
onClick={() =>
// setSelectedOptions(prev =>
// prev.filter((_, i) => i !== indx)
// )
handleSelectOptionDel(indx)
}
className="cursor-pointer text-slate-500 hover:text-slate-600" />
</div>
))
}
</div>
</div>
</>
)
}
export default SelectTagAttr

View File

@ -0,0 +1,108 @@
import { useEffect, useState } from "react";
import { dateFormats, dateTimeFormats, monthFormats, timeFormats, weekFormats, yearFormats } from "../../../../utlity/dateTimeFormats";
import { DROPDOWNTOGGLE, TEMPORAL } from "./RightSideBar";
import { MdKeyboardArrowDown } from "react-icons/md";
import dayjs from "dayjs";
const dateTimeFormatMap = {
"date": dateFormats,
"time": timeFormats,
"date_time": dateTimeFormats,
"year": yearFormats,
"month": monthFormats,
"week": weekFormats
}
interface TemporalDateAttrInt {
subType: string;
format: string | undefined;
dropDownToggle: DROPDOWNTOGGLE | null;
setDropDownToggle: React.Dispatch<React.SetStateAction<DROPDOWNTOGGLE | null>>;
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number, is24Hrs: boolean) => void;
}
const TemporalDateAttr: React.FC<TemporalDateAttrInt> = ({ subType, format, dropDownToggle, setDropDownToggle, handlePropertyChange }) => {
const [is24Hrs, setIs24Hrs] = useState<boolean>(true);
const handleIs24HrsClick = (checked: boolean) => {
if (subType === TEMPORAL.TIME) {
if (!checked) {
handlePropertyChange("format", timeFormats[1].format, is24Hrs)
} else {
handlePropertyChange("format", timeFormats[0].format, is24Hrs)
}
}
setIs24Hrs(checked)
}
useEffect(() => {
if (format === timeFormats[1].format && is24Hrs) {
setIs24Hrs(false);
} else if (format === timeFormats[0].format && !is24Hrs) {
setIs24Hrs(true);
}
}, [format])
return (
<div>
<div className="flex justify-between">
<label htmlFor="" className="">Formats</label>
{(subType === TEMPORAL.DATE_TIME || subType === TEMPORAL.TIME) &&
<div className="flex justify-center items-center gap-1.5">
<input
checked={is24Hrs ? is24Hrs : false}
onChange={(e) => handleIs24HrsClick(e.target.checked)}
id="data_time_format"
type="checkbox" />
<label className={`text-xs ${is24Hrs ? "text-slate-600" : "text-slate-400"} transitions-all duration-150`} htmlFor="data_time_format">
24 Hrs
</label>
</div>
}
</div>
<div
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.DDMMYY_FORMAT ? null : DROPDOWNTOGGLE.DDMMYY_FORMAT)}
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
<p className="cursor-pointer py-1">
{format ? format : dateTimeFormatMap['date'][0].format}
{/* {(subType !== TEMPORAL.DATE && subType !== TEMPORAL.TIME) ? (is24Hrs ? " " + timeFormats[0].format : " " + timeFormats[1].format) : ""} */}
</p>
<MdKeyboardArrowDown />
</div>
{
dropDownToggle === DROPDOWNTOGGLE.DDMMYY_FORMAT &&
<div className="shadow bg-slate-100 mt-1 border-[1px] max-h-[200px] overflow-y-auto border-slate-100 rounded">
{dateTimeFormatMap[subType].map((valObj, indx) => {
// let val = showFormatEg ? valObj["example"] : valObj["format"]
let val = valObj["format"]
if (subType === TEMPORAL.DATE_TIME) {
if (is24Hrs) {
val = val + " " + timeFormats[0].format;
} else {
val = val + " " + timeFormats[1].format;
}
}
return (
<div
key={indx}
onClick={() => handlePropertyChange("format", valObj["format"])}
className="px-2 py-1 hover:bg-slate-200 cursor-pointer"
>
{val}
</div>
)
}
)}
</div>
}
{format && <div className="pt-4">
<p className="text-center">{dayjs().format(format)}</p>
</div>}
</div>
)
}
export default TemporalDateAttr

View File

@ -0,0 +1,54 @@
interface SubSection {
id: string;
label: string;
}
interface AsideSectionProps {
activeSubSection: string;
onSubSectionClick: (subSectionId: string) => void;
subSections: SubSection[];
mainTabTitle: string;
}
const AsideSection = ({
activeSubSection,
onSubSectionClick,
subSections,
mainTabTitle
}: AsideSectionProps) => {
// If no subsections, don't render the aside navigation
if (subSections.length === 0) {
return null;
}
return (
<div className="w-[16vw] p-4">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-800">{mainTabTitle}</h3>
<p className="text-sm text-gray-500 mt-1">Navigate through sections</p>
</div>
<nav className="space-y-2 max-h-96 overflow-y-auto">
{subSections.map((subSection) => (
<a
key={subSection.id}
href={`#${subSection.id}`}
onClick={(e) => {
e.preventDefault(); // Prevent default anchor behavior
onSubSectionClick(subSection.id);
}}
className={`block w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 hover:bg-gray-50 cursor-pointer no-underline ${
activeSubSection === subSection.id
? "bg-blue-50 text-blue-600 border-l-4 border-blue-600 shadow-sm"
: "text-gray-600 hover:text-gray-800 hover:bg-gray-50"
}`}
>
<span className="block">{subSection.label}</span>
</a>
))}
</nav>
</div>
);
};
export default AsideSection;

View File

@ -0,0 +1,342 @@
import React, { useRef, useState } from 'react';
import { FaSpinner } from 'react-icons/fa';
export type ActionType = 'create' | 'invite';
type CreateData = { accountName: string };
type InviteData = { i_accountName: number; email: string; role: string };
export type FormDataType = CreateData | InviteData;
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
interface Props {
onSubmit: (data: FormDataType, action: ActionType) => void;
createAndInviteLoader: boolean;
accountOptions: AccountOption[];
accountOptionsLoading: boolean;
}
const CreateAndInvite = ({
onSubmit,
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 }>({
accountName: '',
});
const [accountSearchTerm, setAccountSearchTerm] = useState('');
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(null);
const [inviteFormData, setInviteFormData] = useState<{
i_accountName: number;
email: string;
role: string;
}>({
i_accountName: 0,
email: '',
role: '',
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const roles = ['ADMIN', 'EDITOR', 'VIEWER'];
// Handle role dropdown
const handleRoleDropdownFocus = () => setIsRoleDropdownOpen(true);
const handleRoleDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement;
if (roleDropdownRef.current && !roleDropdownRef.current.contains(activeElement)) {
setIsRoleDropdownOpen(false);
}
}, 100);
};
const handleRoleSelect = (role: string) => {
setInviteFormData((prev) => ({ ...prev, role }));
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) => {
setCreateFormData({ accountName: value });
// Clear error when user starts typing
if (errors.accountName) {
setErrors(prev => ({ ...prev, accountName: '' }));
}
};
const handleCreateSubmit = () => {
if (!createFormData.accountName.trim()) {
setErrors({ accountName: 'Account name is required' });
return;
}
setErrors({});
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 { i_accountName, email, role } = inviteFormData;
const errs: { [key: string]: string } = {};
if (!i_accountName) errs.i_accountName = 'Account name 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;
}
setErrors({});
onSubmit(inviteFormData, 'invite');
};
// Filter account options based on search term
const filteredAccountOptions = accountOptions.filter((option) =>
option.accountName?.toLowerCase().includes(accountSearchTerm.toLowerCase())
);
return (
<div className="ms-2 space-y-3">
{/* Create Section */}
<section className="border-gray-200 border-b">
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
<div className="flex flex-col mb-6">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Account Name
</label>
<div className="flex w-[40vw]">
<input
type="text"
placeholder="Enter account name"
value={createFormData.accountName}
onChange={(e) => handleCreateChange(e.target.value)}
className="flex-1 px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)]"
/>
<button
type="button"
onClick={handleCreateSubmit}
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}
>
{createAndInviteLoader ? (
<>
<FaSpinner className="animate-spin me-1" />
CREATING...
</>
) : (
'CREATE'
)}
</button>
</div>
{errors.accountName && (
<span className="text-sm text-red-500 mt-1">{errors.accountName}</span>
)}
</div>
</section>
{/* Invite Section */}
<section className="border-gray-300">
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
<div className="grid grid-cols-2 gap-4 mb-6">
{/* Account Name Dropdown */}
<div className="flex flex-col relative">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Account Name
</label>
<div
className="relative w-full"
onBlur={handleAccountDropdownBlur}
ref={accountDropdownRef}
tabIndex={-1}
>
<input
type="text"
placeholder={accountOptionsLoading ? "Loading accounts..." : "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"
/>
{/* 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 && (
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
)}
</div>
{/* Email */}
<div className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Email
</label>
<input
type="email"
placeholder="Enter email address"
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)]"
/>
{errors.email && (
<span className="text-sm text-red-500 mt-1">{errors.email}</span>
)}
</div>
{/* Role Dropdown */}
<div className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Role
</label>
<div
className="relative w-full"
onBlur={handleRoleDropdownBlur}
ref={roleDropdownRef}
tabIndex={-1}
>
<input
type="text"
placeholder="Select role"
value={inviteFormData.role}
readOnly
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 cursor-pointer"
/>
<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 ${
isRoleDropdownOpen
? 'opacity-100 scale-100'
: 'opacity-0 scale-95 pointer-events-none'
}`}
>
{roles.map((role) => (
<div
key={role}
onMouseDown={() => handleRoleSelect(role)}
className="px-4 py-1 text-[13px] hover:bg-gray-100 cursor-pointer"
>
{role}
</div>
))}
</div>
</div>
{errors.role && (
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
)}
</div>
</div>
<div className="flex justify-end">
<button
type="button"
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 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={createAndInviteLoader}
>
{createAndInviteLoader ? (
<>
<FaSpinner className="animate-spin me-1" />
INVITING...
</>
) : (
'INVITE'
)}
</button>
</div>
</section>
</div>
);
};
export default CreateAndInvite;

View File

@ -0,0 +1,38 @@
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
interface UserProfileProps {
userData: UserData;
}
const Language : React.FC<UserProfileProps> = ({ userData }) => {
return (
<div className="max-w-[100vw] bg-white shadow-sm p-5 max-h-screen overflow-hidden">
<div className="px-3 ms-3 w-1/2 border border-gray-200 rounded-md space-y-3 ">
{/* Language */}
<div className="flex items-center justify-between py-3 border-b border-gray-100">
<span className="text-gray-700 font-medium">Language</span>
<span className="text-gray-500">{userData.language}</span>
</div>
</div>
</div>
)
}
export default Language

View File

@ -1,67 +1,434 @@
import { useState } from "react";
import { useEffect, useState, useRef } from "react";
import SettingNav from "./SettingNav";
import { useParams } from "react-router-dom";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import api from "../../config/api";
import UserProfile from './UserProfile';
import Language from "./Language";
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
import AccountNav from "../accounts/AccountNav";
// Define the shape of state object
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
name: string;
email: string;
language: string;
account: Account;
}
interface SubSection {
id: string;
label: string;
component?: React.ReactNode;
}
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
const Setting = () => {
const { page1 } = useParams<{ page1: string }>();
const renderContent = () => {
console.log("page1", page1)
const location = useLocation();
console.log("location", location)
const navigate = useNavigate();
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
// Refs for subsections within each main tab
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState("userProfile");
const [activeSubSection, setActiveSubSection] = useState<string>("");
const [user, setUser] = useState<UserData>({
id: '',
name: '',
email: '',
language: '',
account: {
id: 0,
isDefault: false,
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
const getSubSections = (tabId: string): SubSection[] => {
switch (tabId) {
case "userProfile":
return [
{ id: "profile-info", label: "Profile Information" },
{ id: "signature", label: "Signature" },
];
default:
return [];
}
};
// Function to scroll to specific subsection
const scrollToSubSection = (subSectionId: string) => {
const targetRef = subSectionRefs.current[subSectionId];
if (targetRef && scrollContainerRef?.current) {
const container = scrollContainerRef.current;
const target = targetRef;
// Calculate the position relative to the scroll container
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
// Calculate scroll position (accounting for some padding)
const scrollTop = container.scrollTop + targetRect.top - containerRect.top - 20;
// Smooth scroll to the target
container.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
}
};
// Handle subsection click with hash update
const handleSubSectionClick = (subSectionId: string) => {
setActiveSubSection(subSectionId);
// Update URL hash without causing page reload
navigate(`${location.pathname}#${subSectionId}`, { replace: true });
scrollToSubSection(subSectionId);
};
// Handle main tab click (independent of aside navigation)
const handleMainTabClick = (tabId: string) => {
setActiveTab(tabId);
// Reset subsection when switching main tabs
const subSections = getSubSections(tabId);
if (subSections.length > 0) {
const firstSubSectionId = subSections[0].id;
setActiveSubSection(firstSubSectionId);
// Update URL hash
navigate(`${location.pathname}#${firstSubSectionId}`, { replace: true });
} else {
setActiveSubSection("");
// Clear hash
navigate(location.pathname, { replace: true });
}
};
// Handle browser back/forward and direct URL navigation
useEffect(() => {
const hash = location.hash.replace('#', '');
if (hash) {
// Check if the hash corresponds to a valid subsection
const allSubSections = getSubSections(activeTab);
const validSubSection = allSubSections.find(section => section.id === hash);
if (validSubSection) {
setActiveSubSection(hash);
// Delay scroll to ensure component is rendered
setTimeout(() => {
scrollToSubSection(hash);
}, 100);
}
}
}, [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
const renderSubSectionContent = (subSectionId: string) => {
switch (activeTab) {
case "userProfile":
return <div>User Profile content goes here...</div>;
case "language":
return <div>Language settings content goes here...</div>;
case "contactAll":
return <div>All Contacts content goes here...</div>;
case "contactAdd":
return <div>Add Contact form goes here...</div>;
case "contactRemove":
return <div>Remove Contact UI goes here...</div>;
case "contactShare":
return <div>Share Contact options go here...</div>;
switch (subSectionId) {
case "profile-info":
return <UserProfile userData={user} />;
case "signature":
return (
<Signature />
);
default:
return;
}
default:
return <div>Coming Soon...</div>;
return (
<div className="bg-white p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold mb-4">Coming Soon</h3>
<p>This section is under development.</p>
</div>
);
}
};
const renderContent = () => {
const currentSubSections = getSubSections(activeTab);
if (currentSubSections.length === 0) {
// If no subsections, render the main content directly
switch (activeTab) {
case "userProfile":
return <UserProfile userData={user} />;
case "language":
return <Language userData={user} />;
case "create and invite":
return (
<CreateAndInvite
onSubmit={handleSubmit}
createAndInviteLoader={createAndInviteLoader}
accountOptions={accountOptions}
accountOptionsLoading={accountOptionsLoading}
/>
);
default:
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
}
}
return (
<div className="space-y-8">
{currentSubSections.map((subSection) => (
<div
key={subSection.id}
ref={(el) => (subSectionRefs.current[subSection.id] = el)}
id={subSection.id} // This creates the anchor for hash navigation
className="scroll-mt-6"
>
{renderSubSectionContent(subSection.id)}
</div>
))}
</div>
);
};
const tabs = [
{ id: "userProfile", label: "User Profile", type: "settings" },
{ id: "create and invite", label: "Create and invite", type: "settings" },
{ id: "language", label: "Language", type: "settings" },
{ id: "contactAll", label: "All Contacts", type: "contact" },
{ id: "contactAdd", label: "Add Contact", type: "contact" },
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
{ id: "contactShare", label: "Share Contact", type: "contact" },
];
const [activeTab, setActiveTab] = useState("userProfile");
// Listen for sessionStorage changes
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'activeAccountId') {
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
setActiveAccountId(newActiveAccountId);
}
};
// Listen for storage events (works for changes from other tabs)
window.addEventListener('storage', handleStorageChange);
// Custom event listener for same-tab changes
const handleCustomStorageChange = (e: CustomEvent) => {
if (e.detail.key === 'activeAccountId') {
setActiveAccountId(e.detail.newValue);
}
};
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
};
}, []);
// Re-fetch dashboard data when activeAccountId changes
useEffect(() => {
const rawUser = JSON.parse(sessionStorage.getItem('userDetails') || 'null');
if (rawUser) {
const userData: UserData = {
id: String(rawUser.id || ''),
name: rawUser.name || '',
email: rawUser.email || '',
language: rawUser.language || '',
account: {
id: rawUser.account?.id || 0,
isDefault: rawUser.account?.isDefault || false,
name: rawUser.account?.name || '',
},
};
setUser(userData);
}
}, [activeAccountId]);
useEffect(() => {
// Fetch account list when component mounts
getAccountList()
}, [])
// Set initial subsection when component mounts or activeTab changes
useEffect(() => {
const subSections = getSubSections(activeTab);
console.log("subSections", subSections)
const hash = location.hash.replace('#', '');
if (subSections.length > 0) {
// If there's a hash and it's valid for current tab, use it
if (hash && subSections.find(s => s.id === hash)) {
setActiveSubSection(hash);
}
// If no valid hash or no current activeSubSection, use first subsection
else if (!activeSubSection || !subSections.find(s => s.id === activeSubSection)) {
const firstSubSectionId = subSections[0].id;
setActiveSubSection(firstSubSectionId);
// Update URL hash if no hash exists
if (!hash) {
navigate(`${location.pathname}#${firstSubSectionId}`, { replace: true });
}
}
}
}, [activeTab]);
// Auto-scroll to subsection when activeSubSection changes
useEffect(() => {
if (activeSubSection) {
setTimeout(() => {
scrollToSubSection(activeSubSection);
}, 100);
}
}, [activeSubSection]);
const handleSubmit = async (data: FormDataType, action: ActionType) => {
setCreateAndInviteLoader(true)
try {
let res;
let toastMesaage;
if (action === 'create') {
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') {
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) {
console.error('Submission error:', err);
} finally {
setCreateAndInviteLoader(false)
}
};
return (
<>
<SettingNav />
<div className="min-h-screen bg-[#f9fafa]">
{/* Top Nav Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-4">
{/* <SettingNav /> */}
{/* <AccountNav/> */}
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
{/* Main Navigation Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5">
<div className="flex gap-6">
{tabs.map((tab) => (
tab.type === page1 && <button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
${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-gray-600 hover:text-blue-500"
}`}
>
{tab.label}
</button>
))}
{tabs.map((tab) =>
tab.type === page1 && (
<button
key={tab.id}
onClick={() => handleMainTabClick(tab.id)}
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
${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-gray-600 hover:text-blue-500"
}`}
>
{tab.label}
</button>
)
)}
</div>
</div>
{/* Main Content Area */}
<div className="px-8 py-6">{renderContent()}</div>
{/* Content with Aside Navigation */}
<div className="flex mt-3 px-8 flex-1 min-h-0">
{/* Fixed Aside Navigation - Shows subsections of active tab */}
{activeTab === 'userProfile' &&
<section className="flex-shrink-0 mr-6 w-50 relative">
<div className="sticky top-6 z-20">
<AsideSection
activeSubSection={activeSubSection}
onSubSectionClick={handleSubSectionClick}
subSections={getSubSections(activeTab)}
mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""}
/>
</div>
</section>
}
{/* Scrollable Main Content */}
<div
ref={scrollContainerRef}
className="flex-1 min-w-0 overflow-y-auto no-scrollbar"
>
{renderContent()}
</div>
</div>
</div>
</>
)
}

View File

@ -34,11 +34,12 @@ const SettingNav = () => {
</button>
<Link
to="/settings"
to="/s/settings"
aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
>
<FaCog className="text-sm" />
</Link>
</div>

View File

@ -0,0 +1,540 @@
import React, { useEffect, useState } from 'react';
import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
import { MdCloudUpload } from 'react-icons/md';
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 {
setShowAddSignature?: (value: boolean) => void;
getAllSignatures: () => Promise<void>;
editingSignature?: Signature | null;
}
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
interface SelectStyle {
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 [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: 'TYPE', label: 'TYPE', icon: <GrCheckboxSelected className="w-4 h-4" /> },
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
];
// Initialize form with editing signature data
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
useEffect(() => {
return () => {
if (preview1) URL.revokeObjectURL(preview1);
if (preview2) URL.revokeObjectURL(preview2);
};
}, [preview1, preview2]);
const handleImageChange = async (
e: React.ChangeEvent<HTMLInputElement>,
setFile: React.Dispatch<React.SetStateAction<File | null>>,
setPreview: React.Dispatch<React.SetStateAction<string | null>>,
type: "fullSignature" | "initials"
) => {
const file = e.target.files?.[0];
if (file) {
setFile(file);
const previewUrl = URL.createObjectURL(file);
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 = () => {
switch (activeTab) {
case 'TYPE':
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>
);
case 'SELECT':
return (
<div className="h-full flex flex-col">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Select Signature</h2>
</div>
</div>
);
default:
return null;
}
};
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 'TYPE' | 'DRAW' | 'UPLOAD')}
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
? 'bg-blue-50 text-blue-700 border border-blue-200'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`}
>
<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={() => setShowAddSignature?.(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...'}
</>
) : (
isEditMode ? 'UPDATE SIGNATURE' : 'CREATE SIGNATURE'
)}
</button>
</div>
</div>
</div>
</div>
);
};
export default AddSignatureModal;

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

@ -0,0 +1,266 @@
import React, { useEffect, useState } from 'react';
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
import AddSignatureModal from './AddSignatureModal';
import api from '../../../config/api';
import { Bounce, toast } from "react-toastify";
import { FaSpinner } from 'react-icons/fa';
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;
signatureMimeType:string;
initialsMimeType:string
}
const SignatureManager: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false)
const [signatures, setSignatures] = useState<Signature[]>([]);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
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) => {
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);
};
const handleDelete = async (id: string) => {
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);
setLoading(false)
}
};
const toggleDropdown = (id: string) => {
setActiveDropdown(activeDropdown === id ? null : id);
};
const handleAddNew = () => {
setEditingSignature(null);
setShowAddSignature(true);
};
const handleModalClose = () => {
setShowAddSignature(false);
setEditingSignature(null);
};
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="max-w-[84vw] mx-auto">
<>
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<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>
</div>
<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="" />
ADD
</button>
</div>
{/* Table Card */}
<div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100">
{/* Table Header */}
<div className="bg-gray-50 border-b border-gray-200">
<div className="grid grid-cols-12 gap-4 px-6 py-4">
<div className="col-span-5">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Signature</h3>
</div>
<div className="col-span-5">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Initial</h3>
</div>
<div className="col-span-2">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Actions</h3>
</div>
</div>
</div>
{/* Table Body */}
<div className="divide-y divide-gray-100">
{signatures.map((signature, index) => (
<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 */}
<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">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
Signed by:
</div>
{/* Signature Image */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<img
src={`data:${signature.signatureMimeType};base64,${signature.signatureBlob}`}
className="h-[50px] w-[250px] object-contain"
alt="Signature"
/>
</div>
{/* Signature ID */}
<div className="text-xs text-gray-500 truncate">
{signature.signatureHash}
</div>
</div>
</div>
{/* Initial Column */}
<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">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
Initials
</div>
{/* Signature Image */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<img
src={`data:${signature.initialsMimeType};base64,${signature.initialsBlob}`}
className="h-[50px] object-contain"
alt="DS Signature"
/>
</div>
{/* Initial ID */}
<div className="text-xs text-gray-500 truncate">
{signature.initialsHash}
</div>
</div>
</div>
{/* Actions Column */}
<div className="col-span-2 flex justify-between items-start pt-9 ps-8 ">
<div className="relative ">
<button
onClick={() => toggleDropdown(signature.id)}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-1.5 py-1.5 text-[13px] rounded-lg font-semibold flex items-center transition-all duration-200 border border-gray-300 "
>
ACTIONS
<FiMoreVertical />
</button>
{/* Dropdown Menu */}
{activeDropdown === signature.id && (
<div className="absolute right-0 mt-0.5 w-28 bg-white rounded-lg shadow-xl border border-gray-200 z-10 py-1">
<button
onClick={() => handleEdit(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-blue-50 hover:text-blue-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiEdit3 />
Edit
</button>
<button
onClick={() => handleDelete(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-red-50 hover:text-red-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiTrash2 />
Delete
</button>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Empty State (if no signatures) */}
{signatures.length === 0 && (
<div className="bg-white rounded-xl shadow-lg p-12 text-center mt-8">
<div className="text-gray-400 mb-4">
<FiEdit3 className="w-16 h-16 mx-auto" />
</div>
<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>
</div>
)}
</>
</div>
{/* Click outside to close dropdown */}
{activeDropdown && (
<div
className="fixed inset-0 z-0"
onClick={() => setActiveDropdown(null)}
/>
)}
{showAddSignature && (
<AddSignatureModal
setShowAddSignature={handleModalClose}
getAllSignatures={getAllSignatures}
editingSignature={editingSignature}
/>
)}
</div>
}</>
);
};
export default SignatureManager;

View File

@ -0,0 +1,62 @@
import { FiX, FiEdit2 } from 'react-icons/fi';
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: number;
name:string
email: string;
language: string;
account: Account;
}
interface UserProfileProps {
userData: UserData;
}
const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
return (
<div className="bg-white rounded-md shadow-sm flex w-full p-6">
{/* Profile Picture */}
<div className="flex-shrink-0 text-center">
<div className="w-20 h-20 bg-green-800 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
{userData.name.slice(0,2).toUpperCase()}
</div>
</div>
{/* Info Fields */}
<div className="flex flex-col justify-center flex-1 ms-10">
{/* Row: Name */}
<div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
<div className="text-gray-400 font-medium text-[13px]">
{userData.name}
</div>
</div>
{/* Row: Email */}
<div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Email</div>
<div className="text-gray-400 font-medium text-[13px]">{userData.email}</div>
</div>
{/* Row: Account Name */}
<div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Account name</div>
<div className="text-gray-400 font-medium text-[13px]">
{userData.account.name}
</div>
</div>
</div>
</div>
);
};
export default UserProfile

View File

@ -1,107 +1,549 @@
import React, { useEffect, useState } from 'react'
import Draw from './Draw'
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';
// import React from 'react'
// import { SaveType } from '../client/SignDocument';
interface SignConProps {
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
recipientId: string;
saved: SaveType;
setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
// save: () => void;
// 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';
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
}
const SignCon: React.FC<SignConProps> = ({ setOpenSign, recipientId, saved, setSaved }) => {
interface AddSignatureModalProps {
setShowAddSignature?: (value: boolean) => void;
getAllSignatures: () => Promise<void>;
editingSignature?: Signature | null;
}
const [activeSaveType, setActiveSaveType] = React.useState<SaveTypeEnum>(SaveTypeEnum.DRAW);
interface Account {
id: number;
isDefault: boolean;
name: string;
}
const handleSaveSignature = () => {
if (!saved.save) return
interface UserData {
id: string;
firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
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)
interface SelectStyle {
id: number,
style: string
}
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
setShowAddSignature,
getAllSignatures,
editingSignature
}) => {
const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD'>('SELECT');
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: 'SELECT', label: 'SELECT', 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" /> },
];
// 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 'SELECT' | '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}`);
}
})
.catch((err) => {
console.error('Error saving signature:', err);
})
}
}
}, [editingSignature]);
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
setSignatures(data);
};
// Cleanup preview URLs when component unmounts or image changes
useEffect(() => {
return () => {
if (preview1) URL.revokeObjectURL(preview1);
if (preview2) URL.revokeObjectURL(preview2);
};
}, [preview1, preview2]);
const handleImageChange = async (
e: React.ChangeEvent<HTMLInputElement>,
setFile: React.Dispatch<React.SetStateAction<File | null>>,
setPreview: React.Dispatch<React.SetStateAction<string | null>>,
type: "fullSignature" | "initials"
) => {
const file = e.target.files?.[0];
if (file) {
setFile(file);
const previewUrl = URL.createObjectURL(file);
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 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 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 = () => {
switch (activeTab) {
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">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(() => {
handleSaveSignature();
}, [saved])
getUserDetails();
}, []);
return (
<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>
<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">
<div className='relative lg:min-w-[650px] max-w-max mx-auto shadow-md rounded-sm bg-white pt-3 pb-1 px-3'>
<IoMdClose
onClick={() => setOpenSign(false)}
className='absolute right-3 text-slate-400 text-lg hover:text-[var(--primary)] cursor-pointer transition-all' />
{/* Signature Save types Nav */}
<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"}`}
onClick={() => setActiveSaveType(SaveTypeEnum.DRAW)}>
<p className='tracking-wide'>Draw</p>
{/* 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>
<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>
{/* 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>
<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>
{/* 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 'SELECT' | 'DRAW' | 'UPLOAD')}
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
? '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>
{activeSaveType === SaveTypeEnum.DRAW && <Draw
saved={saved}
setSaved={setSaved}
/>}
{activeSaveType === SaveTypeEnum.TYPE && <TypedSign
saved={saved}
setSaved={setSaved}
/>}
{activeSaveType === SaveTypeEnum.UPLOAD && <SignUpload
saved={saved}
setSaved={setSaved}
/>}
{/* Main Content Area */}
<div className="flex-1 flex flex-col ">
{/* Header with close button */}
{/* <div className="mt-3 flex justify-end gap-2">
<button
// 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"
>
Cancel
</button>
<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
</button>
</div> */}
{/* 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={() => setShowAddSignature?.(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...'}
</>
) : (
isEditMode ? 'UPDATE SIGNATURE' : 'CREATE SIGNATURE'
)}
</button>
</div>
</div>
</div>
</div>
)
}
);
};
export default SignCon;
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

@ -1,9 +1,12 @@
import { AiFillSignature } from 'react-icons/ai';
import { useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { LuGitGraph } from 'react-icons/lu';
import AccountNav from '../accounts/AccountNav';
import { useEffect, useState } from 'react';
import api from '../../config/api';
import SigningOrderDiagram from '../../SigningOrderDiagram ';
import { IoCloseOutline } from 'react-icons/io5';
import { MdEditDocument } from "react-icons/md";
// Sample recipient data — you can replace this with actual API data later
const recipients = [
@ -27,6 +30,8 @@ const recipients = [
const TransactionView = () => {
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
console.log("account" , accountId , transactionId)
if (!accountId || !transactionId) {
window.location.href = "/";
@ -34,6 +39,8 @@ const TransactionView = () => {
}
const [transactionDetails, setTransactionDetails] = useState(null);
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
console.log(transactionDetails, "line 37");
const fetchData = async () => {
try {
@ -57,13 +64,14 @@ const TransactionView = () => {
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
{/* Header */}
<h1 className='text-lg font-semibold text-gray-800'>
Complete with Forms Made Easy: <span className='text-slate-600'>{transactionDetails.transaction.name ? transactionDetails.transaction.name : "Untitled"}</span>
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails?.transaction?.subject ?? "Untitled"}</span>
</h1>
<div className='mt-2'>
{/* new Date(transactionDetails?.transaction?.lastModifiedAt * 1000) */}
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
<p className='text-slate-600'>Last Change on <span>12/02/25</span></p>
<p className='text-slate-600'>Sent on <span>11/02/25</span></p>
<p className=''><span className='text-slate-600 font-semibold'>Last Change on</span> <span className='text-slate-400'>{new Date(transactionDetails?.transaction?.lastModifiedAt * 1000).toISOString()}</span></p>
<p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
</div>
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
@ -77,18 +85,23 @@ const TransactionView = () => {
>
Sign
</button> */}
<button className='cursor-pointer border border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100'>
<button className='cursor-pointer border border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold'>
More
</button>
<Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-two/${accountId}/${transactionId}`}>
Edit
</Link>
</div>
<hr className='my-6 border-slate-200' />
{/* Recipients Section */}
{transactionDetails.recipients.length > 0 &&
<div>
<div className='flex justify-between items-center text-md'>
<h2 className='text-lg font-semibold text-gray-800 cursor-pointer'>Signers</h2>
<p className='font-bold flex items-center gap-1.5 text-slate-800 cursor-pointer'>
<p className='font-bold flex items-center gap-1.5 text-slate-800 cursor-pointer' onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}>
<LuGitGraph className='text-[0.9rem] text-[var(--primary)]' />
Signing Graph
</p>
@ -122,11 +135,12 @@ const TransactionView = () => {
</div>
))}
</div>
}
<div>
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
<p className='mt-2 text-slate-600'>No Message Found</p>
<p className='mt-2 text-slate-600'>{ transactionDetails?.transaction?.message ?? "No Message Found...!"}</p>
</div>
</div>
@ -137,7 +151,11 @@ const TransactionView = () => {
<div className='border-3 rounded-sm border-slate-300 relative text-xs'>
<img style={{ width: "200px", height: "160px" }} src="https://images.unsplash.com/photo-1746469535771-71a672e8719f?q=80&w=870&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="" />
<div className='bg-slate-100 p-2 absolute w-full bottom-[0px]'>
<p className='text-[0.7rem] text-slate-500'>1 of 10</p>
<p className='text-[0.7rem] text-slate-500 flex justify-between items-center'>
<span> 1 of 10</span>
<span className='cursor-pointer'><Link to={`/process-step-two/${accountId}/${transactionId}`}><MdEditDocument size={16}/></Link></span>
</p>
</div>
</div>
@ -146,6 +164,24 @@ const TransactionView = () => {
</div>
</div>
{signingOrderModal.open &&
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center overflow-hidden">
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full ">
<div className="pb-2 text-white flex justify-between items-center bg-[var(--primary)] px-2 py-3 rounded-t-lg">
<p className="font-semibold">Signing Order</p>
<div
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
className="text-xl cursor-pointer hover:text-gray-200 transition-all">
<IoCloseOutline />
</div>
</div>
<span className='overflow-y-auto'>
<SigningOrderDiagram signers={transactionDetails.recipients} />
</span>
</div>
</div>
}
</>
}
</>

View File

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

View File

@ -43,6 +43,9 @@
--hideModalBg: #2d2d2d;
--processStepOneTextHead: #000000;
--modalSubmitBG:#1919d7;
--modalSubmitBGHover:#395ee1;
}
@ -74,3 +77,21 @@ canvas {
.animate-text {
animation: textColorChange 2s infinite ease-in-out;
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
/* Loader CSS */

View File

@ -1,27 +1,188 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
// import { v4 as uuidv4 } from "uuid";
// import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
// // import { v4 as uuidv4 } from "uuid";
import { hideLoader, showLoader } from "./loaderSlice";
// import { showModal } from "./modalSlice";
// import { updateSelectedDocuments } from "./selectedDocsSlice";
// import { hideLoader, showLoader } from "./loaderSlice";
// // import { showModal } from "./modalSlice";
// // import { updateSelectedDocuments } from "./selectedDocsSlice";
import api from "../../config/api";
import { Bounce, toast } from "react-toastify";
// import { createAxiosInstance } from "../../config/axiosInstance";
// import api from "../../config/api";
// import { Bounce, toast } from "react-toastify";
// // 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 {
id: string;
id?: string;
name: string;
extension: string;
original?: boolean;
corrupted?: boolean;
isCompleted: boolean;
status: string;
url: string;
type: string;
pageCount?: number;
[key: string]: any;
}
export interface DocumentState {
// -----------------------
// Initial State
// -----------------------
interface DocumentState {
documents: Document[];
loading: boolean;
error: string | null;
@ -33,80 +194,81 @@ const initialState: DocumentState = {
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 }) => {
// -----------------------
// Thunk: Fetch Documents
// -----------------------
export const fetchDocuments = createAsyncThunk<
Document[],
{ accountId: string; transactionId: string }
>(
'documents/fetchDocuments',
async ({ accountId, transactionId }, { rejectWithValue, getState }) => {
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[];
} 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>(
"documents/uploadDocuments",
async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => {
// -----------------------
// Thunk: Upload Documents
// -----------------------
export const uploadDocuments = createAsyncThunk<
Document[],
{ formData: FormData; accountId: string; transactionId: string }
>(
'documents/uploadDocuments',
async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue, getState }) => {
try {
dispatch(showLoader("Uploading documents..."));
console.log("Form Data", formData.getAll("document"))
dispatch(showLoader('Uploading documents...'));
const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, {
headers: {
"Content-Type": "multipart/form-data"
},
});
const state = getState() as {
selectedRecipients: { transactionTemplateToggle: string };
};
const toggle = state.selectedRecipients.transactionTemplateToggle;
console.log("Form Uploaded Response DocumentSlice Line 185", response.data)
const response = await api.post(
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
dispatch(hideLoader());
return [response.data];
} catch (error: any) {
dispatch(hideLoader());
toast.error("Error Uploading Document", {
position: "bottom-right",
toast.error('Error Uploading Document', {
position: 'bottom-right',
autoClose: 3000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
progress: undefined,
theme: "light",
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
return rejectWithValue('Failed Uploading Documents');
}
}
);
const documentSlice = createSlice({
name: "documents",
// -----------------------
// Slice
// -----------------------
const documentsSlice = createSlice({
name: 'documents',
initialState,
reducers: {
setDocuments: (state, action: PayloadAction<Document[]>) => {
@ -127,29 +289,34 @@ const documentSlice = createSlice({
},
extraReducers: (builder) => {
builder
// Fetch documents
// fetchDocuments
.addCase(fetchDocuments.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
state.loading = false;
console.log(action.payload, "action.payload")
state.documents = action.payload;
})
.addCase(fetchDocuments.rejected, (state, action) => {
state.loading = false;
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) => {
state.loading = true;
state.error = null;
})
.addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
state.loading = false;
state.documents = [...action.payload, ...state.documents];
state.documents.push(...action.payload);
})
.addCase(uploadDocuments.rejected, (state, action) => {
state.loading = false;
@ -158,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 { createAsyncThunk } from '@reduxjs/toolkit';
// import { createSlice, PayloadAction } 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 colors from '../../utlity/colorsArr';
import api from '../../config/api';
export enum recipientType {
SIGNER = "Signer",
INPERSONSIGNER = "InPersonSigner",
@ -26,54 +355,106 @@ export interface Recipient {
export interface SelectedRecipientInt {
recipients: Recipient[],
signingOrder: boolean
signingOrder: boolean,
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE
}
interface AddRecipientArgs {
recipientsData: Recipient[];
accountId: 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 = {
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',
async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => {
console.log(recipientsData, accountId, transactionId, "recipientsData Line 46");
async ({ recipientsData, accountId, transactionId }, { rejectWithValue, getState }) => {
try {
const response = await api.post(
`/api/accounts/${accountId}/transactions/${transactionId}/recipients`,
recipientsData
);
console.log(response)
return response.data as Recipient[];
const state = getState() as { selectedRecipients: SelectedRecipientInt };
const toggle = state.selectedRecipients.transactionTemplateToggle;
const updateRecipientsData = recipientsData.filter(recipient => recipient.id !== null);
const createRecipientsData = recipientsData.filter(recipient => recipient.id == null);
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) {
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',
async (email, { rejectWithValue }) => {
async ({ recipientId, accountId, transactionId, email }, { rejectWithValue, getState }) => {
try {
await api.delete(`/api/recipients`, { data: { email } });
return email; // return email so we can filter from state
const state = getState() as { selectedRecipients: SelectedRecipientInt };
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) {
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',
async ({ accountId, transactionId }, { rejectWithValue }) => {
async ({ accountId, transactionId }, { rejectWithValue, getState }) => {
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[];
} catch (err: any) {
return rejectWithValue(err.response?.data || "Failed to fetch recipients");
@ -85,7 +466,6 @@ 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) {
@ -93,34 +473,29 @@ const selectedRecipientsSlice = createSlice({
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]
state.signingOrder = true;
state.recipients = [action.payload, ...state.recipients];
} else {
state.recipients.push(action.payload);
}
},
resetRecipients: (state) => {
state.recipients = [];
},
// Replace the entire selectedRecipients array
toggleToTemplates: (state) => {
state.transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
},
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) {
if ((recipient.type === recipientType.INPERSONSIGNER && index > 0)
&& action.payload[index - 1].type !== recipientType.INPERSONSIGNER) {
return state;
}
}
@ -128,35 +503,26 @@ const selectedRecipientsSlice = createSlice({
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
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)
debugger
recipient.color = colors[index]
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) {
@ -164,33 +530,22 @@ const selectedRecipientsSlice = createSlice({
}
},
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;
}
resetSelRecipient: () => initialState
},
extraReducers: (builder) => {
builder
.addCase(fetchRecipients.pending, (state) => {
console.log("Fetching recipients...");
})
.addCase(fetchRecipients.pending, () => { })
.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
color: colors[index],
}));
state.recipients = assignedRecipients;
})
.addCase(fetchRecipients.rejected, (state, action) => {
@ -198,39 +553,52 @@ const selectedRecipientsSlice = createSlice({
toast.error("Failed to fetch recipients", {
position: "bottom-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
})
.addCase(addRecipientAsync.pending, (state) => {
console.log("Fetching recipients...");
})
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient>) => {
console.log("Added recipient successfully:", action.payload);
// state.recipients.push(action.payload);
.addCase(addRecipientAsync.pending, () => { })
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
state.recipients = action.payload.map((recipient, index) => ({
...recipient,
color: colors[index % colors.length],
}));
})
.addCase(addRecipientAsync.rejected, (state, action) => {
console.error("Failed to fetch recipients:", action.payload);
toast.error("Failed to fetch recipients", {
console.error("Failed to add recipients:", action.payload);
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",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
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;

View File

@ -79,3 +79,41 @@ export interface AccountData {
role: UserRole;
status: AccountStatus;
}
export interface TagsProperties {
"editable": boolean;
"required": boolean;
"navigationOrder": number;
"type": string;
"lastName"?: boolean;
"firstName"?: boolean;
"maxLength"?: number;
"minLength"?: number;
"readOnly"?: boolean;
"multiLine"?: boolean;
"regex"?: string[];
"format"?: string;
"dateFormat"?: string;
"currentTimeStamp"?: boolean;
"decimalPlace"?: number;
"multiSelect"?: boolean;
}
export enum TagsTypes {
TEXT= "text",
SIGNATURE= "signature",
INITIALS= "initials",
TEMPORAL= "temporal",
NUMBER= "number",
IMAGE= "image",
STAMP= "stamp",
CHECKBOX= "checkbox",
SELECT= "select",
FILE= "file",
ATTACHMENT= "attachment",
RADIO= "radio",
EMAIL= "email",
NAME= "name",
COMPANY= "company",
}

View File

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