Files
edr-platform/apps/edr-freight-web/backoffice/src/layout/components/Header.tsx
2026-08-10 12:00:29 +03:00

664 lines
26 KiB
TypeScript

import { useState, useEffect, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useNavigate, useLocation } from "react-router-dom";
import { ExternalPortal } from "@/external-portal/components/External-Portal-Navigation/PortalHeader";
import { useTranslation } from "react-i18next";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/common/ui/select";
import { toast } from "sonner";
import {
ChevronDown,
FileText,
Search,
Info,
Menu,
X,
} from "lucide-react";
import { useUser } from "@/shared/context/UserContext";
import { useTenantConfig, resolveModuleConfig } from "@/layout/components/TenantConfig";
import {
UI_LANGUAGE_OPTIONS,
resolveUiLanguage,
} from "@/shared/i18n/uiLanguages";
import {
hasComplaintVerification,
isComplaintAuthContext,
} from "@/complaints/utils/complaintVerificationStorage";
import { COMPLAINT_RECORDS_PATH } from "@/complaints/utils/complaintRoutes";
import { useExternalPortalSession } from "@/shared/hooks/useExternalPortalSession";
interface NavItem {
id: string;
label: string;
path?: string;
requiresCompleteRegistration?: true;
children?: NavItem[];
}
const languageOptions = UI_LANGUAGE_OPTIONS;
const Header = () => {
const [activeTab, setActiveTab] = useState("overview");
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [servicesOpen, setServicesOpen] = useState(false);
const [navigateTo, setNaviageTo] = useState("/");
const navigate = useNavigate();
const location = useLocation();
const isComplaintContext = isComplaintAuthContext(location.pathname);
const { isAuthenticated, showExternalPortalChrome } = useExternalPortalSession();
const { config: tenantConfig } = useTenantConfig();
const moduleConfig = resolveModuleConfig(tenantConfig);
const userDetails = useUser();
const { t, i18n } = useTranslation();
const currentLanguage = resolveUiLanguage(i18n.language);
const hasCompletedRegistration = userDetails?.hasFinishedRegistration;
const changeLanguage = (lng: string) => i18n.changeLanguage(lng);
useEffect(() => {
if (!localStorage.getItem("i18nextLng")) {
i18n.changeLanguage("am");
localStorage.setItem("i18nextLng", "am");
}
}, [i18n]);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 10);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const handleSignIn = () => {
navigate("/login");
};
useEffect(() => {
if (location.pathname.includes("/external-portal/portal-outgoing")) {
setActiveTab("records");
return;
}
if (location.pathname.startsWith("/complaints")) {
setActiveTab("complaint");
}
}, [location.pathname]);
const navItems: NavItem[] = useMemo(() => {
if (showExternalPortalChrome) {
return [];
}
if (isAuthenticated) {
return [
{ id: "overview", label: t("landingPage.overview") },
{ id: "workflow", label: t("landingPage.workflow") },
];
}
const complaintServices: NavItem[] = moduleConfig.complaint
? [
{
id: "complaint",
label: t("landingPage.fileComplaint"),
path: "/complaints",
},
{
id: "follow-complaint",
label: t("landingPage.followComplaint"),
path: "/follow-complaint",
},
]
: [];
const items: NavItem[] = [
{ id: "overview", label: t("landingPage.overview") },
{ id: "features", label: t("landingPage.features") },
];
if (complaintServices.length > 0) {
items.push({
id: "services",
label: t("landingPage.services"),
children: complaintServices,
});
}
items.push(
{ id: "workflow", label: t("landingPage.workflow") },
{ id: "howto", label: t("landingPage.manual") },
);
return items;
}, [isAuthenticated, moduleConfig.complaint, showExternalPortalChrome, t]);
const handleNavClick = (item: NavItem) => {
if (
item.requiresCompleteRegistration &&
!hasCompletedRegistration &&
!hasComplaintVerification()
) {
toast.error(t("registration.registrationRequired"));
return;
}
if (item.path) {
navigate(item.path);
setMobileMenuOpen(false);
}
};
// Animation variants
const mobileMenuVariants = {
hidden: {
opacity: 0,
height: 0,
transition: {
duration: 0.3,
when: "afterChildren",
},
},
visible: {
opacity: 1,
height: "auto",
transition: {
duration: 0.3,
when: "beforeChildren",
staggerChildren: 0.1,
},
},
};
const mobileItemVariants = {
hidden: { x: -20, opacity: 0 },
visible: {
x: 0,
opacity: 1,
transition: {
x: { stiffness: 1000, velocity: -100 },
},
},
};
const servicesVariants = {
hidden: {
opacity: 0,
y: -10,
scale: 0.95,
transition: {
duration: 0.2,
},
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.2,
staggerChildren: 0.05,
},
},
};
const serviceItemVariants = {
hidden: { opacity: 0, y: -5 },
visible: { opacity: 1, y: 0 },
};
const getServiceIcon = (id: string) => {
switch (id) {
case "complaint":
return <FileText className="w-4 h-4" />;
case "follow-complaint":
return <Search className="w-4 h-4" />;
case "about-us":
return <Info className="w-4 h-4" />;
default:
return <FileText className="w-4 h-4" />;
}
};
// Get current language display name
const getCurrentLanguageDisplay = () => {
const currentLang = languageOptions.find(
(lang) => lang.value === currentLanguage,
);
return currentLang ? currentLang.label : "English";
};
useMemo(() => {
if (showExternalPortalChrome) {
setNaviageTo(COMPLAINT_RECORDS_PATH);
}
}, [showExternalPortalChrome]);
return (
<>
{/* Registration Alert - Fixed for mobile */}
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? "bg-white dark:bg-gray-900 shadow-lg"
: "bg-white dark:bg-gray-900 shadow-sm"
} `}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center gap-4 h-16 md:h-20">
{/* Logo Section */}
<div className="flex items-center">
{!showExternalPortalChrome ? (
<>
{tenantConfig?.logo ? (
<div className="relative group inline-flex">
{/* Glow */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-primary/20 to-secondary/20 blur-xl opacity-0 group-hover:opacity-100 transition-all duration-500 scale-110" />
{/* Footer Logo Wrapper */}
<motion.div
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 220, damping: 16 }}
className="relative inline-flex items-center justify-center cursor-pointer group"
onClick={() =>
navigate(
showExternalPortalChrome
? COMPLAINT_RECORDS_PATH
: isComplaintContext
? "/complaints"
: "/",
)
}
title={t("nav.homePage")}
>
<motion.img
src={tenantConfig.logo}
alt="Footer Logo"
className="
h-15 md:h-30
w-auto
object-contain
group-hover:scale-105
transition-transform
duration-300
"
onError={(e) => {
const fallback =
e.currentTarget.parentElement?.querySelector(
".footer-logo-fallback",
);
if (fallback) {
fallback.classList.remove("hidden");
}
e.currentTarget.remove();
}}
/>
{/* Fallback when logo fails to load */}
<div
className="
footer-logo-fallback
hidden
h-20
w-20
items-center
justify-center
rounded-full
bg-primary
text-primary-foreground
font-bold
text-2xl
pointer-events-none
"
>
{tenantConfig?.organizationName
?.split(/[\s\-–—]+/)
.filter(Boolean)
.map((word) => word.charAt(0))
.join("")
.toUpperCase() || "SO"}
</div>
</motion.div>
</div>
) : (
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="h-10 w-10 md:h-12 md:w-12 rounded-full bg-primary flex items-center justify-center text-white font-bold cursor-pointer"
onClick={() => navigate(navigateTo)}
title={t("nav.homePage")}
>
SO
</motion.div>
)}
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="ml-2 md:ml-3 text-lg md:text-xl font-bold text-gray-800 dark:text-white cursor-pointer"
onClick={() => navigate(navigateTo)}
title={t("nav.homePage")}
>
{tenantConfig?.organizationName ||
tenantConfig?.organizationName == ""
? tenantConfig?.organizationName
: t("landingPage.smartOffice")}
</motion.span>
</>
) : null}
{/* Desktop Navigation */}
<div className="hidden md:ml-6 lg:ml-10 md:flex md:space-x-4 lg:space-x-6">
{navItems.map((item) => (
<div key={item.id} className="relative">
{item.children ? (
<div
className="relative"
onMouseEnter={() => setServicesOpen(true)}
onMouseLeave={() => setServicesOpen(false)}
>
<button
onClick={() => setServicesOpen(!servicesOpen)}
className={`relative inline-flex items-center px-1 pt-1 text-sm lg:text-base font-medium transition-colors duration-200 cursor-pointer ${
activeTab === item.id
? "text-gray-900 dark:text-white"
: "text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white hover:underline"
}`}
>
{item.label}
<ChevronDown
className={`ml-1 w-4 h-4 transition-transform ${
servicesOpen ? "rotate-180" : ""
}`}
/>
</button>
<AnimatePresence>
{servicesOpen && (
<motion.div
initial="hidden"
animate="visible"
exit="hidden"
variants={servicesVariants}
className="absolute top-full left-0 mt-2 w-64 bg-white rounded-lg shadow-xl border border-gray-200 z-50 overflow-hidden"
>
<div className="py-2">
{item.children.map((child) => (
<motion.button
key={child.id}
variants={serviceItemVariants}
onClick={() => handleNavClick(child)}
className="w-full flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition-colors duration-200"
>
<span className="mr-3 text-primary">
{getServiceIcon(child.id)}
</span>
{child.label}
</motion.button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
) : (
<button
onClick={() => {
setActiveTab(item.id);
if (item.path) {
handleNavClick(item);
} else {
const el = document.getElementById(item.id);
if (el) {
el.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}
}}
className={`relative inline-flex items-center px-1 pt-1 text-sm lg:text-base font-medium transition-colors duration-200 cursor-pointer ${
(item.path
? location.pathname.startsWith(item.path)
: activeTab === item.id)
? "text-gray-900 dark:text-white"
: "text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white hover:underline"
}`}
>
{item.label}
{(item.path
? location.pathname.startsWith(item.path)
: activeTab === item.id) && (
<motion.div
layoutId="activeTabIndicator"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
transition={{
type: "spring",
bounce: 0.2,
duration: 0.6,
}}
/>
)}
</button>
)}
</div>
))}
</div>
</div>
{/* Desktop Sign Up Button */}
{!isAuthenticated && (
<div className="hidden md:flex items-center space-x-2 lg:space-x-4">
<div className="w-28 lg:w-32">
<Select
value={currentLanguage}
onValueChange={(lng) => changeLanguage(lng)}
>
<SelectTrigger className="w-full text-sm rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 px-2 py-1.5 lg:px-3 lg:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
<SelectValue>{getCurrentLanguageDisplay()}</SelectValue>
</SelectTrigger>
<SelectContent className="dark:bg-gray-800 dark:border-gray-700">
{languageOptions.map((lang) => (
<SelectItem
key={lang.value}
value={lang.value}
className="dark:text-gray-200 dark:hover:bg-gray-700"
>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<motion.button
onClick={handleSignIn}
whileHover={{
scale: 1.03,
boxShadow: "0 4px 12px rgba(24, 170, 157, 0.2)",
}}
whileTap={{ scale: 0.98 }}
className="inline-flex items-center px-3 py-1.5 lg:px-4 lg:py-2.5 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all duration-200 whitespace-nowrap"
>
{t("landingPage.signIn")}
</motion.button>
</div>
)}
{showExternalPortalChrome && (
<div className="hidden md:flex items-center">
<ExternalPortal />
</div>
)}
{/* Mobile Menu Button */}
<div className="flex items-center md:hidden">
<motion.button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary transition-colors duration-200"
aria-expanded="false"
whileTap={{ scale: 0.9 }}
>
<span className="sr-only">Open main menu</span>
{mobileMenuOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</motion.button>
</div>
</div>
</div>
{/* Mobile Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial="hidden"
animate="visible"
exit="hidden"
variants={mobileMenuVariants}
className="md:hidden overflow-hidden bg-white dark:bg-gray-800 shadow-xl border-t border-gray-200 dark:border-gray-700"
>
<motion.div className="pt-2 pb-4 space-y-1 px-4">
{navItems.map((item) => (
<div key={item.id}>
{item.children ? (
<div>
<button
onClick={() => setServicesOpen(!servicesOpen)}
className={`block w-full text-left pl-3 pr-4 py-3 border-l-4 text-base font-medium transition-all duration-200 ${
activeTab === item.id
? "bg-primary-500/15 border-primary text-primary"
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800"
}`}
>
<div className="flex items-center justify-between">
{item.label}
<ChevronDown
className={`w-4 h-4 transition-transform ${
servicesOpen ? "rotate-180" : ""
}`}
/>
</div>
</button>
<AnimatePresence>
{servicesOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="pl-6 overflow-hidden"
>
{item.children.map((child) => (
<motion.button
key={child.id}
onClick={() => handleNavClick(child)}
className="block w-full text-left pl-3 pr-4 py-2 text-sm text-gray-600 hover:bg-blue-50 hover:text-blue-700 transition-colors duration-200"
>
<div className="flex items-center">
<span className="mr-2 text-primary">
{getServiceIcon(child.id)}
</span>
{child.label}
</div>
</motion.button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
) : (
<motion.button
variants={mobileItemVariants}
onClick={() => {
setActiveTab(item.id);
setMobileMenuOpen(false);
if (item.path) {
handleNavClick(item);
}
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={`block w-full text-left pl-3 pr-4 py-3 border-l-4 text-base font-medium transition-all duration-200 ${
activeTab === item.id
? "bg-primary-500/15 border-primary text-primary"
: "border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 hover:text-gray-800 dark:hover:text-white"
}`}
>
{item.label}
</motion.button>
)}
</div>
))}
{!isAuthenticated && (
<motion.div
variants={mobileItemVariants}
className="pt-4 pb-2 border-t border-gray-200 dark:border-gray-700 space-y-3"
>
<div className="px-2">
<Select
value={currentLanguage}
onValueChange={(lng) => changeLanguage(lng)}
>
<SelectTrigger className="w-full text-sm bg-gray-100 dark:bg-gray-700 border-none rounded-md px-3 py-2.5 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary dark:text-gray-200">
<SelectValue>
{getCurrentLanguageDisplay()}
</SelectValue>
</SelectTrigger>
<SelectContent className="dark:bg-gray-800 dark:border-gray-700">
{languageOptions.map((lang) => (
<SelectItem
key={lang.value}
value={lang.value}
className="dark:text-gray-200 dark:hover:bg-gray-700"
>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<motion.button
onClick={() => {
handleSignIn();
setMobileMenuOpen(false);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="block w-full px-4 py-3 text-base font-medium text-center text-white bg-primary rounded-lg shadow hover:bg-primary-700 transition-all duration-200"
>
{t("landingPage.signIn")}
</motion.button>
</motion.div>
)}
{showExternalPortalChrome && (
<motion.div
variants={mobileItemVariants}
className="pt-3 border-t border-gray-200"
>
<ExternalPortal
mobileView={true}
onItemClick={() => setMobileMenuOpen(false)}
/>
</motion.div>
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
</nav>
</>
);
};
export default Header;