SME-Web/src/components/setting/Setting.tsx

501 lines
16 KiB
TypeScript

import { useEffect, useState, useRef } from "react";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import api from "../../config/api";
import UserProfile from './UserProfile';
import Language from "./Language";
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
import ContactForm, { ContactData } from "./ContactForm";
import { useSelector } from "react-redux";
import { RootState } from "../../store/store";
import { LuUserRound } from "react-icons/lu";
import { AiFillSignature, AiOutlineSignature } from "react-icons/ai";
import { FaRegUser } from "react-icons/fa";
// Define the shape of state object
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
name: string;
email: string;
language: string;
account: Account;
}
export interface SubSection {
id: string;
label: string;
component?: React.ReactNode;
}
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
const Setting = () => {
const { page1 } = useParams<{ page1: string }>();
const location = useLocation();
const navigate = useNavigate();
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const userFetchData = useSelector((state: RootState) => state.user)
// Refs for subsections within each main tab
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState("userProfile");
const [activeSubSection, setActiveSubSection] = useState<string>("");
const [user, setUser] = useState<UserData>({
id: '',
name: '',
email: '',
language: '',
account: {
id: 0,
isDefault: false,
name: '',
},
});
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
// Dropdown data states
const [accountOptions, setAccountOptions] = useState<AccountOption[]>([]);
const [accountOptionsLoading, setAccountOptionsLoading] = useState(false);
// Define subsections for each main tab
const getSubSections = (tabId: string): SubSection[] => {
switch (tabId) {
case "userProfile":
return [
{ id: "profile-info", label: "Personal Information", component: <FaRegUser /> },
{ id: "signature", label: "Signature", component: <AiOutlineSignature className="text-lg" /> },
];
default:
return [];
}
};
// Function to scroll to specific subsection
const scrollToSubSection = (subSectionId: string) => {
const targetRef = subSectionRefs.current[subSectionId];
if (targetRef && scrollContainerRef?.current) {
const container = scrollContainerRef.current;
const target = targetRef;
// Calculate the position relative to the scroll container
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
// Calculate scroll position (accounting for some padding)
const scrollTop = container.scrollTop + targetRect.top - containerRect.top - 20;
// Smooth scroll to the target
container.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
}
};
// Handle subsection click with hash update
const handleSubSectionClick = (subSectionId: string) => {
setActiveSubSection(subSectionId);
// Update URL hash without causing page reload
navigate(`${location.pathname}#${subSectionId}`, { replace: true });
scrollToSubSection(subSectionId);
};
// Handle main tab click (independent of aside navigation)
const handleMainTabClick = (tabId: string) => {
setActiveTab(tabId);
// Reset subsection when switching main tabs
const subSections = getSubSections(tabId);
if (subSections.length > 0) {
const firstSubSectionId = subSections[0].id;
setActiveSubSection(firstSubSectionId);
// Update URL hash
navigate(`${location.pathname}#${firstSubSectionId}`, { replace: true });
} else {
setActiveSubSection("");
// Clear hash
navigate(location.pathname, { replace: true });
}
};
// Handle browser back/forward and direct URL navigation
useEffect(() => {
const hash = location.hash.replace('#', '');
if (hash) {
// Check if the hash corresponds to a valid subsection
const allSubSections = getSubSections(activeTab);
const validSubSection = allSubSections.find(section => section.id == hash);
if (validSubSection) {
setActiveSubSection(hash);
// Delay scroll to ensure component is rendered
setTimeout(() => {
scrollToSubSection(hash);
}, 100);
}
}
}, [location.hash, activeTab]);
// Fetch account list for dropdown
const getAccountList = async () => {
setAccountOptionsLoading(true);
try {
const res = await api.get('/api/v1/user/accounts');
if (res?.data) {
setAccountOptions(res.data);
}
} catch (error) {
console.error('Error fetching account options:', error);
} finally {
setAccountOptionsLoading(false);
}
};
// Render subsection content based on active tab and subsection
const renderSubSectionContent = (subSectionId: string) => {
switch (activeTab) {
case "userProfile":
switch (subSectionId) {
case "profile-info":
return <UserProfile userData={user} />;
case "signature":
return (
<Signature />
);
default:
return;
}
default:
return (
<div className="bg-white p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold mb-4">Coming Soon</h3>
<p>This section is under development.</p>
</div>
);
}
};
const renderContent = () => {
const currentSubSections = getSubSections(activeTab);
if (currentSubSections.length == 0) {
// If no subsections, render the main content directly
switch (activeTab) {
case "userProfile":
return <UserProfile userData={user} />;
case "language":
return <Language userData={user} />;
case "create and invite":
return (
<CreateAndInvite
onSubmit={handleSubmit}
createAndInviteLoader={createAndInviteLoader}
accountOptions={accountOptions}
accountOptionsLoading={accountOptionsLoading}
/>
);
case "contacts":
return (
<ContactForm
onSubmit={handleContactSubmit}
loading={createAndInviteLoader}
/>
);
default:
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
}
}
return (
<div className="space-y-2">
{currentSubSections.map((subSection) => (
<div
key={subSection.id}
ref={(el) => (subSectionRefs.current[subSection.id] = el)}
id={subSection.id} // This creates the anchor for hash navigation
className="scroll-mt-6"
>
{renderSubSectionContent(subSection.id)}
</div>
))}
</div>
);
};
const tabs = [
{ id: "userProfile", label: "User Profile", type: "settings" },
// { id: "create and invite", label: "Create and invite", type: "settings" },
{ id: "contacts", label: "Contacts", type: "settings" },
// { id: "language", label: "Language", type: "settings" },
{ id: "contactAdd", label: "Add Contact", type: "contact" },
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
{ id: "contactShare", label: "Share Contact", type: "contact" },
];
// Listen for sessionStorage changes
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key == 'activeAccountId') {
const newActiveAccountId = e.newValue ? JSON.parse(e.newValue) : null;
setActiveAccountId(newActiveAccountId);
}
};
// Listen for storage events (works for changes from other tabs)
window.addEventListener('storage', handleStorageChange);
// Custom event listener for same-tab changes
const handleCustomStorageChange = (e: CustomEvent) => {
if (e.detail.key == 'activeAccountId') {
setActiveAccountId(e.detail.newValue);
}
};
window.addEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('sessionStorageChange', handleCustomStorageChange as EventListener);
};
}, []);
// Re-fetch dashboard data when activeAccountId changes
useEffect(() => {
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(() => {
// Fetch account list when component mounts
getAccountList()
}, [])
// Set initial subsection when component mounts or activeTab changes
useEffect(() => {
const subSections = getSubSections(activeTab);
const hash = location.hash.replace('#', '');
if (subSections.length > 0) {
// If there's a hash and it's valid for current tab, use it
if (hash && subSections.find(s => s.id == hash)) {
setActiveSubSection(hash);
}
// If no valid hash or no current activeSubSection, use first subsection
else if (!activeSubSection || !subSections.find(s => s.id == activeSubSection)) {
const firstSubSectionId = subSections[0].id;
setActiveSubSection(firstSubSectionId);
// Update URL hash if no hash exists
if (!hash) {
navigate(`${location.pathname}#${firstSubSectionId}`, { replace: true });
}
}
}
}, [activeTab]);
// Auto-scroll to subsection when activeSubSection changes
useEffect(() => {
if (activeSubSection) {
setTimeout(() => {
scrollToSubSection(activeSubSection);
}, 100);
}
}, [activeSubSection]);
const handleSubmit = async (data: FormDataType, action: ActionType) => {
setCreateAndInviteLoader(true)
try {
let res;
let toastMesaage;
if (action == 'create') {
const payloadAccount = {
name: data.accountName
}
res = await api.post(`/api/v1/user/accounts`, payloadAccount)
if (res.status == 201) {
toastMesaage = `${action.toUpperCase()} Created Successfull...!`
}
} else if (action == 'invite') {
const payloadInvite = {
email: data.email,
role: data.role
}
res = await api.post(`/api/v1/accounts/${data.i_accountName}/invite`, payloadInvite)
toastMesaage = "Invite Successfull...!"
}
if (res?.data) {
toast.success(toastMesaage, {
position: "top-center",
autoClose: 1500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// Refetch account list after successful submission
await getAccountList();
}
} catch (err) {
console.error('Submission error:', err);
} finally {
setCreateAndInviteLoader(false)
}
};
const handleContactSubmit = async (data: ContactData) => {
try {
setCreateAndInviteLoader(true);
const res = await api.post(`/api/v1/accounts/${activeAccountId}/contacts`, [data]);
if (res.status == 200) {
toast.success('Contact created successfully!', {
position: "top-center",
autoClose: 1500,
transition: Bounce,
});
}
// return res;
} catch (err) {
console.error("Error creating contact", err);
toast.error("Failed to create contact");
throw err;
} finally {
setCreateAndInviteLoader(false);
}
};
return (
<>
{/* <SettingNav /> */}
{/* <AccountNav/> */}
<div className="bg-white h-[calc(90vh-20px)] flex flex-col mx-6 my-6 rounded-lg overflow-hidden">
{/* Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-4 md:px-6 lg:px-8 pt-4">
<div className="flex flex-wrap md:gap-4 ">
{tabs.map((tab) =>
tab.type == page1 && (
<button
key={tab.id}
onClick={() => handleMainTabClick(tab.id)}
className={`cursor-pointer relative px-4 pb-3 text-sm font-medium transition-all duration-200
${activeTab == tab.id
? "text-[#34BEFF] after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-[#34BEFF] bg-gradient-to-t from-[#E5F6FF] to-transparent"
: "text-gray-600 hover:text-[#34BEFF]"
}`}
>
{tab.label}
</button>
)
)}
</div>
</div>
{/* Responsive Layout */}
<div className="flex-1 min-h-0 flex flex-col lg:flex-row">
{/* Sidebar (hidden on small, shown on lg) */}
{activeTab == 'userProfile' && (
<section className="hidden lg:block flex-shrink-0 w-70 border-r-1 border-slate-200">
<div className="border-b-1 border-slate-200 py-4 px-6">
<div className="flex justify-start items-center gap-2">
<div>
<p className="bg-[#FAF0FF] text-[#A915FF] font-semibold max-w-max px-3 py-2 rounded-full ">PK</p>
</div>
<div>
<p className="font-semibold">{user.name}</p>
<p className="text-sm text-slate-600 w-[24ch] overflow-hidden text-ellipsis whitespace-nowrap">
{user.email}
</p>
</div>
</div>
</div>
<div className="sticky top-6 z-20 px-6 pt-2">
<AsideSection
activeSubSection={activeSubSection}
onSubSectionClick={handleSubSectionClick}
subSections={getSubSections(activeTab)}
mainTabTitle={tabs.find(t => t.id == activeTab)?.label || ""}
/>
</div>
</section>
)}
{/* Sidebar as horizontal nav (only on mobile) */}
{activeTab == 'userProfile' && (
<section className="lg:hidden mb-2">
<div className="flex overflow-x-auto no-scrollbar gap-4 border-b border-gray-200 pb-2">
{getSubSections(activeTab).map((sub) => (
<button
key={sub.id}
onClick={() => handleSubSectionClick(sub.id)}
className={`whitespace-nowrap text-sm font-medium px-2 py-1 rounded-md
${activeSubSection == sub.id ? "bg-blue-100 text-blue-600" : "text-gray-600"}
`}
>
{sub.label}
</button>
))}
</div>
</section>
)}
{/* Main Content */}
<div
ref={scrollContainerRef}
className="flex-1 min-w-0 overflow-y-auto no-scrollbar"
>
{renderContent()}
</div>
</div>
</div>
</>
)
}
export default Setting;