SME-Web/src/components/CommonNav.tsx

456 lines
15 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa";
import api from '../config/api';
interface AccountNavProps {
accountName?: string | null;
transactionName?: string | null;
showSideBar?: boolean;
setShowSideBar?: (show: boolean) => void;
isMobile?: boolean;
}
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> = ({
showSideBar,
setShowSideBar,
isMobile = false
}) => {
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
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: "",
},
});
const getAccountDetails = async () => {
try {
let res = await api.get('/api/user/accounts');
let items = res?.data;
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) => {
// 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: account.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: account.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`);
const item = res?.data;
if (item) {
const userData = {
id: item?.id,
name: item?.firstName + " " + item?.lastName,
email: item?.email,
language: item?.language,
account: {
id: item?.account.id,
isDefault: false,
name: item?.account.name,
},
};
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);
}
} 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)));
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 handleSidebarToggle = () => {
if (setShowSideBar) {
setShowSideBar(!showSideBar);
}
};
// 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",
};
const formatSegment = (segment: string) =>
segment
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
// If URL has accounts → transactions, only keep "transactions"
let filteredPathnames = [...pathnames];
const accIndex = pathnames.indexOf("accounts");
const txnIndex = pathnames.indexOf("transactions");
if (accIndex !== -1 && txnIndex !== -1 && txnIndex > accIndex) {
// keep only "transactions" and everything after it
filteredPathnames = pathnames.slice(txnIndex);
}
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-3 py-1.5 flex justify-between items-center bg-white border-b border-gray-200">
<div className="flex items-center space-x-4">
{/* Mobile Hamburger Menu - Only show on mobile */}
{isMobile && (
<div className='flex items-center justify-start'>
<span className='text-[var(--primary)] text-xl text-center tracking-widest font-medium text-shadow-2xs'>SME</span>
<button
onClick={handleSidebarToggle}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors duration-200 text-gray-600 hover:text-gray-900"
aria-label="Toggle sidebar"
>
<FaBars className="text-lg" />
</button>
</div>
)}
{/* Breadcrumb Navigation */}
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
{filteredPathnames.length === 0 ? (
<span className="text-gray-900 font-semibold">Home</span>
) : (
filteredPathnames.map((segment, index) => {
const label = labelMap[segment] || formatSegment(segment);
const to = "/" + filteredPathnames.slice(0, index + 1).join("/");
const isLast = index === filteredPathnames.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>
<div className="flex items-center space-x-2">
<div className="relative" ref={dropdownRef}>
<button
id='user-details-avatar'
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-[var(--primary)] 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-[var(--primary)] 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>
);
// 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>
// );
// };
}
export default CommonNav;