SME-Web/src/components/CommonNav.tsx

528 lines
22 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';
import { IoSettings, IoSettingsOutline } from 'react-icons/io5';
import { useSelector } from 'react-redux';
import { RootState } from '../store/store';
import { GoBell } from 'react-icons/go';
import getInitialsUtils from '../utlity/getInitialUtils';
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;
firstName: string;
lastName: 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 userDetailsAsync = 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);
const dropdownRef = useRef<HTMLDivElement>(null);
// const [initial, setInitial] = useState<string>("");
const [user, setUser] = useState<UserData>({
id: 0,
name: "",
firstName: "",
lastName: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
const handleSettingsClick = () => {
setOpen(false);
navigate("/s/settings#profile-info");
}
// useEffect(() => {
// if (initial != "") return
// console.log(userDetailsAsync, 'line 89')
// let initialN = (userDetailsAsync.firstName && userDetailsAsync.firstName[0])
// initialN = initialN + "" + (userDetailsAsync.lastName && userDetailsAsync.lastName[0])
// setInitial(initialN)
// }, [userDetailsAsync])
const getAccountDetails = async () => {
try {
let res = await api.get('/api/v1/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,
firstName: account.firstName,
lastName: account.lastName,
email: account.userEmail,
language: account.language, // set it if available
account: {
id: account.accountId,
isDefault: false, // update as needed
name: account.accountName,
},
});
// if (account.firstName && account.lastName) {
// setInitial((account?.firstName?.slice(0, 1)).toUpperCase() + "" + (account?.lastName?.slice(0, 1)).toUpperCase())
// } else {
// setInitial((account?.name?.slice(0, 1)).toUpperCase())
// }
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/v1/user/details`);
const item = res?.data;
if (item) {
const userData = {
id: item?.id,
name: item?.firstName + " " + item?.lastName,
firstName: item?.firstName,
lastName: item?.lastName,
email: item?.email,
language: item?.language,
account: {
id: item?.account.id,
isDefault: false,
name: item?.account.name,
},
};
setUser(userData);
// if (userData.firstName && userData.lastName) {
// setInitial((userData?.firstName?.slice(0, 1)).toUpperCase() + "" + (userData?.lastName?.slice(0, 1)).toUpperCase())
// } else {
// setInitial((userData?.name?.slice(0, 1)).toUpperCase())
// }
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 () => {
api.get(`${import.meta.env.VITE_BASE_URI}/api/v1/auth/logout`).then((res) => {
sessionStorage.clear();
localStorage.clear();
// Redirect to login or refresh the page after logout
// window.location.href = '/login';
}).catch((err) => {
sessionStorage.clear();
localStorage.clear();
window.location.href = '/login';
});
};
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="hidden sm:block text-sm ml-4">Welcome <span className='text-white ml-1 bg-[#34BEFF] px-2.5 py-1 rounded-2xl'>{user.firstName ? user.firstName : user.lastName}!</span></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="items-center hidden sm:flex">
{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">
{/* SETINGS - START */}
<div
onClick={handleSettingsClick}
className="bg-[#F6F6F6] rounded-md py-1.5 px-2 cursor-pointer transition-all duration-200 hover:bg-[#DCDADA] mr-3">
<svg width="16" height="20" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.54">
<path d="M2 9.10996V14.88C2 17 2 17 4 18.35L9.5 21.53C10.33 22.01 11.68 22.01 12.5 21.53L18 18.35C20 17 20 17 20 14.89V9.10996C20 6.99996 20 6.99996 18 5.64996L12.5 2.46996C11.68 1.98996 10.33 1.98996 9.5 2.46996L4 5.64996C2 6.99996 2 6.99996 2 9.10996Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M11 15C12.6569 15 14 13.6569 14 12C14 10.3431 12.6569 9 11 9C9.34315 9 8 10.3431 8 12C8 13.6569 9.34315 15 11 15Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</g>
</svg>
</div>
{/* SETINGS - END */}
{/* Bell START */}
<div className="bg-[#F6F6F6] rounded-md py-1.5 px-2 cursor-pointer transition-all duration-200 hover:bg-[#DCDADA]">
<svg width="14" height="20" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.7493 8.74963C16.3353 8.74963 15.9993 8.41363 15.9993 7.99963C15.9993 4.92988 13.6087 2.30563 10.5562 2.02513C7.49653 1.7435 4.6574 3.88475 4.0994 6.9005C4.03265 7.25975 3.9989 7.6295 3.9989 7.99963C3.9989 8.41363 3.6629 8.74963 3.2489 8.74963C2.8349 8.74963 2.4989 8.41363 2.4989 7.99963C2.4989 7.538 2.5409 7.07638 2.62415 6.6275C3.32165 2.85763 6.8639 0.177876 10.6938 0.531126C14.5102 0.882126 17.4993 4.16263 17.4993 7.99963C17.4993 8.414 17.1637 8.74963 16.7493 8.74963Z" fill="#757575" />
<path d="M16.7493 17.7496C16.3353 17.7496 15.9993 17.4136 15.9993 16.9996C15.9993 16.5856 16.3353 16.2496 16.7493 16.2496C17.4873 16.2496 17.9433 15.7445 18.1237 15.2716C18.3033 14.8014 18.2996 14.1234 17.7495 13.6314C17.4408 13.3554 17.4138 12.881 17.6906 12.5724C17.9662 12.2637 18.441 12.2371 18.7492 12.5135C19.6702 13.337 19.9747 14.63 19.5247 15.8067C19.074 16.9869 17.9846 17.7496 16.7493 17.7496Z" fill="#757575" />
<path d="M16.7493 10.4686C16.3353 10.4686 15.9993 10.1326 15.9993 9.71863V8C15.9993 7.586 16.3353 7.25 16.7493 7.25C17.1633 7.25 17.4993 7.586 17.4993 8V9.71863C17.4993 10.1326 17.1637 10.4686 16.7493 10.4686Z" fill="#757575" />
<path d="M18.249 13.8226C18.0708 13.8226 17.8923 13.7596 17.7495 13.6318C16.6372 12.6369 15.9993 11.2108 15.9993 9.71863C15.9993 9.30463 16.3353 8.96863 16.7493 8.96863C17.1633 8.96863 17.4993 9.30463 17.4993 9.71863C17.4993 10.7844 17.955 11.8033 18.7492 12.5139C19.0578 12.7899 19.0848 13.2643 18.8081 13.5729C18.6603 13.7383 18.4552 13.8226 18.249 13.8226Z" fill="#757575" />
<path d="M3.24927 10.4686C2.83527 10.4686 2.49927 10.1326 2.49927 9.71863V8C2.49927 7.586 2.83527 7.25 3.24927 7.25C3.66327 7.25 3.99927 7.586 3.99927 8V9.71863C3.99927 10.1326 3.66364 10.4686 3.24927 10.4686Z" fill="#757575" />
<path d="M1.74974 13.8226C1.54349 13.8226 1.33837 13.7382 1.19062 13.5725C0.914245 13.2639 0.94087 12.7895 1.24949 12.5135C2.04374 11.8029 2.49937 10.7844 2.49937 9.71825C2.49937 9.30425 2.83537 8.96825 3.24937 8.96825C3.66337 8.96825 3.99937 9.30425 3.99937 9.71825C3.99937 11.2104 3.36149 12.6365 2.24924 13.6314C2.10637 13.7596 1.92787 13.8226 1.74974 13.8226Z" fill="#757575" />
<path d="M9.99933 21.4996C7.55995 21.4996 5.49933 19.439 5.49933 16.9996C5.49933 16.5856 5.83533 16.2496 6.24933 16.2496C6.66333 16.2496 6.99933 16.5856 6.99933 16.9996C6.99933 18.6256 8.37295 19.9996 9.99933 19.9996C11.6257 19.9996 12.9993 18.6256 12.9993 16.9996C12.9993 16.5856 13.3353 16.2496 13.7493 16.2496C14.1633 16.2496 14.4993 16.5856 14.4993 16.9996C14.4993 19.439 12.4387 21.4996 9.99933 21.4996Z" fill="#757575" />
<path d="M3.24931 17.7496C2.01406 17.7496 0.925435 16.9888 0.47581 15.8113C0.02506 14.6315 0.32881 13.337 1.24944 12.5139C1.55844 12.2379 2.03244 12.2641 2.30844 12.5731C2.58444 12.8821 2.55781 13.3558 2.24919 13.6321C1.69944 14.1238 1.69644 14.804 1.87681 15.2765C2.05644 15.7471 2.51169 16.2504 3.24931 16.2504C3.66331 16.2504 3.99931 16.5864 3.99931 17.0004C3.99931 17.4144 3.66369 17.7496 3.24931 17.7496Z" fill="#757575" />
<path d="M16.7493 17.7496H3.24927C2.83527 17.7496 2.49927 17.4136 2.49927 16.9996C2.49927 16.5856 2.83527 16.2496 3.24927 16.2496H16.7493C17.1633 16.2496 17.4993 16.5856 17.4993 16.9996C17.4993 17.4136 17.1636 17.7496 16.7493 17.7496Z" fill="#757575" />
</svg>
</div>
{/* Bell END */}
<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-md 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 capitalize">
{getInitialsUtils((user.firstName ?? '') + (user.firstName && user.lastName ? ' ' : '') + (user.lastName ?? ''))}
</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 capitalize">
{getInitialsUtils((user.firstName ?? '') + (user.firstName && user.lastName ? ' ' : '') + (user.lastName ?? ''))}
</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={handleSettingsClick}
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">
<IoSettings className="mr-2.5 text-lg text-gray-500" />
<span>Settings</span>
</div>
{/* <FaChevronDown className={`text-gray-400 transition-transform ${accountsOpen ? 'rotate-180' : ''}`} /> */}
</button>
</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;