Compare commits
No commits in common. "313217eded9cceb45db39bd86d6886f199fec226" and "3d7293100744302825061c30b19622fdac5a1f06" have entirely different histories.
313217eded
...
3d72931007
34
src/App.tsx
34
src/App.tsx
|
|
@ -14,12 +14,18 @@ import SignDocument from "./components/client/SignDocument";
|
||||||
import Transactions from "./components/transactions/Transactions";
|
import Transactions from "./components/transactions/Transactions";
|
||||||
import ProtectedRoute from "./components/routes_helper/ProtectedRoute";
|
import ProtectedRoute from "./components/routes_helper/ProtectedRoute";
|
||||||
|
|
||||||
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { fetchUserDetails } from "./store/slices/userSlice";
|
||||||
|
import { AppDispatch, RootState } from "./store/store";
|
||||||
|
import { useEffect, useLayoutEffect } from "react";
|
||||||
|
|
||||||
import Setting from "./components/setting/Setting";
|
import Setting from "./components/setting/Setting";
|
||||||
import Accounts from "./components/accounts/Accounts";
|
import Accounts from "./components/accounts/Accounts";
|
||||||
import Account from "./components/accounts/Account";
|
import Account from "./components/accounts/Account";
|
||||||
import TransactionView from "./components/transactions/TransactionView";
|
import TransactionView from "./components/transactions/TransactionView";
|
||||||
import Templates from "./components/templates/Templates";
|
import AccountTransactionWrapper from "./components/AccountTransactionWrapper";
|
||||||
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
|
import SigningOrderDiagram from "./SigningOrderDiagram ";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -35,26 +41,22 @@ const App = () => {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}>
|
}>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
{/* <Route path="/s/settings" element={<Setting />} /> */}
|
<Route path="/templates" element={<Home />} />
|
||||||
{/* <Route element={<AccountTransactionWrapper />}> */}
|
|
||||||
|
<Route element={<AccountTransactionWrapper />}>
|
||||||
<Route path="/accounts" element={<Accounts />} />
|
<Route path="/accounts" element={<Accounts />} />
|
||||||
<Route path="/accounts/:accountId/transactions" element={<Account />} />
|
<Route path="/accounts/:accountId/transactions" element={<Account />} />
|
||||||
<Route path="/transactions" element={<Account />} />
|
|
||||||
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
|
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
|
||||||
<Route path="/transactions/:filterParam" element={<Transactions />} />
|
<Route path="/transactions/:filterParam" element={<Transactions />} />
|
||||||
<Route path="/templates" element={<Templates />} />
|
</Route>
|
||||||
{/* </Route> */}
|
|
||||||
|
|
||||||
<Route path="/s/:page1" element={<Setting />} />
|
<Route path="/s/:page1" element={<Setting />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/process-step-one/:accountId/:transactionId" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<ProcessStepOne />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/process-step-one/:accountId/template/:templateId" element={
|
|
||||||
|
|
||||||
|
<Route path="/process-step-one/:accountId/:transactionId" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<ProcessStepOne />
|
<ProcessStepOne />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|
@ -66,12 +68,6 @@ const App = () => {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/document-completion-screen/:accountId/:transactionId" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<DocumentCompletionScreen />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
{/* Links work wothout Login */}
|
{/* Links work wothout Login */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
|
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
// import AccountNav from './accounts/AccountNav'
|
import AccountNav from './accounts/AccountNav'
|
||||||
import CommonNav from './CommonNav'
|
|
||||||
|
|
||||||
const AccountTransactionWrapper = () => {
|
const AccountTransactionWrapper = () => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CommonNav />
|
<AccountNav />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,411 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -4,20 +4,6 @@ import AccountNav from "./AccountNav";
|
||||||
import api from "../../config/api";
|
import api from "../../config/api";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { FaCheck } from "react-icons/fa";
|
import { FaCheck } from "react-icons/fa";
|
||||||
import Cards from "../home/Cards";
|
|
||||||
import AddInviteModal from "./AddInviteModal";
|
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
import {
|
|
||||||
FaCheckCircle,
|
|
||||||
FaClock,
|
|
||||||
FaHourglassHalf,
|
|
||||||
FaTasks,
|
|
||||||
FaChevronDown,
|
|
||||||
FaPlus,
|
|
||||||
FaUserPlus,
|
|
||||||
FaFilter,
|
|
||||||
FaCalendarAlt
|
|
||||||
} from 'react-icons/fa';
|
|
||||||
|
|
||||||
interface Transaction {
|
interface Transaction {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -32,173 +18,14 @@ const formatDate = (timestamp: number) => {
|
||||||
return new Date(timestamp * 1000).toLocaleString();
|
return new Date(timestamp * 1000).toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Account = () => {
|
const Account = () => {
|
||||||
const mapApiDataToCards = (data: any): CardData[] => {
|
const { accountId } = useParams<{ accountId: string }>();
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: 'Action Needed',
|
|
||||||
count: data.actionNeeded || 0,
|
|
||||||
icon: <FaTasks size={18} className="text-indigo-600" />,
|
|
||||||
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
|
|
||||||
shadowColor: 'shadow-indigo-200/50',
|
|
||||||
iconBg: 'bg-indigo-100 border-indigo-200',
|
|
||||||
textColor: 'text-indigo-700',
|
|
||||||
trend: 'up'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Expiring Soon',
|
|
||||||
count: data.expiring || 0,
|
|
||||||
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
|
|
||||||
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
|
|
||||||
shadowColor: 'shadow-rose-200/50',
|
|
||||||
iconBg: 'bg-rose-100 border-rose-200',
|
|
||||||
textColor: 'text-rose-700',
|
|
||||||
trend: 'up'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Completed',
|
|
||||||
count: data.completed || 0,
|
|
||||||
icon: <FaCheckCircle size={18} className="text-teal-600" />,
|
|
||||||
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
|
|
||||||
shadowColor: 'shadow-teal-200/50',
|
|
||||||
iconBg: 'bg-teal-100 border-teal-200',
|
|
||||||
textColor: 'text-teal-700',
|
|
||||||
trend: 'up'
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: 'Waiting for Others',
|
|
||||||
// count: data.waitingForOthers || 0,
|
|
||||||
// icon: <FaClock size={18} className="text-stone-600" />,
|
|
||||||
// gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
|
|
||||||
// shadowColor: 'shadow-blue-200/50',
|
|
||||||
// iconBg: 'bg-blue-100 border-blue-200',
|
|
||||||
// textColor: 'text-blue-700',
|
|
||||||
// trend: 'down'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: 'declined',
|
|
||||||
// count: data.declined || 0,
|
|
||||||
// icon: <FaClock size={18} className="text-purple-600" />,
|
|
||||||
// gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
|
|
||||||
// shadowColor: 'shadow-purple-200/50',
|
|
||||||
// iconBg: 'bg-purple-100 border-purple-200',
|
|
||||||
// textColor: 'text-purple-700',
|
|
||||||
// trend: 'down'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: 'cancelled',
|
|
||||||
// count: data.cancelled || 0,
|
|
||||||
// icon: <FaClock size={18} className="text-green-600" />,
|
|
||||||
// gradient: 'bg-gradient-to-br from-green-50 to-green-100',
|
|
||||||
// shadowColor: 'shadow-green-200/50',
|
|
||||||
// iconBg: 'bg-green-100 border-green-200',
|
|
||||||
// textColor: 'text-green-700',
|
|
||||||
// trend: 'down'
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
title: 'draft',
|
|
||||||
count: data.draft || 0,
|
|
||||||
icon: <FaClock size={18} className="text-stone-600" />,
|
|
||||||
gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
|
|
||||||
shadowColor: 'shadow-stone-200/50',
|
|
||||||
iconBg: 'bg-stone-100 border-stone-200',
|
|
||||||
textColor: 'text-stone-700',
|
|
||||||
trend: 'down'
|
|
||||||
}
|
|
||||||
// Add more cards here if needed
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const [activeAccountId, setActiveAccountId] = useState<string | null>(
|
|
||||||
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
|
|
||||||
);
|
|
||||||
|
|
||||||
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
|
|
||||||
|
|
||||||
console.log("activeAccountId Trans.", activeAccountId)
|
|
||||||
// const { accountId } = useParams<{ accountId: string }>();
|
|
||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
|
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
|
||||||
const [accountsData, setAccountsData] = useState([])
|
|
||||||
const [inviteLoading, setInviteLoading] = useState(false);
|
|
||||||
const [cards, setCards] = useState<CardData[]>([]);
|
|
||||||
const [accountID, setAccountID] = useState<number>()
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
|
|
||||||
// const getUserDetails = async () => {
|
|
||||||
// try {
|
|
||||||
// let res = await api.get('/api/user/details')
|
|
||||||
// let accountIDData = res?.data?.account?.id
|
|
||||||
// setAccountID(accountIDData)
|
|
||||||
// } catch (error) {
|
|
||||||
// console.log(error)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// getUserDetails()
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const userDetails = JSON.parse(sessionStorage.getItem('userDetails'));
|
|
||||||
setCurrentEmailId(userDetails?.email)
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// Listen for sessionStorage changes
|
|
||||||
useEffect(() => {
|
|
||||||
const handleStorageChange = (e: StorageEvent) => {
|
|
||||||
if (e.key === 'activeAccountId') {
|
|
||||||
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
|
|
||||||
setActiveAccountId(newActiveAccountId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for storage events (works for changes from other tabs)
|
|
||||||
window.addEventListener('storage', handleStorageChange);
|
|
||||||
|
|
||||||
// Custom event listener for same-tab changes
|
|
||||||
const handleCustomStorageChange = (e: CustomEvent) => {
|
|
||||||
if (e.detail.key === 'activeAccountId') {
|
|
||||||
setActiveAccountId(e.detail.newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('storage', handleStorageChange);
|
|
||||||
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
const getDashboardData = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
const payload = {
|
|
||||||
account: activeAccountId
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await api.get('/api/dashboard', { params: payload });
|
|
||||||
console.log("Dashboard Data:", res.data);
|
|
||||||
const cardDataArray = mapApiDataToCards(res.data);
|
|
||||||
setCards(cardDataArray);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching dashboard data:", error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchTransactions = async () => {
|
const fetchTransactions = async () => {
|
||||||
console.log("fetchTransactions")
|
|
||||||
try {
|
try {
|
||||||
const response = await api(`/api/accounts/${activeAccountId}/transactions`);
|
const response = await api(`/api/accounts/${accountId}/transactions`);
|
||||||
setTransactions(response.data || []);
|
setTransactions(response.data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching transactions:", error);
|
console.error("Error fetching transactions:", error);
|
||||||
|
|
@ -206,80 +33,19 @@ const Account = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeAccountId) {
|
if (accountId) fetchTransactions();
|
||||||
getDashboardData()
|
}, [accountId]);
|
||||||
fetchTransactions();
|
|
||||||
}
|
|
||||||
}, [activeAccountId]);
|
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (accountID) fetchTransactions();
|
|
||||||
// }, [accountID]);
|
|
||||||
|
|
||||||
// const getAccountDetails = async () => {
|
|
||||||
// try {
|
|
||||||
// let res = await api.get('/api/user/accounts')
|
|
||||||
// let items = res?.data
|
|
||||||
// setAccountsData(items)
|
|
||||||
// } catch (error) {
|
|
||||||
// console.log(error)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// getAccountDetails()
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleTransactionClick = (id: number) => {
|
const handleTransactionClick = (id: number) => {
|
||||||
navigate(`/accounts/${activeAccountId}/transactions/${id}`);
|
navigate(`/accounts/${accountId}/transactions/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
|
|
||||||
setInviteLoading(true);
|
|
||||||
try {
|
|
||||||
const payloadInvite = {
|
|
||||||
email: data.email,
|
|
||||||
role: data.role
|
|
||||||
}
|
|
||||||
let res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
|
|
||||||
if (res?.data) {
|
|
||||||
toast.success("Invite Sent Successfully...!", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 1500,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
await fetchTransactions()
|
|
||||||
setIsModalOpen(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to invite', err);
|
|
||||||
} finally {
|
|
||||||
setInviteLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Cards type="transactions" onOpenModal={handleOpenModal} cards={cards}
|
|
||||||
isLoading={isLoading} />
|
|
||||||
{transactions.length > 0 && (
|
{transactions.length > 0 && (
|
||||||
<div className="overflow-x-auto py-2">
|
<div className="overflow-x-auto py-4">
|
||||||
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
||||||
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
|
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -289,8 +55,7 @@ const Account = () => {
|
||||||
<th className="px-4 py-3 text-center">Active</th>
|
<th className="px-4 py-3 text-center">Active</th>
|
||||||
<th className="px-4 py-3 text-center">Last Change</th>
|
<th className="px-4 py-3 text-center">Last Change</th>
|
||||||
<th className="px-4 py-3 text-center">Expire After (days)</th>
|
<th className="px-4 py-3 text-center">Expire After (days)</th>
|
||||||
<th className="px-4 py-3 text-center">Sign</th>
|
<th className="px-4 py-3 text-center"></th>
|
||||||
<th className="px-4 py-3 text-center">Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
|
@ -327,9 +92,6 @@ const Account = () => {
|
||||||
<td className="px-4 py-3 text-center">
|
<td className="px-4 py-3 text-center">
|
||||||
{tx.expireAfter ?? "-"}
|
{tx.expireAfter ?? "-"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-center ">
|
|
||||||
{tx?.createdBy?.email === currentEmailId ? <button className="px-2 py-0.5 rounded-xl text-xs font-medium bg-green-100 text-green-700 cursor-pointer">Sign now</button> : "-"}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-center">
|
<td className="px-4 py-3 text-center">
|
||||||
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
|
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -340,8 +102,6 @@ const Account = () => {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}
|
|
||||||
accountOptions={accountsData} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,405 +1,139 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { use, useEffect } from 'react';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { useSelector } from 'react-redux';
|
||||||
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
|
import { Link, useLocation, 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 api from '../../config/api';
|
import api from '../../config/api';
|
||||||
|
|
||||||
|
// const fetchAccountData = async (accountId: string) => {
|
||||||
|
// try {
|
||||||
|
// if(!accountId) return null;
|
||||||
|
// const response = await api.get(`/api/accounts/${accountId}`);
|
||||||
|
// return response.data;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error fetching account data:", error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const fetchTransactionData = async (accountId: string, transactionId: string) => {
|
||||||
|
// try {
|
||||||
|
// if(!accountId || !transactionId) return null;
|
||||||
|
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
|
||||||
|
// return response.data;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error fetching account data:", error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
interface AccountNavProps {
|
interface AccountNavProps {
|
||||||
accountName?: string | null;
|
accountName: string | null;
|
||||||
transactionName?: string | null;
|
transactionName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Account {
|
|
||||||
id: number;
|
|
||||||
isDefault: boolean;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserData {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
language: string;
|
|
||||||
account: Account;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AccountDataArray {
|
|
||||||
userId: number,
|
|
||||||
userEmail: string,
|
|
||||||
name: string,
|
|
||||||
accountId: number;
|
|
||||||
accountName: string;
|
|
||||||
role: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to dispatch custom storage event
|
|
||||||
const dispatchSessionStorageChange = (key: string, newValue: any) => {
|
|
||||||
window.dispatchEvent(new CustomEvent('sessionStorageChange', {
|
|
||||||
detail: { key, newValue }
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transactionName = null }) => {
|
const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transactionName = null }) => {
|
||||||
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
|
const user: UserState = useSelector((state: RootState) => state.user);
|
||||||
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
|
|
||||||
console.log("activeAccountId", activeAccountId);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const initials = user.username
|
||||||
const [accountsOpen, setAccountsOpen] = useState(false);
|
? user.username
|
||||||
const [loadingAccounts, setLoadingAccounts] = useState(false);
|
.split(' ')
|
||||||
const [accounts, setAccounts] = useState<AccountDataArray[]>([]);
|
.map((n) => n[0])
|
||||||
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
|
.join('')
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
.slice(0, 2)
|
||||||
|
.toUpperCase()
|
||||||
|
: 'U';
|
||||||
|
|
||||||
const [user, setUser] = useState<UserData>({
|
// useEffect(() => {
|
||||||
id: 0,
|
// if (accountId) {
|
||||||
name: "",
|
// api.get(`/api/accounts/${accountId}`)
|
||||||
email: "",
|
// .then(response => {
|
||||||
language: "",
|
// // Handle the account data if needed
|
||||||
account: {
|
// console.log(response.data, "Line 29");
|
||||||
id: 0,
|
// })
|
||||||
isDefault: false,
|
// .catch(error => {
|
||||||
name: "",
|
// console.error("Error fetching account data:", error);
|
||||||
},
|
// });
|
||||||
});
|
// }
|
||||||
|
// }, [accountId, transactionId]);
|
||||||
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 location = useLocation();
|
||||||
|
|
||||||
const rawPathnames = location.pathname
|
const pathnames = location.pathname
|
||||||
.split("/")
|
.split("/")
|
||||||
.filter((x) => x && isNaN(Number(x)));
|
.filter((x) => x && isNaN(Number(x))); // remove numeric IDs
|
||||||
|
|
||||||
// Special case: if first segment is 's' and next is 'settings', remove 's'
|
const labelMap = {
|
||||||
const pathnames =
|
|
||||||
rawPathnames[0] === "s" && rawPathnames[1] === "settings"
|
|
||||||
? rawPathnames.slice(1)
|
|
||||||
: rawPathnames;
|
|
||||||
|
|
||||||
const labelMap: Record<string, string> = {
|
|
||||||
accounts: "Accounts",
|
accounts: "Accounts",
|
||||||
transactions: "Transactions",
|
transactions: "Transactions",
|
||||||
templates: "Templates"
|
// Add more routes here if needed
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function handleClickOutside(event: MouseEvent) {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
if (open) {
|
|
||||||
setHasModalBeenClosed(true);
|
|
||||||
}
|
|
||||||
setOpen(false);
|
|
||||||
setAccountsOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
let res = await api.post('/api/auth/logout');
|
|
||||||
if (res.status === 200) {
|
|
||||||
sessionStorage.clear();
|
|
||||||
setOpen(false);
|
|
||||||
navigate("/login");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMainDropdownToggle = () => {
|
|
||||||
if (open) {
|
|
||||||
setHasModalBeenClosed(true);
|
|
||||||
}
|
|
||||||
setOpen(!open);
|
|
||||||
if (!open) {
|
|
||||||
setAccountsOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatSegment = (segment: string) =>
|
|
||||||
segment
|
|
||||||
.replace(/[-_]/g, " ")
|
|
||||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
||||||
|
|
||||||
const ShimmerRow = () => (
|
|
||||||
<div className="animate-pulse px-4 py-3 border-b border-gray-100 last:border-b-0">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="w-8 h-8 bg-gray-200 rounded-full"></div>
|
|
||||||
<div className="flex-1 space-y-2">
|
|
||||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
||||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
|
||||||
</div>
|
|
||||||
<div className="w-16 h-6 bg-gray-200 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
|
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
|
||||||
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
|
|
||||||
{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 (
|
<nav className="text-md font-medium text-gray-800 capitalize">
|
||||||
<span key={to} className="flex items-center">
|
{pathnames.map((segment, index) => {
|
||||||
{index > 0 && <span className="mx-2 text-gray-400">{">"}</span>}
|
const label = labelMap[segment] || segment;
|
||||||
{!isLast ? (
|
const to = "/" + pathnames.slice(0, index + 1).join("/");
|
||||||
<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">
|
const isLast = index === pathnames.length - 1;
|
||||||
<div className="relative" ref={dropdownRef}>
|
|
||||||
|
return (
|
||||||
|
<span key={to}>
|
||||||
|
{!isLast ? (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
<span className="mx-1">{">"}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>{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">
|
||||||
<button
|
<button
|
||||||
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
|
aria-label="Notifications"
|
||||||
onClick={handleMainDropdownToggle}
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
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">
|
<IoIosNotifications className="text-xl" />
|
||||||
{user ? (
|
|
||||||
<span className="w-full h-full object-cover p-1.5 font-bold">
|
|
||||||
{(user?.name?.slice(0, 2)).toUpperCase()}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<FaUser className="text-lg" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
<Link
|
||||||
<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">
|
to="/s/settings"
|
||||||
<div className="px-4 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b border-gray-100">
|
aria-label="Settings"
|
||||||
<div className="flex items-center space-x-3">
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
<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 ? (
|
<FaCog className="text-sm" />
|
||||||
<span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold">
|
</Link>
|
||||||
{(user?.name?.slice(0, 2)).toUpperCase()}
|
</div>
|
||||||
</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">
|
{/* Avatar with separate spacing */}
|
||||||
<button
|
<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">
|
||||||
onClick={handleAccountsClick}
|
{user.picture ? (
|
||||||
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"
|
<img
|
||||||
>
|
src={user.picture}
|
||||||
<div className="flex items-center">
|
alt="User avatar"
|
||||||
<FaBuilding className="mr-3 text-gray-500" />
|
className="w-full h-full object-cover"
|
||||||
<span>Switch Account</span>
|
/>
|
||||||
</div>
|
) : (
|
||||||
<FaChevronDown className={`text-gray-400 transition-transform ${accountsOpen ? 'rotate-180' : ''}`} />
|
initials
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ import AccountNav from "./AccountNav";
|
||||||
import api from "../../config/api";
|
import api from "../../config/api";
|
||||||
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
|
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Cards from "../home/Cards";
|
|
||||||
import AddAccountModal from "./AddAccountModal";
|
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
import Loader from "../../helper/Loader";
|
|
||||||
|
|
||||||
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
|
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
|
||||||
|
|
||||||
|
|
@ -34,16 +30,8 @@ const getStatusLabel = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const Accounts = () => {
|
const Accounts = () => {
|
||||||
|
|
||||||
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
|
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
|
||||||
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
|
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [accounts, setAccounts] = useState<string[]>([]);
|
|
||||||
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
@ -65,65 +53,8 @@ const Accounts = () => {
|
||||||
navigate(`/accounts/${id}/transactions`);
|
navigate(`/accounts/${id}/transactions`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddAccount = async (accountName: string) => {
|
|
||||||
try {
|
|
||||||
const payloadAccount = {
|
|
||||||
name: accountName
|
|
||||||
}
|
|
||||||
let res = await api.post(`/api/user/accounts`, payloadAccount)
|
|
||||||
if (res?.data) {
|
|
||||||
toast.success("Account Add Successfully...!", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
await fetchAccounts()
|
|
||||||
setIsModalOpen(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMenuToggle = (accountId: string) => {
|
|
||||||
setOpenMenuId(openMenuId === accountId ? null : accountId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAction = async (accountId: string, action: 'accept' | 'reject') => {
|
|
||||||
try {
|
|
||||||
let response = await api.post(`/api/accounts/${accountId}/accept/invite`)
|
|
||||||
console.log(response)
|
|
||||||
if (response.data.status === 202) {
|
|
||||||
toast.success("Login Successful", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error posting data:', error);
|
|
||||||
} finally {
|
|
||||||
setOpenMenuId(null); // close menu
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <Cards type="account" onOpenModal={handleOpenModal} /> */}
|
|
||||||
{allAccounts.length > 0 && (
|
{allAccounts.length > 0 && (
|
||||||
<div className="overflow-x-auto py-2">
|
<div className="overflow-x-auto py-2">
|
||||||
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
||||||
|
|
@ -170,38 +101,8 @@ const Accounts = () => {
|
||||||
{status.text}
|
{status.text}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-center relative">
|
<td className="px-4 py-3 text-center">
|
||||||
<button
|
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleMenuToggle(account.accountId);
|
|
||||||
}}
|
|
||||||
className="mx-auto"
|
|
||||||
aria-label="Actions menu"
|
|
||||||
>
|
|
||||||
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown menu */}
|
|
||||||
{openMenuId === account.accountId && (
|
|
||||||
<div
|
|
||||||
className="absolute right-13 top-6 mt-1 w-18 bg-white border text-[13px] font-bold border-gray-300 rounded-md shadow-lg z-10"
|
|
||||||
onClick={(e) => e.stopPropagation()} // prevent row click closing
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="w-full px-3 py-1 text-left text-green-700 hover:bg-green-100 rounded-t-md cursor-pointer"
|
|
||||||
onClick={() => handleAction(account.accountId, 'accept')}
|
|
||||||
>
|
|
||||||
Accept
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="w-full px-3 py-1 text-left text-red-700 hover:bg-red-100 rounded-b-md cursor-pointer"
|
|
||||||
onClick={() => handleAction(account.accountId, 'reject')}
|
|
||||||
>
|
|
||||||
Reject
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
@ -210,8 +111,6 @@ const Accounts = () => {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isModalOpen && <AddAccountModal onClose={() => setIsModalOpen(false)}
|
|
||||||
onAddAccount={handleAddAccount} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { MdKeyboardArrowDown } from 'react-icons/md';
|
|
||||||
|
|
||||||
interface DocumentPagesInterface {
|
|
||||||
selectedDocuments: DocumentType[];
|
|
||||||
setDropDownTogglePage: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
dropDownTogglePage: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DocumentPages = ({ selectedDocuments, setDropDownTogglePage, dropDownTogglePage }: DocumentPagesInterface) => {
|
|
||||||
return (
|
|
||||||
<div className='col-span-2 bg-white h-full p-4 overflow-y-auto'>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{selectedDocuments && selectedDocuments.length && selectedDocuments.map((document, indx) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={indx}
|
|
||||||
// tabIndex={1}
|
|
||||||
// onBlur={() => setDropDownTogglePage('')}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={() => (`${(dropDownTogglePage === 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`)}
|
|
||||||
className={`p-2 ${(dropDownTogglePage === 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
|
|
||||||
{
|
|
||||||
document.images != undefined && <img
|
|
||||||
key={indx}
|
|
||||||
className='rounded-sm shadow'
|
|
||||||
src={document.images[0].src}
|
|
||||||
alt="" />
|
|
||||||
}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between w-full text-xs rounded-sm cursor-pointer ${(dropDownTogglePage === 'pageDropDown' + indx) ? "text-slate-100" : "text-slate-600"}`}>
|
|
||||||
<p className={`whitespace-nowrap overflow-hidden text-ellipsis max-w-[18ch]`}>{document.name}</p>
|
|
||||||
<MdKeyboardArrowDown className={`${(dropDownTogglePage === 'pageDropDown' + indx) ? 'rotate-180' : ''}`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
dropDownTogglePage === 'pageDropDown' + indx &&
|
|
||||||
<div className='my-1 flex justify-center items-center gap-1 flex-wrap'>
|
|
||||||
{document.images != undefined && document.images.map((img, indx) => (
|
|
||||||
<a
|
|
||||||
href={"#" + document.documentId + indx}
|
|
||||||
className='max-w-max px-4 py-2 shadow cursor-pointer border-1 rounded-sm border-slate-200 hover:border-[var(--primary)] transition-all duration-300 bg-slate-300'
|
|
||||||
>
|
|
||||||
<p className='text-center text-xs text-slate-600'>{indx + 1}</p>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DocumentPages
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { RootState } from "../../store/store";
|
import { RootState } from "../../store/store";
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { DroppedItem, DroppedItemsObj } from "../../utlity/types";
|
import { DroppedItem } from "../../utlity/types";
|
||||||
|
|
||||||
import Loader from "../dashboard/Loader";
|
import Loader from "../dashboard/Loader";
|
||||||
import SignCon from "../signature/SignCon";
|
import SignCon from "../signature/SignCon";
|
||||||
|
|
@ -11,7 +11,7 @@ import { Bounce, toast } from "react-toastify";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
|
import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
|
||||||
|
|
||||||
import { FaRegImage, FaFileAlt, FaSignature, FaStamp, FaCheck, FaQuestionCircle } from 'react-icons/fa'
|
import { FaRegImage, FaFileAlt, FaSignature, FaStamp } from 'react-icons/fa'
|
||||||
import { IoIosDocument } from 'react-icons/io'
|
import { IoIosDocument } from 'react-icons/io'
|
||||||
import { MdKeyboardArrowDown } from 'react-icons/md'
|
import { MdKeyboardArrowDown } from 'react-icons/md'
|
||||||
import { TbChevronsDownLeft, TbSignature } from "react-icons/tb";
|
import { TbChevronsDownLeft, TbSignature } from "react-icons/tb";
|
||||||
|
|
@ -20,10 +20,6 @@ import dayjs from "dayjs";
|
||||||
import maskUtils from "../../utlity/maskUtils";
|
import maskUtils from "../../utlity/maskUtils";
|
||||||
import StampUpload from "../stamp/StampUpload";
|
import StampUpload from "../stamp/StampUpload";
|
||||||
import TermsModal from "./TermsModal";
|
import TermsModal from "./TermsModal";
|
||||||
import DocumentPages from "./DocumentPages";
|
|
||||||
import FieldList from "../process/processStepTwo/FieldList";
|
|
||||||
import typeToIconMap from "../../helper/iconMap";
|
|
||||||
import { droppedTagsDefaultField } from "../process/processStepTwo/DocumentContainer";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,30 +100,6 @@ export interface StampTypeObj {
|
||||||
blob: string;
|
blob: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FIT {
|
|
||||||
label: string,
|
|
||||||
value: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const fitObj = [
|
|
||||||
{
|
|
||||||
label: "Fit",
|
|
||||||
value: "FIT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "50%",
|
|
||||||
value: "50"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "150%",
|
|
||||||
value: "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "200%",
|
|
||||||
value: "200"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Functions - LIST
|
// Functions - LIST
|
||||||
// handleSignClick - Handle when sign || Initial tag got clicked
|
// handleSignClick - Handle when sign || Initial tag got clicked
|
||||||
// fetchData - Fetch document images and tags data
|
// fetchData - Fetch document images and tags data
|
||||||
|
|
@ -143,12 +115,9 @@ const SignDocument = () => {
|
||||||
const [tagJson, setTagJson] = useState<DroppedItem[] | []>([]);
|
const [tagJson, setTagJson] = useState<DroppedItem[] | []>([]);
|
||||||
const [transaction, setTransaction] = useState<TransactionType | null>(null);
|
const [transaction, setTransaction] = useState<TransactionType | null>(null);
|
||||||
const [recSignStatus, setRecSignStatus] = useState<string | null>(null);
|
const [recSignStatus, setRecSignStatus] = useState<string | null>(null);
|
||||||
const [droppedItems, setDroppedItems] = useState([]);
|
|
||||||
|
|
||||||
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
|
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
|
||||||
|
|
||||||
console.log(recipient, "recipient Line 145")
|
|
||||||
|
|
||||||
// Handling Date Opacity
|
// Handling Date Opacity
|
||||||
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
|
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
|
||||||
const [isDateOpen, setIsDateOpen] = useState<string | null>(null);
|
const [isDateOpen, setIsDateOpen] = useState<string | null>(null);
|
||||||
|
|
@ -157,9 +126,6 @@ const SignDocument = () => {
|
||||||
// Show is fields - Showing all fields There
|
// Show is fields - Showing all fields There
|
||||||
const [isFields, setIsFields] = useState<boolean>(true);
|
const [isFields, setIsFields] = useState<boolean>(true);
|
||||||
|
|
||||||
const [isFitDd, setIsFitDd] = useState<boolean>(false)
|
|
||||||
const [selFitIndx, setSelFitIndx] = useState<number>(0);
|
|
||||||
|
|
||||||
const globalLoader = useSelector((state: RootState) => state.loader)
|
const globalLoader = useSelector((state: RootState) => state.loader)
|
||||||
|
|
||||||
const [openSign, setOpenSign] = useState<boolean>(false);
|
const [openSign, setOpenSign] = useState<boolean>(false);
|
||||||
|
|
@ -180,21 +146,6 @@ const SignDocument = () => {
|
||||||
|
|
||||||
const [signConfObj, setSignConfObj] = useState<SignConfTypeObj | {}>({});
|
const [signConfObj, setSignConfObj] = useState<SignConfTypeObj | {}>({});
|
||||||
|
|
||||||
const zoom = (percentage: number, contentClass: string) => {
|
|
||||||
const content = document.querySelector(`.${contentClass}`);
|
|
||||||
if (content) {
|
|
||||||
(content as HTMLElement).style.zoom = `${percentage}%`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let indx = fitObj[selFitIndx].value
|
|
||||||
if (indx === "FIT") {
|
|
||||||
indx = "100";
|
|
||||||
}
|
|
||||||
zoom(Number(indx), 'zoom-con')
|
|
||||||
}, [selFitIndx])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(showLoader("Loading ..."));
|
dispatch(showLoader("Loading ..."));
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
@ -202,19 +153,18 @@ const SignDocument = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!focusElement) {
|
if (!focusElement) {
|
||||||
if (droppedItems && droppedItems.length) {
|
if (tagJson && tagJson.length) {
|
||||||
setFocusElement({ ...droppedItems[0], focusElementIndx: 0 });
|
setFocusElement({ ...tagJson[0], focusElementIndx: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [droppedItems])
|
}, [tagJson])
|
||||||
|
|
||||||
console.log(saved, "Line 162")
|
console.log(saved, "Line 162")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recSignStatus === "COMPLETED") {
|
if (recSignStatus === "COMPLETE") {
|
||||||
console.log("SETTING HIDE TAGS to TRUE LIne 161")
|
console.log("SETTING HIDE TAGS to TRUE LIne 161")
|
||||||
// setHideTags(true);
|
setHideTags(true);
|
||||||
setTransparentTags(true);
|
|
||||||
} else {
|
} else {
|
||||||
console.log("SETTING HIDE TAGS to TRUE LIne 161")
|
console.log("SETTING HIDE TAGS to TRUE LIne 161")
|
||||||
setHideTags(false);
|
setHideTags(false);
|
||||||
|
|
@ -289,12 +239,9 @@ const SignDocument = () => {
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.dir(res)
|
console.dir(res)
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
|
|
||||||
console.log(res, "Line 288")
|
|
||||||
|
|
||||||
setSelectedDocuments(res.data.documents)
|
setSelectedDocuments(res.data.documents)
|
||||||
setRecipient(res.data.recipient)
|
setRecipient(res.data.recipient)
|
||||||
setDroppedItems(res.data.tagJsons)
|
setTagJson(res.data.tagJsons)
|
||||||
setTransaction(res.data.transaction)
|
setTransaction(res.data.transaction)
|
||||||
setRecSignStatus(res.data.status)
|
setRecSignStatus(res.data.status)
|
||||||
|
|
||||||
|
|
@ -311,7 +258,7 @@ const SignDocument = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
console.log(droppedItems, "Line 261")
|
console.log(tagJson, "Line 261")
|
||||||
|
|
||||||
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -319,7 +266,7 @@ const SignDocument = () => {
|
||||||
tags: []
|
tags: []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!droppedItems) return;
|
if (!tagJson) return;
|
||||||
|
|
||||||
// const form = e.currentTarget;
|
// const form = e.currentTarget;
|
||||||
// const formData = new FormData(form);
|
// const formData = new FormData(form);
|
||||||
|
|
@ -331,8 +278,8 @@ const SignDocument = () => {
|
||||||
// });
|
// });
|
||||||
|
|
||||||
let signInitialsCount = 0;
|
let signInitialsCount = 0;
|
||||||
for (let i = 0; i < droppedItems.length; i++) {
|
for (let i = 0; i < tagJson.length; i++) {
|
||||||
if (droppedItems[i].type === "SIGNATURE" || droppedItems[i].type === "INITIALS") {
|
if (tagJson[i].type === "SIGNATURE" || tagJson[i].type === "INITIALS") {
|
||||||
signInitialsCount += 1;
|
signInitialsCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +301,7 @@ const SignDocument = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tag of droppedItems) {
|
for (const tag of tagJson) {
|
||||||
console.log(tag, "Tag Data")
|
console.log(tag, "Tag Data")
|
||||||
|
|
||||||
// if ((tag.type === "SIGNATURE" || tag.subType === "INITIALS") && !tag.value) {
|
// if ((tag.type === "SIGNATURE" || tag.subType === "INITIALS") && !tag.value) {
|
||||||
|
|
@ -500,37 +447,19 @@ const SignDocument = () => {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const fetchSignatureData = async (signatureId: string) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/signatures/${signatureId}`, {
|
|
||||||
headers: { "ngrok-skip-browser-warning": "69420" }
|
|
||||||
});
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.data.signature;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching signature data:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
if (tagJson) {
|
||||||
if (droppedItems) {
|
for (let i = 0; i < tagJson?.length; i++) {
|
||||||
for (const tag of droppedItems) {
|
|
||||||
if (tag.type === "SIGNATURE" && "value" in tag) {
|
|
||||||
const signatureData = await fetchSignatureData(tag.value);
|
|
||||||
console.log(signatureData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})()
|
}
|
||||||
}, [droppedItems])
|
}, [tagJson])
|
||||||
|
|
||||||
|
|
||||||
const handleTagUpdate = (updateIndx: number, updateValue: string) => {
|
const handleTagUpdate = (updateIndx: number, updateValue: string) => {
|
||||||
if (droppedItems && droppedItems.length) {
|
if (tagJson && tagJson.length) {
|
||||||
const updatedTag = droppedItems.map((val, indx) => {
|
const updatedTag = tagJson.map((val, indx) => {
|
||||||
if (indx === updateIndx) {
|
if (indx === updateIndx) {
|
||||||
return {
|
return {
|
||||||
...val,
|
...val,
|
||||||
|
|
@ -539,7 +468,7 @@ const SignDocument = () => {
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
})
|
})
|
||||||
setDroppedItems(updatedTag);
|
setTagJson(updatedTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// setTagJson((prev) => {
|
// setTagJson((prev) => {
|
||||||
|
|
@ -551,198 +480,6 @@ const SignDocument = () => {
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// HANDLE DROP - START
|
|
||||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
|
|
||||||
console.log("// HANDLE DROP - START")
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const data = window.dragElemData;
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
const dragElemData = JSON.parse(data); // Get the dropped label
|
|
||||||
|
|
||||||
const field = { ...dragElemData }
|
|
||||||
if (!field.label) return;
|
|
||||||
|
|
||||||
// const field = fields.find((item) => item.label === dragElemData.label); // Find corresponding field
|
|
||||||
// if (!field) return; // If not found, exit
|
|
||||||
|
|
||||||
const dropZone = event.currentTarget.getBoundingClientRect();
|
|
||||||
const x = event.clientX - dropZone.left - dragElemData.offsetX;
|
|
||||||
const y = event.clientY - dropZone.top - dragElemData.offsetY;
|
|
||||||
|
|
||||||
const key = `${docId}_${pageIndex}`;
|
|
||||||
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
|
|
||||||
|
|
||||||
let prepData = {
|
|
||||||
properties: data.properties,
|
|
||||||
icon: typeToIconMap[field.label.toUpperCase()] || <FaQuestionCircle />,
|
|
||||||
type: dragElemData.label.toUpperCase(),
|
|
||||||
subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '',
|
|
||||||
label: dragElemData.label,
|
|
||||||
dimensions: {
|
|
||||||
left: x,
|
|
||||||
top: y,
|
|
||||||
width: dragElemData.width,
|
|
||||||
height: dragElemData.height,
|
|
||||||
},
|
|
||||||
styles: dragElemData.label === "text" ? {
|
|
||||||
"font": "Great Vibes",
|
|
||||||
"italic": false,
|
|
||||||
"bold": false,
|
|
||||||
"underline": false,
|
|
||||||
"color": "black",
|
|
||||||
"textSize": 14.0
|
|
||||||
} : null,
|
|
||||||
documentId: docId,
|
|
||||||
pageIndex: pageIndex,
|
|
||||||
recipientId: dragElemData.recipientId,
|
|
||||||
recipientIndx: dragElemData.recipientIndx,
|
|
||||||
update: true,
|
|
||||||
isDeleted: false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("group" in dragElemData) {
|
|
||||||
prepData.group = dragElemData.group;
|
|
||||||
prepData.isGroupExist = false;
|
|
||||||
|
|
||||||
// if(uniqGroupIds.includes(dragElemData.group)) {
|
|
||||||
// prepData.isGroupExist = true;
|
|
||||||
// } else {
|
|
||||||
// prepData.isGroupExist = false;
|
|
||||||
// setUniqGroupIds([...uniqGroupIds, dragElemData.group]);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prepData.type === "temporal") {
|
|
||||||
prepData.currentTimeStamp = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prepData.type.toLowerCase() === "number") {
|
|
||||||
prepData.decimalPlace = 0;
|
|
||||||
prepData.isCurrency = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// const dataToSet: DroppedItemsObj = {
|
|
||||||
// ...droppedItems,
|
|
||||||
// [key]: [...(droppedItems[key] || []),
|
|
||||||
// prepData],
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const dataToSet: DroppedItemsObj = {
|
|
||||||
// ...droppedItems,
|
|
||||||
// [key]: [...(droppedItems[key] || []),
|
|
||||||
// {
|
|
||||||
// ...prepData,
|
|
||||||
// radioGroups: dragElemData.label === "radio" ?
|
|
||||||
// [
|
|
||||||
// {
|
|
||||||
// ...prepData,
|
|
||||||
// dimensions: {
|
|
||||||
// ...prepData.dimensions, top: 8, left: 10
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// ...prepData, dimensions: {
|
|
||||||
// ...prepData.dimensions,
|
|
||||||
// top: 8,
|
|
||||||
// left: 10
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// :
|
|
||||||
// []
|
|
||||||
// }],
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (dragElemData.label === "radio") {
|
|
||||||
// setDroppedItems(dataToSet);
|
|
||||||
// } else {
|
|
||||||
// setDroppedItems(dataToSet);
|
|
||||||
// }
|
|
||||||
|
|
||||||
setDroppedItems((prev) => [...prev, prepData]);
|
|
||||||
// setDroppedItemsCounter((prev) => prev + 1);
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
|
|
||||||
// handleTagClick(key, tagIndex)
|
|
||||||
// }, 0)
|
|
||||||
};
|
|
||||||
// HANDLE DROP - END
|
|
||||||
|
|
||||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
event.preventDefault(); // Allows the drop event to work
|
|
||||||
};
|
|
||||||
|
|
||||||
// HANDLE TAG MOUSE DOWN - START
|
|
||||||
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
|
|
||||||
let targetElement = event?.currentTarget;
|
|
||||||
|
|
||||||
if (!targetContainer || !targetElement) return;
|
|
||||||
|
|
||||||
const containerRect = targetContainer.getBoundingClientRect();
|
|
||||||
const elementRect = targetElement.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Calculate the initial offset (where the mouse was clicked inside the element)
|
|
||||||
const offsetX = event.clientX - elementRect.left;
|
|
||||||
const offsetY = event.clientY - elementRect.top;
|
|
||||||
|
|
||||||
const conScrollWidth = targetContainer?.scrollWidth;
|
|
||||||
const conScrollHeight = targetContainer?.scrollHeight;
|
|
||||||
|
|
||||||
let isGroupAttr = event.currentTarget.hasAttribute("group");
|
|
||||||
let groupId = null;
|
|
||||||
if (isGroupAttr) {
|
|
||||||
groupId = event.currentTarget.getAttribute("group");
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupElements: NodeListOf<HTMLElement> | null = null
|
|
||||||
if (isGroupAttr && groupId) {
|
|
||||||
groupElements = document.querySelectorAll(`div[group="${groupId}"]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
|
||||||
// Get new X and Y positions relative to the container
|
|
||||||
let left = event.clientX - containerRect.left - offsetX;
|
|
||||||
let top = event.clientY - containerRect.top - offsetY;
|
|
||||||
|
|
||||||
|
|
||||||
// **Restrict movement within container bounds**
|
|
||||||
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
|
|
||||||
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
|
|
||||||
|
|
||||||
console.log(left, top, "LEFTTOP")
|
|
||||||
|
|
||||||
if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
|
|
||||||
|
|
||||||
// Apply the movement to the element
|
|
||||||
setDroppedItems(
|
|
||||||
droppedItems.map((item, i) =>
|
|
||||||
i === index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
document.removeEventListener("pointermove", handleMouseMove);
|
|
||||||
document.removeEventListener("pointerup", handleMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add event listeners
|
|
||||||
document.addEventListener("pointermove", handleMouseMove);
|
|
||||||
document.addEventListener("pointerup", handleMouseUp);
|
|
||||||
};
|
|
||||||
// HANDLE TAG MOUSE DOWN - END
|
|
||||||
|
|
||||||
|
|
||||||
console.log(droppedItems, "DROPPED ITEMS LINE 680")
|
|
||||||
|
|
||||||
// const [showTerms, setShowTerms] = useState<boolean>(true);
|
// const [showTerms, setShowTerms] = useState<boolean>(true);
|
||||||
return (
|
return (
|
||||||
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>
|
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>
|
||||||
|
|
@ -761,40 +498,6 @@ const SignDocument = () => {
|
||||||
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
<p className='tracking-wide font-semibold text-[1rem] text-[var(--primary)]'>Sign Made Easy</p>
|
<p className='tracking-wide font-semibold text-[1rem] text-[var(--primary)]'>Sign Made Easy</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* FIT - START */}
|
|
||||||
<div
|
|
||||||
tabIndex={1}
|
|
||||||
onBlur={() => setIsFitDd(false)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={() => setIsFitDd(!isFitDd)}
|
|
||||||
className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
|
||||||
<p className='tracking-wide'>Fit</p>
|
|
||||||
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isFitDd && <div className='absolute bg-white rounded-sm z-50'>
|
|
||||||
{fitObj.map((val, indx) => {
|
|
||||||
return (<div
|
|
||||||
key={indx}
|
|
||||||
onClick={() => setSelFitIndx(indx)}
|
|
||||||
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
|
|
||||||
<div
|
|
||||||
|
|
||||||
className='flex items-center justify-between w-full'>
|
|
||||||
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
|
|
||||||
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
{/* FIT - END */}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -844,6 +547,9 @@ const SignDocument = () => {
|
||||||
|
|
||||||
<p>{recipient && recipient.firstName} {recipient && recipient.lastName}</p>
|
<p>{recipient && recipient.firstName} {recipient && recipient.lastName}</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/* Right Side - ENDS */}
|
{/* Right Side - ENDS */}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -854,23 +560,65 @@ const SignDocument = () => {
|
||||||
|
|
||||||
{/* Left Side pages start */}
|
{/* Left Side pages start */}
|
||||||
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
|
{/* <div className="lg:col-span-2 bg-slate-100 h-full p-4 overflow-y-auto"></div> */}
|
||||||
|
|
||||||
<div className='col-span-2 bg-white h-full p-4 overflow-y-auto'>
|
<div className='col-span-2 bg-white h-full p-4 overflow-y-auto'>
|
||||||
{recipientId && <FieldList color={"#8f87f1"} recipientId={Number(recipientId)} recipientIndx={0} acntId={"1"} />}
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{selectedDocuments && selectedDocuments.length && selectedDocuments.map((document, indx) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
// tabIndex={1}
|
||||||
|
// onBlur={() => setDropDownTogglePage('')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => setDropDownTogglePage(`${(dropDownTogglePage === 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`)}
|
||||||
|
className={`p-2 ${(dropDownTogglePage === 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
|
||||||
|
{
|
||||||
|
document.images != undefined && <img
|
||||||
|
key={indx}
|
||||||
|
className='rounded-sm shadow'
|
||||||
|
src={document.images[0].src}
|
||||||
|
alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between w-full text-xs rounded-sm cursor-pointer ${(dropDownTogglePage === 'pageDropDown' + indx) ? "text-slate-100" : "text-slate-600"}`}>
|
||||||
|
<p className={`whitespace-nowrap overflow-hidden text-ellipsis max-w-[18ch]`}>{document.name}</p>
|
||||||
|
<MdKeyboardArrowDown className={`${(dropDownTogglePage === 'pageDropDown' + indx) ? 'rotate-180' : ''}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
dropDownTogglePage === 'pageDropDown' + indx &&
|
||||||
|
<div className='my-1 flex justify-center items-center gap-1 flex-wrap'>
|
||||||
|
{document.images != undefined && document.images.map((img, indx) => (
|
||||||
|
<a
|
||||||
|
href={"#" + document.documentId + indx}
|
||||||
|
className='max-w-max px-4 py-2 shadow cursor-pointer border-1 rounded-sm border-slate-200 hover:border-[var(--primary)] transition-all duration-300 bg-slate-300'
|
||||||
|
>
|
||||||
|
<p className='text-center text-xs text-slate-600'>{indx + 1}</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Pages - END */}
|
{/* Pages - END */}
|
||||||
|
|
||||||
{/* Main container */}
|
{/* Main container */}
|
||||||
<div
|
<div
|
||||||
id="doc-con"
|
id="doc-con"
|
||||||
className="zoom-con relative col-span-8 px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth"
|
className="relative col-span-10 px-4 bg-slate-100 h-full w-full overflow-auto scroll-smooth"
|
||||||
>
|
>
|
||||||
|
|
||||||
{
|
{
|
||||||
selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => {
|
selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => {
|
||||||
console.log(droppedItems, "tagJsontagJsontagJson")
|
console.log(tagJson, "tagJsontagJsontagJson")
|
||||||
return (
|
return (
|
||||||
<div key={doc.documentId} className='flex flex-col'>
|
<div key={doc.documentId} className='flex flex-col'>
|
||||||
<div className='py-2 text-sm'>
|
<div className='py-2 text-sm'>
|
||||||
|
|
@ -879,20 +627,16 @@ const SignDocument = () => {
|
||||||
|
|
||||||
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'>
|
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'>
|
||||||
{
|
{
|
||||||
doc.images && doc.images.length > 0 && doc.images.map((page, indx) => {
|
tagJson && tagJson.length > 0 && doc.images && doc.images.length > 0 && doc.images.map((page, indx) => {
|
||||||
const key = `${doc.documentId}_${page.index}`;
|
const key = `${doc.documentId}_${page.index}`;
|
||||||
console.log(droppedItems, "line 820")
|
const currDocTags = tagJson.filter((val) => val.pageIndex === page.index)
|
||||||
// const tagJsonData = [...droppedItems];
|
console.log(currDocTags, tagJson, page, doc, "innnnnnnnnnnnn")
|
||||||
// console.log(tagJsonData, "Line 821")
|
|
||||||
|
|
||||||
const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index)
|
|
||||||
console.log(currDocTags, droppedItems, page, doc, "innnnnnnnnnnnn")
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={indx}
|
key={indx}
|
||||||
id={doc.documentId + indx}
|
id={doc.documentId + indx}
|
||||||
onDrop={(e) => handleDrop(e, doc.documentId, doc.documentId, page.index)}
|
// onDrop={(e) => handleDrop(e, doc.documentId, doc.documentId, page.index)}
|
||||||
onDragOver={(e) => handleDragOver(e)}
|
// onDragOver={handleDragOver}
|
||||||
className='drop-page-in shadow-md relative'
|
className='drop-page-in shadow-md relative'
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
@ -900,15 +644,13 @@ const SignDocument = () => {
|
||||||
|
|
||||||
|
|
||||||
{currDocTags.map((item, index) => {
|
{currDocTags.map((item, index) => {
|
||||||
console.log(item, "micCheck 842")
|
|
||||||
if (item.pageIndex !== page.index) return;
|
if (item.pageIndex !== page.index) return;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
onFocus={() => setFocusElement({ ...item, focusElementIndx: index })}
|
onFocus={() => setFocusElement({ ...item, focusElementIndx: index })}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
// draggable="false"
|
draggable="false"
|
||||||
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
|
|
||||||
className="absolute rounded dropped-item text-sm"
|
className="absolute rounded dropped-item text-sm"
|
||||||
// onClick={() => handleTagClick(key, index)}
|
// onClick={() => handleTagClick(key, index)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -926,7 +668,7 @@ const SignDocument = () => {
|
||||||
<div
|
<div
|
||||||
id={item.id}
|
id={item.id}
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "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-none" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
|
||||||
// style={{ height: item.dimensions.height + "px" }}
|
// style={{ height: item.dimensions.height + "px" }}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
@ -1225,7 +967,7 @@ const SignDocument = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* tag pagination Start */}
|
{/* tag pagination Start */}
|
||||||
{/* {!transparentTags && !hideTags && <div className="sticky rounded-md bottom-2 left-[50%] translate-[-50%] max-w-max flex bg-slate-900 text-white text-sm opacity-70">
|
{!transparentTags && !hideTags && <div className="sticky rounded-md bottom-2 left-[50%] translate-[-50%] max-w-max flex bg-slate-900 text-white text-sm opacity-70">
|
||||||
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-l-md transition-all duration-150 "
|
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-l-md transition-all duration-150 "
|
||||||
onClick={() => handleTagNavClick("P")}
|
onClick={() => handleTagNavClick("P")}
|
||||||
>Prev</div>
|
>Prev</div>
|
||||||
|
|
@ -1233,14 +975,10 @@ const SignDocument = () => {
|
||||||
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-r-md transition-all duration-150 "
|
<div className="px-3 py-1 cursor-pointer hover:bg-black rounded-r-md transition-all duration-150 "
|
||||||
onClick={() => handleTagNavClick("N")}
|
onClick={() => handleTagNavClick("N")}
|
||||||
>Next</div>
|
>Next</div>
|
||||||
</div>} */}
|
</div>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<DocumentPages selectedDocuments={selectedDocuments} setDropDownTogglePage={setDropDownTogglePage} dropDownTogglePage={dropDownTogglePage} />
|
|
||||||
|
|
||||||
|
|
||||||
{/* Right Side - START */}
|
{/* Right Side - START */}
|
||||||
{/* <div className="col-span-2 bg-slate-100 h-full p-4 overflow-y-auto">
|
{/* <div className="col-span-2 bg-slate-100 h-full p-4 overflow-y-auto">
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import confetti from 'canvas-confetti'
|
||||||
import { useWindowSize } from 'react-use'
|
import { useWindowSize } from 'react-use'
|
||||||
import { FaCheckCircle } from 'react-icons/fa'
|
import { FaCheckCircle } from 'react-icons/fa'
|
||||||
import { IoClose } from 'react-icons/io5'
|
import { IoClose } from 'react-icons/io5'
|
||||||
import CommonNav from '../CommonNav'
|
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const [showSideBar, setShowSideBar] = React.useState(true)
|
const [showSideBar, setShowSideBar] = React.useState(true)
|
||||||
|
|
@ -103,32 +102,32 @@ function Dashboard() {
|
||||||
}, [documentSignType])
|
}, [documentSignType])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showConfetti) {
|
if (showConfetti) {
|
||||||
const duration = 3000;
|
const duration = 3000;
|
||||||
const end = Date.now() + duration;
|
const end = Date.now() + duration;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (Date.now() > end) {
|
if (Date.now() > end) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
confetti({
|
confetti({
|
||||||
particleCount: 80, // more particles
|
particleCount: 80, // more particles
|
||||||
angle: Math.random() * 360, // fire in random direction
|
angle: Math.random() * 360, // fire in random direction
|
||||||
spread: 220, // wider spread
|
spread: 220, // wider spread
|
||||||
startVelocity: 15, // more power
|
startVelocity: 15, // more power
|
||||||
scalar: 1.2, // bigger pieces (default is 1)
|
scalar: 1.2, // bigger pieces (default is 1)
|
||||||
origin: {
|
origin: {
|
||||||
x: Math.random(),
|
x: Math.random(),
|
||||||
y: Math.random() * 0.6,
|
y: Math.random() * 0.6,
|
||||||
},
|
},
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
});
|
});
|
||||||
}, 350); // fire every 150ms
|
}, 350); // fire every 150ms
|
||||||
}
|
}
|
||||||
}, [showConfetti]);
|
}, [showConfetti]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -174,7 +173,6 @@ function Dashboard() {
|
||||||
<div className={`overflow-y-auto absolute ${showSideBar ? 'w-[calc(100%-250px)]' : 'w-[calc(100%-60px)]'} h-full right-0 bottom-0 overflow-hidden bg-[var(--mainViewBg)] transition-all duration-300`}>
|
<div className={`overflow-y-auto absolute ${showSideBar ? 'w-[calc(100%-250px)]' : 'w-[calc(100%-60px)]'} h-full right-0 bottom-0 overflow-hidden bg-[var(--mainViewBg)] transition-all duration-300`}>
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className='px-2 py-1.5'>
|
<div className='px-2 py-1.5'>
|
||||||
<CommonNav />
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { IoSettingsOutline } from "react-icons/io5";
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { MdManageAccounts } from 'react-icons/md';
|
import { MdManageAccounts } from 'react-icons/md';
|
||||||
import { GrTransaction } from 'react-icons/gr';
|
|
||||||
|
|
||||||
type SideBarProps = {
|
type SideBarProps = {
|
||||||
showSideBar: boolean;
|
showSideBar: boolean;
|
||||||
|
|
@ -25,8 +24,7 @@ const menuPathMap: MenuPathMap = {
|
||||||
'/': 'Home',
|
'/': 'Home',
|
||||||
'/templates': 'Templates',
|
'/templates': 'Templates',
|
||||||
'/s/contact': 'Contact',
|
'/s/contact': 'Contact',
|
||||||
// '/accounts': 'Accounts',
|
'/accounts': 'Accounts',
|
||||||
'transactions' : "Account"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
||||||
|
|
@ -42,7 +40,6 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
||||||
headers: { "ngrok-skip-browser-warning": "69420" }
|
headers: { "ngrok-skip-browser-warning": "69420" }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
sessionStorage.clear();
|
|
||||||
// Redirect to login or refresh the page after logout
|
// Redirect to login or refresh the page after logout
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -64,8 +61,7 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
||||||
<SendForSign showSideBar={showSideBar} />
|
<SendForSign showSideBar={showSideBar} />
|
||||||
<NavLink to="/" icon={<FaHome />} label="Home" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
<NavLink to="/" icon={<FaHome />} label="Home" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
{/* <NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */}
|
<NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
<NavLink to="transactions" icon={<GrTransaction />} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,328 +1,97 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import { FaCheckCircle, FaClock, FaHourglassHalf, FaTasks } from 'react-icons/fa';
|
||||||
FaCheckCircle,
|
|
||||||
FaClock,
|
|
||||||
FaHourglassHalf,
|
|
||||||
FaTasks,
|
|
||||||
FaChevronDown,
|
|
||||||
FaPlus,
|
|
||||||
FaUserPlus,
|
|
||||||
FaFilter,
|
|
||||||
FaCalendarAlt
|
|
||||||
} from 'react-icons/fa';
|
|
||||||
|
|
||||||
// Define a type for card data
|
const cards = [
|
||||||
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: 'Active Accounts',
|
title: 'Action Required',
|
||||||
count: 12,
|
count: 12,
|
||||||
icon: <FaTasks size={18} className="text-blue-600" />,
|
icon: <FaTasks size={20} className="text-orange-500" />,
|
||||||
gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
|
ring: 'ring-orange-400/20',
|
||||||
shadowColor: 'shadow-blue-200/50',
|
bg: 'bg-orange-50',
|
||||||
iconBg: 'bg-blue-100 border-blue-200',
|
|
||||||
textColor: 'text-blue-700',
|
|
||||||
trend: 'up'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'In-Progress',
|
title: 'Waiting for Others',
|
||||||
count: 8,
|
count: 8,
|
||||||
icon: <FaClock size={18} className="text-purple-600" />,
|
icon: <FaClock size={20} className="text-yellow-500" />,
|
||||||
gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
|
ring: 'ring-yellow-400/20',
|
||||||
shadowColor: 'shadow-purple-200/50',
|
bg: 'bg-yellow-50',
|
||||||
iconBg: 'bg-purple-100 border-purple-200',
|
},
|
||||||
textColor: 'text-purple-700',
|
{
|
||||||
trend: 'stable'
|
title: 'Expiring Soon',
|
||||||
|
count: 5,
|
||||||
|
icon: <FaHourglassHalf size={20} className="text-red-500" />,
|
||||||
|
ring: 'ring-red-400/20',
|
||||||
|
bg: 'bg-red-50',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: 'Completed',
|
title: 'Completed',
|
||||||
count: 27,
|
count: 27,
|
||||||
icon: <FaCheckCircle size={18} className="text-green-600" />,
|
icon: <FaCheckCircle size={20} className="text-green-500" />,
|
||||||
gradient: 'bg-gradient-to-br from-green-50 to-green-100',
|
ring: 'ring-green-400/20',
|
||||||
shadowColor: 'shadow-green-200/50',
|
bg: 'bg-green-50',
|
||||||
iconBg: 'bg-green-100 border-green-200',
|
|
||||||
textColor: 'text-green-700',
|
|
||||||
trend: 'up'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Inactive',
|
|
||||||
count: 5,
|
|
||||||
icon: <FaHourglassHalf size={18} className="text-gray-600" />,
|
|
||||||
gradient: 'bg-gradient-to-br from-gray-50 to-gray-100',
|
|
||||||
shadowColor: 'shadow-gray-200/50',
|
|
||||||
iconBg: 'bg-gray-100 border-gray-200',
|
|
||||||
textColor: 'text-gray-700',
|
|
||||||
trend: 'down'
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year'];
|
const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year'];
|
||||||
const accounts = ['All Accounts', 'Account 1', 'Account 2', 'Account 3'];
|
|
||||||
|
|
||||||
const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRangeChange, cards, isLoading }) => {
|
const Cards = () => {
|
||||||
const [selectedFilter, setSelectedFilter] = useState<string>('Last 7 Days');
|
const [selectedFilter, setSelectedFilter] = useState('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 (
|
return (
|
||||||
<div className="w-full px-4 py-3 mt-2 rounded-md">
|
<div className="w-full px-4 py-3">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-2 gap-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<h2 className="text-lg font-medium text-gray-700">Overview</h2>
|
||||||
{/* <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 className="flex gap-2 w-full md:w-auto">
|
||||||
</div> */}
|
{/* Account Selector */}
|
||||||
<div>
|
<select
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Overview</h2>
|
value={'All'}
|
||||||
{/* <p className="text-gray-600 text-sm">Monitor performance metrics</p> */}
|
onChange={() => {}}
|
||||||
</div>
|
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
|
||||||
</div>
|
>
|
||||||
|
<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>
|
|
||||||
|
|
||||||
{/* Cards */}
|
{/* Cards */}
|
||||||
{isLoading ? <div className="w-full [h-50vh] px-6 py-8">
|
<div className="cursor-pointer grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div className="animate-pulse">
|
{cards.map((card) => (
|
||||||
<div className="h-8 bg-gray-200 rounded w-32 mb-8"></div>
|
<div
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
key={card.title}
|
||||||
{[...Array(7)].map((_, i) => (
|
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 key={i} className="h-32 bg-gray-200 rounded-xl"></div>
|
>
|
||||||
))}
|
<div className="flex items-center space-x-4">
|
||||||
</div>
|
<div className={`p-3 rounded-full ${card.bg} ${card.ring}`}>
|
||||||
</div>
|
{card.icon}
|
||||||
</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>
|
</div>
|
||||||
|
<div>
|
||||||
{/* Content */}
|
<p className="text-sm text-gray-500">{card.title}</p>
|
||||||
<div className="relative z-10">
|
<p className="text-xl font-bold text-gray-800">{card.count}</p>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Hover Effect */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Cards;
|
export default Cards;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* <div className="flex flex-wrap gap-3 w-full lg:w-auto">
|
|
||||||
{type === 'dashboard' && <>
|
|
||||||
Account Selector
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={selectedAccount}
|
|
||||||
onChange={(e) => setSelectedAccount(e.target.value)}
|
|
||||||
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
|
|
||||||
>
|
|
||||||
{accounts.map((account) => (
|
|
||||||
<option key={account} value={account}>{account}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<FaChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Date Filter
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={selectedFilter}
|
|
||||||
onChange={(e) => setSelectedFilter(e.target.value)}
|
|
||||||
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
|
|
||||||
>
|
|
||||||
{filters.map((filter) => (
|
|
||||||
<option key={filter} value={filter}>{filter}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<FaCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
|
|
||||||
</div>
|
|
||||||
</>}
|
|
||||||
|
|
||||||
Action Button
|
|
||||||
{buttonConfig && (
|
|
||||||
<button
|
|
||||||
className={`flex items-center gap-1 px-3 py-3 text-sm font-semibold text-white rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95 cursor-pointer ${buttonConfig.gradient}`}
|
|
||||||
onClick={onOpenModal}
|
|
||||||
>
|
|
||||||
{buttonConfig.icon}
|
|
||||||
{buttonConfig.text}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div> */}
|
|
||||||
|
|
@ -1,175 +1,22 @@
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React from 'react'
|
||||||
import AccountNav from '../accounts/AccountNav';
|
import HomeTopNav from './HomeTopNav'
|
||||||
import Cards from './Cards';
|
import UploadDoc from './UploadDoc'
|
||||||
import StartSection from './StartSection';
|
import FilterSort from './FilterSort'
|
||||||
import api from '../../config/api';
|
import ShowDocs from './ShowDocs'
|
||||||
import {
|
import Cards from './Cards'
|
||||||
FaCheckCircle,
|
import StartSection from './StartSection'
|
||||||
FaClock,
|
|
||||||
FaHourglassHalf,
|
|
||||||
FaTasks,
|
|
||||||
FaChevronDown,
|
|
||||||
FaPlus,
|
|
||||||
FaUserPlus,
|
|
||||||
FaFilter,
|
|
||||||
FaCalendarAlt
|
|
||||||
} from 'react-icons/fa';
|
|
||||||
|
|
||||||
interface CardData {
|
|
||||||
title: string;
|
|
||||||
count: number;
|
|
||||||
icon: JSX.Element;
|
|
||||||
gradient: string;
|
|
||||||
shadowColor: string;
|
|
||||||
iconBg: string;
|
|
||||||
textColor: string;
|
|
||||||
percentage?: number;
|
|
||||||
trend?: 'up' | 'down' | 'stable';
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Cards
|
<HomeTopNav />
|
||||||
onDateRangeChange={handleDateRangeChange}
|
<Cards />
|
||||||
dashboardData={dashboardData}
|
|
||||||
fromDate={fromDate}
|
|
||||||
toDate={toDate}
|
|
||||||
cards={cards}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
<StartSection />
|
<StartSection />
|
||||||
|
{/* <UploadDoc /> */}
|
||||||
|
{/* <FilterSort /> */}
|
||||||
|
{/* <ShowDocs /> */}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home
|
||||||
|
|
@ -33,7 +33,7 @@ const HomeTopNav = () => {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="settings"
|
to="/s/settings"
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const StartSection = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="px-4 py-3 w-full">
|
<section className="px-4 py-3 w-full">
|
||||||
<h2 className="text-lg font-semibold text-gray-800 mb-2">Get Started</h2>
|
<h2 className="text-lg font-semibold text-gray-800 mb-6">Get Started</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{cards.map((card, index) => (
|
{cards.map((card, index) => (
|
||||||
|
|
|
||||||
|
|
@ -1,356 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { RootState } from "../../../store/store";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { setImages } from "../../../store/slices/selectedDocsSlice";
|
import { setImages } from "../../../store/slices/selectedDocsSlice";
|
||||||
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
|
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
|
||||||
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
|
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
|
||||||
import { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
|
import { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
|
||||||
import Loader from "../../dashboard/Loader";
|
import Loader from "../../dashboard/Loader";
|
||||||
|
|
@ -22,24 +22,15 @@ const ProcessStepOne = () => {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
const templateId = useParams<{ templateId: string }>().templateId;
|
|
||||||
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
if (!transactionId) {
|
|
||||||
transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
|
|
||||||
transactionId = templateId;
|
|
||||||
dispatch(toggleToTemplates());
|
|
||||||
}
|
|
||||||
console.log("ProcessStepOne called with accountId:", accountId, "and transactionId:", transactionId);
|
|
||||||
if (!accountId || !transactionId) {
|
if (!accountId || !transactionId) {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||||||
// const { selectedDocuments, signType } = useSelector((state: RootState) => state.selectedDocuments)
|
// const { selectedDocuments, signType } = useSelector((state: RootState) => state.selectedDocuments)
|
||||||
|
|
||||||
|
|
@ -62,30 +53,10 @@ const ProcessStepOne = () => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleNextClick = async (isSendNow: boolean) => {
|
const handleNextClick = async (isSendNow: boolean) => {
|
||||||
let formError = {
|
if (!selectedDocuments.length || !selectedRecipients.length) {
|
||||||
documents: false,
|
|
||||||
recipients: false,
|
|
||||||
subject: false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedDocuments.length) {
|
|
||||||
formError.documents = true
|
|
||||||
}
|
|
||||||
if (!selectedRecipients.length) {
|
|
||||||
formError.recipients = true
|
|
||||||
}
|
|
||||||
if (!emailDtls.subject.length) {
|
|
||||||
formError.subject = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formError.documents || formError.recipients || formError.subject) {
|
|
||||||
console.log(selectedDocuments, 'line 193')
|
console.log(selectedDocuments, 'line 193')
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
toast.error(`${formError.documents ? "Add Document" : ""}
|
toast.error("Add Documents | Recipients First", {
|
||||||
${(formError.documents && formError.recipients) ? " | " : ""}
|
|
||||||
${formError.recipients ? "Add Recipient" : ""}
|
|
||||||
${(formError.documents || formError.recipients) ? " | " : ""}
|
|
||||||
${formError.subject ? "Add Subject" : ""}`, {
|
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
hideProgressBar: false,
|
hideProgressBar: false,
|
||||||
|
|
@ -108,9 +79,9 @@ const ProcessStepOne = () => {
|
||||||
order: index,
|
order: index,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await api.patch(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents`, { "documents": documentsData });
|
await api.patch(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, { "documents": documentsData });
|
||||||
|
|
||||||
await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/documents?include_images=true`).then(res => {
|
await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`).then(res => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
dispatch(setDocuments(res.data));
|
dispatch(setDocuments(res.data));
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +92,7 @@ const ProcessStepOne = () => {
|
||||||
dispatch(showLoader("Processing Recipients..."));
|
dispatch(showLoader("Processing Recipients..."));
|
||||||
|
|
||||||
const recipientsData = selectedRecipients.map((recipient, index) => ({
|
const recipientsData = selectedRecipients.map((recipient, index) => ({
|
||||||
id: "id" in recipient ? recipient.id : null,
|
// index,
|
||||||
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
|
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
|
||||||
type: recipient.type,
|
type: recipient.type,
|
||||||
firstName: recipient.firstName,
|
firstName: recipient.firstName,
|
||||||
|
|
@ -142,12 +113,14 @@ const ProcessStepOne = () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
dispatch(addRecipientAsync({ recipientsData, accountId, transactionId }));
|
dispatch(addRecipientAsync({ recipientsData, accountId, transactionId }));
|
||||||
|
|
||||||
dispatch(showLoader("Processing Message..."));
|
dispatch(showLoader("Processing Message..."));
|
||||||
|
|
||||||
await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { subject: emailDtls.subject, message: emailDtls.message, status: "CREATED" }).then((res) => {
|
await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, { subject: emailDtls.subject, message: emailDtls.message, status: "CREATED" }).then((res) => {
|
||||||
console.log(res);
|
console.log(res);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
dispatch(hideLoader());
|
||||||
|
|
||||||
if (isSendNow) {
|
if (isSendNow) {
|
||||||
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS));
|
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS));
|
||||||
|
|
@ -245,8 +218,7 @@ const ProcessStepOne = () => {
|
||||||
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
|
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
value={emailDtls.subject || "Documents for Signature"}
|
||||||
value={emailDtls.subject}
|
|
||||||
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
|
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
|
||||||
placeholder="Email Subject"
|
placeholder="Email Subject"
|
||||||
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
|
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const RecipientAddCard = () => {
|
||||||
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (recipient.lastName === "" || recipient.email === "" || !recipient.type) return;
|
if (recipient.firstName === "" || recipient.lastName === "" || recipient.email === "" || !recipient.type) return;
|
||||||
|
|
||||||
// Validate Email Format
|
// Validate Email Format
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
@ -106,7 +106,7 @@ const RecipientAddCard = () => {
|
||||||
{/* Name Inputs */}
|
{/* Name Inputs */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<input
|
<input
|
||||||
// required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
onFocus={() => setOpenContactModal(true)}
|
onFocus={() => setOpenContactModal(true)}
|
||||||
onBlur={handleContactModalBlur}
|
onBlur={handleContactModalBlur}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
removeRecipientAsync,
|
|
||||||
removeSelectedRecipient,
|
removeSelectedRecipient,
|
||||||
// setSigningOrder,
|
// setSigningOrder,
|
||||||
updateRecipientOrder, // 👈 You need to define this in your slice
|
updateRecipientOrder, // 👈 You need to define this in your slice
|
||||||
|
|
@ -12,7 +11,6 @@ import { FaUsersLine } from "react-icons/fa6";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
|
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
|
||||||
import { IoCloseOutline } from "react-icons/io5";
|
import { IoCloseOutline } from "react-icons/io5";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
const getInitials = (name: string) => {
|
const getInitials = (name: string) => {
|
||||||
if (!name) return "";
|
if (!name) return "";
|
||||||
|
|
@ -24,15 +22,7 @@ const getInitials = (name: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const RecipientCards = () => {
|
const RecipientCards = () => {
|
||||||
const templateId = useParams<{ templateId: string }>().templateId;
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
if (!transactionId) {
|
|
||||||
transactionId = templateId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||||||
// const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder);
|
// const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder);
|
||||||
const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
|
const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
|
||||||
|
|
@ -44,14 +34,7 @@ const RecipientCards = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveRecipient = (recipient) => {
|
const handleRemoveRecipient = (recipient) => {
|
||||||
if (!accountId || !transactionId) return;
|
dispatch(removeSelectedRecipient(recipient))
|
||||||
// dispatch(removeSelectedRecipient(recipient))
|
|
||||||
dispatch(removeRecipientAsync({
|
|
||||||
recipientId: recipient.id,
|
|
||||||
email: recipient.email,
|
|
||||||
accountId: accountId,
|
|
||||||
transactionId: transactionId,
|
|
||||||
}))
|
|
||||||
if (recipients.length === 2) {
|
if (recipients.length === 2) {
|
||||||
let leftRecipient = recipients.filter((obj) => {
|
let leftRecipient = recipients.filter((obj) => {
|
||||||
if ("id" in obj && "id" in recipient) {
|
if ("id" in obj && "id" in recipient) {
|
||||||
|
|
@ -169,8 +152,8 @@ const RecipientCards = () => {
|
||||||
<div className="pb-2 text-white flex justify-between items-center">
|
<div className="pb-2 text-white flex justify-between items-center">
|
||||||
<p className="font-semibold">Signing Order</p>
|
<p className="font-semibold">Signing Order</p>
|
||||||
<div
|
<div
|
||||||
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
|
onClick={() => setSigningOrderModal({...signingOrderModal, open: false})}
|
||||||
className="text-xl cursor-pointer hover:text-gray-200 transition-all">
|
className="text-xl cursor-pointer hover:text-gray-200 transition-all">
|
||||||
<IoCloseOutline />
|
<IoCloseOutline />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,15 @@ import { Bounce, toast } from "react-toastify";
|
||||||
import api from '../../../config/api';
|
import api from '../../../config/api';
|
||||||
import DocumentCard from './DocumentCard';
|
import DocumentCard from './DocumentCard';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice';
|
|
||||||
|
|
||||||
const SelectedDocuments = () => {
|
const SelectedDocuments = () => {
|
||||||
// let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
const documents = useSelector((state: RootState) => state.documents.documents);
|
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();
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
if (!transactionId) {
|
window.location.href = "/";
|
||||||
transactionId = templateId;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isModalOpen, setModalOpen] = useState(false);
|
const [isModalOpen, setModalOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,6 @@ import { fetchDocuments } from '../../../store/slices/documentSlice';
|
||||||
import { FaQuestionCircle } from 'react-icons/fa';
|
import { FaQuestionCircle } from 'react-icons/fa';
|
||||||
import typeToIconMap from '../../../helper/iconMap';
|
import typeToIconMap from '../../../helper/iconMap';
|
||||||
|
|
||||||
import { IoAdd } from 'react-icons/io5';
|
|
||||||
import Loader from '../../dashboard/Loader';
|
|
||||||
import { handleGroupBorderPosition } from '../../../utlity/tagGroupConUtils';
|
|
||||||
|
|
||||||
export interface DroppedItemsObj {
|
export interface DroppedItemsObj {
|
||||||
[key: string]: DroppedItem[]
|
[key: string]: DroppedItem[]
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +23,7 @@ interface DroppedTagsDefaultFieldType {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = {
|
const droppedTagsDefaultField: DroppedTagsDefaultFieldType = {
|
||||||
text: "paragraph",
|
text: "paragraph",
|
||||||
temporal: "date",
|
temporal: "date",
|
||||||
number: "integer",
|
number: "integer",
|
||||||
|
|
@ -56,8 +52,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||||||
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
|
||||||
// const [uniqGroupIds, setUniqGroupIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
||||||
|
|
@ -137,7 +131,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
// debugger
|
// debugger
|
||||||
|
|
||||||
for (const [recipientId, tags] of Object.entries(tagsData)) {
|
for (const [recipientId, tags] of Object.entries(tagsData)) {
|
||||||
// console.log(recipientId, tags, "line 133")
|
console.log(recipientId, tags, "line 133")
|
||||||
const updateTags = []
|
const updateTags = []
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
if ("update" in tag && tag.update) {
|
if ("update" in tag && tag.update) {
|
||||||
|
|
@ -196,7 +190,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
let mask = null
|
let mask = null
|
||||||
|
|
||||||
|
|
||||||
// console.log(dropTag, "line 132")
|
console.log(dropTag, "line 132")
|
||||||
|
|
||||||
// if (dropTag.subType?.toLowerCase() === "password") {
|
// if (dropTag.subType?.toLowerCase() === "password") {
|
||||||
// console.log(dropTag)
|
// console.log(dropTag)
|
||||||
|
|
@ -206,7 +200,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// console.log(dropTag, "line 204")
|
console.log(dropTag, "line 204")
|
||||||
|
|
||||||
const isDeleted = "isDeleted" in dropTag ? dropTag.isDeleted : false
|
const isDeleted = "isDeleted" in dropTag ? dropTag.isDeleted : false
|
||||||
|
|
||||||
|
|
@ -216,23 +210,23 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
"dimensions": dropTag.dimensions,
|
"dimensions": dropTag.dimensions,
|
||||||
"type": dropTag.type.toUpperCase(),
|
"type": dropTag.type.toUpperCase(),
|
||||||
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
|
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
|
||||||
"minLength": "minLength" in dropTag ? dropTag.minLength : 0,
|
validation: {
|
||||||
"maxLength": "maxLength" in dropTag ? dropTag.maxLength : 0,
|
"minLength": 0,
|
||||||
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
|
"maxLength": 0,
|
||||||
"regex": "regex" in dropTag ? dropTag.regex : []
|
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
|
||||||
// }
|
"regex": "regex" in dropTag ? dropTag.regex : []
|
||||||
,
|
},
|
||||||
"documentId": dropTag.documentId,
|
"documentId": dropTag.documentId,
|
||||||
"options": ("options" in dropTag) ? dropTag.options : [],
|
"options": ("options" in dropTag) ? dropTag.options : [],
|
||||||
"pageIndex": dropTag.pageIndex,
|
"pageIndex": dropTag.pageIndex,
|
||||||
"recipientId": dropTag.recipientId,
|
"recipientId": dropTag.recipientId,
|
||||||
"styles": "styles" in dropTag ? dropTag.styles : {
|
"styles": "styles" in dropTag ? dropTag.styles : {
|
||||||
"font": "",
|
"font": "Great Vibes",
|
||||||
"italic": false,
|
"italic": false,
|
||||||
"bold": false,
|
"bold": false,
|
||||||
"underline": false,
|
"underline": false,
|
||||||
"color": "black",
|
"color": "black",
|
||||||
"textSize": 0
|
"textSize": 14.0
|
||||||
},
|
},
|
||||||
// "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true,
|
// "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true,
|
||||||
"required": "required" in dropTag ? dropTag.required : true,
|
"required": "required" in dropTag ? dropTag.required : true,
|
||||||
|
|
@ -281,10 +275,10 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
tagsData = groupByRecipientId(tagsData);
|
tagsData = groupByRecipientId(tagsData);
|
||||||
sendTagsToRecipients(tagsData, accountId, transactionId);
|
sendTagsToRecipients(tagsData, accountId, transactionId);
|
||||||
|
|
||||||
(async () => {
|
// (async () => {
|
||||||
const res = await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, { status: "SENT" })
|
// const res = await api.put(`/api/accounts/${accountId}/transactions/${transactionId}`, {status: "SENT"})
|
||||||
console.log(res, "Check123")
|
// console.log(res, "Check123")
|
||||||
})()
|
// })()
|
||||||
}, [isSend])
|
}, [isSend])
|
||||||
// Save Tags - END
|
// Save Tags - END
|
||||||
|
|
||||||
|
|
@ -294,7 +288,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
|
|
||||||
// HANDLE DROP - START
|
// HANDLE DROP - START
|
||||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
|
||||||
console.log("// HANDLE DROP - START")
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const data = window.dragElemData;
|
const data = window.dragElemData;
|
||||||
|
|
@ -343,18 +336,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
isDeleted: false
|
isDeleted: false
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("group" in dragElemData) {
|
|
||||||
prepData.group = dragElemData.group;
|
|
||||||
prepData.isGroupExist = false;
|
|
||||||
|
|
||||||
// if(uniqGroupIds.includes(dragElemData.group)) {
|
|
||||||
// prepData.isGroupExist = true;
|
|
||||||
// } else {
|
|
||||||
// prepData.isGroupExist = false;
|
|
||||||
// setUniqGroupIds([...uniqGroupIds, dragElemData.group]);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prepData.type === "temporal") {
|
if (prepData.type === "temporal") {
|
||||||
prepData.currentTimeStamp = true;
|
prepData.currentTimeStamp = true;
|
||||||
}
|
}
|
||||||
|
|
@ -367,44 +348,36 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
const dataToSet: DroppedItemsObj = {
|
const dataToSet: DroppedItemsObj = {
|
||||||
...droppedItems,
|
...droppedItems,
|
||||||
[key]: [...(droppedItems[key] || []),
|
[key]: [...(droppedItems[key] || []),
|
||||||
prepData],
|
{
|
||||||
|
...prepData,
|
||||||
|
radioGroups: dragElemData.label === "radio" ?
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...prepData,
|
||||||
|
dimensions: {
|
||||||
|
...prepData.dimensions, top: 8, left: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...prepData, dimensions: {
|
||||||
|
...prepData.dimensions,
|
||||||
|
top: 8,
|
||||||
|
left: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
:
|
||||||
|
[]
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
|
|
||||||
// const dataToSet: DroppedItemsObj = {
|
if (dragElemData.label === "radio") {
|
||||||
// ...droppedItems,
|
setDroppedItems(dataToSet);
|
||||||
// [key]: [...(droppedItems[key] || []),
|
} else {
|
||||||
// {
|
setDroppedItems(dataToSet);
|
||||||
// ...prepData,
|
}
|
||||||
// radioGroups: dragElemData.label === "radio" ?
|
|
||||||
// [
|
|
||||||
// {
|
|
||||||
// ...prepData,
|
|
||||||
// dimensions: {
|
|
||||||
// ...prepData.dimensions, top: 8, left: 10
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// ...prepData, dimensions: {
|
|
||||||
// ...prepData.dimensions,
|
|
||||||
// top: 8,
|
|
||||||
// left: 10
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// :
|
|
||||||
// []
|
|
||||||
// }],
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (dragElemData.label === "radio") {
|
|
||||||
// setDroppedItems(dataToSet);
|
|
||||||
// } else {
|
|
||||||
// setDroppedItems(dataToSet);
|
|
||||||
// }
|
|
||||||
|
|
||||||
setDroppedItems(dataToSet);
|
|
||||||
setDroppedItemsCounter((prev) => prev + 1);
|
setDroppedItemsCounter((prev) => prev + 1);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
|
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
|
||||||
handleTagClick(key, tagIndex)
|
handleTagClick(key, tagIndex)
|
||||||
|
|
@ -417,9 +390,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
|
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
|
||||||
|
|
||||||
console.log(targetContainer, "targetContainer line 421")
|
|
||||||
|
|
||||||
let targetElement = event?.currentTarget?.parentElement;
|
let targetElement = event?.currentTarget?.parentElement;
|
||||||
|
|
||||||
if (!targetContainer || !targetElement) return;
|
if (!targetContainer || !targetElement) return;
|
||||||
|
|
@ -434,29 +404,16 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
const conScrollWidth = targetContainer?.scrollWidth;
|
const conScrollWidth = targetContainer?.scrollWidth;
|
||||||
const conScrollHeight = targetContainer?.scrollHeight;
|
const conScrollHeight = targetContainer?.scrollHeight;
|
||||||
|
|
||||||
let isGroupAttr = event.currentTarget.hasAttribute("group");
|
|
||||||
let groupId = null;
|
|
||||||
if (isGroupAttr) {
|
|
||||||
groupId = event.currentTarget.getAttribute("group");
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupElements: NodeListOf<HTMLElement> | null = null
|
|
||||||
if (isGroupAttr && groupId) {
|
|
||||||
groupElements = document.querySelectorAll(`div[group="${groupId}"]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
// Get new X and Y positions relative to the container
|
// Get new X and Y positions relative to the container
|
||||||
let left = event.clientX - containerRect.left - offsetX;
|
let left = event.clientX - containerRect.left - offsetX;
|
||||||
let top = event.clientY - containerRect.top - offsetY;
|
let top = event.clientY - containerRect.top - offsetY;
|
||||||
|
|
||||||
|
|
||||||
// **Restrict movement within container bounds**
|
// **Restrict movement within container bounds**
|
||||||
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
|
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
|
||||||
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
|
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
|
||||||
|
|
||||||
if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
|
|
||||||
|
|
||||||
// Apply the movement to the element
|
// Apply the movement to the element
|
||||||
setDroppedItems({
|
setDroppedItems({
|
||||||
...droppedItems,
|
...droppedItems,
|
||||||
|
|
@ -482,7 +439,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
|
|
||||||
// HANDLE EXPAND TAG - START
|
// HANDLE EXPAND TAG - START
|
||||||
const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
|
const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
|
||||||
console.log("HANDLE EXPAND TAG - START")
|
|
||||||
event.preventDefault(); // Prevent text selection issues
|
event.preventDefault(); // Prevent text selection issues
|
||||||
|
|
||||||
const expandingElemIcon = event.currentTarget;
|
const expandingElemIcon = event.currentTarget;
|
||||||
|
|
@ -563,30 +519,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
// const handleAddRadioClick = (key: string) => {
|
|
||||||
// console.log("Handle Radio Clicled", key)
|
|
||||||
// console.log(droppedItems, "line 525")
|
|
||||||
// }
|
|
||||||
|
|
||||||
const handleAddGroupItemClicked = (key: string, item) => {
|
|
||||||
|
|
||||||
|
|
||||||
const groupId = item.group;
|
|
||||||
|
|
||||||
item = { ...item, dimensions: { ...item.dimensions, top: item.dimensions.top + item.dimensions.height + 20 } }
|
|
||||||
|
|
||||||
// dimensions: { ...item.dimensions, left: newLeft, top: newTop }
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
handleIGroupBorderPosition(item.group)
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
setDroppedItems({
|
|
||||||
...droppedItems,
|
|
||||||
[key]: [...droppedItems[key], { ...item, isGroupExist: true }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// const handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => {
|
// const handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => {
|
||||||
// // const dataToSet: DroppedItemsObj = {
|
// // const dataToSet: DroppedItemsObj = {
|
||||||
// // ...droppedItems,
|
// // ...droppedItems,
|
||||||
|
|
@ -688,75 +620,13 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
// NEW DIRECTIONAL MOVEMENT HANDLER - START
|
|
||||||
const handleDirectionalMove = (direction: 'up' | 'down' | 'left' | 'right', stepSize: number = 5) => {
|
|
||||||
if (!selectedTag) return;
|
|
||||||
|
|
||||||
const { key, index } = selectedTag;
|
|
||||||
const currentItem = droppedItems[key]?.[index];
|
|
||||||
|
|
||||||
if (!currentItem) return;
|
|
||||||
|
|
||||||
let newLeft = currentItem.dimensions.left;
|
|
||||||
let newTop = currentItem.dimensions.top;
|
|
||||||
|
|
||||||
switch (direction) {
|
|
||||||
case 'up':
|
|
||||||
newTop = Math.max(0, newTop - stepSize);
|
|
||||||
break;
|
|
||||||
case 'down':
|
|
||||||
newTop = newTop + stepSize;
|
|
||||||
break;
|
|
||||||
case 'left':
|
|
||||||
newLeft = Math.max(0, newLeft - stepSize);
|
|
||||||
break;
|
|
||||||
case 'right':
|
|
||||||
newLeft = newLeft + stepSize;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDroppedItems({
|
|
||||||
...droppedItems,
|
|
||||||
[key]: Array.isArray(droppedItems[key])
|
|
||||||
? droppedItems[key].map((item, i) =>
|
|
||||||
i === index
|
|
||||||
? { ...item, dimensions: { ...item.dimensions, left: newLeft, top: newTop }, update: true }
|
|
||||||
: item
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a wrapper that matches DirectionalControls' expected signature
|
|
||||||
const directionalControlsHandler = (direction: 'up' | 'down' | 'left' | 'right') => {
|
|
||||||
handleDirectionalMove(direction, 5); // Use default stepSize of 5
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function handleIGroupBorderPosition(groupId: string) {
|
|
||||||
|
|
||||||
const groupElements: NodeListOf<HTMLElement> = document.querySelectorAll(`div[group="${groupId}"]`);
|
|
||||||
|
|
||||||
let targetContainer = groupElements[0].parentElement?.parentElement;
|
|
||||||
console.log(targetContainer, "targetContainer line 421")
|
|
||||||
let targetElement = groupElements[0].parentElement;
|
|
||||||
|
|
||||||
if (!targetContainer || !targetElement) return;
|
|
||||||
const containerRect = targetContainer.getBoundingClientRect();
|
|
||||||
|
|
||||||
handleGroupBorderPosition(groupElements, containerRect, 40, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Center Doc Container - START */}
|
{/* Center Doc Container - START */}
|
||||||
<div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'>
|
<div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'>
|
||||||
|
|
||||||
{
|
{
|
||||||
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
|
selectedDocuments.length > 0 && selectedDocuments.map((doc, indxDoc) => {
|
||||||
return (
|
return (
|
||||||
<div key={indxDoc} className='flex flex-col'>
|
<div key={indxDoc} className='flex flex-col'>
|
||||||
<div className='py-2 text-sm'>
|
<div className='py-2 text-sm'>
|
||||||
|
|
@ -780,91 +650,70 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
{/* DROPPED ITEMS - Render only the dropped items for this specific docId and page - STARTS */}
|
{/* DROPPED ITEMS - Render only the dropped items for this specific docId and page - STARTS */}
|
||||||
{droppedItems[key]?.map((item, index) => {
|
{droppedItems[key]?.map((item, index) => {
|
||||||
if (item.isDeleted) return;
|
if (item.isDeleted) return;
|
||||||
|
|
||||||
const type = item.type.toUpperCase();
|
const type = item.type.toUpperCase();
|
||||||
const IconComponent = typeToIconMap[type] || <FaQuestionCircle />;
|
const IconComponent = typeToIconMap[type] || <FaQuestionCircle />;
|
||||||
const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id === item.recipientId);
|
const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id === item.recipientId);
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
key={index}
|
||||||
key={index}
|
draggable="false"
|
||||||
draggable="false"
|
className="absolute rounded dropped-item"
|
||||||
className="absolute rounded dropped-item"
|
onClick={() => handleTagClick(key, index)}
|
||||||
onClick={() => handleTagClick(key, index)}
|
style={{
|
||||||
style={{
|
left: `${item.dimensions.left}px`,
|
||||||
left: `${item.dimensions.left}px`,
|
top: `${item.dimensions.top}px`,
|
||||||
top: `${item.dimensions.top}px`,
|
width: item.type === "radio" ? `${item.dimensions.width + 20}px` : `${item.dimensions.width}px`,
|
||||||
width: item.type === "radio" ? `${item.dimensions.width + 20}px` : `${item.dimensions.width}px`,
|
height: item.type === "radio" ? `max-content` : `${item.dimensions.height}px`
|
||||||
height: item.type === "radio" ? `max-content` : `${item.dimensions.height}px`
|
}}
|
||||||
}}
|
>
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
{/* If Selected Tag Recipient Info- START */}
|
{/* If Selected Tag Recipient Info- START */}
|
||||||
{selectedTag?.key && selectedTag.key === key && selectedTag.index === index &&
|
{selectedTag?.key && selectedTag.key === key && selectedTag.index === index &&
|
||||||
<div className='absolute flex justify-between items-center w-full max-w-max bg-white top-[-22px] shadow z-20 text-sm rounded-t-sm'>
|
<div className='absolute flex justify-between items-center w-full max-w-max bg-white top-[-22px] shadow z-20 text-sm rounded-t-sm'>
|
||||||
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
|
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
|
||||||
<p className='w-2.5 h-2.5 rounded'
|
<p className='w-2.5 h-2.5 rounded'
|
||||||
style={{ background: selectedRecipient[0]?.color }}
|
style={{ background: selectedRecipient[0]?.color }}
|
||||||
></p>
|
></p>
|
||||||
<p
|
|
||||||
className=''>{IconComponent}
|
|
||||||
</p>
|
|
||||||
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
|
|
||||||
<IoMdClose onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveTag(key, index)
|
|
||||||
}} className='cursor-pointer' />
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
{/* If Selected Tag Recipient Info- END */}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Expand Circle - STARTS */}
|
|
||||||
{item.type !== "radio" && <div
|
|
||||||
onPointerDown={(e) => handleExpandTag(e, key, index)}
|
|
||||||
className='w-2.5 h-2.5 absolute bg-red-400 rounded-full bottom-[-8px] right-[-8px] cursor-nw-resize hover:bg-blue-400'>
|
|
||||||
</div>}
|
|
||||||
{/* Expand Circle - ENDS */}
|
|
||||||
|
|
||||||
{/* Tag - START */}
|
|
||||||
{
|
|
||||||
<div
|
|
||||||
{...("group" in item && item.group ? { "group": item.group } : {})}
|
|
||||||
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
|
|
||||||
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
|
|
||||||
style={{ background: selectedRecipient[0]?.color }}
|
|
||||||
>
|
|
||||||
<p
|
<p
|
||||||
className={`flex justify-center items-center w-full text-xl text-white h-full`}
|
className=''>{IconComponent}
|
||||||
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
|
|
||||||
>
|
|
||||||
{IconComponent}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
|
||||||
|
<IoMdClose onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveTag(key, index)
|
||||||
|
}} className='cursor-pointer' />
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>}
|
||||||
{/* Tag - END */}
|
{/* If Selected Tag Recipient Info- END */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Expand Circle - STARTS */}
|
||||||
|
{item.type !== "radio" && <div
|
||||||
|
onPointerDown={(e) => handleExpandTag(e, key, index)}
|
||||||
|
className='w-2.5 h-2.5 absolute bg-red-400 rounded-full bottom-[-8px] right-[-8px] cursor-nw-resize hover:bg-blue-400'>
|
||||||
|
</div>}
|
||||||
|
{/* Expand Circle - ENDS */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
{
|
</div>
|
||||||
"group" in item && item.group && "isGroupExist" in item && !item.isGroupExist &&
|
|
||||||
<>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="group-border" border-group={item.group} border-pos="top"></div>
|
|
||||||
<div className="group-border" border-group={item.group} border-pos="right"></div>
|
|
||||||
<div className="group-border bottom-border" border-group={item.group} border-pos="bottom">
|
|
||||||
<div onClick={() => handleAddGroupItemClicked(key, item)}><IoAdd /></div>
|
|
||||||
</div>
|
|
||||||
<div className="group-border" border-group={item.group} border-pos="left"></div>
|
|
||||||
</>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -901,19 +750,13 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}) : <Loader />
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</div >
|
</div>
|
||||||
|
|
||||||
{selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} DirectionalControlsProps={{
|
{selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} />}
|
||||||
onMove: directionalControlsHandler,
|
|
||||||
disabled: false,
|
|
||||||
stepSize: 5,
|
|
||||||
className: ''
|
|
||||||
}} />
|
|
||||||
}
|
|
||||||
{/* Center Doc Container - END */}
|
{/* Center Doc Container - END */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import fields from '../../../helper/fieldsListData';
|
import fields from '../../../helper/fieldsListData';
|
||||||
// import api from '../../../config/api';
|
import api from '../../../config/api';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import typeToIconMap from '../../../helper/iconMap';
|
import typeToIconMap from '../../../helper/iconMap';
|
||||||
import { FaQuestionCircle } from 'react-icons/fa';
|
import { FaQuestionCircle } from 'react-icons/fa';
|
||||||
import { TagsProperties } from '../../../utlity/types';
|
import { TagsProperties } from '../../../utlity/types';
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string, recipientId: number, recipientIndx: number, tagsProperties: TagsProperties) => {
|
const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string, recipientId: number, recipientIndx: number, tagsProperties: TagsProperties) => {
|
||||||
const dragElemRect = event.currentTarget.getBoundingClientRect();
|
const dragElemRect = event.currentTarget.getBoundingClientRect();
|
||||||
|
|
@ -33,8 +31,6 @@ const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string
|
||||||
properties: tagsProperties
|
properties: tagsProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.label.toLowerCase() === "radio" || obj.label.toLowerCase() === "checkbox") obj.group = uuidv4();
|
|
||||||
|
|
||||||
window.dragElemData = JSON.stringify(obj);
|
window.dragElemData = JSON.stringify(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,31 +38,21 @@ interface FieldListProps {
|
||||||
color: string;
|
color: string;
|
||||||
recipientId: number;
|
recipientId: number;
|
||||||
recipientIndx: number;
|
recipientIndx: number;
|
||||||
acntId?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx, acntId = null }) => {
|
const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx }) => {
|
||||||
|
|
||||||
const [tagsData, setTagsData] = useState<TagsProperties[]>([]);
|
const [tagsData, setTagsData] = useState<TagsProperties[]>([]);
|
||||||
|
|
||||||
let { accountId } = useParams<{ accountId: string }>();
|
const { accountId } = useParams<{ accountId: string }>();
|
||||||
|
|
||||||
if(acntId) {
|
|
||||||
accountId = acntId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios
|
api
|
||||||
.get(`${import.meta.env.VITE_BASE_URI}/api/accounts/${accountId}/tags`, {
|
.get(`/api/accounts/${accountId}/tags`)
|
||||||
headers: {
|
|
||||||
"ngrok-skip-browser-warning": "69420",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP'];
|
const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP'];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { MdKeyboardArrowDown } from 'react-icons/md'
|
||||||
import { RiResetLeftLine, RiResetRightLine } from 'react-icons/ri'
|
import { RiResetLeftLine, RiResetRightLine } from 'react-icons/ri'
|
||||||
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice'
|
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice'
|
||||||
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
interface NavTwoProps {
|
interface NavTwoProps {
|
||||||
isFields: boolean;
|
isFields: boolean;
|
||||||
|
|
@ -57,8 +57,6 @@ const NavTwo: React.FC<NavTwoProps> = ({ isFields, setIsFields, isSend, setIsSen
|
||||||
const [isFitDd, setIsFitDd] = useState<boolean>(false)
|
const [isFitDd, setIsFitDd] = useState<boolean>(false)
|
||||||
const [selFitIndx, setSelFitIndx] = useState<number>(0);
|
const [selFitIndx, setSelFitIndx] = useState<number>(0);
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
||||||
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
|
||||||
|
|
@ -78,7 +76,6 @@ const NavTwo: React.FC<NavTwoProps> = ({ isFields, setIsFields, isSend, setIsSen
|
||||||
setIsSend(false);
|
setIsSend(false);
|
||||||
}
|
}
|
||||||
setIsSend(!isSend)
|
setIsSend(!isSend)
|
||||||
navigate(`/document-completion-screen/${accountId}/${transactionId}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSetIsFields = (setTo: boolean) => {
|
const handleSetIsFields = (setTo: boolean) => {
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,68 +1,29 @@
|
||||||
import React, { useRef } from 'react';
|
import React from 'react'
|
||||||
|
|
||||||
interface MaxMinLenAttrIntr {
|
interface MaxMinLenAttrIntr {
|
||||||
maxLength: number;
|
maxLength: number;
|
||||||
minLength: number;
|
minLength: number;
|
||||||
handlePropertyChange: (changeTo: string, changeWith: string | boolean | number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MaxMinLenAttr: React.FC<MaxMinLenAttrIntr> = ({ maxLength, minLength, handlePropertyChange }) => {
|
const MaxMinLenAttr : React.FC<MaxMinLenAttrIntr> = ({ maxLength, minLength }) => {
|
||||||
const firstChangedField = useRef<null | 'minLength' | 'maxLength'>(null);
|
|
||||||
|
|
||||||
const handleMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newMax = Number(e.target.value);
|
|
||||||
if (!firstChangedField.current) {
|
|
||||||
firstChangedField.current = 'maxLength';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstChangedField.current === 'minLength' && newMax < minLength) {
|
|
||||||
alert(`Max Length must be greater than or equal to Min Length (${minLength})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePropertyChange('maxLength', newMax);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newMin = Number(e.target.value);
|
|
||||||
if (!firstChangedField.current) {
|
|
||||||
firstChangedField.current = 'minLength';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstChangedField.current === 'maxLength' && newMin > maxLength) {
|
|
||||||
alert(`Min Length must be less than or equal to Max Length (${maxLength})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePropertyChange('minLength', newMin);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="maxLength">Max Length</label>
|
<label htmlFor="">Max Length</label>
|
||||||
<input
|
<input
|
||||||
id="maxLength"
|
value={minLength ? minLength : 0}
|
||||||
value={maxLength}
|
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
className="outline-none border-1 rounded px-2 py-1 border-slate-200" />
|
||||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
|
||||||
onChange={handleMaxChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<label htmlFor="minLength">Min Length</label>
|
<label htmlFor="">Min Length</label>
|
||||||
<input
|
<input
|
||||||
id="minLength"
|
value={maxLength ? maxLength : 255}
|
||||||
value={minLength}
|
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
className="outline-none border-1 rounded px-2 py-1 border-slate-200" />
|
||||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
|
||||||
onChange={handleMinChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default MaxMinLenAttr;
|
export default MaxMinLenAttr
|
||||||
|
|
@ -65,7 +65,7 @@ const RecipientsAttrCon: React.FC<RecipientsAttrConIntr> = ({ selectedRecipientI
|
||||||
key={indx}
|
key={indx}
|
||||||
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-pointer hover:bg-slate-100'
|
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-pointer hover:bg-slate-100'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePropertyChange('recipientId', recipient.id);
|
// handlePropertyChange()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setDropDownToggle(false);
|
setDropDownToggle(false);
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
|
||||||
|
|
@ -16,27 +16,12 @@ import TemporalDateAttr from "./TemporalDateAttr";
|
||||||
import SelectTagAttr from "./SelectTagAttr";
|
import SelectTagAttr from "./SelectTagAttr";
|
||||||
import MaxMinLenAttr from "./MaxMinLenAttr";
|
import MaxMinLenAttr from "./MaxMinLenAttr";
|
||||||
import RecipientsAttrCon from "./RecipientsAttrCon";
|
import RecipientsAttrCon from "./RecipientsAttrCon";
|
||||||
import MovableFieldContainer from "./MovableField";
|
|
||||||
import DirectionalControls from "./DirectionalControls";
|
|
||||||
|
|
||||||
// interface DirectionalControlsProps {
|
|
||||||
// onMove: (direction: 'up' | 'down' | 'left' | 'right') => void;
|
|
||||||
// disabled?: boolean;
|
|
||||||
// stepSize?: number;
|
|
||||||
// className?: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export interface DroppedItemProps {
|
export interface DroppedItemProps {
|
||||||
selectedTag: DroppedItem
|
selectedTag: DroppedItem
|
||||||
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>
|
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>
|
||||||
selTagKey: string,
|
selTagKey: string,
|
||||||
selTagIndx: number,
|
selTagIndx: number
|
||||||
DirectionalControlsProps: {
|
|
||||||
onMove: (direction: 'up' | 'down' | 'left' | 'right', stepSize?: number) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
stepSize?: number;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TEXT {
|
export enum TEXT {
|
||||||
|
|
@ -92,32 +77,15 @@ export enum DROPDOWNTOGGLE {
|
||||||
DDMMYY_FORMAT
|
DDMMYY_FORMAT
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultStyles = {
|
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx }) => {
|
||||||
font: "",
|
|
||||||
italic: false,
|
|
||||||
bold: false,
|
|
||||||
underline: false,
|
|
||||||
color: "#000000",
|
|
||||||
textSize: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx, DirectionalControlsProps }) => {
|
|
||||||
|
|
||||||
const selectedType = selectedTag.type in enumMap && enumMap[selectedTag.type as keyof typeof enumMap];
|
const selectedType = selectedTag.type in enumMap && enumMap[selectedTag.type as keyof typeof enumMap];
|
||||||
|
|
||||||
// console.log("selectedTag", selectedTag, selectedType)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [styles, setStyles] = useState(defaultStyles);
|
|
||||||
|
|
||||||
const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null);
|
const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null);
|
||||||
const [decimalPlace, setDecimalPlace] = useState<number>(2)
|
const [decimalPlace, setDecimalPlace] = useState<number>(2)
|
||||||
const [styleOpen, setStyleOpen] = useState(false);
|
const [styleOpen, setStyleOpen] = useState(false);
|
||||||
|
|
||||||
const handlePropertyChange = (changeTo: string, changeWith: string | boolean | number, is24Hrs = null) => {
|
const handlePropertyChange = (changeTo: string, changeWith: string | boolean | number, is24Hrs=null) => {
|
||||||
console.log("Handle Property Change Called", "changeTo", changeTo, "changeWith", changeWith)
|
console.log("Handle Property Change Called", "changeTo", changeTo, "changeWith", changeWith)
|
||||||
if (changeTo === '' || changeWith === '') return
|
if (changeTo === '' || changeWith === '') return
|
||||||
|
|
||||||
|
|
@ -182,23 +150,16 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
||||||
));
|
));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
setDroppedItems((prev) => {
|
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,
|
...prev,
|
||||||
[selTagKey]: updatedItems,
|
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
|
||||||
};
|
? prev[selTagKey].map((item, i) =>
|
||||||
});
|
i === selTagIndx ? { ...item, [changeTo]: changeWith, update: true } : item
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}// Provide a fallback in case it's undefined or not an array
|
||||||
|
));
|
||||||
}
|
}
|
||||||
setDropDownToggle(null)
|
setDropDownToggle(null)
|
||||||
}
|
}
|
||||||
|
|
@ -232,67 +193,24 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
||||||
handlePropertyChange("decimalPlace", val)
|
handlePropertyChange("decimalPlace", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if ("styles" in selectedTag) {
|
|
||||||
// console.log("styles" in selectedTag)
|
|
||||||
// const fontSize = selectedTag?.styles?.textSize;
|
|
||||||
// const tagHeight = selectedTag?.dimensions?.height - 18;
|
|
||||||
|
|
||||||
// console.log(fontSize)
|
|
||||||
|
|
||||||
// if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.PARAGRAPH && tagHeight > (fontSize + fontSize)) {
|
|
||||||
// handlePropertyChange("subType", TEXT.PARAGRAPH)
|
|
||||||
// } else if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.SINGLE_LINE && tagHeight <= (fontSize + fontSize)) {
|
|
||||||
// console.log("Change subType Called from elseif")
|
|
||||||
// handlePropertyChange("subType", TEXT.SINGLE_LINE)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, [selectedTag])
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedTag) return;
|
if ("styles" in selectedTag) {
|
||||||
|
const fontSize = selectedTag?.styles?.textSize;
|
||||||
|
const tagHeight = selectedTag?.dimensions?.height - 18;
|
||||||
|
|
||||||
if (!selectedTag.styles) {
|
if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.PARAGRAPH && tagHeight > (fontSize + fontSize)) {
|
||||||
handlePropertyChange("styles", defaultStyles);
|
handlePropertyChange("subType", TEXT.PARAGRAPH)
|
||||||
setStyles(defaultStyles);
|
} else if (selectedTag.subType !== TEXT.PASSWORD && selectedTag.subType !== TEXT.SINGLE_LINE && tagHeight <= (fontSize + fontSize)) {
|
||||||
} else {
|
console.log("Change subType Called from elseif")
|
||||||
setStyles({ ...defaultStyles, ...selectedTag.styles });
|
handlePropertyChange("subType", TEXT.SINGLE_LINE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}, [selectedTag])
|
||||||
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Right Side Bar Start */}
|
{/* Right Side Bar Start */}
|
||||||
<div className='col-span-2 w-full h-full bg-white text-sm overflow-y-auto'>
|
<div className='col-span-2 w-full h-full bg-white text-sm'>
|
||||||
<p className="p-2 font-semibold text-md">Properties</p>
|
<p className="p-2 font-semibold text-md">Properties</p>
|
||||||
<hr className="text-slate-200" />
|
<hr className="text-slate-200" />
|
||||||
|
|
||||||
|
|
@ -302,20 +220,11 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
||||||
<RecipientsAttrCon selectedRecipientId={selectedTag.recipientId} handlePropertyChange={handlePropertyChange} />
|
<RecipientsAttrCon selectedRecipientId={selectedTag.recipientId} handlePropertyChange={handlePropertyChange} />
|
||||||
}
|
}
|
||||||
|
|
||||||
<DirectionalControls {...DirectionalControlsProps} selectedType={selectedTag.dimensions}/>
|
|
||||||
|
|
||||||
|
|
||||||
{selectedTag.label &&
|
{selectedTag.label &&
|
||||||
<LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />}
|
<LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />}
|
||||||
|
|
||||||
{(selectedTag.type !== TagsTypes.SIGNATURE && selectedTag.type !== TagsTypes.INITIALS) &&
|
{(selectedTag.type !== TagsTypes.SIGNATURE && selectedTag.type !== TagsTypes.INITIALS) &&
|
||||||
|
<RequiredAttr required={selectedTag.required ? selectedTag.required : false} handlePropertyChange={handlePropertyChange} />}
|
||||||
<>
|
|
||||||
<RequiredAttr required={selectedTag.required ? selectedTag.required : false} handlePropertyChange={handlePropertyChange} />
|
|
||||||
</>
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
{selectedTag.type.toLowerCase() === TagsTypes.TEMPORAL &&
|
{selectedTag.type.toLowerCase() === TagsTypes.TEMPORAL &&
|
||||||
<CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />}
|
<CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />}
|
||||||
|
|
@ -381,13 +290,11 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
(selectedTag.type.toLowerCase() === TagsTypes.TEXT || selectedTag.type.toLowerCase() === TagsTypes.COMPANY) &&
|
selectedTag.type === TagsTypes.TEXT &&
|
||||||
<MaxMinLenAttr maxLength={selectedTag.maxLength} minLength={selectedTag.minLength} handlePropertyChange={handlePropertyChange} />
|
<MaxMinLenAttr maxLength={selectedTag.maxLength} minLength={selectedTag.minLength} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex items-center justify-between mr-2 cursor-pointer"
|
<div className="flex items-center justify-between mr-2 cursor-pointer"
|
||||||
|
|
@ -406,8 +313,6 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. Arial"
|
placeholder="e.g. Arial"
|
||||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
||||||
value={styles.font}
|
|
||||||
onChange={(e) => handleStyleChange("font", e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -418,10 +323,6 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
||||||
// value={}
|
// value={}
|
||||||
type="number"
|
type="number"
|
||||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
||||||
value={styles.textSize}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleStyleChange("textSize", parseInt(e.target.value, 10) || 0)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -431,27 +332,21 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
className="outline-none border-1 rounded px-2 py-1 border-slate-200 w-10 h-8"
|
className="outline-none border-1 rounded px-2 py-1 border-slate-200 w-10 h-8"
|
||||||
value={styles.color}
|
|
||||||
onChange={(e) => handleStyleChange("color", e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toggles for Bold, Italic, Underline */}
|
{/* Toggles for Bold, Italic, Underline */}
|
||||||
<div className="flex gap-4 items-center mt-2">
|
<div className="flex gap-4 items-center mt-2">
|
||||||
<label className="flex gap-1 items-center">
|
<label className="flex gap-1 items-center">
|
||||||
<input type="checkbox" checked={styles.bold}
|
<input type="checkbox" />
|
||||||
onChange={(e) => handleStyleChange("bold", e.target.checked)} />
|
|
||||||
Bold
|
Bold
|
||||||
</label>
|
</label>
|
||||||
<label className="flex gap-1 items-center">
|
<label className="flex gap-1 items-center">
|
||||||
<input type="checkbox" checked={styles.italic} onChange={(e) => handleStyleChange("italic", e.target.checked)} />
|
<input type="checkbox" />
|
||||||
Italic
|
Italic
|
||||||
</label>
|
</label>
|
||||||
<label className="flex gap-1 items-center">
|
<label className="flex gap-1 items-center">
|
||||||
<input type="checkbox" checked={styles.underline}
|
<input type="checkbox" />
|
||||||
onChange={(e) =>
|
|
||||||
handleStyleChange("underline", e.target.checked)
|
|
||||||
} />
|
|
||||||
Underline
|
Underline
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,32 +15,22 @@ const SelectTagAttr: React.FC<SelectTagAttrInt> = ({ options, selTagKey, selTagI
|
||||||
const [selectedOption, setSelectedOption] = useState<string>()
|
const [selectedOption, setSelectedOption] = useState<string>()
|
||||||
|
|
||||||
const handleAddSelectOption = () => {
|
const handleAddSelectOption = () => {
|
||||||
if (selectedOption && selectedOption.length) {
|
if (selectedOption && selectedOption.length) {
|
||||||
setDroppedItems((prev) => {
|
setDroppedItems((prev) => (
|
||||||
console.log("Previous droppedItems:", prev);
|
{
|
||||||
|
...prev,
|
||||||
const newState = {
|
[selTagKey]: Array.isArray(prev[selTagKey])
|
||||||
...prev,
|
? prev[selTagKey].map((item, i) => {
|
||||||
[selTagKey]: Array.isArray(prev[selTagKey])
|
if (i === selTagIndx) {
|
||||||
? prev[selTagKey].map((item, i) => {
|
return { ...item, options: item.options ? [...item.options, selectedOption] : [selectedOption], update: true };
|
||||||
if (i === selTagIndx) {
|
}
|
||||||
return {
|
return item;
|
||||||
...item,
|
})
|
||||||
options: item.options ? [...item.options, selectedOption] : [selectedOption],
|
: [],
|
||||||
update: true,
|
}
|
||||||
};
|
));
|
||||||
}
|
}
|
||||||
return item;
|
|
||||||
})
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("New droppedItems state:", newState);
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleSelectOptionDel = (indx: number) => {
|
const handleSelectOptionDel = (indx: number) => {
|
||||||
setDroppedItems((prev) => (
|
setDroppedItems((prev) => (
|
||||||
|
|
|
||||||
|
|
@ -1,119 +1,63 @@
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { FaSpinner } from 'react-icons/fa';
|
|
||||||
|
|
||||||
export type ActionType = 'create' | 'invite';
|
export type ActionType = 'create' | 'invite';
|
||||||
|
|
||||||
type CreateData = { accountName: string };
|
type CreateData = { accountName: string };
|
||||||
type InviteData = { i_accountName: number; email: string; role: string };
|
type InviteData = { i_accountName: string; email: string; role: string };
|
||||||
|
|
||||||
export type FormDataType = CreateData | InviteData;
|
type FormDataType = CreateData | InviteData;
|
||||||
|
|
||||||
interface AccountOption {
|
|
||||||
userId: number;
|
|
||||||
userEmail: string;
|
|
||||||
accountId: number;
|
|
||||||
accountName: string;
|
|
||||||
role: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: (data: FormDataType, action: ActionType) => void;
|
onSubmit: (data: FormDataType, action: ActionType) => void;
|
||||||
createAndInviteLoader: boolean;
|
|
||||||
accountOptions: AccountOption[];
|
|
||||||
accountOptionsLoading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateAndInvite = ({
|
const CreateAndInvite = ({ onSubmit }: Props) => {
|
||||||
onSubmit,
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
createAndInviteLoader,
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
accountOptions,
|
|
||||||
accountOptionsLoading
|
|
||||||
}: Props) => {
|
|
||||||
const [isRoleDropdownOpen, setIsRoleDropdownOpen] = useState(false);
|
|
||||||
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
|
|
||||||
const roleDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const accountDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [createFormData, setCreateFormData] = useState<{ accountName: string }>({
|
const [createFormData, setCreateFormData] = useState<{ accountName: string }>({
|
||||||
accountName: '',
|
accountName: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [accountSearchTerm, setAccountSearchTerm] = useState('');
|
|
||||||
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(null);
|
|
||||||
|
|
||||||
const [inviteFormData, setInviteFormData] = useState<{
|
const [inviteFormData, setInviteFormData] = useState<{
|
||||||
i_accountName: number;
|
i_accountName: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
}>({
|
}>({
|
||||||
i_accountName: 0,
|
i_accountName: '',
|
||||||
email: '',
|
email: '',
|
||||||
role: '',
|
role: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
const roles = ['ADMIN', 'EDITOR', 'VIEWER'];
|
|
||||||
|
|
||||||
// Handle role dropdown
|
const roles = ['Owner', 'Client'];
|
||||||
const handleRoleDropdownFocus = () => setIsRoleDropdownOpen(true);
|
|
||||||
|
|
||||||
const handleRoleDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
const handleFocus = () => setIsOpen(true);
|
||||||
|
|
||||||
|
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const activeElement = document.activeElement as HTMLElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
if (roleDropdownRef.current && !roleDropdownRef.current.contains(activeElement)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(activeElement)) {
|
||||||
setIsRoleDropdownOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRoleSelect = (role: string) => {
|
const handleSelect = (role: string) => {
|
||||||
setInviteFormData((prev) => ({ ...prev, role }));
|
setInviteFormData((prev) => ({ ...prev, role }));
|
||||||
setIsRoleDropdownOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle account dropdown
|
|
||||||
const handleAccountDropdownFocus = () => setIsAccountDropdownOpen(true);
|
|
||||||
|
|
||||||
const handleAccountDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const activeElement = document.activeElement as HTMLElement;
|
|
||||||
if (accountDropdownRef.current && !accountDropdownRef.current.contains(activeElement)) {
|
|
||||||
setIsAccountDropdownOpen(false);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAccountSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setAccountSearchTerm(value);
|
|
||||||
|
|
||||||
// Clear selected account if user is typing
|
|
||||||
if (selectedAccount && value !== selectedAccount.accountName) {
|
|
||||||
setSelectedAccount(null);
|
|
||||||
setInviteFormData((prev) => ({ ...prev, i_accountName: 0 }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAccountSelect = (option: AccountOption) => {
|
|
||||||
setSelectedAccount(option);
|
|
||||||
setAccountSearchTerm(option.accountName);
|
|
||||||
setInviteFormData((prev) => ({ ...prev, i_accountName: option.accountId }));
|
|
||||||
setIsAccountDropdownOpen(false);
|
|
||||||
|
|
||||||
// Clear any existing account name error
|
|
||||||
if (errors.i_accountName) {
|
|
||||||
setErrors(prev => ({ ...prev, i_accountName: '' }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle create form
|
|
||||||
const handleCreateChange = (value: string) => {
|
const handleCreateChange = (value: string) => {
|
||||||
setCreateFormData({ accountName: value });
|
setCreateFormData({ accountName: value });
|
||||||
// Clear error when user starts typing
|
};
|
||||||
if (errors.accountName) {
|
|
||||||
setErrors(prev => ({ ...prev, accountName: '' }));
|
const handleInviteChange = (
|
||||||
}
|
field: keyof typeof inviteFormData,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
setInviteFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateSubmit = () => {
|
const handleCreateSubmit = () => {
|
||||||
|
|
@ -125,20 +69,11 @@ const CreateAndInvite = ({
|
||||||
onSubmit(createFormData, 'create');
|
onSubmit(createFormData, 'create');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle invite form
|
|
||||||
const handleInviteChange = (field: keyof typeof inviteFormData, value: string) => {
|
|
||||||
setInviteFormData((prev) => ({ ...prev, [field]: value }));
|
|
||||||
// Clear error when user starts typing
|
|
||||||
if (errors[field]) {
|
|
||||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInviteSubmit = () => {
|
const handleInviteSubmit = () => {
|
||||||
const { i_accountName, email, role } = inviteFormData;
|
const { i_accountName, email, role } = inviteFormData;
|
||||||
const errs: { [key: string]: string } = {};
|
const errs: { [key: string]: string } = {};
|
||||||
|
|
||||||
if (!i_accountName) errs.i_accountName = 'Account name is required';
|
if (!i_accountName.trim()) errs.i_accountName = 'Account name is required';
|
||||||
if (!email.trim()) errs.email = 'Email is required';
|
if (!email.trim()) errs.email = 'Email is required';
|
||||||
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email';
|
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email';
|
||||||
if (!role.trim()) errs.role = 'Role is required';
|
if (!role.trim()) errs.role = 'Role is required';
|
||||||
|
|
@ -152,14 +87,9 @@ const CreateAndInvite = ({
|
||||||
onSubmit(inviteFormData, 'invite');
|
onSubmit(inviteFormData, 'invite');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter account options based on search term
|
|
||||||
const filteredAccountOptions = accountOptions.filter((option) =>
|
|
||||||
option.accountName?.toLowerCase().includes(accountSearchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ms-2 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
{/* Create Section */}
|
{/* Create */}
|
||||||
<section className="border-gray-200 border-b">
|
<section className="border-gray-200 border-b">
|
||||||
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
|
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
|
||||||
<div className="flex flex-col mb-6">
|
<div className="flex flex-col mb-6">
|
||||||
|
|
@ -177,17 +107,9 @@ const CreateAndInvite = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCreateSubmit}
|
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"
|
className="ms-1 text-[14px] px-3 py-[1.1] bg-gray-300 text-slate-600 font-medium rounded-md border border-gray-300 hover:bg-opacity-90 transition cursor-pointer"
|
||||||
disabled={createAndInviteLoader}
|
|
||||||
>
|
>
|
||||||
{createAndInviteLoader ? (
|
Create
|
||||||
<>
|
|
||||||
<FaSpinner className="animate-spin me-1" />
|
|
||||||
CREATING...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'CREATE'
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{errors.accountName && (
|
{errors.accountName && (
|
||||||
|
|
@ -196,62 +118,22 @@ const CreateAndInvite = ({
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Invite Section */}
|
{/* Invite */}
|
||||||
<section className="border-gray-300">
|
<section className="border-gray-300">
|
||||||
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
|
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
{/* Account Name Dropdown */}
|
{/* Account Name */}
|
||||||
<div className="flex flex-col relative">
|
<div className="flex flex-col">
|
||||||
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
||||||
Account Name
|
Account Name
|
||||||
</label>
|
</label>
|
||||||
<div
|
<input
|
||||||
className="relative w-full"
|
type="text"
|
||||||
onBlur={handleAccountDropdownBlur}
|
placeholder="Enter account name"
|
||||||
ref={accountDropdownRef}
|
value={inviteFormData.i_accountName}
|
||||||
tabIndex={-1}
|
onChange={(e) => handleInviteChange('i_accountName', 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)]"
|
||||||
<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 && (
|
{errors.i_accountName && (
|
||||||
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
|
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -281,8 +163,8 @@ const CreateAndInvite = ({
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
onBlur={handleRoleDropdownBlur}
|
onBlur={handleBlur}
|
||||||
ref={roleDropdownRef}
|
ref={dropdownRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -290,30 +172,30 @@ const CreateAndInvite = ({
|
||||||
placeholder="Select role"
|
placeholder="Select role"
|
||||||
value={inviteFormData.role}
|
value={inviteFormData.role}
|
||||||
readOnly
|
readOnly
|
||||||
onFocus={handleRoleDropdownFocus}
|
onFocus={handleFocus}
|
||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${
|
className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${
|
||||||
isRoleDropdownOpen
|
isOpen
|
||||||
? 'opacity-100 scale-100'
|
? 'opacity-100 scale-100'
|
||||||
: 'opacity-0 scale-95 pointer-events-none'
|
: 'opacity-0 scale-95 pointer-events-none'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{roles.map((role) => (
|
{roles.map((r) => (
|
||||||
<div
|
<div
|
||||||
key={role}
|
key={r}
|
||||||
onMouseDown={() => handleRoleSelect(role)}
|
onMouseDown={() => handleSelect(r)}
|
||||||
className="px-4 py-1 text-[13px] hover:bg-gray-100 cursor-pointer"
|
className="px-4 py-2 text-[13px] hover:bg-gray-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
{role}
|
{r}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{errors.role && (
|
||||||
|
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{errors.role && (
|
|
||||||
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -321,17 +203,9 @@ const CreateAndInvite = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleInviteSubmit}
|
onClick={handleInviteSubmit}
|
||||||
className="px-4 py-2 text-sm font-medium rounded-md cursor-pointer hover:bg-opacity-90 transition text-[14px] bg-gray-300 text-slate-600 disabled:opacity-60 disabled:cursor-not-allowed"
|
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={createAndInviteLoader}
|
|
||||||
>
|
>
|
||||||
{createAndInviteLoader ? (
|
Invite
|
||||||
<>
|
|
||||||
<FaSpinner className="animate-spin me-1" />
|
|
||||||
INVITING...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'INVITE'
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import Language from "./Language";
|
||||||
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
|
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
|
||||||
import AsideSection from "./AsideSection";
|
import AsideSection from "./AsideSection";
|
||||||
import Signature from "./Signature/Signature";
|
import Signature from "./Signature/Signature";
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
import AccountNav from "../accounts/AccountNav";
|
|
||||||
|
|
||||||
// Define the shape of state object
|
// Define the shape of state object
|
||||||
interface Account {
|
interface Account {
|
||||||
|
|
@ -19,7 +17,8 @@ interface Account {
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
language: string;
|
language: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
|
|
@ -31,50 +30,30 @@ interface SubSection {
|
||||||
component?: React.ReactNode;
|
component?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountOption {
|
|
||||||
userId: number;
|
|
||||||
userEmail: string;
|
|
||||||
accountId: number;
|
|
||||||
accountName: string;
|
|
||||||
role: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Setting = () => {
|
const Setting = () => {
|
||||||
const { page1 } = useParams<{ page1: string }>();
|
const { page1 } = useParams<{ page1: string }>();
|
||||||
console.log("page1", page1)
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
console.log("location", location)
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [activeAccountId, setActiveAccountId] = useState<string | null>(
|
|
||||||
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refs for subsections within each main tab
|
// Refs for subsections within each main tab
|
||||||
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState("userProfile");
|
const [activeTab, setActiveTab] = useState("userProfile");
|
||||||
const [activeSubSection, setActiveSubSection] = useState<string>("");
|
const [activeSubSection, setActiveSubSection] = useState<string>("");
|
||||||
const [user, setUser] = useState<UserData>({
|
const [user, setUser] = useState<UserData>({
|
||||||
id: '',
|
id: "",
|
||||||
name: '',
|
firstName: "",
|
||||||
email: '',
|
lastName: "",
|
||||||
language: '',
|
email: "",
|
||||||
|
language: "",
|
||||||
account: {
|
account: {
|
||||||
id: 0,
|
id: 0,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
name: '',
|
name: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
|
|
||||||
|
|
||||||
// Dropdown data states
|
|
||||||
const [accountOptions, setAccountOptions] = useState<AccountOption[]>([]);
|
|
||||||
const [accountOptionsLoading, setAccountOptionsLoading] = useState(false);
|
|
||||||
|
|
||||||
// Define subsections for each main tab
|
// Define subsections for each main tab
|
||||||
const getSubSections = (tabId: string): SubSection[] => {
|
const getSubSections = (tabId: string): SubSection[] => {
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
|
|
@ -83,6 +62,16 @@ const Setting = () => {
|
||||||
{ id: "profile-info", label: "Profile Information" },
|
{ id: "profile-info", label: "Profile Information" },
|
||||||
{ id: "signature", label: "Signature" },
|
{ id: "signature", label: "Signature" },
|
||||||
];
|
];
|
||||||
|
// case "create and invite":
|
||||||
|
// return [
|
||||||
|
// { id: "create-account", label: "Create Account" },
|
||||||
|
// { id: "invite-users", label: "Invite Users" },
|
||||||
|
// { id: "manage-invitations", label: "Manage Invitations" }
|
||||||
|
// ];
|
||||||
|
// return [
|
||||||
|
// { id: "share-options", label: "Share Options" },
|
||||||
|
// { id: "privacy-settings", label: "Privacy Settings" }
|
||||||
|
// ];
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -154,21 +143,6 @@ const Setting = () => {
|
||||||
}
|
}
|
||||||
}, [location.hash, activeTab]);
|
}, [location.hash, activeTab]);
|
||||||
|
|
||||||
// Fetch account list for dropdown
|
|
||||||
const getAccountList = async () => {
|
|
||||||
setAccountOptionsLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await api.get('/api/user/accounts');
|
|
||||||
if (res?.data) {
|
|
||||||
setAccountOptions(res.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching account options:', error);
|
|
||||||
} finally {
|
|
||||||
setAccountOptionsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render subsection content based on active tab and subsection
|
// Render subsection content based on active tab and subsection
|
||||||
const renderSubSectionContent = (subSectionId: string) => {
|
const renderSubSectionContent = (subSectionId: string) => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
|
|
@ -178,7 +152,7 @@ const Setting = () => {
|
||||||
return <UserProfile userData={user} />;
|
return <UserProfile userData={user} />;
|
||||||
case "signature":
|
case "signature":
|
||||||
return (
|
return (
|
||||||
<Signature />
|
<Signature/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
|
|
@ -204,14 +178,7 @@ const Setting = () => {
|
||||||
case "language":
|
case "language":
|
||||||
return <Language userData={user} />;
|
return <Language userData={user} />;
|
||||||
case "create and invite":
|
case "create and invite":
|
||||||
return (
|
return <CreateAndInvite onSubmit={handleSubmit} />;
|
||||||
<CreateAndInvite
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
createAndInviteLoader={createAndInviteLoader}
|
|
||||||
accountOptions={accountOptions}
|
|
||||||
accountOptionsLoading={accountOptionsLoading}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -242,66 +209,23 @@ const Setting = () => {
|
||||||
{ id: "contactShare", label: "Share Contact", type: "contact" },
|
{ id: "contactShare", label: "Share Contact", type: "contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getUserDetails = async () => {
|
||||||
// Listen for sessionStorage changes
|
try {
|
||||||
useEffect(() => {
|
let res = await api.get(`/api/user/details`)
|
||||||
const handleStorageChange = (e: StorageEvent) => {
|
console.log("getuser", res)
|
||||||
if (e.key === 'activeAccountId') {
|
setUser(res?.data)
|
||||||
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
|
} catch (error) {
|
||||||
setActiveAccountId(newActiveAccountId);
|
console.error("Error fetching user details:", error);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for storage events (works for changes from other tabs)
|
|
||||||
window.addEventListener('storage', handleStorageChange);
|
|
||||||
|
|
||||||
// Custom event listener for same-tab changes
|
|
||||||
const handleCustomStorageChange = (e: CustomEvent) => {
|
|
||||||
if (e.detail.key === 'activeAccountId') {
|
|
||||||
setActiveAccountId(e.detail.newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('storage', handleStorageChange);
|
|
||||||
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Re-fetch dashboard data when activeAccountId changes
|
|
||||||
useEffect(() => {
|
|
||||||
const rawUser = JSON.parse(sessionStorage.getItem('userDetails') || 'null');
|
|
||||||
|
|
||||||
if (rawUser) {
|
|
||||||
const userData: UserData = {
|
|
||||||
id: String(rawUser.id || ''),
|
|
||||||
name: rawUser.name || '',
|
|
||||||
email: rawUser.email || '',
|
|
||||||
language: rawUser.language || '',
|
|
||||||
account: {
|
|
||||||
id: rawUser.account?.id || 0,
|
|
||||||
isDefault: rawUser.account?.isDefault || false,
|
|
||||||
name: rawUser.account?.name || '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
setUser(userData);
|
|
||||||
}
|
}
|
||||||
}, [activeAccountId]);
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch account list when component mounts
|
getUserDetails()
|
||||||
getAccountList()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Set initial subsection when component mounts or activeTab changes
|
// Set initial subsection when component mounts or activeTab changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subSections = getSubSections(activeTab);
|
const subSections = getSubSections(activeTab);
|
||||||
console.log("subSections", subSections)
|
|
||||||
const hash = location.hash.replace('#', '');
|
const hash = location.hash.replace('#', '');
|
||||||
|
|
||||||
if (subSections.length > 0) {
|
if (subSections.length > 0) {
|
||||||
|
|
@ -331,58 +255,20 @@ const Setting = () => {
|
||||||
}, [activeSubSection]);
|
}, [activeSubSection]);
|
||||||
|
|
||||||
const handleSubmit = async (data: FormDataType, action: ActionType) => {
|
const handleSubmit = async (data: FormDataType, action: ActionType) => {
|
||||||
setCreateAndInviteLoader(true)
|
|
||||||
try {
|
try {
|
||||||
let res;
|
|
||||||
let toastMesaage;
|
|
||||||
if (action === 'create') {
|
if (action === 'create') {
|
||||||
console.log('Creating account:', data);
|
console.log('Creating account:', data);
|
||||||
const payloadAccount = {
|
|
||||||
name: data.accountName
|
|
||||||
}
|
|
||||||
res = await api.post(`/api/user/accounts`, payloadAccount)
|
|
||||||
console.log("resAccountName", res)
|
|
||||||
if (res.status === 201) {
|
|
||||||
toastMesaage = `${action.toUpperCase()} Created Successfull...!`
|
|
||||||
}
|
|
||||||
} else if (action === 'invite') {
|
} else if (action === 'invite') {
|
||||||
console.log('Inviting user:', data);
|
console.log('Inviting user:', data);
|
||||||
const payloadInvite = {
|
|
||||||
email: data.email,
|
|
||||||
role: data.role
|
|
||||||
}
|
|
||||||
res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
|
|
||||||
console.log("InviteNAme", res)
|
|
||||||
toastMesaage = "Invite Successfull...!"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res?.data) {
|
|
||||||
toast.success(toastMesaage, {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 1500,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refetch account list after successful submission
|
|
||||||
await getAccountList();
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Submission error:', err);
|
console.error('Submission error:', err);
|
||||||
} finally {
|
|
||||||
setCreateAndInviteLoader(false)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <SettingNav /> */}
|
<SettingNav />
|
||||||
{/* <AccountNav/> */}
|
|
||||||
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
|
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
|
||||||
{/* Main Navigation Tabs */}
|
{/* Main Navigation Tabs */}
|
||||||
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5">
|
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5">
|
||||||
|
|
@ -393,10 +279,11 @@ const Setting = () => {
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => handleMainTabClick(tab.id)}
|
onClick={() => handleMainTabClick(tab.id)}
|
||||||
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
|
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
|
||||||
${activeTab === tab.id
|
${
|
||||||
|
activeTab === tab.id
|
||||||
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
|
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
|
||||||
: "text-gray-600 hover:text-blue-500"
|
: "text-gray-600 hover:text-blue-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -409,17 +296,17 @@ const Setting = () => {
|
||||||
<div className="flex mt-3 px-8 flex-1 min-h-0">
|
<div className="flex mt-3 px-8 flex-1 min-h-0">
|
||||||
{/* Fixed Aside Navigation - Shows subsections of active tab */}
|
{/* Fixed Aside Navigation - Shows subsections of active tab */}
|
||||||
{activeTab === 'userProfile' &&
|
{activeTab === 'userProfile' &&
|
||||||
<section className="flex-shrink-0 mr-6 w-50 relative">
|
<section className="flex-shrink-0 mr-6 w-50 relative">
|
||||||
<div className="sticky top-6 z-20">
|
<div className="sticky top-6 z-20">
|
||||||
<AsideSection
|
<AsideSection
|
||||||
activeSubSection={activeSubSection}
|
activeSubSection={activeSubSection}
|
||||||
onSubSectionClick={handleSubSectionClick}
|
onSubSectionClick={handleSubSectionClick}
|
||||||
subSections={getSubSections(activeTab)}
|
subSections={getSubSections(activeTab)}
|
||||||
mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""}
|
mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
{/* Scrollable Main Content */}
|
{/* Scrollable Main Content */}
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ const SettingNav = () => {
|
||||||
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
>
|
>
|
||||||
<FaCog className="text-sm" />
|
<FaCog className="text-sm" />
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,34 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
|
// import { X, Upload, Edit3, Image, Cloud, HardDrive, Droplets } from 'lucide-react';
|
||||||
import { MdCloudUpload } from 'react-icons/md';
|
import { AiOutlineClose, AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
|
||||||
|
import { MdImage, MdCloudUpload, MdStorage } from 'react-icons/md';
|
||||||
import api from '../../../config/api';
|
import api from '../../../config/api';
|
||||||
import DrawSignatureCanvas from './DrawSignatureCanvas';
|
|
||||||
import { FaSpinner } from "react-icons/fa";
|
|
||||||
import { GrCheckboxSelected } from "react-icons/gr";
|
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
|
|
||||||
interface Signature {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
signatureType: string;
|
|
||||||
lastName: string;
|
|
||||||
firstName: string
|
|
||||||
font: string;
|
|
||||||
initialsBlob: string;
|
|
||||||
initialsStampedBlob: string;
|
|
||||||
initialsHash: string;
|
|
||||||
signatureHash: string;
|
|
||||||
signatureBlob: string;
|
|
||||||
signatureStampedBlob: string;
|
|
||||||
default: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddSignatureModalProps {
|
interface AddSignatureModalProps {
|
||||||
setShowAddSignature?: (value: boolean) => void;
|
setShowAddSignature?: (value: boolean) => void;
|
||||||
getAllSignatures: () => Promise<void>;
|
|
||||||
editingSignature?: Signature | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: string;
|
id: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
language: string;
|
language: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectStyle {
|
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignature }) => {
|
||||||
id: number,
|
const [activeTab, setActiveTab] = useState<'choose' | 'draw' | 'upload'>('choose');
|
||||||
style: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
|
||||||
setShowAddSignature,
|
|
||||||
getAllSignatures,
|
|
||||||
editingSignature
|
|
||||||
}) => {
|
|
||||||
const [activeTab, setActiveTab] = useState<'TYPE' | 'DRAW' | 'UPLOAD' | 'SELECT'>('TYPE');
|
|
||||||
const [fullName, setFullName] = useState('');
|
const [fullName, setFullName] = useState('');
|
||||||
const [initials, setInitials] = useState('');
|
const [initials, setInitials] = useState('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
||||||
const [signatures, setSignatures] = useState<{
|
|
||||||
fullSignature: string | null;
|
|
||||||
initials: string | null;
|
|
||||||
}>({ fullSignature: null, initials: null });
|
|
||||||
|
|
||||||
|
// Sample signature options for the CHOOSE tab
|
||||||
const signatureOptions = [
|
const signatureOptions = [
|
||||||
{ id: 1, name: 'print', initials: 'LA', style: 'print' },
|
{ id: 1, name: 'print', initials: 'LA', style: 'print' },
|
||||||
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
|
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
|
||||||
|
|
@ -72,9 +38,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' },
|
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [selectedSignature, setSelectedSignature] = useState<SelectStyle>(
|
const [selectedSignature, setSelectedSignature] = useState<number>(1);
|
||||||
signatureOptions.length > 0 ? { id: signatureOptions[0].id, style: signatureOptions[0].style } : { id: 0, style: "" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store the actual file
|
// Store the actual file
|
||||||
const [file1, setFile1] = useState<File | null>(null);
|
const [file1, setFile1] = useState<File | null>(null);
|
||||||
|
|
@ -84,7 +48,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
const [preview1, setPreview1] = useState<string | null>(null);
|
const [preview1, setPreview1] = useState<string | null>(null);
|
||||||
const [preview2, setPreview2] = useState<string | null>(null);
|
const [preview2, setPreview2] = useState<string | null>(null);
|
||||||
|
|
||||||
// User data
|
//user
|
||||||
const [user, setUser] = useState<UserData>({
|
const [user, setUser] = useState<UserData>({
|
||||||
id: "",
|
id: "",
|
||||||
firstName: "",
|
firstName: "",
|
||||||
|
|
@ -92,55 +56,19 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
email: "",
|
email: "",
|
||||||
language: "",
|
language: "",
|
||||||
account: {
|
account: {
|
||||||
id: 0,
|
id: 0,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
name: "",
|
name: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEditMode = !!editingSignature;
|
|
||||||
|
|
||||||
// Navigation items
|
// Navigation items
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ id: 'TYPE', label: 'TYPE', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
{ id: 'choose', label: 'Choose', icon: <MdImage className="w-5 h-5" /> },
|
||||||
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
{ id: 'draw', label: 'Draw', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
||||||
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
|
{ id: 'upload', label: 'Upload', icon: <AiOutlineUpload className="w-5 h-5" /> },
|
||||||
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Initialize form with editing signature data
|
|
||||||
useEffect(() => {
|
|
||||||
if (editingSignature) {
|
|
||||||
setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
|
|
||||||
setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
|
|
||||||
setActiveTab(editingSignature.signatureType as 'TYPE' | 'DRAW' | 'UPLOAD');
|
|
||||||
|
|
||||||
// Set font for SELECT tab
|
|
||||||
const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
|
|
||||||
if (fontOption) {
|
|
||||||
setSelectedSignature({ id: fontOption.id, style: fontOption.style });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set signature blobs for all tabs
|
|
||||||
if (editingSignature.signatureBlob && editingSignature.initialsBlob) {
|
|
||||||
setSignatures({
|
|
||||||
fullSignature: `data:image/png;base64,${editingSignature.signatureBlob}`,
|
|
||||||
initials: `data:image/png;base64,${editingSignature.initialsBlob}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set preview URLs for UPLOAD tab
|
|
||||||
if (editingSignature.signatureType === 'UPLOAD') {
|
|
||||||
setPreview1(`data:image/png;base64,${editingSignature.signatureBlob}`);
|
|
||||||
setPreview2(`data:image/png;base64,${editingSignature.initialsBlob}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [editingSignature]);
|
|
||||||
|
|
||||||
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
|
|
||||||
setSignatures(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cleanup preview URLs when component unmounts or image changes
|
// Cleanup preview URLs when component unmounts or image changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -149,121 +77,23 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
};
|
};
|
||||||
}, [preview1, preview2]);
|
}, [preview1, preview2]);
|
||||||
|
|
||||||
const handleImageChange = async (
|
const handleImageChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
setFile: React.Dispatch<React.SetStateAction<File | null>>,
|
setFile: React.Dispatch<React.SetStateAction<File | null>>,
|
||||||
setPreview: React.Dispatch<React.SetStateAction<string | null>>,
|
setPreview: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
type: "fullSignature" | "initials"
|
|
||||||
) => {
|
) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setFile(file);
|
setFile(file);
|
||||||
const previewUrl = URL.createObjectURL(file);
|
const previewUrl = URL.createObjectURL(file);
|
||||||
setPreview(previewUrl);
|
setPreview(previewUrl);
|
||||||
|
|
||||||
// Convert to base64
|
|
||||||
const base64 = await convertToBase64(file);
|
|
||||||
|
|
||||||
// Update the appropriate signature
|
|
||||||
setSignatures((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[type]: base64,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertToBase64 = (file: File): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => resolve(reader.result as string);
|
|
||||||
reader.onerror = (error) => reject(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
if (!signatures.fullSignature || !signatures.initials) {
|
|
||||||
alert('Please provide both signature and initials');
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
signatureType: activeTab,
|
|
||||||
signatureBase64: signatures.fullSignature,
|
|
||||||
initialsBase64: signatures.initials,
|
|
||||||
firstName: fullName.split(' ')[0] || user.firstName,
|
|
||||||
lastName: fullName.split(' ')[1] || user.lastName,
|
|
||||||
font: selectedSignature.style
|
|
||||||
};
|
|
||||||
|
|
||||||
let res;
|
|
||||||
if (isEditMode && editingSignature) {
|
|
||||||
// Update existing signature
|
|
||||||
const updatePayload = {
|
|
||||||
...payload,
|
|
||||||
signatureId: editingSignature.id
|
|
||||||
};
|
|
||||||
res = await api.put(`/api/user/signatures`, updatePayload);
|
|
||||||
console.log("handleUpdate", res);
|
|
||||||
|
|
||||||
toast.success("Signature Updated Successfully", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 500,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Create new signature
|
|
||||||
res = await api.post(`/api/user/signatures`, payload);
|
|
||||||
console.log("handleSubmit", res);
|
|
||||||
|
|
||||||
toast.success("Signature Created Successfully", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 500,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res?.data) {
|
|
||||||
await getAllSignatures();
|
|
||||||
setShowAddSignature?.(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error submitting signature:", error);
|
|
||||||
toast.error(isEditMode ? "Failed to update signature" : "Failed to create signature", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 2000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'TYPE':
|
case 'choose':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col ">
|
<div className="h-full flex flex-col ">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -279,8 +109,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
type="radio"
|
type="radio"
|
||||||
id={`signature-${option.id}`}
|
id={`signature-${option.id}`}
|
||||||
name="signature"
|
name="signature"
|
||||||
checked={selectedSignature.id === option.id}
|
checked={selectedSignature === option.id}
|
||||||
onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
|
onChange={() => setSelectedSignature(option.id)}
|
||||||
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 grid grid-cols-2 gap-2">
|
<div className="flex-1 grid grid-cols-2 gap-2">
|
||||||
|
|
@ -317,15 +147,56 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'DRAW':
|
case 'draw':
|
||||||
return (
|
return (
|
||||||
<DrawSignatureCanvas
|
<div className="h-full flex flex-col">
|
||||||
onSignatureChange={handleSignatureChange}
|
<div className="mb-6">
|
||||||
initialSignatures={isEditMode ? signatures : undefined}
|
<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-6">
|
||||||
|
{/* Draw area 1 */}
|
||||||
|
<div className="col-span-8">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
||||||
|
<div className="border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
|
<div className="text-center">
|
||||||
|
{/* <Edit3 className="mx-auto h-12 w-12 text-gray-400 mb-3" /> */}
|
||||||
|
<p className="text-sm text-gray-500">Click and drag to draw your signature</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Use mouse or touch to create your signature</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Draw area 2 */}
|
||||||
|
<div className="col-span-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
||||||
|
<div className="border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
|
<div className="text-center">
|
||||||
|
{/* <Edit3 className="mx-auto h-8 w-8 text-gray-400 mb-2" /> */}
|
||||||
|
<p className="text-xs text-gray-500">Draw initials</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors flex items-center gap-2">
|
||||||
|
{/* <X className="w-4 h-4" /> */}
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||||
|
Redo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'UPLOAD':
|
case 'upload':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -337,7 +208,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
{/* Upload 1 */}
|
{/* Upload 1 */}
|
||||||
<div className="col-span-8">
|
<div className="col-span-8">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
||||||
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
{preview1 ? (
|
{preview1 ? (
|
||||||
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
|
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -353,19 +224,19 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleImageChange(e, setFile1, setPreview1, "fullSignature")}
|
onChange={(e) => handleImageChange(e, setFile1, setPreview1)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload 2 */}
|
{/* Upload 2 */}
|
||||||
<div className="col-span-4">
|
<div className="col-span-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
||||||
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
{preview2 ? (
|
{preview2 ? (
|
||||||
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
|
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
<MdImage className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
||||||
<p className="text-xs text-gray-500">Upload initials</p>
|
<p className="text-xs text-gray-500">Upload initials</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -375,7 +246,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => handleImageChange(e, setFile2, setPreview2, "initials")}
|
onChange={(e) => handleImageChange(e, setFile2, setPreview2)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -384,41 +255,24 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
case 'SELECT':
|
|
||||||
return (
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Select Signature</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserDetails = async () => {
|
const getUserDetails = async () => {
|
||||||
try {
|
try {
|
||||||
let res = await api.get(`/api/user/details`);
|
let res = await api.get(`/api/user/details`)
|
||||||
console.log("getuser", res);
|
console.log("getuser", res)
|
||||||
let currUser = res?.data;
|
setUser(res?.data)
|
||||||
setUser(currUser);
|
} catch (error) {
|
||||||
|
console.error("Error fetching user details:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only set default values if not in edit mode
|
useEffect(() => {
|
||||||
if (!isEditMode) {
|
getUserDetails()
|
||||||
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 (
|
return (
|
||||||
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
|
@ -473,8 +327,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
{navigationItems.map((item) => (
|
{navigationItems.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => setActiveTab(item.id as 'TYPE' | 'DRAW' | 'UPLOAD')}
|
onClick={() => setActiveTab(item.id as 'choose' | '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
|
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg transition-colors ${activeTab === item.id
|
||||||
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -497,37 +351,25 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 py-5 px-5 overflow-hidden ">
|
<div className="flex-1 py-5 px-5 overflow-hidden ">
|
||||||
{/* <button
|
<button
|
||||||
onClick={() => setShowAddSignature?.(false)}
|
onClick={() => setShowAddSignature?.(false)}
|
||||||
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
|
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
|
||||||
>
|
>
|
||||||
<AiOutlineClose className="w-5 h-5 text-gray-500" />
|
<AiOutlineClose className="w-5 h-5 text-gray-500" />
|
||||||
</button> */}
|
</button>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Buttons in Main Area */}
|
{/* Footer Buttons in Main Area */}
|
||||||
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
|
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
className="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"
|
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
|
||||||
onClick={() => setShowAddSignature?.(false)}
|
onClick={() => setShowAddSignature?.(false)}
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
>
|
||||||
CANCEL
|
CANCEL
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors">
|
||||||
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"
|
CREATE SIGNATURE
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<FaSpinner className="animate-spin" />
|
|
||||||
{isEditMode ? 'UPDATING...' : 'CREATING...'}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
isEditMode ? 'UPDATE SIGNATURE' : 'CREATE SIGNATURE'
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,286 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,265 +1,199 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
|
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
|
||||||
import AddSignatureModal from './AddSignatureModal';
|
import AddSignatureModal from './AddSignatureModal';
|
||||||
import api from '../../../config/api';
|
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
import { FaSpinner } from 'react-icons/fa';
|
|
||||||
|
|
||||||
interface Signature {
|
interface Signature {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
signature: string;
|
||||||
signatureType: string;
|
initial: string;
|
||||||
lastName: string;
|
createdAt: string;
|
||||||
firstName: string
|
|
||||||
font: string;
|
|
||||||
initialsBlob: string;
|
|
||||||
initialsStampedBlob: string;
|
|
||||||
initialsHash: string;
|
|
||||||
signatureHash: string;
|
|
||||||
signatureBlob: string;
|
|
||||||
signatureStampedBlob: string;
|
|
||||||
default: boolean;
|
|
||||||
signatureMimeType:string;
|
|
||||||
initialsMimeType:string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignatureManager: React.FC = () => {
|
const SignatureManager: React.FC = () => {
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [signatures, setSignatures] = useState<Signature[]>([
|
||||||
const [signatures, setSignatures] = useState<Signature[]>([]);
|
{
|
||||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
id: '1',
|
||||||
const [showAddSignature, setShowAddSignature] = useState<boolean>(false);
|
signature: 'https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg',
|
||||||
const [editingSignature, setEditingSignature] = useState<Signature | null>(null);
|
initial: 'AB',
|
||||||
|
createdAt: 'dca3e646-62cd-4cc...'
|
||||||
const getAllSignatures = async () => {
|
},
|
||||||
setLoading(true)
|
{
|
||||||
try {
|
id: '2',
|
||||||
let res = await api.get("/api/user/signatures")
|
signature: '∼∼⌒∼∼⌒∼',
|
||||||
console.log("getAllSignatures", res.data)
|
initial: 'AB',
|
||||||
let signatureData = res?.data
|
createdAt: 'dca3e646-62cd-4cc...'
|
||||||
setSignatures(signatureData)
|
},
|
||||||
} catch (error) {
|
{
|
||||||
console.log("getAllSignatures", error)
|
id: '3',
|
||||||
} finally {
|
signature: 'Digital Signature',
|
||||||
setLoading(false)
|
initial: 'DS',
|
||||||
|
createdAt: 'dca3e646-62cd-4cc...'
|
||||||
}
|
}
|
||||||
}
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||||
getAllSignatures()
|
const [showAddSignature , setShowAddSignature] = useState<boolean>(false)
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleEdit = (id: string) => {
|
const handleEdit = (id: string) => {
|
||||||
console.log('Edit signature:', id);
|
console.log('Edit signature:', id);
|
||||||
let editSignature = signatures.find((item) => item.id === id);
|
|
||||||
console.log("editSignature", editSignature);
|
|
||||||
|
|
||||||
if (editSignature) {
|
|
||||||
setEditingSignature(editSignature);
|
|
||||||
setShowAddSignature(true);
|
|
||||||
}
|
|
||||||
setActiveDropdown(null);
|
setActiveDropdown(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
if (!id) return;
|
setSignatures(signatures.filter(sig => sig.id !== id));
|
||||||
setLoading(true)
|
setActiveDropdown(null);
|
||||||
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) => {
|
const toggleDropdown = (id: string) => {
|
||||||
setActiveDropdown(activeDropdown === id ? null : id);
|
setActiveDropdown(activeDropdown === id ? null : id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddNew = () => {
|
|
||||||
setEditingSignature(null);
|
|
||||||
setShowAddSignature(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModalClose = () => {
|
|
||||||
setShowAddSignature(false);
|
|
||||||
setEditingSignature(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen bg-white p-3 rounded-md px-4 mb-6">
|
||||||
{loading ? <div className=' flex items-center justify-center'>
|
<div className="max-w-[84vw] mx-auto">
|
||||||
<div className="p-12 text-center mt-8">
|
{/* Header */}
|
||||||
<div className="text-gray-400 mb-4">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<FaSpinner className="w-20 h-20 mx-auto animate-spin" />
|
<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>
|
</div>
|
||||||
<h3 className="text-xl font-semibold text-gray-700 mb-2">Loading...</h3>
|
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={()=>setShowAddSignature((prev) => !prev)}>
|
||||||
|
<FiPlus className="" />
|
||||||
|
ADD
|
||||||
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Table Card */}
|
||||||
<div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100">
|
<div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100">
|
||||||
{/* Table Header */}
|
{/* Table Header */}
|
||||||
<div className="bg-gray-50 border-b border-gray-200">
|
<div className="bg-gray-50 border-b border-gray-200">
|
||||||
<div className="grid grid-cols-12 gap-4 px-6 py-4">
|
<div className="grid grid-cols-12 gap-4 px-6 py-4">
|
||||||
<div className="col-span-5">
|
<div className="col-span-5">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Signature</h3>
|
<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-6 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>
|
</div>
|
||||||
<div className="col-span-5">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Initial</h3>
|
{/* Signature Image */}
|
||||||
|
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
||||||
|
<img
|
||||||
|
src="https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg"
|
||||||
|
className="h-[50px] w-[250px] object-contain"
|
||||||
|
alt="Signature"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Actions</h3>
|
{/* Signature ID */}
|
||||||
|
<div className="text-xs text-gray-500 truncate">
|
||||||
|
{signature.createdAt}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 */}
|
{/* Initial Column */}
|
||||||
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
<div className="col-span-5">
|
||||||
<img
|
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
|
||||||
src={`data:${signature.signatureMimeType};base64,${signature.signatureBlob}`}
|
{/* Label */}
|
||||||
className="h-[50px] w-[250px] object-contain"
|
<div className="text-sm text-gray-600 font-medium mb-1">
|
||||||
alt="Signature"
|
DS
|
||||||
/>
|
|
||||||
</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>
|
||||||
))}
|
|
||||||
|
{/* Signature Image */}
|
||||||
|
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
|
||||||
|
<img
|
||||||
|
src="https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg"
|
||||||
|
className="h-[50px] object-contain"
|
||||||
|
alt="DS Signature"
|
||||||
|
/>
|
||||||
|
</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>
|
</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>
|
</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>
|
||||||
|
<button className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold flex items-center gap-2 mx-auto transition-all duration-200">
|
||||||
|
<FiPlus className="w-5 h-5" />
|
||||||
|
ADD SIGNATURE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Click outside to close dropdown */}
|
||||||
|
{activeDropdown && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-0"
|
||||||
|
onClick={() => setActiveDropdown(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAddSignature && (
|
||||||
|
<AddSignatureModal setShowAddSignature={setShowAddSignature}/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ interface Account {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: number;
|
id: string;
|
||||||
name:string
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
language: string;
|
language: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
|
|
@ -25,7 +26,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
|
||||||
{/* Profile Picture */}
|
{/* Profile Picture */}
|
||||||
<div className="flex-shrink-0 text-center">
|
<div className="flex-shrink-0 text-center">
|
||||||
<div className="w-20 h-20 bg-green-800 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
|
<div className="w-20 h-20 bg-green-800 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
|
||||||
{userData.name.slice(0,2).toUpperCase()}
|
{getInitials(userData.firstName, userData.lastName)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -35,7 +36,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
|
||||||
<div className="flex py-1">
|
<div className="flex py-1">
|
||||||
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
|
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
|
||||||
<div className="text-gray-400 font-medium text-[13px]">
|
<div className="text-gray-400 font-medium text-[13px]">
|
||||||
{userData.name}
|
{userData.firstName} {userData.lastName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,581 +1,107 @@
|
||||||
// import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
// import { SaveType } from '../client/SignDocument';
|
import Draw from './Draw'
|
||||||
|
import TypedSign from './TypedSign';
|
||||||
// interface SignConProps {
|
import SignUpload from './SignUpload';
|
||||||
// setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
|
import { IoMdClose } from 'react-icons/io';
|
||||||
// recipientId: string;
|
|
||||||
// saved: SaveType;
|
|
||||||
// setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
|
|
||||||
// // save: () => void;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const SignCon: React.FC<SignConProps> = ({ setOpenSign, recipientId, saved, setSaved }) => {
|
|
||||||
// return (
|
|
||||||
// <div>SignCon</div>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default SignCon
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
// import api from '../../config/api';
|
|
||||||
import DrawSignatureCanvas from '../setting/Signature/DrawSignatureCanvas';
|
|
||||||
import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
|
|
||||||
import { MdCloudUpload } from 'react-icons/md';
|
|
||||||
import { FaSpinner } from "react-icons/fa";
|
|
||||||
import { GrCheckboxSelected } from "react-icons/gr";
|
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { SaveType } from '../client/SignDocument';
|
import { store } from '../../store/store';
|
||||||
import { CiSaveDown1 } from 'react-icons/ci';
|
import { SaveType, SaveTypeEnum } from '../client/SignDocument';
|
||||||
|
|
||||||
interface Signature {
|
interface SignConProps {
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
signatureType: string;
|
|
||||||
lastName: string;
|
|
||||||
firstName: string
|
|
||||||
font: string;
|
|
||||||
initialsBlob: string;
|
|
||||||
initialsStampedBlob: string;
|
|
||||||
initialsHash: string;
|
|
||||||
signatureHash: string;
|
|
||||||
signatureBlob: string;
|
|
||||||
signatureStampedBlob: string;
|
|
||||||
default: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddSignatureModalProps {
|
|
||||||
// setShowAddSignature?: (value: boolean) => void;
|
|
||||||
// getAllSignatures: () => Promise<void>;
|
|
||||||
// editingSignature?: Signature | null;
|
|
||||||
|
|
||||||
recipientId?: string;
|
|
||||||
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
recipientId: string;
|
||||||
saved: SaveType;
|
saved: SaveType;
|
||||||
setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
|
setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
|
||||||
|
// save: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Account {
|
const SignCon: React.FC<SignConProps> = ({ setOpenSign, recipientId, saved, setSaved }) => {
|
||||||
id: number;
|
|
||||||
isDefault: boolean;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserData {
|
const [activeSaveType, setActiveSaveType] = React.useState<SaveTypeEnum>(SaveTypeEnum.DRAW);
|
||||||
id: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
email: string;
|
|
||||||
language: string;
|
|
||||||
account: Account;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectStyle {
|
const handleSaveSignature = () => {
|
||||||
id: number,
|
if (!saved.save) return
|
||||||
style: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
console.log("signature:", saved.initial, "initials:", saved.signature, "subType:", saved.saveType, "MIC CHECK")
|
||||||
setOpenSign,
|
// return
|
||||||
recipientId,
|
|
||||||
saved,
|
|
||||||
setSaved
|
|
||||||
}) => {
|
|
||||||
const [activeTab, setActiveTab] = useState<'TYPED' | 'DRAW' | 'UPLOAD'>('TYPED');
|
|
||||||
const [fullName, setFullName] = useState('');
|
|
||||||
const [initials, setInitials] = useState('');
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
||||||
const [signatures, setSignatures] = useState<{
|
|
||||||
fullSignature: string | null;
|
|
||||||
initials: string | null;
|
|
||||||
}>({ fullSignature: null, initials: null });
|
|
||||||
|
|
||||||
const signatureOptions = [
|
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/signature?recipientId=${recipientId}`,
|
||||||
{ id: 1, name: 'print', initials: 'LA', style: 'print' },
|
{
|
||||||
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
|
signature: saved.signature,
|
||||||
{ id: 3, name: 'script', initials: 'LA', style: 'script' },
|
initials: saved.initial,
|
||||||
{ id: 4, name: 'elegant', initials: 'LA', style: 'elegant' },
|
subType: saved.saveType
|
||||||
{ id: 5, name: 'modern', initials: 'LA', style: 'modern' },
|
},
|
||||||
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' },
|
{ headers: { "ngrok-skip-browser-warning": "69420", } })
|
||||||
];
|
.then((res) => {
|
||||||
|
console.log(res, "Signature saved response")
|
||||||
const [selectedSignature, setSelectedSignature] = useState<SelectStyle>(
|
if (res.status === 200 || res.status === 201) {
|
||||||
signatureOptions.length > 0 ? { id: signatureOptions[0].id, style: signatureOptions[0].style } : { id: 0, style: "" }
|
setOpenSign(false)
|
||||||
);
|
setActiveSaveType(saved.saveType as SaveTypeEnum)
|
||||||
|
|
||||||
// Store the actual file
|
|
||||||
const [file1, setFile1] = useState<File | null>(null);
|
|
||||||
const [file2, setFile2] = useState<File | null>(null);
|
|
||||||
|
|
||||||
// Store the preview URL
|
|
||||||
const [preview1, setPreview1] = useState<string | null>(null);
|
|
||||||
const [preview2, setPreview2] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// User data
|
|
||||||
// const [user, setUser] = useState<UserData>({
|
|
||||||
// id: "",
|
|
||||||
// firstName: "",
|
|
||||||
// lastName: "",
|
|
||||||
// email: "",
|
|
||||||
// language: "",
|
|
||||||
// account: {
|
|
||||||
// id: 0,
|
|
||||||
// isDefault: false,
|
|
||||||
// name: "",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const isEditMode = !!editingSignature;
|
|
||||||
|
|
||||||
// Navigation items
|
|
||||||
const navigationItems = [
|
|
||||||
{ id: 'TYPED', label: 'TYPED', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
|
||||||
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
|
||||||
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
|
|
||||||
{ id: 'SAVED', label: 'SAVED', icon: <CiSaveDown1 className="w-5 h-5" /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Initialize form with editing signature data
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (editingSignature) {
|
|
||||||
// setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
|
|
||||||
// setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
|
|
||||||
// setActiveTab(editingSignature.signatureType as 'TYPED' | 'DRAW' | 'UPLOAD');
|
|
||||||
|
|
||||||
// // Set font for SELECT tab
|
|
||||||
// const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
|
|
||||||
// if (fontOption) {
|
|
||||||
// setSelectedSignature({ id: fontOption.id, style: fontOption.style });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Set signature blobs for all tabs
|
|
||||||
// if (editingSignature.signatureBlob && editingSignature.initialsBlob) {
|
|
||||||
// setSignatures({
|
|
||||||
// fullSignature: `data:image/png;base64,${editingSignature.signatureBlob}`,
|
|
||||||
// initials: `data:image/png;base64,${editingSignature.initialsBlob}`
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Set preview URLs for UPLOAD tab
|
|
||||||
// if (editingSignature.signatureType === 'UPLOAD') {
|
|
||||||
// setPreview1(`data:image/png;base64,${editingSignature.signatureBlob}`);
|
|
||||||
// setPreview2(`data:image/png;base64,${editingSignature.initialsBlob}`);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, [editingSignature]);
|
|
||||||
|
|
||||||
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
|
|
||||||
setSignatures(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Getting Signatures - START
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const res = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/sign/signatures?recipientId=${recipientId}`, {
|
|
||||||
headers: {
|
|
||||||
"ngrok-skip-browser-warning": "69420"
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error saving signature:', err);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
console.log(res, "Geting Signatures")
|
|
||||||
})()
|
|
||||||
}, [])
|
|
||||||
// Getting Signatures - END
|
|
||||||
|
|
||||||
// Cleanup preview URLs when component unmounts or image changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
handleSaveSignature();
|
||||||
if (preview1) URL.revokeObjectURL(preview1);
|
}, [saved])
|
||||||
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);
|
|
||||||
console.log("handleSubmit Called")
|
|
||||||
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 axios.post(`${import.meta.env.VITE_BASE_URI}/api/user/signatures`,
|
|
||||||
payload,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"ngrok-skip-browser-warning": "69420"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log("handleSubmit", res);
|
|
||||||
|
|
||||||
toast.success("Signature Created Successfully", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 500,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (res?.data) {
|
|
||||||
await getAllSignatures();
|
|
||||||
setShowAddSignature?.(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error submitting signature:", error);
|
|
||||||
// toast.error(isEditMode ? "Failed to update signature" : "Failed to create signature", {
|
|
||||||
// position: "top-center",
|
|
||||||
// autoClose: 2000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeOnClick: false,
|
|
||||||
// pauseOnHover: true,
|
|
||||||
// draggable: true,
|
|
||||||
// progress: undefined,
|
|
||||||
// theme: "light",
|
|
||||||
// transition: Bounce,
|
|
||||||
// });
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (activeTab) {
|
|
||||||
case 'TYPED':
|
|
||||||
return (
|
|
||||||
<div className="h-full flex flex-col ">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Choose Signature Style</h2>
|
|
||||||
<p className="text-sm text-gray-600">Select from pre-designed signature styles</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{signatureOptions.map((option) => (
|
|
||||||
<div key={option.id} className="flex items-center space-x-4 px-2 py-1 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id={`signature-${option.id}`}
|
|
||||||
name="signature"
|
|
||||||
checked={selectedSignature.id === option.id}
|
|
||||||
onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
|
|
||||||
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 grid grid-cols-2 gap-2">
|
|
||||||
<div className=" border-gray-300 rounded-lg px-2 py-1 bg-white ">
|
|
||||||
<div className="text-xs text-gray-500 mb-2">Signed by:</div>
|
|
||||||
<div className={`text-lg ${option.style === 'cursive' ? 'font-serif italic' :
|
|
||||||
option.style === 'script' ? 'font-mono' :
|
|
||||||
option.style === 'elegant' ? 'font-serif' :
|
|
||||||
option.style === 'modern' ? 'font-sans tracking-wide' :
|
|
||||||
'font-sans'
|
|
||||||
}`}>
|
|
||||||
{fullName || option.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 mt-2">
|
|
||||||
dca3e646-62cd-4cc...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className=" rounded-lg px-2 py-2 bg-white ">
|
|
||||||
<div className="text-xs text-gray-500 mb-2">DS</div>
|
|
||||||
<div className={`text-2xl ${option.style === 'cursive' ? 'font-serif italic' :
|
|
||||||
option.style === 'script' ? 'font-mono' :
|
|
||||||
option.style === 'elegant' ? 'font-serif' :
|
|
||||||
option.style === 'modern' ? 'font-sans tracking-wide' :
|
|
||||||
'font-sans'
|
|
||||||
}`}>
|
|
||||||
{initials || option.initials}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'DRAW':
|
|
||||||
return (
|
|
||||||
<DrawSignatureCanvas
|
|
||||||
onSignatureChange={handleSignatureChange}
|
|
||||||
// initialSignatures={isEditMode ? signatures : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'UPLOAD':
|
|
||||||
return (
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Upload Signature</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<div>
|
|
||||||
<div className="grid grid-cols-12 gap-6">
|
|
||||||
{/* Upload 1 */}
|
|
||||||
<div className="col-span-8">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
|
||||||
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
|
||||||
{preview1 ? (
|
|
||||||
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
|
|
||||||
) : (
|
|
||||||
<div className="text-center">
|
|
||||||
<MdCloudUpload className="mx-auto h-12 w-12 text-gray-400 mb-3" />
|
|
||||||
<p className="text-sm text-gray-500 mb-1">Click to upload or drag and drop</p>
|
|
||||||
<p className="text-xs text-gray-400">PNG, JPG, GIF up to 10MB</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="upload1"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
onChange={(e) => handleImageChange(e, setFile1, setPreview1, "fullSignature")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Upload 2 */}
|
|
||||||
<div className="col-span-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
|
||||||
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
|
||||||
{preview2 ? (
|
|
||||||
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
|
|
||||||
) : (
|
|
||||||
<div className="text-center">
|
|
||||||
<MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
|
||||||
<p className="text-xs text-gray-500">Upload initials</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="upload2"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
onChange={(e) => handleImageChange(e, setFile2, setPreview2, "initials")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// const getUserDetails = async () => {
|
|
||||||
// try {
|
|
||||||
// let res = await api.get(`/api/user/details`);
|
|
||||||
// console.log("getuser", res);
|
|
||||||
// let currUser = res?.data;
|
|
||||||
// setUser(currUser);
|
|
||||||
|
|
||||||
// // Only set default values if not in edit mode
|
|
||||||
// if (!isEditMode) {
|
|
||||||
// setFullName(currUser.firstName + " " + currUser.lastName);
|
|
||||||
// setInitials(currUser.firstName.slice(0, 1) + currUser.lastName.slice(0, 1));
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Error fetching user details:", error);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// getUserDetails();
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div className='absolute top-0 left-0 w-full h-full flex items-center justify-center '>
|
||||||
<div className="bg-white rounded-md shadow-xl w-full max-w-[80vw] h-[80vh] flex relative">
|
<div className='absolute top-0 left-0 w-full h-full bg-slate-100 opacity-60'></div>
|
||||||
|
|
||||||
{/* Sidebar Navigation */}
|
<div className='relative lg:min-w-[650px] max-w-max mx-auto shadow-md rounded-sm bg-white pt-3 pb-1 px-3'>
|
||||||
<div className="w-64 bg-gray-50 rounded-l-lg border-r border-gray-200 flex flex-col">
|
<IoMdClose
|
||||||
{/* Header */}
|
onClick={() => setOpenSign(false)}
|
||||||
<div className="px-4 py-4 border-b border-gray-200">
|
className='absolute right-3 text-slate-400 text-lg hover:text-[var(--primary)] cursor-pointer transition-all' />
|
||||||
<div className="flex items-center space-x-3">
|
{/* Signature Save types Nav */}
|
||||||
<div className='px-1 py-1'>
|
<div className='flex text-sm'>
|
||||||
<h3 className="text-md font-semibold text-gray-900">Add Signature</h3>
|
<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"}`}
|
||||||
<p className="text-xs text-gray-500">Create your digital signature</p>
|
onClick={() => setActiveSaveType(SaveTypeEnum.DRAW)}>
|
||||||
</div>
|
<p className='tracking-wide'>Draw</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"}`}
|
||||||
{/* Form Inputs */}
|
onClick={() => setActiveSaveType(SaveTypeEnum.TYPE)}>
|
||||||
<div className="px-6 py-2.5 border-b border-gray-200">
|
<p className='tracking-wide'>Type</p>
|
||||||
<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>
|
||||||
|
<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"}`}
|
||||||
{/* Navigation Menu */}
|
onClick={() => setActiveSaveType(SaveTypeEnum.UPLOAD)}>
|
||||||
<div className="flex-1 px-6 py-2.5">
|
<p className='tracking-wide'>Upload</p>
|
||||||
<nav className="space-y-2">
|
|
||||||
{navigationItems.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => setActiveTab(item.id as 'TYPED' | 'DRAW' | 'UPLOAD')}
|
|
||||||
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
|
|
||||||
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className={activeTab === item.id ? 'text-blue-600' : 'text-gray-400'}>
|
|
||||||
{item.icon}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{activeSaveType === SaveTypeEnum.DRAW && <Draw
|
||||||
<div className="flex-1 flex flex-col ">
|
saved={saved}
|
||||||
{/* Header with close button */}
|
setSaved={setSaved}
|
||||||
|
/>}
|
||||||
|
{activeSaveType === SaveTypeEnum.TYPE && <TypedSign
|
||||||
|
saved={saved}
|
||||||
|
setSaved={setSaved}
|
||||||
|
/>}
|
||||||
|
{activeSaveType === SaveTypeEnum.UPLOAD && <SignUpload
|
||||||
|
saved={saved}
|
||||||
|
setSaved={setSaved}
|
||||||
|
/>}
|
||||||
|
|
||||||
{/* Content */}
|
{/* <div className="mt-3 flex justify-end gap-2">
|
||||||
<div className="flex-1 py-5 px-5 overflow-hidden ">
|
<button
|
||||||
{/* <button
|
// onClick={close}
|
||||||
onClick={() => setShowAddSignature?.(false)}
|
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"
|
||||||
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
|
>
|
||||||
>
|
Cancel
|
||||||
<AiOutlineClose className="w-5 h-5 text-gray-500" />
|
</button>
|
||||||
</button> */}
|
<button
|
||||||
{renderContent()}
|
// onClick={save}
|
||||||
</div>
|
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"
|
||||||
|
>
|
||||||
{/* Footer Buttons in Main Area */}
|
Save
|
||||||
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
|
</button>
|
||||||
<button
|
</div> */}
|
||||||
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
||||||
onClick={() => setOpenSign?.(false)}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md bg-[var(--modalSubmitBG)] text-white hover:bg-[var(--modalSubmitBGHover)] transition-colors flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<FaSpinner className="animate-spin" />
|
|
||||||
{/* {isEditMode ? 'UPDATING...' : 'CREATING...'} */}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
CREATE SIGNATURE
|
|
||||||
{/* isEditMode ? 'UPDATE SIGNATURE' : 'CREATE SIGNATURE' */}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default AddSignatureModal;
|
export default SignCon;
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -64,14 +64,13 @@ const TransactionView = () => {
|
||||||
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
|
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<h1 className='text-lg font-semibold text-gray-800'>
|
<h1 className='text-lg font-semibold text-gray-800'>
|
||||||
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails?.transaction?.subject ?? "Untitled"}</span>
|
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails.transaction.name ?? "Untitled"}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
{/* new Date(transactionDetails?.transaction?.lastModifiedAt * 1000) */}
|
|
||||||
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
|
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
|
||||||
<p className=''><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'>Last Change on <span>12/02/25</span></p>
|
||||||
<p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
|
<p className='text-slate-600'>Sent on <span>11/02/25</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
|
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
|
||||||
|
|
@ -140,7 +139,7 @@ const TransactionView = () => {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
|
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
|
||||||
<p className='mt-2 text-slate-600'>{ transactionDetails?.transaction?.message ?? "No Message Found...!"}</p>
|
<p className='mt-2 text-slate-600'>No Message Found</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ api.interceptors.response.use(
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
// Prevent multiple refresh attempts
|
// Prevent multiple refresh attempts
|
||||||
// if (error.response?.status === 401 || error.response?.status === 500) {
|
// if (error.response?.status === 401 || error.response?.status === 500) {
|
||||||
if (error.response?.status === 401 || error.response?.status === 500) {
|
if (error.response?.status === 401) {
|
||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
try {
|
try {
|
||||||
if (!isRefreshing) {
|
if (!isRefreshing) {
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,6 @@
|
||||||
--hideModalBg: #2d2d2d;
|
--hideModalBg: #2d2d2d;
|
||||||
|
|
||||||
--processStepOneTextHead: #000000;
|
--processStepOneTextHead: #000000;
|
||||||
|
|
||||||
--modalSubmitBG: #1919d7;
|
|
||||||
--modalSubmitBGHover: #395ee1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -62,70 +59,30 @@
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
-ms-touch-action: none;
|
-ms-touch-action: none; /* For older IE/Edge */
|
||||||
/* For older IE/Edge */
|
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes textColorChange {
|
@keyframes textColorChange {
|
||||||
|
0%, 100% {
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
color: #41644A;
|
color: #41644A; /* Tailwind blue-500 */
|
||||||
/* Tailwind blue-500 */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-text {
|
.animate-text {
|
||||||
animation: textColorChange 2s infinite ease-in-out;
|
animation: textColorChange 2s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
.no-scrollbar::-webkit-scrollbar {
|
display: none;
|
||||||
display: none;
|
}
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar for IE, Edge and Firefox */
|
|
||||||
.no-scrollbar {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
/* IE and Edge */
|
|
||||||
scrollbar-width: none;
|
|
||||||
/* Firefox */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Group Border - START */
|
|
||||||
.group-border {
|
|
||||||
position: absolute;
|
|
||||||
border: 1px dashed var(--primary);
|
|
||||||
/* pointer-events: none; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-border>div {
|
|
||||||
position: absolute;
|
|
||||||
top: -10px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 50;
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: var(--primaryLight);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-border>div:hover {
|
|
||||||
background-color: var(--primaryLight2) ;
|
|
||||||
opacity: 0.9;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Group Border - END */
|
|
||||||
|
|
||||||
/* Loader CSS */
|
|
||||||
|
|
@ -1,188 +1,27 @@
|
||||||
// import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||||
// // import { v4 as uuidv4 } from "uuid";
|
// import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
// import { hideLoader, showLoader } from "./loaderSlice";
|
import { hideLoader, showLoader } from "./loaderSlice";
|
||||||
// // import { showModal } from "./modalSlice";
|
// import { showModal } from "./modalSlice";
|
||||||
// // import { updateSelectedDocuments } from "./selectedDocsSlice";
|
// import { updateSelectedDocuments } from "./selectedDocsSlice";
|
||||||
|
|
||||||
// import api from "../../config/api";
|
import api from "../../config/api";
|
||||||
// import { Bounce, toast } from "react-toastify";
|
import { Bounce, toast } from "react-toastify";
|
||||||
// // import { createAxiosInstance } from "../../config/axiosInstance";
|
// import { createAxiosInstance } from "../../config/axiosInstance";
|
||||||
|
|
||||||
// // const axiosInstance = createAxiosInstance();
|
// const axiosInstance = createAxiosInstance();
|
||||||
|
|
||||||
// export interface Document {
|
|
||||||
// id: string;
|
|
||||||
// name: string;
|
|
||||||
// extension: string;
|
|
||||||
// original?: boolean;
|
|
||||||
// corrupted?: boolean;
|
|
||||||
// isCompleted: boolean;
|
|
||||||
// status: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface DocumentState {
|
|
||||||
// documents: Document[];
|
|
||||||
// loading: boolean;
|
|
||||||
// error: string | null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const initialState: DocumentState = {
|
|
||||||
// documents: [],
|
|
||||||
// loading: false,
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// interface uploadArgs {
|
|
||||||
// formData: FormData;
|
|
||||||
// accountId: string;
|
|
||||||
// transactionId: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// // Fetch documents from API
|
|
||||||
// export const fetchDocuments = createAsyncThunk<Document[], { accountId: string, transactionId: string }>(
|
|
||||||
// "documents/fetchDocuments",
|
|
||||||
// async ({ accountId, transactionId }, { rejectWithValue }) => {
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`);
|
|
||||||
// return response.data as Document[];
|
|
||||||
// } catch (error: any) {
|
|
||||||
// // if(error.status === 401) {}
|
|
||||||
// return rejectWithValue(error.response?.data || "Failed to fetch documents");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Upload multiple documents using FormData (Key: "document")
|
|
||||||
// export const uploadDocuments = createAsyncThunk<Document[], uploadArgs>(
|
|
||||||
// "documents/uploadDocuments",
|
|
||||||
// async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => {
|
|
||||||
// try {
|
|
||||||
// dispatch(showLoader("Uploading documents..."));
|
|
||||||
// console.log("Form Data", formData.getAll("document"))
|
|
||||||
|
|
||||||
// const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, {
|
|
||||||
// headers: {
|
|
||||||
// "Content-Type": "multipart/form-data"
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// console.log("Form Uploaded Response DocumentSlice Line 185", response.data)
|
|
||||||
|
|
||||||
// dispatch(hideLoader());
|
|
||||||
// return [response.data];
|
|
||||||
// } catch (error: any) {
|
|
||||||
// dispatch(hideLoader());
|
|
||||||
// toast.error("Error Uploading Document", {
|
|
||||||
// position: "bottom-right",
|
|
||||||
// autoClose: 3000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeOnClick: false,
|
|
||||||
// pauseOnHover: false,
|
|
||||||
// draggable: false,
|
|
||||||
// progress: undefined,
|
|
||||||
// theme: "light",
|
|
||||||
// transition: Bounce,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// console.log("Failed Uploading Documents")
|
|
||||||
// throw new Error("Failed Uploading Documents")
|
|
||||||
|
|
||||||
// // Mock data in case of failure
|
|
||||||
// // const failedDocuments = formData.getAll("document").map((file: any) => ({
|
|
||||||
// // id: uuidv4(),
|
|
||||||
// // name: file.name,
|
|
||||||
// // extension: file.name.split(".").pop() || "unknown",
|
|
||||||
// // original: Math.random() < 0.5,
|
|
||||||
// // corrupted: Math.random() < 0.5,
|
|
||||||
// // }));
|
|
||||||
|
|
||||||
// // dispatch(updateSelectedDocuments({ documents: failedDocuments }));
|
|
||||||
// // return failedDocuments; // Instead of rejecting, return mock data
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const documentSlice = createSlice({
|
|
||||||
// name: "documents",
|
|
||||||
// initialState,
|
|
||||||
// reducers: {
|
|
||||||
// setDocuments: (state, action: PayloadAction<Document[]>) => {
|
|
||||||
// state.documents = action.payload;
|
|
||||||
// },
|
|
||||||
// setDocProgComplete: (state, action: PayloadAction<string>) => {
|
|
||||||
// const docIndex = state.documents.findIndex(doc => doc.id === action.payload);
|
|
||||||
// if (docIndex !== -1) {
|
|
||||||
// state.documents[docIndex].isCompleted = true;
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// resetDocuments: (state) => {
|
|
||||||
// state.documents = [];
|
|
||||||
// },
|
|
||||||
// deleteDocument: (state, action: PayloadAction<string>) => {
|
|
||||||
// state.documents = state.documents.filter(doc => doc.id !== action.payload);
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// extraReducers: (builder) => {
|
|
||||||
// builder
|
|
||||||
// // Fetch documents
|
|
||||||
// .addCase(fetchDocuments.pending, (state) => {
|
|
||||||
// state.loading = true;
|
|
||||||
// state.error = null;
|
|
||||||
// })
|
|
||||||
// .addCase(fetchDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
|
|
||||||
// state.loading = false;
|
|
||||||
// state.documents = action.payload;
|
|
||||||
// })
|
|
||||||
// .addCase(fetchDocuments.rejected, (state, action) => {
|
|
||||||
// state.loading = false;
|
|
||||||
// state.error = action.payload as string;
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // Upload multiple documents
|
|
||||||
// .addCase(uploadDocuments.pending, (state) => {
|
|
||||||
// state.loading = true;
|
|
||||||
// state.error = null;
|
|
||||||
// })
|
|
||||||
// .addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
|
|
||||||
// state.loading = false;
|
|
||||||
// state.documents = [...action.payload, ...state.documents];
|
|
||||||
// })
|
|
||||||
// .addCase(uploadDocuments.rejected, (state, action) => {
|
|
||||||
// state.loading = false;
|
|
||||||
// state.error = action.payload as string;
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// export const { resetDocuments, setDocuments, setDocProgComplete, deleteDocument } = documentSlice.actions;
|
|
||||||
// export default documentSlice.reducer;
|
|
||||||
|
|
||||||
|
|
||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { toast, Bounce } from 'react-toastify';
|
|
||||||
import api from '../../config/api';
|
|
||||||
import { hideLoader, showLoader } from './loaderSlice';
|
|
||||||
// import { showLoader, hideLoader } from '../loader/loaderSlice';
|
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// Document Type
|
|
||||||
// -----------------------
|
|
||||||
export interface Document {
|
export interface Document {
|
||||||
id?: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
extension: string;
|
||||||
type: string;
|
original?: boolean;
|
||||||
pageCount?: number;
|
corrupted?: boolean;
|
||||||
[key: string]: any;
|
isCompleted: boolean;
|
||||||
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
export interface DocumentState {
|
||||||
// Initial State
|
|
||||||
// -----------------------
|
|
||||||
interface DocumentState {
|
|
||||||
documents: Document[];
|
documents: Document[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
@ -194,81 +33,80 @@ const initialState: DocumentState = {
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------
|
interface uploadArgs {
|
||||||
// Thunk: Fetch Documents
|
formData: FormData;
|
||||||
// -----------------------
|
accountId: string;
|
||||||
export const fetchDocuments = createAsyncThunk<
|
transactionId: string;
|
||||||
Document[],
|
}
|
||||||
{ accountId: string; transactionId: string }
|
|
||||||
>(
|
|
||||||
'documents/fetchDocuments',
|
// Fetch documents from API
|
||||||
async ({ accountId, transactionId }, { rejectWithValue, getState }) => {
|
export const fetchDocuments = createAsyncThunk<Document[], { accountId: string, transactionId: string }>(
|
||||||
|
"documents/fetchDocuments",
|
||||||
|
async ({ accountId, transactionId }, { rejectWithValue }) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const state = getState() as {
|
const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`);
|
||||||
selectedRecipients: { transactionTemplateToggle: string };
|
|
||||||
};
|
|
||||||
const toggle = state.selectedRecipients.transactionTemplateToggle;
|
|
||||||
|
|
||||||
const response = await api.get(
|
|
||||||
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents?include_images=true`
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data as Document[];
|
return response.data as Document[];
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return rejectWithValue(error.response?.data || 'Failed to fetch documents');
|
// if(error.status === 401) {}
|
||||||
|
return rejectWithValue(error.response?.data || "Failed to fetch documents");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// -----------------------
|
// Upload multiple documents using FormData (Key: "document")
|
||||||
// Thunk: Upload Documents
|
export const uploadDocuments = createAsyncThunk<Document[], uploadArgs>(
|
||||||
// -----------------------
|
"documents/uploadDocuments",
|
||||||
export const uploadDocuments = createAsyncThunk<
|
async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => {
|
||||||
Document[],
|
|
||||||
{ formData: FormData; accountId: string; transactionId: string }
|
|
||||||
>(
|
|
||||||
'documents/uploadDocuments',
|
|
||||||
async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue, getState }) => {
|
|
||||||
try {
|
try {
|
||||||
dispatch(showLoader('Uploading documents...'));
|
dispatch(showLoader("Uploading documents..."));
|
||||||
|
console.log("Form Data", formData.getAll("document"))
|
||||||
|
|
||||||
const state = getState() as {
|
const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, {
|
||||||
selectedRecipients: { transactionTemplateToggle: string };
|
headers: {
|
||||||
};
|
"Content-Type": "multipart/form-data"
|
||||||
const toggle = state.selectedRecipients.transactionTemplateToggle;
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const response = await api.post(
|
console.log("Form Uploaded Response DocumentSlice Line 185", response.data)
|
||||||
`/api/accounts/${accountId}/${toggle}/${transactionId}/documents`,
|
|
||||||
formData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(hideLoader());
|
dispatch(hideLoader());
|
||||||
return [response.data];
|
return [response.data];
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
dispatch(hideLoader());
|
dispatch(hideLoader());
|
||||||
|
toast.error("Error Uploading Document", {
|
||||||
toast.error('Error Uploading Document', {
|
position: "bottom-right",
|
||||||
position: 'bottom-right',
|
|
||||||
autoClose: 3000,
|
autoClose: 3000,
|
||||||
theme: 'light',
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: false,
|
||||||
|
draggable: false,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
transition: Bounce,
|
transition: Bounce,
|
||||||
});
|
});
|
||||||
|
|
||||||
return rejectWithValue('Failed Uploading Documents');
|
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({
|
||||||
// Slice
|
name: "documents",
|
||||||
// -----------------------
|
|
||||||
const documentsSlice = createSlice({
|
|
||||||
name: 'documents',
|
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setDocuments: (state, action: PayloadAction<Document[]>) => {
|
setDocuments: (state, action: PayloadAction<Document[]>) => {
|
||||||
|
|
@ -289,7 +127,7 @@ const documentsSlice = createSlice({
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
// fetchDocuments
|
// Fetch documents
|
||||||
.addCase(fetchDocuments.pending, (state) => {
|
.addCase(fetchDocuments.pending, (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
|
|
@ -301,22 +139,16 @@ const documentsSlice = createSlice({
|
||||||
.addCase(fetchDocuments.rejected, (state, action) => {
|
.addCase(fetchDocuments.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
toast.error('Failed to fetch documents', {
|
|
||||||
position: 'bottom-right',
|
|
||||||
autoClose: 3000,
|
|
||||||
theme: 'light',
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// uploadDocuments
|
// Upload multiple documents
|
||||||
.addCase(uploadDocuments.pending, (state) => {
|
.addCase(uploadDocuments.pending, (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
|
.addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.documents.push(...action.payload);
|
state.documents = [...action.payload, ...state.documents];
|
||||||
})
|
})
|
||||||
.addCase(uploadDocuments.rejected, (state, action) => {
|
.addCase(uploadDocuments.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
|
@ -325,8 +157,5 @@ const documentsSlice = createSlice({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// -----------------------
|
export const { resetDocuments, setDocuments, setDocProgComplete, deleteDocument } = documentSlice.actions;
|
||||||
// Exports
|
export default documentSlice.reducer;
|
||||||
// -----------------------
|
|
||||||
export const { deleteDocument, resetDocuments, setDocuments, setDocProgComplete } = documentsSlice.actions;
|
|
||||||
export default documentsSlice.reducer;
|
|
||||||
|
|
@ -1,340 +1,11 @@
|
||||||
// import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
// import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
// import { toast } from 'react-toastify';
|
|
||||||
// import colors from '../../utlity/colorsArr';
|
|
||||||
// import api from '../../config/api';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// export enum recipientType {
|
|
||||||
// SIGNER = "Signer",
|
|
||||||
// INPERSONSIGNER = "InPersonSigner",
|
|
||||||
// CC = "CC"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface Recipient {
|
|
||||||
// id?: string;
|
|
||||||
// firstName: string;
|
|
||||||
// lastName: string;
|
|
||||||
// routingOrder: number;
|
|
||||||
// email: string;
|
|
||||||
// hostName: string;
|
|
||||||
// hostEmail: string;
|
|
||||||
// type: recipientType;
|
|
||||||
// color?: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface SelectedRecipientInt {
|
|
||||||
// recipients: Recipient[],
|
|
||||||
// signingOrder: boolean,
|
|
||||||
// transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE
|
|
||||||
// }
|
|
||||||
|
|
||||||
// interface AddRecipientArgs {
|
|
||||||
// recipientsData: Recipient[];
|
|
||||||
// accountId: string;
|
|
||||||
// transactionId: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export enum TRANSACTION_TEMPLATE_TOGGLE {
|
|
||||||
// TRANSACTIONS = "transactions",
|
|
||||||
// TEMPLATES = "templates"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const initialState: SelectedRecipientInt = {
|
|
||||||
// recipients: [],
|
|
||||||
// signingOrder: false,
|
|
||||||
// transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export const addRecipientAsync = createAsyncThunk<Recipient[], AddRecipientArgs>(
|
|
||||||
// 'selectedRecipients/addRecipientAsync',
|
|
||||||
// async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => {
|
|
||||||
// console.log(recipientsData, accountId, transactionId, "recipientsData Line 46");
|
|
||||||
// try {
|
|
||||||
// console.log(recipientsData, "recipientsData --- Line 48");
|
|
||||||
|
|
||||||
// const updateRecipientsData = recipientsData.filter((recipient, idx) => recipient.id !== null);
|
|
||||||
// const createRecipientsData = recipientsData.filter((recipient, idx) => recipient.id == null);
|
|
||||||
|
|
||||||
// console.log(updateRecipientsData, "updateRecipientsData Line 52");
|
|
||||||
// console.log(createRecipientsData, "createRecipientsData Line 53");
|
|
||||||
|
|
||||||
// const resUpdate = await api.put(
|
|
||||||
// `/api/accounts/${accountId}/transactions/${transactionId}/recipients`,
|
|
||||||
// updateRecipientsData
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const resCreate = await api.post(
|
|
||||||
// `/api/accounts/${accountId}/transactions/${transactionId}/recipients`,
|
|
||||||
// createRecipientsData
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if (createRecipientsData.length === 0) resCreate.data = [];
|
|
||||||
// if (updateRecipientsData.length === 0) resUpdate.data = [];
|
|
||||||
|
|
||||||
// console.log(resCreate, "resCreate");
|
|
||||||
// console.log(resUpdate, "resUpdate");
|
|
||||||
|
|
||||||
// return [...resCreate.data, ...resUpdate.data] as Recipient[];
|
|
||||||
// } catch (err: any) {
|
|
||||||
// return rejectWithValue(err.response?.data || "Failed to add recipient");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// interface RemoveRecipientAsyncInterfaceObj {
|
|
||||||
// id?: string;
|
|
||||||
// email?: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// interface RemoveRecipientAsyncInterface {
|
|
||||||
// recipientId: number | null;
|
|
||||||
// accountId: string;
|
|
||||||
// transactionId: string;
|
|
||||||
// email: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const removeRecipientAsync = createAsyncThunk<string | number, RemoveRecipientAsyncInterface>(
|
|
||||||
// 'selectedRecipients/removeRecipientAsync',
|
|
||||||
// async (obj, { rejectWithValue }) => {
|
|
||||||
// try {
|
|
||||||
// if (obj.recipientId) {
|
|
||||||
// const res = await api.delete(`/api/accounts/${obj.accountId}/transactions/${obj.transactionId}/recipients/${obj.recipientId}`);
|
|
||||||
// console.log(res, "Line 77")
|
|
||||||
// return obj.recipientId;
|
|
||||||
// }
|
|
||||||
// return obj.email
|
|
||||||
// } catch (err: any) {
|
|
||||||
// return rejectWithValue("Failed to remove recipient");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// export const fetchRecipients = createAsyncThunk<Recipient[], { accountId: string, transactionId: string }>(
|
|
||||||
// 'selectedRecipients/fetchRecipients',
|
|
||||||
// async ({ accountId, transactionId }, { rejectWithValue }) => {
|
|
||||||
// try {
|
|
||||||
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`);
|
|
||||||
// return response.data as Recipient[];
|
|
||||||
// } catch (err: any) {
|
|
||||||
// return rejectWithValue(err.response?.data || "Failed to fetch recipients");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const selectedRecipientsSlice = createSlice({
|
|
||||||
// name: 'selectedRecipients',
|
|
||||||
// initialState,
|
|
||||||
// reducers: {
|
|
||||||
// // Add a single recipient
|
|
||||||
// addRecipient: (state, action: PayloadAction<Recipient>) => {
|
|
||||||
// for (const recipient of state.recipients) {
|
|
||||||
// if (recipient.email === action.payload.email) {
|
|
||||||
// toast.dismiss();
|
|
||||||
// toast.error("Email already exists !", {
|
|
||||||
// position: "bottom-right",
|
|
||||||
// autoClose: 5000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeOnClick: false,
|
|
||||||
// pauseOnHover: true,
|
|
||||||
// draggable: true,
|
|
||||||
// progress: undefined,
|
|
||||||
// theme: "light",
|
|
||||||
// });
|
|
||||||
// return state;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Adding Recipient on top if type is inPersonSigner
|
|
||||||
// console.log("Getting Type ", action.payload.type)
|
|
||||||
// if (action.payload.type === recipientType.INPERSONSIGNER) {
|
|
||||||
// state.signingOrder = true
|
|
||||||
// state.recipients = [action.payload, ...state.recipients]
|
|
||||||
// } else {
|
|
||||||
// state.recipients.push(action.payload);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// },
|
|
||||||
// resetRecipients: (state) => {
|
|
||||||
// state.recipients = [];
|
|
||||||
// },
|
|
||||||
// toggleToTemplates: (state) => {
|
|
||||||
// state.transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
|
|
||||||
// },
|
|
||||||
// // Replace the entire selectedRecipients array
|
|
||||||
// setSelectedRecipients: (state, action: PayloadAction<Recipient[]>) => {
|
|
||||||
// for (const [index, recipient] of action.payload.entries()) {
|
|
||||||
// if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) {
|
|
||||||
// return state;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// state.recipients = action.payload;
|
|
||||||
// },
|
|
||||||
// setSigningOrder: (state, action: PayloadAction<boolean>) => {
|
|
||||||
|
|
||||||
// for (const recipient of state.recipients) {
|
|
||||||
// if (recipient.type === recipientType.INPERSONSIGNER) {
|
|
||||||
// if (state.signingOrder) return state;
|
|
||||||
// state.signingOrder = true;
|
|
||||||
// return state
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// state.signingOrder = action.payload;
|
|
||||||
// },
|
|
||||||
// // Remove Recipients
|
|
||||||
// removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => {
|
|
||||||
// console.log("removeSelectedRecipient Called")
|
|
||||||
// state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email);
|
|
||||||
// },
|
|
||||||
|
|
||||||
// // Add Color in Recipient
|
|
||||||
// addColorsToRecipient: (state) => {
|
|
||||||
// // const colors = generateUniqueColors(state.recipients.length);
|
|
||||||
// for (const [index, recipient] of state.recipients.entries()) {
|
|
||||||
// console.log(state)
|
|
||||||
// recipient.color = colors[index]
|
|
||||||
// }
|
|
||||||
// return state;
|
|
||||||
// },
|
|
||||||
// updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
|
|
||||||
// console.log("recipient order called Line 159")
|
|
||||||
// const { id, routingOrder } = action.payload;
|
|
||||||
// const recipient = state.recipients.find((r) => r.id === id || r.email === id);
|
|
||||||
// if (recipient) {
|
|
||||||
// recipient.routingOrder = routingOrder;
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => {
|
|
||||||
// console.log("updateRecipientIds", console.table(action.payload));
|
|
||||||
// for (const recObj of state.recipients) {
|
|
||||||
// const email = recObj.email
|
|
||||||
// for (const recObj2 of action.payload) {
|
|
||||||
// recObj.id = recObj2.id;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// resetSelRecipient: (state) => {
|
|
||||||
// state = initialState;
|
|
||||||
// return state;
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// extraReducers: (builder) => {
|
|
||||||
// builder
|
|
||||||
// // Fetch Recipient - START
|
|
||||||
// .addCase(fetchRecipients.pending, (state) => {
|
|
||||||
// })
|
|
||||||
// .addCase(fetchRecipients.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
|
|
||||||
// // console.log("Fetched recipients successfully:", action.payload);
|
|
||||||
|
|
||||||
// // Assign colors to recipients
|
|
||||||
// const assignedRecipients = action.payload.map((recipient, index) => ({
|
|
||||||
// ...recipient,
|
|
||||||
// color: colors[index], // Cycle if more recipients than colors
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// state.recipients = assignedRecipients;
|
|
||||||
// })
|
|
||||||
// .addCase(fetchRecipients.rejected, (state, action) => {
|
|
||||||
// console.error("Failed to fetch recipients:", action.payload);
|
|
||||||
// toast.error("Failed to fetch recipients", {
|
|
||||||
// position: "bottom-right",
|
|
||||||
// autoClose: 5000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeOnClick: false,
|
|
||||||
// pauseOnHover: true,
|
|
||||||
// draggable: true,
|
|
||||||
// progress: undefined,
|
|
||||||
// theme: "light",
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// // Fetch Recipient - END
|
|
||||||
|
|
||||||
// // Add Recipient - START
|
|
||||||
// .addCase(addRecipientAsync.pending, (state) => {
|
|
||||||
|
|
||||||
// })
|
|
||||||
|
|
||||||
|
|
||||||
// .addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
|
|
||||||
// // Optionally avoid duplicates if needed
|
|
||||||
|
|
||||||
// console.log("addRecipientAsync Called Line 258", action.payload);
|
|
||||||
// // const newRecipients = action.payload.filter(newRec => {
|
|
||||||
// // return !state.recipients.some(existing => existing existing.id === newRec.id);
|
|
||||||
// // });
|
|
||||||
|
|
||||||
// // const newRecipients = action.payload.filter(newRec => {
|
|
||||||
// // return !state.recipients.some(existing => "id" in existing && existing.id === newRec.id);
|
|
||||||
// // });
|
|
||||||
|
|
||||||
// // // Add new recipients and assign colors
|
|
||||||
// // const startIndex = state.recipients.length;
|
|
||||||
// // newRecipients.forEach((rec, index) => {
|
|
||||||
// // rec.color = colors[startIndex + index];
|
|
||||||
// // state.recipients.push(rec);
|
|
||||||
// // });
|
|
||||||
|
|
||||||
// state.recipients = action.payload.map((recipient, index) => ({
|
|
||||||
// ...recipient,
|
|
||||||
// color: colors[index % colors.length], // Cycle through colors
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// })
|
|
||||||
|
|
||||||
|
|
||||||
// .addCase(addRecipientAsync.rejected, (state, action) => {
|
|
||||||
// console.error("Failed to fetch recipients:", action.payload);
|
|
||||||
// toast.error("Failed to fetch recipients", {
|
|
||||||
// position: "bottom-right",
|
|
||||||
// autoClose: 5000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeOnClick: false,
|
|
||||||
// pauseOnHover: true,
|
|
||||||
// draggable: true,
|
|
||||||
// progress: undefined,
|
|
||||||
// theme: "light",
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// // Add Recipient - END
|
|
||||||
|
|
||||||
// // Fetch Recipient - START
|
|
||||||
// .addCase(removeRecipientAsync.pending, (state) => {
|
|
||||||
// })
|
|
||||||
// .addCase(removeRecipientAsync.fulfilled, (state, action: PayloadAction<string | number>) => {
|
|
||||||
// state.recipients = state.recipients.filter(
|
|
||||||
// (recipient) => recipient.id !== action.payload && recipient.email !== action.payload
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
// .addCase(removeRecipientAsync.rejected, (state, action) => {
|
|
||||||
// console.error("Failed to Delete recipients:", action.payload);
|
|
||||||
// toast.error("Failed to Delete recipients", {
|
|
||||||
// position: "bottom-right",
|
|
||||||
// autoClose: 5000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeOnClick: false,
|
|
||||||
// pauseOnHover: true,
|
|
||||||
// draggable: true,
|
|
||||||
// progress: undefined,
|
|
||||||
// theme: "light",
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// // Fetch Recipient - END
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// export const { addRecipient, setSelectedRecipients, toggleToTemplates, removeSelectedRecipient, setSigningOrder, addColorsToRecipient, updateRecipientIds, resetSelRecipient, updateRecipientOrder, resetRecipients } = selectedRecipientsSlice.actions;
|
|
||||||
// export default selectedRecipientsSlice.reducer;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import colors from '../../utlity/colorsArr';
|
import colors from '../../utlity/colorsArr';
|
||||||
import api from '../../config/api';
|
import api from '../../config/api';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export enum recipientType {
|
export enum recipientType {
|
||||||
SIGNER = "Signer",
|
SIGNER = "Signer",
|
||||||
INPERSONSIGNER = "InPersonSigner",
|
INPERSONSIGNER = "InPersonSigner",
|
||||||
|
|
@ -355,106 +26,54 @@ export interface Recipient {
|
||||||
|
|
||||||
export interface SelectedRecipientInt {
|
export interface SelectedRecipientInt {
|
||||||
recipients: Recipient[],
|
recipients: Recipient[],
|
||||||
signingOrder: boolean,
|
signingOrder: boolean
|
||||||
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddRecipientArgs {
|
interface AddRecipientArgs {
|
||||||
recipientsData: Recipient[];
|
recipientsData: Recipient[];
|
||||||
accountId: string;
|
accountId: string;
|
||||||
transactionId: string;
|
transactionId: string;
|
||||||
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RemoveRecipientAsyncInterface {
|
|
||||||
recipientId: number | null;
|
|
||||||
accountId: string;
|
|
||||||
transactionId: string;
|
|
||||||
email: string;
|
|
||||||
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TRANSACTION_TEMPLATE_TOGGLE {
|
|
||||||
TRANSACTIONS = "transactions",
|
|
||||||
TEMPLATES = "templates"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SelectedRecipientInt = {
|
const initialState: SelectedRecipientInt = {
|
||||||
recipients: [],
|
recipients: [],
|
||||||
signingOrder: false,
|
signingOrder: false
|
||||||
transactionTemplateToggle: TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Add Recipient Async
|
export const addRecipientAsync = createAsyncThunk<Recipient[], AddRecipientArgs>(
|
||||||
export const addRecipientAsync = createAsyncThunk<
|
|
||||||
Recipient[],
|
|
||||||
{ recipientsData: Recipient[]; accountId: string; transactionId: string }
|
|
||||||
>(
|
|
||||||
'selectedRecipients/addRecipientAsync',
|
'selectedRecipients/addRecipientAsync',
|
||||||
async ({ recipientsData, accountId, transactionId }, { rejectWithValue, getState }) => {
|
async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => {
|
||||||
|
console.log(recipientsData, accountId, transactionId, "recipientsData Line 46");
|
||||||
try {
|
try {
|
||||||
const state = getState() as { selectedRecipients: SelectedRecipientInt };
|
const response = await api.post(
|
||||||
const toggle = state.selectedRecipients.transactionTemplateToggle;
|
`/api/accounts/${accountId}/transactions/${transactionId}/recipients`,
|
||||||
|
recipientsData
|
||||||
const updateRecipientsData = recipientsData.filter(recipient => recipient.id !== null);
|
);
|
||||||
const createRecipientsData = recipientsData.filter(recipient => recipient.id == null);
|
console.log(response)
|
||||||
|
return response.data as Recipient[];
|
||||||
const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`;
|
|
||||||
|
|
||||||
const resUpdate = updateRecipientsData.length
|
|
||||||
? await api.put(basePath, updateRecipientsData)
|
|
||||||
: { data: [] };
|
|
||||||
|
|
||||||
const resCreate = createRecipientsData.length
|
|
||||||
? await api.post(basePath, createRecipientsData)
|
|
||||||
: { data: [] };
|
|
||||||
|
|
||||||
return [...resCreate.data, ...resUpdate.data] as Recipient[];
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data || "Failed to add recipient");
|
return rejectWithValue(err.response?.data || "Failed to add recipient");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Remove Recipient Async
|
export const removeRecipientAsync = createAsyncThunk<string, string>(
|
||||||
export const removeRecipientAsync = createAsyncThunk<
|
|
||||||
string | number,
|
|
||||||
{ recipientId: number | null; accountId: string; transactionId: string; email: string }
|
|
||||||
>(
|
|
||||||
'selectedRecipients/removeRecipientAsync',
|
'selectedRecipients/removeRecipientAsync',
|
||||||
async ({ recipientId, accountId, transactionId, email }, { rejectWithValue, getState }) => {
|
async (email, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const state = getState() as { selectedRecipients: SelectedRecipientInt };
|
await api.delete(`/api/recipients`, { data: { email } });
|
||||||
const toggle = state.selectedRecipients.transactionTemplateToggle;
|
return email; // return email so we can filter from state
|
||||||
|
|
||||||
const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`;
|
|
||||||
|
|
||||||
if (recipientId) {
|
|
||||||
await api.delete(`${basePath}/${recipientId}`);
|
|
||||||
return recipientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return email;
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue("Failed to remove recipient");
|
return rejectWithValue(err.response?.data || "Failed to remove recipient");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Fetch Recipients Async
|
export const fetchRecipients = createAsyncThunk<Recipient[], { accountId: string, transactionId: string }>(
|
||||||
export const fetchRecipients = createAsyncThunk<
|
|
||||||
Recipient[],
|
|
||||||
{ accountId: string; transactionId: string }
|
|
||||||
>(
|
|
||||||
'selectedRecipients/fetchRecipients',
|
'selectedRecipients/fetchRecipients',
|
||||||
async ({ accountId, transactionId }, { rejectWithValue, getState }) => {
|
async ({ accountId, transactionId }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const state = getState() as { selectedRecipients: SelectedRecipientInt };
|
const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`);
|
||||||
const toggle = state.selectedRecipients.transactionTemplateToggle;
|
|
||||||
|
|
||||||
const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`;
|
|
||||||
const response = await api.get(basePath);
|
|
||||||
|
|
||||||
return response.data as Recipient[];
|
return response.data as Recipient[];
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data || "Failed to fetch recipients");
|
return rejectWithValue(err.response?.data || "Failed to fetch recipients");
|
||||||
|
|
@ -466,6 +85,7 @@ const selectedRecipientsSlice = createSlice({
|
||||||
name: 'selectedRecipients',
|
name: 'selectedRecipients',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
// Add a single recipient
|
||||||
addRecipient: (state, action: PayloadAction<Recipient>) => {
|
addRecipient: (state, action: PayloadAction<Recipient>) => {
|
||||||
for (const recipient of state.recipients) {
|
for (const recipient of state.recipients) {
|
||||||
if (recipient.email === action.payload.email) {
|
if (recipient.email === action.payload.email) {
|
||||||
|
|
@ -473,29 +93,34 @@ const selectedRecipientsSlice = createSlice({
|
||||||
toast.error("Email already exists !", {
|
toast.error("Email already exists !", {
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
});
|
});
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding Recipient on top if type is inPersonSigner
|
||||||
|
console.log("Getting Type ", action.payload.type)
|
||||||
if (action.payload.type === recipientType.INPERSONSIGNER) {
|
if (action.payload.type === recipientType.INPERSONSIGNER) {
|
||||||
state.signingOrder = true;
|
state.signingOrder = true
|
||||||
state.recipients = [action.payload, ...state.recipients];
|
state.recipients = [action.payload, ...state.recipients]
|
||||||
} else {
|
} else {
|
||||||
state.recipients.push(action.payload);
|
state.recipients.push(action.payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
resetRecipients: (state) => {
|
resetRecipients: (state) => {
|
||||||
state.recipients = [];
|
state.recipients = [];
|
||||||
},
|
},
|
||||||
toggleToTemplates: (state) => {
|
// Replace the entire selectedRecipients array
|
||||||
state.transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
|
|
||||||
},
|
|
||||||
setSelectedRecipients: (state, action: PayloadAction<Recipient[]>) => {
|
setSelectedRecipients: (state, action: PayloadAction<Recipient[]>) => {
|
||||||
for (const [index, recipient] of action.payload.entries()) {
|
for (const [index, recipient] of action.payload.entries()) {
|
||||||
if ((recipient.type === recipientType.INPERSONSIGNER && index > 0)
|
if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) {
|
||||||
&& action.payload[index - 1].type !== recipientType.INPERSONSIGNER) {
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -503,26 +128,35 @@ const selectedRecipientsSlice = createSlice({
|
||||||
state.recipients = action.payload;
|
state.recipients = action.payload;
|
||||||
},
|
},
|
||||||
setSigningOrder: (state, action: PayloadAction<boolean>) => {
|
setSigningOrder: (state, action: PayloadAction<boolean>) => {
|
||||||
|
|
||||||
for (const recipient of state.recipients) {
|
for (const recipient of state.recipients) {
|
||||||
if (recipient.type === recipientType.INPERSONSIGNER) {
|
if (recipient.type === recipientType.INPERSONSIGNER) {
|
||||||
if (state.signingOrder) return state;
|
if (state.signingOrder) return state;
|
||||||
state.signingOrder = true;
|
state.signingOrder = true;
|
||||||
return state;
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.signingOrder = action.payload;
|
state.signingOrder = action.payload;
|
||||||
},
|
},
|
||||||
|
// Remove Recipients
|
||||||
removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => {
|
removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => {
|
||||||
|
console.log("removeSelectedRecipient Called")
|
||||||
state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email);
|
state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Add Color in Recipient
|
||||||
addColorsToRecipient: (state) => {
|
addColorsToRecipient: (state) => {
|
||||||
|
// const colors = generateUniqueColors(state.recipients.length);
|
||||||
for (const [index, recipient] of state.recipients.entries()) {
|
for (const [index, recipient] of state.recipients.entries()) {
|
||||||
recipient.color = colors[index];
|
console.log(state)
|
||||||
|
debugger
|
||||||
|
recipient.color = colors[index]
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
|
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
|
||||||
|
console.log("recipient order called Line 159")
|
||||||
const { id, routingOrder } = action.payload;
|
const { id, routingOrder } = action.payload;
|
||||||
const recipient = state.recipients.find((r) => r.id === id || r.email === id);
|
const recipient = state.recipients.find((r) => r.id === id || r.email === id);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
|
|
@ -530,22 +164,32 @@ const selectedRecipientsSlice = createSlice({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => {
|
updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => {
|
||||||
|
console.log("updateRecipientIds", console.table(action.payload));
|
||||||
for (const recObj of state.recipients) {
|
for (const recObj of state.recipients) {
|
||||||
|
const email = recObj.email
|
||||||
for (const recObj2 of action.payload) {
|
for (const recObj2 of action.payload) {
|
||||||
recObj.id = recObj2.id;
|
recObj.id = recObj2.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetSelRecipient: () => initialState
|
resetSelRecipient: (state) => {
|
||||||
|
state = initialState;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
.addCase(fetchRecipients.pending, () => { })
|
.addCase(fetchRecipients.pending, (state) => {
|
||||||
|
})
|
||||||
.addCase(fetchRecipients.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
|
.addCase(fetchRecipients.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
|
||||||
|
// console.log("Fetched recipients successfully:", action.payload);
|
||||||
|
|
||||||
|
// Assign colors to recipients
|
||||||
const assignedRecipients = action.payload.map((recipient, index) => ({
|
const assignedRecipients = action.payload.map((recipient, index) => ({
|
||||||
...recipient,
|
...recipient,
|
||||||
color: colors[index],
|
color: colors[index], // Cycle if more recipients than colors
|
||||||
}));
|
}));
|
||||||
|
|
||||||
state.recipients = assignedRecipients;
|
state.recipients = assignedRecipients;
|
||||||
})
|
})
|
||||||
.addCase(fetchRecipients.rejected, (state, action) => {
|
.addCase(fetchRecipients.rejected, (state, action) => {
|
||||||
|
|
@ -553,52 +197,39 @@ const selectedRecipientsSlice = createSlice({
|
||||||
toast.error("Failed to fetch recipients", {
|
toast.error("Failed to fetch recipients", {
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.addCase(addRecipientAsync.pending, () => { })
|
|
||||||
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
|
|
||||||
state.recipients = action.payload.map((recipient, index) => ({
|
.addCase(addRecipientAsync.pending, (state) => {
|
||||||
...recipient,
|
|
||||||
color: colors[index % colors.length],
|
})
|
||||||
}));
|
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient>) => {
|
||||||
|
console.log("Added recipient successfully:", action.payload);
|
||||||
|
// state.recipients.push(action.payload);
|
||||||
})
|
})
|
||||||
.addCase(addRecipientAsync.rejected, (state, action) => {
|
.addCase(addRecipientAsync.rejected, (state, action) => {
|
||||||
console.error("Failed to add recipients:", action.payload);
|
console.error("Failed to fetch recipients:", action.payload);
|
||||||
toast.error("Failed to add recipients", {
|
toast.error("Failed to fetch recipients", {
|
||||||
position: "bottom-right",
|
|
||||||
autoClose: 5000,
|
|
||||||
theme: "light",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.addCase(removeRecipientAsync.pending, () => { })
|
|
||||||
.addCase(removeRecipientAsync.fulfilled, (state, action: PayloadAction<string | number>) => {
|
|
||||||
state.recipients = state.recipients.filter(
|
|
||||||
(recipient) => recipient.id !== action.payload && recipient.email !== action.payload
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.addCase(removeRecipientAsync.rejected, (state, action) => {
|
|
||||||
console.error("Failed to delete recipient:", action.payload);
|
|
||||||
toast.error("Failed to delete recipient", {
|
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const { addRecipient, setSelectedRecipients, removeSelectedRecipient, setSigningOrder, addColorsToRecipient, updateRecipientIds, resetSelRecipient, updateRecipientOrder, resetRecipients } = selectedRecipientsSlice.actions;
|
||||||
addRecipient,
|
|
||||||
setSelectedRecipients,
|
|
||||||
toggleToTemplates,
|
|
||||||
removeSelectedRecipient,
|
|
||||||
setSigningOrder,
|
|
||||||
addColorsToRecipient,
|
|
||||||
updateRecipientIds,
|
|
||||||
resetSelRecipient,
|
|
||||||
updateRecipientOrder,
|
|
||||||
resetRecipients
|
|
||||||
} = selectedRecipientsSlice.actions;
|
|
||||||
|
|
||||||
export default selectedRecipientsSlice.reducer;
|
export default selectedRecipientsSlice.reducer;
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
interface Dimension {
|
|
||||||
maxTop: number | null;
|
|
||||||
minTop: number | null;
|
|
||||||
maxLeft: number | null;
|
|
||||||
minLeft: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGroupDimension = (
|
|
||||||
group: NodeListOf<HTMLElement>,
|
|
||||||
containerRect: DOMRect,
|
|
||||||
offsetX: number,
|
|
||||||
offsetY: number
|
|
||||||
) => {
|
|
||||||
let dimension: Dimension = { maxTop: null, minTop: null, maxLeft: null, minLeft: null };
|
|
||||||
|
|
||||||
group.forEach((element: HTMLElement) => {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
dimension.minTop = Math.min(dimension.minTop ?? Infinity, rect.top);
|
|
||||||
dimension.maxTop = Math.max(dimension.maxTop ?? 0, rect.bottom);
|
|
||||||
|
|
||||||
dimension.minLeft = Math.min(dimension.minLeft ?? Infinity, rect.left);
|
|
||||||
dimension.maxLeft = Math.max(dimension.maxLeft ?? 0, rect.right);
|
|
||||||
});
|
|
||||||
|
|
||||||
dimension.maxTop = dimension.maxTop !== null ? dimension.maxTop - containerRect.top + (offsetY - 10) : null;
|
|
||||||
dimension.minTop = dimension.minTop !== null ? dimension.minTop - containerRect.top - (offsetY - 10) : null;
|
|
||||||
|
|
||||||
dimension.maxLeft = dimension.maxLeft !== null ? dimension.maxLeft - containerRect.left + (offsetX - 10) : null;
|
|
||||||
dimension.minLeft = dimension.minLeft !== null ? dimension.minLeft - containerRect.left - (offsetX - 10) : null;
|
|
||||||
|
|
||||||
return dimension;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const handleGroupBorderPosition = (groupElements: NodeListOf<HTMLElement>, containerRect: DOMRect, offsetX: number, offsetY: number) => {
|
|
||||||
const dimension = getGroupDimension(groupElements, containerRect, offsetX, offsetY);
|
|
||||||
const borderTop: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='top']");
|
|
||||||
const borderRight: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='right']");
|
|
||||||
const borderBottom: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='bottom']");
|
|
||||||
const borderLeft: HTMLElement | null = document.querySelector("[border-group='" + groupElements[0].getAttribute("group") + "'][border-pos='left']");
|
|
||||||
|
|
||||||
if (borderTop !== null && dimension.maxLeft !== null && dimension.minLeft !== null) {
|
|
||||||
borderTop.style.width = (dimension.maxLeft - dimension.minLeft) + "px";
|
|
||||||
borderTop.style.height = "1px";
|
|
||||||
borderTop.style.top = dimension.minTop + "px";
|
|
||||||
borderTop.style.left = dimension.minLeft + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (borderRight !== null && dimension.maxTop !== null && dimension.minTop !== null) {
|
|
||||||
borderRight.style.width = "1px";
|
|
||||||
borderRight.style.height = (dimension.maxTop - dimension.minTop) + "px";
|
|
||||||
borderRight.style.top = dimension.minTop + "px";
|
|
||||||
borderRight.style.left = dimension.maxLeft + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (borderBottom !== null && dimension.maxLeft !== null && dimension.minLeft !== null) {
|
|
||||||
borderBottom.style.width = (dimension.maxLeft - dimension.minLeft) + "px";
|
|
||||||
borderBottom.style.height = "1px";
|
|
||||||
borderBottom.style.top = dimension.maxTop + "px";
|
|
||||||
borderBottom.style.left = dimension.minLeft + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (borderLeft !== null && dimension.maxTop !== null && dimension.minTop !== null) {
|
|
||||||
borderLeft.style.width = "1px";
|
|
||||||
borderLeft.style.height = (dimension.maxTop - dimension.minTop) + "px";
|
|
||||||
borderLeft.style.top = dimension.minTop + "px";
|
|
||||||
borderLeft.style.left = dimension.minLeft + "px";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const handleTargetBorderEventListener = (groupId: string, containerRect: DOMRect) => {
|
|
||||||
// console.log("handleTargetBorderEventListener called for groupId:", groupId);
|
|
||||||
// if (groupId) {
|
|
||||||
// const groupElements: NodeListOf<HTMLElement> = document.querySelectorAll(`div[group="${groupId}"]`);
|
|
||||||
// handleGroupBorderPosition(groupElements, containerRect);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const handleInitialGroupBorderPosition = (groupElements: NodeListOf<HTMLElement>, containerRect: DOMRect, offsetX: number, offsetY: number) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY)
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getGroupDimension,
|
|
||||||
handleGroupBorderPosition,
|
|
||||||
handleInitialGroupBorderPosition
|
|
||||||
// handleTargetBorderEventListener
|
|
||||||
}
|
|
||||||
|
|
@ -11,9 +11,7 @@ export default defineConfig({
|
||||||
'quality-friendly-goldfish.ngrok-free.app',
|
'quality-friendly-goldfish.ngrok-free.app',
|
||||||
'officially-powerful-goat.ngrok-free.app',
|
'officially-powerful-goat.ngrok-free.app',
|
||||||
'89f0-49-36-189-120.ngrok-free.app',
|
'89f0-49-36-189-120.ngrok-free.app',
|
||||||
'c8ee-49-36-191-130.ngrok-free.app',
|
'c8ee-49-36-191-130.ngrok-free.app'
|
||||||
'c76f1251c173.ngrok-free.app',
|
|
||||||
'enabled-upright-boxer.ngrok-free.app'
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue