import { useSession } from "next-auth/react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import React from "react";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import ListItemIcon from "@mui/material/ListItemIcon";
import Dashboard from "@mui/icons-material/Dashboard";
import Storefront from "@mui/icons-material/Storefront";
import LocalShipping from "@mui/icons-material/LocalShipping";
import Assignment from "@mui/icons-material/Assignment";
import Inventory from "@mui/icons-material/Inventory";
import AssignmentTurnedIn from "@mui/icons-material/AssignmentTurnedIn";
import ReportProblem from "@mui/icons-material/ReportProblem";
import QrCodeIcon from "@mui/icons-material/QrCode";
import ViewModule from "@mui/icons-material/ViewModule";
import Description from "@mui/icons-material/Description";
import CalendarMonth from "@mui/icons-material/CalendarMonth";
import Factory from "@mui/icons-material/Factory";
import PostAdd from "@mui/icons-material/PostAdd";
import Kitchen from "@mui/icons-material/Kitchen";
import Inventory2 from "@mui/icons-material/Inventory2";
import Print from "@mui/icons-material/Print";
import Assessment from "@mui/icons-material/Assessment";
import ShowChart from "@mui/icons-material/ShowChart";
import Settings from "@mui/icons-material/Settings";
import Person from "@mui/icons-material/Person";
import Group from "@mui/icons-material/Group";
import Category from "@mui/icons-material/Category";
import TrendingUp from "@mui/icons-material/TrendingUp";
import Build from "@mui/icons-material/Build";
import Warehouse from "@mui/icons-material/Warehouse";
import VerifiedUser from "@mui/icons-material/VerifiedUser";
import Label from "@mui/icons-material/Label";
import Checklist from "@mui/icons-material/Checklist";
import Science from "@mui/icons-material/Science";
import UploadFile from "@mui/icons-material/UploadFile";
import Sync from "@mui/icons-material/Sync";
import Layers from "@mui/icons-material/Layers";
import Devices from "@mui/icons-material/Devices";
import { useTranslation } from "react-i18next";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
import Logo from "../Logo";
import { AUTH } from "../../authorities";
import { isMonitoringEnabled } from "@/config/monitoring";
import PurchaseStockInNavAlerts from "./PurchaseStockInNavAlerts";
import JobOrderFgStockInNavAlerts from "./JobOrderFgStockInNavAlerts";
import MasterDataIssuesNavBadge from "./MasterDataIssuesNavBadge";
interface NavigationItem {
id: string;
icon: React.ReactNode;
labelKey: string;
path: string;
children?: NavigationItem[];
isHidden?: boolean | undefined;
requiredAbility?: string | string[];
}
const NavigationContent: React.FC = () => {
const { data: session, status } = useSession();
const abilities = session?.user?.abilities ?? [];
// Helper: check if user has required permission
const hasAbility = (required?: string | string[]): boolean => {
if (!required) return true; // no requirement → always show
if (Array.isArray(required)) {
return required.some(ability => abilities.includes(ability));
}
return abilities.includes(required);
};
const navigationItems: NavigationItem[] = [
{
id: "nav.dashboard",
icon: ,
labelKey: "nav.dashboard",
path: "/dashboard",
},
{
id: "nav.storeManagement",
icon: ,
labelKey: "nav.storeManagement",
path: "",
requiredAbility: [AUTH.PURCHASE, AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_FG, AUTH.STOCK_IN_BIND, AUTH.ITEM_TRACING, AUTH.ADMIN],
children: [
{
id: "nav.store.purchaseOrder",
icon: ,
labelKey: "nav.store.purchaseOrder",
requiredAbility: [AUTH.PURCHASE, AUTH.ADMIN],
path: "/po",
},
{
id: "nav.store.pickOrder",
icon: ,
labelKey: "nav.store.pickOrder",
requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
path: "/pickOrder",
},
{
id: "nav.store.inventoryLedger",
icon: ,
labelKey: "nav.store.inventoryLedger",
requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
path: "/inventory",
},
{
id: "nav.store.itemTracing",
icon: ,
labelKey: "nav.store.itemTracing",
requiredAbility: [AUTH.ITEM_TRACING],
path: "/itemTracing",
},
{
id: "nav.store.stockTake",
icon: ,
labelKey: "nav.store.stockTake",
requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
path: "/stocktakemanagement",
},
{
id: "nav.store.stockIssue",
icon: ,
labelKey: "nav.store.stockIssue",
requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
path: "/stockIssue",
},
{
id: "nav.store.putAwayScan",
icon: ,
labelKey: "nav.store.putAwayScan",
requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.ADMIN],
path: "/putAway",
},
{
id: "nav.store.finishedGoodManagement",
icon: ,
labelKey: "nav.store.finishedGoodManagement",
requiredAbility: [AUTH.ADMIN],
path: "/finishedGood/management",
},
{
id: "nav.store.stockRecord",
icon: ,
labelKey: "nav.store.stockRecord",
requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
path: "/stockRecord",
},
{
id: "nav.store.doWorkbench",
icon: ,
labelKey: "nav.store.doWorkbench",
requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
path: "/doworkbench",
},
],
},
{
id: "nav.deliveryOrder",
icon: ,
labelKey: "nav.deliveryOrder",
path: "/do",
requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN],
},
{
id: "nav.scheduling",
icon: ,
labelKey: "nav.scheduling",
path: "/ps",
requiredAbility: [AUTH.FORECAST, AUTH.ADMIN],
isHidden: false,
},
{
id: "nav.jobOrderManagement",
icon: ,
labelKey: "nav.jobOrderManagement",
path: "",
requiredAbility: [AUTH.JOB_CREATE, AUTH.JOB_PICK, AUTH.JOB_PROD, AUTH.ADMIN],
children: [
{
id: "nav.jobOrder.searchCreate",
icon: ,
labelKey: "nav.jobOrder.searchCreate",
requiredAbility: [AUTH.JOB_CREATE, AUTH.ADMIN],
path: "/jo",
},
{
id: "nav.jobOrder.pickExecution",
icon: ,
labelKey: "nav.jobOrder.pickExecution",
requiredAbility: [AUTH.JOB_PICK, AUTH.JOB_MAT, AUTH.ADMIN],
path: "/jodetail",
},
{
id: "nav.jobOrder.productionProcess",
icon: ,
labelKey: "nav.jobOrder.productionProcess",
requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
path: "/productionProcess",
},
{
id: "nav.jobOrder.bagUsage",
icon: ,
labelKey: "nav.jobOrder.bagUsage",
requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
path: "/bag",
},
],
},
{
id: "nav.bagPrint",
icon: ,
labelKey: "nav.bagPrint",
path: "/bagPrint",
requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
isHidden: false,
},
{
id: "nav.laserPrint",
icon: ,
labelKey: "nav.laserPrint",
path: "/laserPrint",
requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
isHidden: false,
},
{
id: "nav.report",
icon: ,
labelKey: "nav.report",
path: "/report",
requiredAbility: [AUTH.REPORT_MGMT, AUTH.ADMIN],
isHidden: false,
},
{
id: "nav.m18Sync",
icon: ,
labelKey: "nav.m18Sync",
path: "/m18Syn",
requiredAbility: [AUTH.ADMIN],
isHidden: false,
},
{
id: "nav.chartReports",
icon: ,
labelKey: "nav.chartReports",
path: "",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
isHidden: false,
children: [
{
id: "nav.chart.purchase",
icon: ,
labelKey: "nav.chart.purchase",
path: "/chart/purchase",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
},
{
id: "nav.chart.jobOrder",
icon: ,
labelKey: "nav.chart.jobOrder",
path: "/chart/joborder",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
},
{
id: "nav.chart.jobOrderBoard",
icon: ,
labelKey: "nav.chart.jobOrderBoard",
path: "/chart/joborder/board",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
},
{
id: "nav.chart.delivery",
icon: ,
labelKey: "nav.chart.delivery",
path: "/chart/delivery",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
},
{
id: "nav.chart.warehouse",
icon: ,
labelKey: "nav.chart.warehouse",
path: "/chart/warehouse",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
},
{
id: "nav.chart.forecast",
icon: ,
labelKey: "nav.chart.forecast",
path: "/chart/forecast",
requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
},
],
},
{
id: "nav.settings",
icon: ,
labelKey: "nav.settings",
path: "",
requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
children: [
{
id: "nav.settings.user",
icon: ,
labelKey: "nav.settings.user",
path: "/settings/user",
requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
},
{
id: "nav.settings.clientMonitor",
icon: ,
labelKey: "nav.settings.clientMonitor",
path: "/settings/clientMonitor",
requiredAbility: [AUTH.ADMIN, AUTH.TESTING],
isHidden: !isMonitoringEnabled,
},
{
id: "nav.settings.items",
icon: ,
labelKey: "nav.settings.items",
path: "/settings/items",
},
{
id: "nav.settings.equipment",
icon: ,
labelKey: "nav.settings.equipment",
path: "/settings/equipment",
},
{
id: "nav.settings.warehouse",
icon: ,
labelKey: "nav.settings.warehouse",
path: "/settings/warehouse",
},
{
id: "nav.settings.printer",
icon: ,
labelKey: "nav.settings.printer",
path: "/settings/printer",
},
{
id: "nav.settings.priceInquiry",
icon: ,
labelKey: "nav.settings.priceInquiry",
path: "/settings/itemPrice",
},
{
id: "nav.settings.qcItem",
icon: ,
labelKey: "nav.settings.qcItem",
path: "/settings/qcItem",
},
{
id: "nav.settings.qcCategory",
icon: ,
labelKey: "nav.settings.qcCategory",
path: "/settings/qcCategory",
},
{
id: "nav.settings.qcItemAll",
icon: ,
labelKey: "nav.settings.qcItemAll",
path: "/settings/qcItemAll",
},
{
id: "nav.settings.shopAndTruck",
icon: ,
labelKey: "nav.settings.shopAndTruck",
path: "/settings/shop/board",
},
{
id: "nav.settings.deliveryOrderFloor",
icon: ,
labelKey: "nav.settings.deliveryOrderFloor",
path: "/settings/deliveryOrderFloor",
},
{
id: "nav.settings.demandForecast",
icon: ,
labelKey: "nav.settings.demandForecast",
path: "/settings/rss",
},
{
id: "nav.settings.bomWeighting",
icon: ,
labelKey: "nav.settings.bomWeighting",
path: "/settings/bomWeighting",
},
{
id: "nav.settings.masterDataIssues",
icon: ,
labelKey: "nav.settings.masterDataIssues",
path: "/settings/masterDataIssues",
requiredAbility: [AUTH.ADMIN],
},
{
id: "nav.settings.qrCodeHandle",
icon: ,
labelKey: "nav.settings.qrCodeHandle",
path: "/settings/qrCodeHandle",
},
{
id: "nav.settings.importTesting",
icon: ,
labelKey: "nav.settings.importTesting",
path: "/settings/m18ImportTesting",
},
{
id: "nav.settings.importExcel",
icon: ,
labelKey: "nav.settings.importExcel",
path: "/settings/importExcel",
},
{
id: "nav.settings.importBom",
icon: ,
labelKey: "nav.settings.importBom",
path: "/settings/importBom",
},
],
},
];
const { t } = useTranslation("navigation");
const pathname = usePathname();
const abilitySet = new Set(abilities.map((a) => String(a).trim()));
/** 採購入庫側欄紅點:TESTING / ADMIN / STOCK */
const canSeePoAlerts =
abilitySet.has(AUTH.TESTING) || abilitySet.has(AUTH.ADMIN) || abilitySet.has(AUTH.STOCK);
/** 工單 QC/上架紅點:仍僅 TESTING */
const canSeeJoFgAlerts = abilitySet.has(AUTH.TESTING);
const canSeeMasterDataIssueBadge = abilitySet.has(AUTH.ADMIN);
const [openItems, setOpenItems] = React.useState([]);
/** Keep parent sections expanded on deep links (e.g. /po/edit from nav red spot) so alerts stay visible. */
React.useEffect(() => {
const ensureOpen: string[] = [];
if (pathname.startsWith("/chart")) {
ensureOpen.push("nav.chartReports");
}
if (pathname === "/po" || pathname.startsWith("/po/")) {
ensureOpen.push("nav.storeManagement");
}
if (pathname === "/productionProcess" || pathname.startsWith("/productionProcess/")) {
ensureOpen.push("nav.jobOrderManagement");
}
if (
pathname === "/jo/workbench" ||
pathname.startsWith("/jo/workbench/") ||
pathname === "/jodetail" ||
pathname.startsWith("/jodetail/")
) {
ensureOpen.push("nav.jobOrderManagement");
}
if (
pathname === "/doworkbench" ||
pathname.startsWith("/doworkbench/") ||
pathname === "/doworkbenchsearch" ||
pathname.startsWith("/doworkbenchsearch/")
) {
ensureOpen.push("nav.storeManagement");
}
if (ensureOpen.length === 0) return;
setOpenItems((prev) => {
const set = new Set(prev);
let changed = false;
for (const id of ensureOpen) {
if (!set.has(id)) {
set.add(id);
changed = true;
}
}
return changed ? Array.from(set) : prev;
});
}, [pathname]);
const toggleItem = (id: string) => {
setOpenItems((prevOpenItems) =>
prevOpenItems.includes(id)
? prevOpenItems.filter((item) => item !== id)
: [...prevOpenItems, id],
);
};
const selectedLeafPath = React.useMemo(() => {
const leafPaths: string[] = [];
const walk = (items: NavigationItem[]) => {
for (const it of items) {
if (it.isHidden) continue;
if (!hasAbility(it.requiredAbility)) continue;
if (it.path) leafPaths.push(it.path);
if (it.children?.length) walk(it.children);
}
};
walk(navigationItems);
// Pick the most specific (longest) match to avoid double-highlighting
const matches = leafPaths.filter((p) => {
if (pathname === p) return true;
if (!pathname.startsWith(p + "/")) return false;
// `/doworkbench` must not claim `/doworkbenchsearch` (prefix without trailing slash)
if (p === "/doworkbench" && pathname.startsWith("/doworkbenchsearch")) return false;
// Shop sub-routes (detail, truckdetail, legacy tab page) share one nav entry → board
if (
p === "/settings/shop/board" &&
(pathname === "/settings/shop" || pathname.startsWith("/settings/shop/"))
) {
return true;
}
return true;
});
matches.sort((a, b) => b.length - a.length);
return matches[0] ?? "";
}, [hasAbility, navigationItems, pathname]);
const renderNavigationItem = (item: NavigationItem) => {
if (!hasAbility(item.requiredAbility)) {
return null;
}
const isOpen = openItems.includes(item.id);
const hasVisibleChildren = item.children?.some(child => hasAbility(child.requiredAbility));
const isLeaf = Boolean(item.path);
const isSelected = isLeaf && item.path
? item.path === selectedLeafPath
: hasVisibleChildren && item.children?.some((c) => c.path && c.path === selectedLeafPath);
const content = (
toggleItem(item.id)}
sx={{
mx: 1,
"&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
}}
>
{item.icon}
);
return (
{isLeaf ? (
{content}
) : (
content
)}
{item.children && isOpen && hasVisibleChildren && (
{item.children.map(
(child) => !child.isHidden && hasAbility(child.requiredAbility) && (
child.path === "/po" ? (
{child.icon}
) : child.path === "/settings/masterDataIssues" ? (
{child.icon}
) : child.path === "/productionProcess" ? (
{child.icon}
) : (
{child.icon}
)
),
)}
)}
);
};
if (status === "loading") {
return Loading...;
}
return (
{process.env.NODE_ENV === "production" && (
{t("nav.productionServer")}
)}
{navigationItems
.filter(item => !item.isHidden)
.map(renderNavigationItem)
.filter(Boolean)}
);
};
export default NavigationContent;