Your descriptive commit message here
This commit is contained in:
parent
a877766da7
commit
0ad679fef7
10
src/App.tsx
10
src/App.tsx
|
|
@ -19,6 +19,7 @@ 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 Templates from "./components/templates/Templates";
|
||||||
|
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -47,9 +48,6 @@ const App = () => {
|
||||||
<Route path="/s/:page1" element={<Setting />} />
|
<Route path="/s/:page1" element={<Setting />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Route path="/process-step-one/:accountId/:transactionId" element={
|
<Route path="/process-step-one/:accountId/:transactionId" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<ProcessStepOne />
|
<ProcessStepOne />
|
||||||
|
|
@ -62,6 +60,12 @@ 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,363 +1,411 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
// import { useSelector } from 'react-redux';
|
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
|
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
|
||||||
import api from '../config/api';
|
import api from '../config/api';
|
||||||
|
|
||||||
// import { FaUser, FaSignOutAlt, FaBuilding, FaChevronDown, FaCheck } from 'react-icons/fa';
|
|
||||||
|
|
||||||
// import { RootState } from '../../store/store';
|
interface AccountNavProps {
|
||||||
// import { UserState } from '../../store/slices/userSlice';
|
accountName?: string | null;
|
||||||
|
transactionName?: string | null;
|
||||||
|
|
||||||
|
|
||||||
interface CommonNavProps {
|
|
||||||
accountName?: string | null;
|
|
||||||
transactionName?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the shape of state object
|
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: string;
|
id: number;
|
||||||
firstName: string;
|
name: string;
|
||||||
lastName: string;
|
email: string;
|
||||||
email: string;
|
language: string;
|
||||||
language: string;
|
account: Account;
|
||||||
account: Account;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountDataArray {
|
interface AccountDataArray {
|
||||||
accountId: number, accountName: string, role: string, status: boolean
|
userId: number,
|
||||||
|
userEmail: string,
|
||||||
|
name: string,
|
||||||
|
accountId: number;
|
||||||
|
accountName: string;
|
||||||
|
role: string;
|
||||||
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommonNav: React.FC<CommonNavProps> = ({ accountName = null, transactionName = null }) => {
|
// Helper function to dispatch custom storage event
|
||||||
// const user: UserState = useSelector((state: RootState) => state.user);
|
const dispatchSessionStorageChange = (key: string, newValue: any) => {
|
||||||
|
window.dispatchEvent(new CustomEvent('sessionStorageChange', {
|
||||||
|
detail: { key, newValue }
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const CommonNav: React.FC<AccountNavProps> = () => {
|
||||||
const [accountsOpen, setAccountsOpen] = useState(false);
|
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
|
||||||
const [loadingAccounts, setLoadingAccounts] = useState(false);
|
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
|
||||||
const [accounts, setAccounts] = useState<AccountDataArray[]>([]);
|
console.log("activeAccountId", activeAccountId);
|
||||||
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false); // Track if modal was closed
|
|
||||||
const dropdownRef = useRef(null);
|
|
||||||
|
|
||||||
const [user, setUser] = useState<UserData>({
|
const [open, setOpen] = useState(false);
|
||||||
id: "",
|
const [accountsOpen, setAccountsOpen] = useState(false);
|
||||||
firstName: "",
|
const [loadingAccounts, setLoadingAccounts] = useState(false);
|
||||||
lastName: "",
|
const [accounts, setAccounts] = useState<AccountDataArray[]>([]);
|
||||||
email: "",
|
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
|
||||||
language: "",
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
account: {
|
|
||||||
id: 0,
|
const [user, setUser] = useState<UserData>({
|
||||||
isDefault: false,
|
id: 0,
|
||||||
name: "",
|
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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getAccountDetails = async () => {
|
sessionStorage.setItem('userDetails', JSON.stringify({
|
||||||
try {
|
id: account.userId,
|
||||||
let res = await api.get('/api/user/accounts')
|
name: account.name,
|
||||||
let items = res?.data
|
email: account.userEmail,
|
||||||
console.log('items', items)
|
language: "", // set it if available
|
||||||
// Extract only the fields you want
|
account: {
|
||||||
const cleanedData: AccountDataArray[] = items.map((item: any) => ({
|
id: account.accountId,
|
||||||
accountId: item.accountId,
|
isDefault: false, // update as needed
|
||||||
accountName: item.accountName,
|
name: account.accountName,
|
||||||
role: item.role,
|
},
|
||||||
status: item.status,
|
}));
|
||||||
}));
|
|
||||||
|
|
||||||
setAccounts(cleanedData);
|
setAccountsOpen(false);
|
||||||
} catch (error) {
|
setOpen(false);
|
||||||
console.log(error)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
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(() => {
|
||||||
const handleAccountsClick = async () => {
|
if (!activeAccountId) {
|
||||||
setAccountsOpen(!accountsOpen);
|
getUserDetails();
|
||||||
|
} else {
|
||||||
if (!accountsOpen) {
|
setUser(userDetails);
|
||||||
setLoadingAccounts(true);
|
|
||||||
|
|
||||||
// Fetch API if:
|
|
||||||
// 1. No accounts data exists (first time), OR
|
|
||||||
// 2. Modal was previously closed (need fresh data)
|
|
||||||
if (accounts.length === 0 || hasModalBeenClosed) {
|
|
||||||
await getAccountDetails();
|
|
||||||
setHasModalBeenClosed(false); // Reset the flag after fetching
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingAccounts(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAccountSwitch = (account) => {
|
|
||||||
setUser(prev => ({
|
|
||||||
...prev,
|
|
||||||
account: { name: account.name, id: account.id }
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Update accounts active status
|
|
||||||
setAccounts(prevAccounts =>
|
|
||||||
prevAccounts.map(acc => ({
|
|
||||||
...acc,
|
|
||||||
isActive: acc.id === account.id
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
setAccountsOpen(false);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const getUserDetails = async () => {
|
|
||||||
try {
|
|
||||||
let res = await api.get(`/api/user/details`)
|
|
||||||
console.log("getuser", res)
|
|
||||||
setUser(res?.data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user details:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const location = useLocation();
|
||||||
getUserDetails()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
const rawPathnames = location.pathname
|
||||||
|
.split("/")
|
||||||
|
.filter((x) => x && isNaN(Number(x)));
|
||||||
|
|
||||||
const location = useLocation();
|
// 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 pathnames = location.pathname
|
const labelMap: Record<string, string> = {
|
||||||
.split("/")
|
accounts: "Accounts",
|
||||||
.filter((x) => x && isNaN(Number(x))); // remove numeric IDs
|
transactions: "Transactions",
|
||||||
|
templates: "Templates"
|
||||||
|
};
|
||||||
|
|
||||||
const labelMap: Record<string, string> = {
|
useEffect(() => {
|
||||||
accounts: "Accounts",
|
function handleClickOutside(event: MouseEvent) {
|
||||||
transactions: "Transactions",
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
templates: "Templates"
|
|
||||||
// Add more routes here if needed
|
|
||||||
};
|
|
||||||
|
|
||||||
// const [open, setOpen] = useState(false);
|
|
||||||
// const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Close dropdown when clicked outside
|
|
||||||
useEffect(() => {
|
|
||||||
function handleClickOutside(event: MouseEvent) {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
if (open) {
|
|
||||||
setHasModalBeenClosed(true); // Mark modal as closed when clicking outside
|
|
||||||
}
|
|
||||||
setOpen(false);
|
|
||||||
setAccountsOpen(false); // Also close accounts dropdown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
setOpen(false);
|
|
||||||
navigate("/login")
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle main dropdown toggle
|
|
||||||
const handleMainDropdownToggle = () => {
|
|
||||||
if (open) {
|
if (open) {
|
||||||
setHasModalBeenClosed(true); // Mark as closed when manually closing
|
setHasModalBeenClosed(true);
|
||||||
}
|
}
|
||||||
setOpen(!open);
|
setOpen(false);
|
||||||
if (!open) {
|
setAccountsOpen(false);
|
||||||
setAccountsOpen(false); // Close accounts dropdown when opening main dropdown
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const formatSegment = (segment: string) =>
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
segment
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
.replace(/[-_]/g, " ")
|
}, [open]);
|
||||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
||||||
|
|
||||||
|
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 ShimmerRow = () => (
|
const handleMainDropdownToggle = () => {
|
||||||
<div className="animate-pulse px-4 py-3 border-b border-gray-100 last:border-b-0">
|
if (open) {
|
||||||
<div className="flex items-center space-x-3">
|
setHasModalBeenClosed(true);
|
||||||
<div className="w-8 h-8 bg-gray-200 rounded-full"></div>
|
}
|
||||||
<div className="flex-1 space-y-2">
|
setOpen(!open);
|
||||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
if (!open) {
|
||||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
setAccountsOpen(false);
|
||||||
</div>
|
}
|
||||||
<div className="w-16 h-6 bg-gray-200 rounded"></div>
|
};
|
||||||
</div>
|
|
||||||
|
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>
|
||||||
);
|
<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 (
|
return (
|
||||||
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
|
<span key={to} className="flex items-center">
|
||||||
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
|
{index > 0 && <span className="mx-2 text-gray-400">{">"}</span>}
|
||||||
{pathnames.length === 0 ? (
|
{!isLast ? (
|
||||||
<span className="text-gray-900 font-semibold">Dashboard</span>
|
<Link
|
||||||
|
to={to}
|
||||||
|
className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
pathnames.map((segment, index) => {
|
<span className="text-gray-900 font-semibold">{label}</span>
|
||||||
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>
|
</span>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
{/* User Dropdown */}
|
<button
|
||||||
<div className="relative" ref={dropdownRef}>
|
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
|
||||||
{/* Avatar Button */}
|
onClick={handleMainDropdownToggle}
|
||||||
<button
|
aria-expanded={open}
|
||||||
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
|
aria-haspopup="true"
|
||||||
onClick={handleMainDropdownToggle}
|
>
|
||||||
aria-expanded={open}
|
<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">
|
||||||
aria-haspopup="true"
|
{user ? (
|
||||||
>
|
<span className="w-full h-full object-cover p-1.5 font-bold">
|
||||||
<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?.name?.slice(0, 2)).toUpperCase()}
|
||||||
{user ? (
|
</span>
|
||||||
<span
|
) : (
|
||||||
className="w-full h-full object-cover p-1.5 font-bold"
|
<FaUser className="text-lg" />
|
||||||
>{(user?.firstName + " " + user?.lastName).split(' ').map((item) => item.slice(0, 1)).join("").toUpperCase()}</span>
|
)}
|
||||||
) : (
|
|
||||||
<FaUser className="text-lg" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Enhanced Dropdown */}
|
|
||||||
{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">
|
|
||||||
{/* User Info Section */}
|
|
||||||
<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?.firstName + " " + user?.lastName).split(' ').map((item) => item.slice(0, 1)).join("").toUpperCase()}</span>
|
|
||||||
) : (
|
|
||||||
<FaUser className="text-lg" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="font-semibold text-gray-900 truncate">{user.firstName + ' ' + user.lastName || '-'}</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>
|
|
||||||
|
|
||||||
{/* Account List Section */}
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Accounts Dropdown */}
|
|
||||||
{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.status && (
|
|
||||||
<FaCheck className="text-green-500 text-xs" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-600 truncate">{account.company}</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>
|
|
||||||
|
|
||||||
{/* Logout Section */}
|
|
||||||
<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>
|
||||||
</header>
|
</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;
|
export default CommonNav;
|
||||||
|
|
@ -67,36 +67,36 @@ const Account = () => {
|
||||||
textColor: 'text-teal-700',
|
textColor: 'text-teal-700',
|
||||||
trend: 'up'
|
trend: 'up'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'Waiting for Others',
|
// title: 'Waiting for Others',
|
||||||
count: data.waitingForOthers || 0,
|
// count: data.waitingForOthers || 0,
|
||||||
icon: <FaClock size={18} className="text-stone-600" />,
|
// icon: <FaClock size={18} className="text-stone-600" />,
|
||||||
gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
|
// gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
|
||||||
shadowColor: 'shadow-blue-200/50',
|
// shadowColor: 'shadow-blue-200/50',
|
||||||
iconBg: 'bg-blue-100 border-blue-200',
|
// iconBg: 'bg-blue-100 border-blue-200',
|
||||||
textColor: 'text-blue-700',
|
// textColor: 'text-blue-700',
|
||||||
trend: 'down'
|
// trend: 'down'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: 'declined',
|
// title: 'declined',
|
||||||
count: data.declined || 0,
|
// count: data.declined || 0,
|
||||||
icon: <FaClock size={18} className="text-purple-600" />,
|
// icon: <FaClock size={18} className="text-purple-600" />,
|
||||||
gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
|
// gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
|
||||||
shadowColor: 'shadow-purple-200/50',
|
// shadowColor: 'shadow-purple-200/50',
|
||||||
iconBg: 'bg-purple-100 border-purple-200',
|
// iconBg: 'bg-purple-100 border-purple-200',
|
||||||
textColor: 'text-purple-700',
|
// textColor: 'text-purple-700',
|
||||||
trend: 'down'
|
// trend: 'down'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: 'cancelled',
|
// title: 'cancelled',
|
||||||
count: data.cancelled || 0,
|
// count: data.cancelled || 0,
|
||||||
icon: <FaClock size={18} className="text-green-600" />,
|
// icon: <FaClock size={18} className="text-green-600" />,
|
||||||
gradient: 'bg-gradient-to-br from-green-50 to-green-100',
|
// gradient: 'bg-gradient-to-br from-green-50 to-green-100',
|
||||||
shadowColor: 'shadow-green-200/50',
|
// shadowColor: 'shadow-green-200/50',
|
||||||
iconBg: 'bg-green-100 border-green-200',
|
// iconBg: 'bg-green-100 border-green-200',
|
||||||
textColor: 'text-green-700',
|
// textColor: 'text-green-700',
|
||||||
trend: 'down'
|
// trend: 'down'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: 'draft',
|
title: 'draft',
|
||||||
count: data.draft || 0,
|
count: data.draft || 0,
|
||||||
|
|
@ -115,7 +115,9 @@ const Account = () => {
|
||||||
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
|
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("activeAccountId Trans.",activeAccountId)
|
const [currentEmailId, setCurrentEmailId] = useState<string | null>('null');
|
||||||
|
|
||||||
|
console.log("activeAccountId Trans.", activeAccountId)
|
||||||
// const { accountId } = useParams<{ accountId: string }>();
|
// const { accountId } = useParams<{ accountId: string }>();
|
||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
|
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
|
||||||
|
|
@ -141,32 +143,38 @@ const Account = () => {
|
||||||
// getUserDetails()
|
// getUserDetails()
|
||||||
// }, [])
|
// }, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userDetails = JSON.parse(sessionStorage.getItem('userDetails'));
|
||||||
|
setCurrentEmailId(userDetails?.email)
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Listen for sessionStorage changes
|
// Listen for sessionStorage changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleStorageChange = (e: StorageEvent) => {
|
const handleStorageChange = (e: StorageEvent) => {
|
||||||
if (e.key === 'activeAccountId') {
|
if (e.key === 'activeAccountId') {
|
||||||
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
|
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
|
||||||
setActiveAccountId(newActiveAccountId);
|
setActiveAccountId(newActiveAccountId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen for storage events (works for changes from other tabs)
|
// Listen for storage events (works for changes from other tabs)
|
||||||
window.addEventListener('storage', handleStorageChange);
|
window.addEventListener('storage', handleStorageChange);
|
||||||
|
|
||||||
// Custom event listener for same-tab changes
|
// Custom event listener for same-tab changes
|
||||||
const handleCustomStorageChange = (e: CustomEvent) => {
|
const handleCustomStorageChange = (e: CustomEvent) => {
|
||||||
if (e.detail.key === 'activeAccountId') {
|
if (e.detail.key === 'activeAccountId') {
|
||||||
setActiveAccountId(e.detail.newValue);
|
setActiveAccountId(e.detail.newValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('storage', handleStorageChange);
|
window.removeEventListener('storage', handleStorageChange);
|
||||||
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const getDashboardData = async () => {
|
const getDashboardData = async () => {
|
||||||
|
|
@ -281,7 +289,8 @@ const Account = () => {
|
||||||
<th className="px-4 py-3 text-center">Active</th>
|
<th className="px-4 py-3 text-center">Active</th>
|
||||||
<th className="px-4 py-3 text-center">Last Change</th>
|
<th className="px-4 py-3 text-center">Last Change</th>
|
||||||
<th className="px-4 py-3 text-center">Expire After (days)</th>
|
<th className="px-4 py-3 text-center">Expire After (days)</th>
|
||||||
<th className="px-4 py-3 text-center"></th>
|
<th className="px-4 py-3 text-center">Sign</th>
|
||||||
|
<th className="px-4 py-3 text-center">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
|
@ -318,6 +327,9 @@ const Account = () => {
|
||||||
<td className="px-4 py-3 text-center">
|
<td className="px-4 py-3 text-center">
|
||||||
{tx.expireAfter ?? "-"}
|
{tx.expireAfter ?? "-"}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center ">
|
||||||
|
{tx?.createdBy?.email === currentEmailId ? <button className="px-2 py-0.5 rounded-xl text-xs font-medium bg-green-100 text-green-700">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>
|
||||||
|
|
|
||||||
|
|
@ -103,32 +103,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]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
FaCheck,
|
||||||
|
FaFileAlt,
|
||||||
|
FaUsers,
|
||||||
|
FaEnvelope,
|
||||||
|
FaClock,
|
||||||
|
FaDownload,
|
||||||
|
FaShare,
|
||||||
|
FaShieldAlt,
|
||||||
|
FaLock,
|
||||||
|
FaPen,
|
||||||
|
FaCheckCircle
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import { fetchRecipients } from "../../../store/slices/selectedRecipientsSlice";
|
||||||
|
import { fetchDocuments } from "../../../store/slices/documentSlice";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
import Loader from "../../dashboard/Loader";
|
||||||
|
import confetti from 'canvas-confetti'
|
||||||
|
import { IoClose } from "react-icons/io5";
|
||||||
|
import { IoMdHome } from "react-icons/io";
|
||||||
|
|
||||||
|
type Signer = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
routingOrder: number;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Signers List Component
|
||||||
|
const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-md shadow-sm border border-gray-200">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||||
|
<FaUsers className="text-blue-600" />
|
||||||
|
Signature Status
|
||||||
|
</h3>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{signers.filter(s => s.completed).length} of {signers.length} signed
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 max-h-[29vh] overflow-y-auto no-scrollbar">
|
||||||
|
{signers.map((signer) => (
|
||||||
|
<div key={signer.id} className="flex items-center gap-4 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium">
|
||||||
|
{signer.name.split(" ").map(n => n[0].toUpperCase()).join("")}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-gray-800">{signer.name}</div>
|
||||||
|
<div className="text-sm text-gray-600">{signer.email}</div>
|
||||||
|
<div className="text-xs text-gray-500">Order: {signer.routingOrder}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{signer.completed ? (
|
||||||
|
<div className="flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm">
|
||||||
|
<FaCheck className="w-3 h-3" />
|
||||||
|
Signed
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-700 rounded-full text-sm">
|
||||||
|
<FaClock className="w-3 h-3" />
|
||||||
|
Pending
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fixed Signing Order Diagram Component
|
||||||
|
const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
||||||
|
const groupByRoutingOrder = (data: Signer[]) => {
|
||||||
|
const grouped: Record<number, Signer[]> = {};
|
||||||
|
data.forEach((signer) => {
|
||||||
|
if (!grouped[signer.routingOrder]) {
|
||||||
|
grouped[signer.routingOrder] = [];
|
||||||
|
}
|
||||||
|
grouped[signer.routingOrder].push(signer);
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitials = (name: string) => {
|
||||||
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0].toUpperCase())
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const grouped = groupByRoutingOrder(signers);
|
||||||
|
const sortedOrders = Object.keys(grouped)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
const currentProcessingOrder =
|
||||||
|
sortedOrders.find((order) =>
|
||||||
|
grouped[order].some((signer) => !signer.completed)
|
||||||
|
) || -1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-md shadow-sm border border-gray-200">
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 mb-4">Signing Flow</h3>
|
||||||
|
<div className="overflow-x-auto overflow-y-auto max-h-96 no-scrollbar ">
|
||||||
|
<div className="flex justify-center items-center gap-4 min-w-max">
|
||||||
|
{/* Mail icon */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="bg-blue-100 p-4 rounded-full">
|
||||||
|
<FaEnvelope className="text-blue-600 text-2xl" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">Sent</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dotted line */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Signers by routing order */}
|
||||||
|
<div className="flex items-center justify-center space-x-6">
|
||||||
|
{sortedOrders.map((order, index) => (
|
||||||
|
<React.Fragment key={order}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-2">
|
||||||
|
Step {order}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center space-y-3">
|
||||||
|
{grouped[order].map((signer) => {
|
||||||
|
let bgColor = "";
|
||||||
|
let icon = null;
|
||||||
|
|
||||||
|
if (signer.completed) {
|
||||||
|
bgColor = "bg-[var(--primary)]";
|
||||||
|
icon = <FaCheck className="text-green-600" />;
|
||||||
|
} else if (order === currentProcessingOrder) {
|
||||||
|
bgColor = "bg-yellow-500";
|
||||||
|
icon = <FaClock className="text-yellow-800" />;
|
||||||
|
} else {
|
||||||
|
bgColor = "bg-gray-400";
|
||||||
|
icon = <FaLock className="text-gray-500" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={signer.id} className="relative">
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${bgColor}`}
|
||||||
|
title={signer.name}
|
||||||
|
>
|
||||||
|
{getInitials(signer.name)}
|
||||||
|
<span className="absolute bottom-[-4px] right-[-8px] p-1 rounded-full bg-white shadow">
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-gray-600 text-center max-w-16 truncate">
|
||||||
|
{signer.name.split(" ")[0]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{index < sortedOrders.length - 1 && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main DocumentCompletionScreen Component
|
||||||
|
const DocumentCompletionScreen = () => {
|
||||||
|
const documentList = [
|
||||||
|
{ title: 'Document 1', pages: 12 },
|
||||||
|
{ title: 'Document 2', pages: 8 },
|
||||||
|
{ title: 'Document 3', pages: 5 },
|
||||||
|
{ title: 'Document 4', pages: 22 },
|
||||||
|
{ title: 'Document 5', pages: 10 },
|
||||||
|
{ title: 'Document 6', pages: 15 },
|
||||||
|
{ title: 'Document 7', pages: 7 },
|
||||||
|
{ title: 'Document 8', pages: 9 },
|
||||||
|
{ title: 'Document 9', pages: 6 },
|
||||||
|
{ title: 'Document 10', pages: 11 },
|
||||||
|
{ title: 'Document 11', pages: 4 }, // Won't be shown due to slice
|
||||||
|
];
|
||||||
|
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
const [showConfetti, setShowConfetti] = React.useState(true)
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const transactionId = useSelector((state: RootState) => state.transaction.transactionId)
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||||||
|
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||||||
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
||||||
|
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showConfetti) {
|
||||||
|
const duration = 3000;
|
||||||
|
const end = Date.now() + duration;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Date.now() > end) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 80, // more particles
|
||||||
|
angle: Math.random() * 360, // fire in random direction
|
||||||
|
spread: 220, // wider spread
|
||||||
|
startVelocity: 15, // more power
|
||||||
|
scalar: 1.2, // bigger pieces (default is 1)
|
||||||
|
origin: {
|
||||||
|
x: Math.random(),
|
||||||
|
y: Math.random() * 0.6,
|
||||||
|
},
|
||||||
|
zIndex: 9999,
|
||||||
|
});
|
||||||
|
}, 350); // fire every 150ms
|
||||||
|
}
|
||||||
|
}, [showConfetti]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
showConfetti && (
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full z-[9999] bg-white/80 backdrop-blur-sm flex items-center justify-center">
|
||||||
|
{/* <Confetti width={width} height={height} /> */}
|
||||||
|
|
||||||
|
<div className="relative bg-white bg-opacity-90 backdrop-blur-lg border border-green-200 shadow-xl rounded-md px-8 py-6 w-[90%] max-w-sm text-center animate-fade-in-up">
|
||||||
|
{/* Close Icon */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowConfetti(false)} // or whatever logic hides the modal
|
||||||
|
className="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<IoClose className="text-2xl" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Success Icon */}
|
||||||
|
<FaCheckCircle className="text-green-500 text-5xl mb-4 mx-auto drop-shadow-lg" />
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-800 mb-1">Transaction Successful</h2>
|
||||||
|
<p className="text-gray-600 text-sm">Your transaction has been created and saved successfully.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
<div className="min-h-screen bg-[var(--mainViewBg)]">
|
||||||
|
{/* Top Banner */}
|
||||||
|
|
||||||
|
<div className="bg-[var(--primary)] text-white shadow-lg">
|
||||||
|
<div className="max-w-[97vw] mx-auto px-2 py-2.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link to='/' className="w-5 h-5 rounded-full flex items-center justify-center">
|
||||||
|
<IoMdHome className="text-xl" />
|
||||||
|
</Link>
|
||||||
|
<div className="w-5 h-5 bg-white/20 rounded-full flex items-center justify-center">
|
||||||
|
<FaCheck className="text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-white/80">Awesome! Your document is sent for signature</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link to={`/accounts/${accountId}/transactions/${transactionId}`} className="flex items-center gap-1 cursor-pointer bg-white text-green-600 px-2.5 py-1 rounded-md font-semibold hover:bg-gray-100 transition-colors">
|
||||||
|
<FaPen className="text-[10px]" />
|
||||||
|
<span className="text-[13px]">Edit</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(selectedRecipients.length > 0 && selectedDocuments.length > 0) ?
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="max-w-[97vw] mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
|
||||||
|
{/* Left Section - Document Details */}
|
||||||
|
<div className="lg:col-span-5 space-y-3">
|
||||||
|
{/* Document Preview Card */}
|
||||||
|
<div className="bg-white rounded-md shadow-sm border border-gray-200 overflow-hidden max-h-[88vh] no-scrollbar overflow-y-auto">
|
||||||
|
<div className="p-3 ">
|
||||||
|
{selectedDocuments?.map((item, index) => (
|
||||||
|
<div key={item.id} className="flex items-start justify-between mb-2 px-2 py-3 bg-gray-50">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||||
|
{/* <FaFileAlt className="text-blue-600 text-xl" /> */}
|
||||||
|
<img src={item.images[0].src} alt="document" className="h-10 w-10" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md font-semibold text-gray-800">
|
||||||
|
Name : {item.name ?? '--'}
|
||||||
|
</h3>
|
||||||
|
<div className="text-gray-600 text-sm">
|
||||||
|
Status : {item.status ?? '--'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Section - Signer Components */}
|
||||||
|
<div className="lg:col-span-7 space-y-1.5">
|
||||||
|
{/* Signing Order Diagram */}
|
||||||
|
<SigningOrderDiagram signers={selectedRecipients} />
|
||||||
|
|
||||||
|
{/* Signers List */}
|
||||||
|
<SignersList signers={selectedRecipients} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <Loader />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentCompletionScreen;
|
||||||
|
|
@ -10,7 +10,7 @@ import { MdKeyboardArrowDown } from 'react-icons/md'
|
||||||
import { RiResetLeftLine, RiResetRightLine } from 'react-icons/ri'
|
import { RiResetLeftLine, RiResetRightLine } from 'react-icons/ri'
|
||||||
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice'
|
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice'
|
||||||
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
interface NavTwoProps {
|
interface NavTwoProps {
|
||||||
isFields: boolean;
|
isFields: boolean;
|
||||||
|
|
@ -57,6 +57,8 @@ const NavTwo: React.FC<NavTwoProps> = ({ isFields, setIsFields, isSend, setIsSen
|
||||||
const [isFitDd, setIsFitDd] = useState<boolean>(false)
|
const [isFitDd, setIsFitDd] = useState<boolean>(false)
|
||||||
const [selFitIndx, setSelFitIndx] = useState<number>(0);
|
const [selFitIndx, setSelFitIndx] = useState<number>(0);
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
||||||
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
|
||||||
|
|
@ -76,6 +78,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ isFields, setIsFields, isSend, setIsSen
|
||||||
setIsSend(false);
|
setIsSend(false);
|
||||||
}
|
}
|
||||||
setIsSend(!isSend)
|
setIsSend(!isSend)
|
||||||
|
navigate(`/document-completion-screen/${accountId}/${transactionId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSetIsFields = (setTo: boolean) => {
|
const handleSetIsFields = (setTo: boolean) => {
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
|
||||||
<button onClick={() => handleMove('right')} disabled={disabled} className={buttonClass} title="Move right" type="button">
|
<button onClick={() => handleMove('right')} disabled={disabled} className={buttonClass} title="Move right" type="button">
|
||||||
<FaArrowRight size={12} />
|
<FaArrowRight size={12} />
|
||||||
</button>
|
</button>
|
||||||
<span className="mt-1">Right: {Math.round(right)}px</span>
|
{/* <span className="mt-1">Right: {Math.round(right)}px</span> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Row */}
|
{/* Bottom Row */}
|
||||||
|
|
@ -110,7 +110,7 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
|
||||||
<button onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move down" type="button">
|
<button onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move down" type="button">
|
||||||
<FaArrowDown size={12} />
|
<FaArrowDown size={12} />
|
||||||
</button>
|
</button>
|
||||||
<span className="mt-1">Bottom: {Math.round(bottom)}px</span>
|
{/* <span className="mt-1">Bottom: {Math.round(bottom)}px</span> */}
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ interface Account {
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: string;
|
id: string;
|
||||||
firstName: string;
|
name: string;
|
||||||
lastName: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
language: string;
|
language: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
|
|
@ -43,27 +42,30 @@ interface AccountOption {
|
||||||
|
|
||||||
const Setting = () => {
|
const Setting = () => {
|
||||||
const { page1 } = useParams<{ page1: string }>();
|
const { page1 } = useParams<{ page1: string }>();
|
||||||
console.log("page1" , page1)
|
console.log("page1", page1)
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
console.log("location",location)
|
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: '',
|
||||||
firstName: "",
|
name: '',
|
||||||
lastName: "",
|
email: '',
|
||||||
email: "",
|
language: '',
|
||||||
language: "",
|
|
||||||
account: {
|
account: {
|
||||||
id: 0,
|
id: 0,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
name: "",
|
name: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -240,18 +242,58 @@ const Setting = () => {
|
||||||
{ id: "contactShare", label: "Share Contact", type: "contact" },
|
{ id: "contactShare", label: "Share Contact", type: "contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getUserDetails = async () => {
|
|
||||||
try {
|
// Listen for sessionStorage changes
|
||||||
let res = await api.get(`/api/user/details`)
|
useEffect(() => {
|
||||||
console.log("getuser", res)
|
const handleStorageChange = (e: StorageEvent) => {
|
||||||
setUser(res?.data)
|
if (e.key === 'activeAccountId') {
|
||||||
} catch (error) {
|
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
|
||||||
console.error("Error fetching user details:", error);
|
setActiveAccountId(newActiveAccountId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for storage events (works for changes from other tabs)
|
||||||
|
window.addEventListener('storage', handleStorageChange);
|
||||||
|
|
||||||
|
// Custom event listener for same-tab changes
|
||||||
|
const handleCustomStorageChange = (e: CustomEvent) => {
|
||||||
|
if (e.detail.key === 'activeAccountId') {
|
||||||
|
setActiveAccountId(e.detail.newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', handleStorageChange);
|
||||||
|
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Re-fetch dashboard data when activeAccountId changes
|
||||||
|
useEffect(() => {
|
||||||
|
const rawUser = JSON.parse(sessionStorage.getItem('userDetails') || 'null');
|
||||||
|
|
||||||
|
if (rawUser) {
|
||||||
|
const userData: UserData = {
|
||||||
|
id: String(rawUser.id || ''),
|
||||||
|
name: rawUser.name || '',
|
||||||
|
email: rawUser.email || '',
|
||||||
|
language: rawUser.language || '',
|
||||||
|
account: {
|
||||||
|
id: rawUser.account?.id || 0,
|
||||||
|
isDefault: rawUser.account?.isDefault || false,
|
||||||
|
name: rawUser.account?.name || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setUser(userData);
|
||||||
}
|
}
|
||||||
}
|
}, [activeAccountId]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserDetails()
|
|
||||||
// Fetch account list when component mounts
|
// Fetch account list when component mounts
|
||||||
getAccountList()
|
getAccountList()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
@ -259,7 +301,7 @@ const Setting = () => {
|
||||||
// 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)
|
console.log("subSections", subSections)
|
||||||
const hash = location.hash.replace('#', '');
|
const hash = location.hash.replace('#', '');
|
||||||
|
|
||||||
if (subSections.length > 0) {
|
if (subSections.length > 0) {
|
||||||
|
|
@ -292,7 +334,7 @@ const Setting = () => {
|
||||||
setCreateAndInviteLoader(true)
|
setCreateAndInviteLoader(true)
|
||||||
try {
|
try {
|
||||||
let res;
|
let res;
|
||||||
let toastMesaage ;
|
let toastMesaage;
|
||||||
if (action === 'create') {
|
if (action === 'create') {
|
||||||
console.log('Creating account:', data);
|
console.log('Creating account:', data);
|
||||||
const payloadAccount = {
|
const payloadAccount = {
|
||||||
|
|
@ -310,7 +352,7 @@ const Setting = () => {
|
||||||
role: data.role
|
role: data.role
|
||||||
}
|
}
|
||||||
res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
|
res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
|
||||||
console.log("InviteNAme",res)
|
console.log("InviteNAme", res)
|
||||||
toastMesaage = "Invite Successfull...!"
|
toastMesaage = "Invite Successfull...!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,7 +382,7 @@ const Setting = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <SettingNav /> */}
|
{/* <SettingNav /> */}
|
||||||
<AccountNav/>
|
{/* <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">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
|
import { AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
|
||||||
import { MdCloudUpload } from 'react-icons/md';
|
import { MdCloudUpload } from 'react-icons/md';
|
||||||
import api from '../../../config/api';
|
import api from '../../../config/api';
|
||||||
import DrawSignatureCanvas from './DrawSignatureCanvas';
|
import DrawSignatureCanvas from './DrawSignatureCanvas';
|
||||||
import { FaSpinner } from "react-icons/fa";
|
import { FaSpinner } from "react-icons/fa";
|
||||||
|
|
@ -54,7 +54,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
getAllSignatures,
|
getAllSignatures,
|
||||||
editingSignature
|
editingSignature
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD'>('SELECT');
|
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 [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
|
|
@ -102,9 +102,10 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
|
|
||||||
// Navigation items
|
// Navigation items
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
{ id: 'TYPE', label: 'TYPE', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
||||||
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
{ id: '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
|
// Initialize form with editing signature data
|
||||||
|
|
@ -112,7 +113,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
if (editingSignature) {
|
if (editingSignature) {
|
||||||
setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
|
setFullName(`${editingSignature.firstName} ${editingSignature.lastName}`);
|
||||||
setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
|
setInitials(`${editingSignature.firstName.slice(0, 1)}${editingSignature.lastName.slice(0, 1)}`);
|
||||||
setActiveTab(editingSignature.signatureType as 'SELECT' | 'DRAW' | 'UPLOAD');
|
setActiveTab(editingSignature.signatureType as 'TYPE' | 'DRAW' | 'UPLOAD');
|
||||||
|
|
||||||
// Set font for SELECT tab
|
// Set font for SELECT tab
|
||||||
const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
|
const fontOption = signatureOptions.find(option => option.style === editingSignature.font);
|
||||||
|
|
@ -262,7 +263,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'SELECT':
|
case 'TYPE':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col ">
|
<div className="h-full flex flex-col ">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -383,6 +384,16 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -462,7 +473,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
{navigationItems.map((item) => (
|
{navigationItems.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
|
onClick={() => setActiveTab(item.id as 'TYPE' | 'DRAW' | 'UPLOAD')}
|
||||||
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
|
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
|
||||||
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ const SignatureManager: React.FC = () => {
|
||||||
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
|
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
<div className="text-sm text-gray-600 font-medium mb-1">
|
<div className="text-sm text-gray-600 font-medium mb-1">
|
||||||
DS
|
Initials
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Signature Image */}
|
{/* Signature Image */}
|
||||||
|
|
@ -186,6 +186,10 @@ const SignatureManager: React.FC = () => {
|
||||||
alt="DS Signature"
|
alt="DS Signature"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Initial ID */}
|
||||||
|
<div className="text-xs text-gray-500 truncate">
|
||||||
|
{signature.initialsHash}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ interface Account {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: string;
|
id: number;
|
||||||
firstName: string;
|
name:string
|
||||||
lastName: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
language: string;
|
language: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
|
|
@ -26,7 +25,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
|
||||||
{/* Profile Picture */}
|
{/* Profile Picture */}
|
||||||
<div className="flex-shrink-0 text-center">
|
<div className="flex-shrink-0 text-center">
|
||||||
<div className="w-20 h-20 bg-green-800 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
|
<div className="w-20 h-20 bg-green-800 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
|
||||||
{getInitials(userData.firstName, userData.lastName)}
|
{userData.name.slice(0,2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -36,7 +35,7 @@ const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
|
||||||
<div className="flex py-1">
|
<div className="flex py-1">
|
||||||
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
|
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
|
||||||
<div className="text-gray-400 font-medium text-[13px]">
|
<div className="text-gray-400 font-medium text-[13px]">
|
||||||
{userData.firstName} {userData.lastName}
|
{userData.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,14 @@ const TransactionView = () => {
|
||||||
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
|
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<h1 className='text-lg font-semibold text-gray-800'>
|
<h1 className='text-lg font-semibold text-gray-800'>
|
||||||
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails.transaction.name ?? "Untitled"}</span>
|
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails?.transaction?.subject ?? "Untitled"}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
|
{/* new Date(transactionDetails?.transaction?.lastModifiedAt * 1000) */}
|
||||||
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
|
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
|
||||||
<p className='text-slate-600'>Last Change on <span>12/02/25</span></p>
|
<p className=''><span className='text-slate-600 font-semibold'>Last Change on</span> <span className='text-slate-400'>{new Date(transactionDetails?.transaction?.lastModifiedAt * 1000).toISOString()}</span></p>
|
||||||
<p className='text-slate-600'>Sent on <span>11/02/25</span></p>
|
<p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
|
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
|
||||||
|
|
@ -139,7 +140,7 @@ const TransactionView = () => {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
|
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
|
||||||
<p className='mt-2 text-slate-600'>No Message Found</p>
|
<p className='mt-2 text-slate-600'>{ transactionDetails?.transaction?.message ?? "No Message Found...!"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ export default defineConfig({
|
||||||
'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'
|
'c76f1251c173.ngrok-free.app',
|
||||||
|
'enabled-upright-boxer.ngrok-free.app'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue