363 lines
16 KiB
TypeScript
363 lines
16 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
// import { useSelector } from 'react-redux';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
|
|
import api from '../config/api';
|
|
|
|
// import { FaUser, FaSignOutAlt, FaBuilding, FaChevronDown, FaCheck } from 'react-icons/fa';
|
|
|
|
// import { RootState } from '../../store/store';
|
|
// import { UserState } from '../../store/slices/userSlice';
|
|
|
|
|
|
|
|
interface CommonNavProps {
|
|
accountName?: string | null;
|
|
transactionName?: string | null;
|
|
}
|
|
|
|
// Define the shape of state object
|
|
interface Account {
|
|
id: number;
|
|
isDefault: boolean;
|
|
name: string;
|
|
}
|
|
|
|
interface UserData {
|
|
id: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
language: string;
|
|
account: Account;
|
|
}
|
|
|
|
interface AccountDataArray {
|
|
accountId: number, accountName: string, role: string, status: boolean
|
|
}
|
|
|
|
const CommonNav: React.FC<CommonNavProps> = ({ accountName = null, transactionName = null }) => {
|
|
// const user: UserState = useSelector((state: RootState) => state.user);
|
|
|
|
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); // Track if modal was closed
|
|
const dropdownRef = useRef(null);
|
|
|
|
const [user, setUser] = useState<UserData>({
|
|
id: "",
|
|
firstName: "",
|
|
lastName: "",
|
|
email: "",
|
|
language: "",
|
|
account: {
|
|
id: 0,
|
|
isDefault: false,
|
|
name: "",
|
|
},
|
|
});
|
|
|
|
const getAccountDetails = async () => {
|
|
try {
|
|
let res = await api.get('/api/user/accounts')
|
|
let items = res?.data
|
|
console.log('items', items)
|
|
// Extract only the fields you want
|
|
const cleanedData: AccountDataArray[] = items.map((item: any) => ({
|
|
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);
|
|
|
|
// 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(() => {
|
|
getUserDetails()
|
|
}, [])
|
|
|
|
|
|
const location = useLocation();
|
|
|
|
const pathnames = location.pathname
|
|
.split("/")
|
|
.filter((x) => x && isNaN(Number(x))); // remove numeric IDs
|
|
|
|
const labelMap: Record<string, string> = {
|
|
accounts: "Accounts",
|
|
transactions: "Transactions",
|
|
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) {
|
|
setHasModalBeenClosed(true); // Mark as closed when manually closing
|
|
}
|
|
setOpen(!open);
|
|
if (!open) {
|
|
setAccountsOpen(false); // Close accounts dropdown when opening main dropdown
|
|
}
|
|
};
|
|
|
|
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">
|
|
|
|
{/* User Dropdown */}
|
|
<div className="relative" ref={dropdownRef}>
|
|
{/* Avatar Button */}
|
|
<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?.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>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
export default CommonNav; |