FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

NavigationContent.tsx 26 KiB

4ヶ月前
4ヶ月前
1週間前
1年前
1週間前
9ヶ月前
9ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
5ヶ月前
10ヶ月前
10ヶ月前
7ヶ月前
7ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. import { useSession } from "next-auth/react";
  2. import Box from "@mui/material/Box";
  3. import Stack from "@mui/material/Stack";
  4. import Typography from "@mui/material/Typography";
  5. import React from "react";
  6. import List from "@mui/material/List";
  7. import ListItemButton from "@mui/material/ListItemButton";
  8. import ListItemText from "@mui/material/ListItemText";
  9. import ListItemIcon from "@mui/material/ListItemIcon";
  10. import Dashboard from "@mui/icons-material/Dashboard";
  11. import Storefront from "@mui/icons-material/Storefront";
  12. import LocalShipping from "@mui/icons-material/LocalShipping";
  13. import Assignment from "@mui/icons-material/Assignment";
  14. import Inventory from "@mui/icons-material/Inventory";
  15. import AssignmentTurnedIn from "@mui/icons-material/AssignmentTurnedIn";
  16. import ReportProblem from "@mui/icons-material/ReportProblem";
  17. import QrCodeIcon from "@mui/icons-material/QrCode";
  18. import ViewModule from "@mui/icons-material/ViewModule";
  19. import Description from "@mui/icons-material/Description";
  20. import CalendarMonth from "@mui/icons-material/CalendarMonth";
  21. import Factory from "@mui/icons-material/Factory";
  22. import PostAdd from "@mui/icons-material/PostAdd";
  23. import Kitchen from "@mui/icons-material/Kitchen";
  24. import Inventory2 from "@mui/icons-material/Inventory2";
  25. import Print from "@mui/icons-material/Print";
  26. import Assessment from "@mui/icons-material/Assessment";
  27. import ShowChart from "@mui/icons-material/ShowChart";
  28. import Settings from "@mui/icons-material/Settings";
  29. import Person from "@mui/icons-material/Person";
  30. import Group from "@mui/icons-material/Group";
  31. import Category from "@mui/icons-material/Category";
  32. import TrendingUp from "@mui/icons-material/TrendingUp";
  33. import Build from "@mui/icons-material/Build";
  34. import Warehouse from "@mui/icons-material/Warehouse";
  35. import VerifiedUser from "@mui/icons-material/VerifiedUser";
  36. import Label from "@mui/icons-material/Label";
  37. import Checklist from "@mui/icons-material/Checklist";
  38. import Science from "@mui/icons-material/Science";
  39. import UploadFile from "@mui/icons-material/UploadFile";
  40. import Sync from "@mui/icons-material/Sync";
  41. import Layers from "@mui/icons-material/Layers";
  42. import Devices from "@mui/icons-material/Devices";
  43. import { useTranslation } from "react-i18next";
  44. import { usePathname } from "next/navigation";
  45. import Link from "next/link";
  46. import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
  47. import Logo from "../Logo";
  48. import { AUTH } from "../../authorities";
  49. import { isMonitoringEnabled } from "@/config/monitoring";
  50. import PurchaseStockInNavAlerts from "./PurchaseStockInNavAlerts";
  51. import JobOrderFgStockInNavAlerts from "./JobOrderFgStockInNavAlerts";
  52. import MasterDataIssuesNavBadge from "./MasterDataIssuesNavBadge";
  53. interface NavigationItem {
  54. id: string;
  55. icon: React.ReactNode;
  56. labelKey: string;
  57. path: string;
  58. children?: NavigationItem[];
  59. isHidden?: boolean | undefined;
  60. requiredAbility?: string | string[];
  61. }
  62. const NavigationContent: React.FC = () => {
  63. const { data: session, status } = useSession();
  64. const abilities = session?.user?.abilities ?? [];
  65. // Helper: check if user has required permission
  66. const hasAbility = (required?: string | string[]): boolean => {
  67. if (!required) return true; // no requirement → always show
  68. if (Array.isArray(required)) {
  69. return required.some(ability => abilities.includes(ability));
  70. }
  71. return abilities.includes(required);
  72. };
  73. const navigationItems: NavigationItem[] = [
  74. {
  75. id: "nav.dashboard",
  76. icon: <Dashboard />,
  77. labelKey: "nav.dashboard",
  78. path: "/dashboard",
  79. },
  80. {
  81. id: "nav.storeManagement",
  82. icon: <Storefront />,
  83. labelKey: "nav.storeManagement",
  84. path: "",
  85. requiredAbility: [AUTH.PURCHASE, AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_FG, AUTH.STOCK_IN_BIND, AUTH.ITEM_TRACING, AUTH.ADMIN],
  86. children: [
  87. {
  88. id: "nav.store.purchaseOrder",
  89. icon: <LocalShipping />,
  90. labelKey: "nav.store.purchaseOrder",
  91. requiredAbility: [AUTH.PURCHASE, AUTH.ADMIN],
  92. path: "/po",
  93. },
  94. {
  95. id: "nav.store.pickOrder",
  96. icon: <Assignment />,
  97. labelKey: "nav.store.pickOrder",
  98. requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
  99. path: "/pickOrder",
  100. },
  101. {
  102. id: "nav.store.inventoryLedger",
  103. icon: <Inventory />,
  104. labelKey: "nav.store.inventoryLedger",
  105. requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
  106. path: "/inventory",
  107. },
  108. {
  109. id: "nav.store.itemTracing",
  110. icon: <QrCodeIcon />,
  111. labelKey: "nav.store.itemTracing",
  112. requiredAbility: [AUTH.ITEM_TRACING],
  113. path: "/itemTracing",
  114. },
  115. {
  116. id: "nav.store.stockTake",
  117. icon: <AssignmentTurnedIn />,
  118. labelKey: "nav.store.stockTake",
  119. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
  120. path: "/stocktakemanagement",
  121. },
  122. {
  123. id: "nav.store.stockIssue",
  124. icon: <ReportProblem />,
  125. labelKey: "nav.store.stockIssue",
  126. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
  127. path: "/stockIssue",
  128. },
  129. {
  130. id: "nav.store.putAwayScan",
  131. icon: <QrCodeIcon />,
  132. labelKey: "nav.store.putAwayScan",
  133. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.ADMIN],
  134. path: "/putAway",
  135. },
  136. {
  137. id: "nav.store.finishedGoodManagement",
  138. icon: <ViewModule />,
  139. labelKey: "nav.store.finishedGoodManagement",
  140. requiredAbility: [AUTH.ADMIN],
  141. path: "/finishedGood/management",
  142. },
  143. {
  144. id: "nav.store.stockRecord",
  145. icon: <Description />,
  146. labelKey: "nav.store.stockRecord",
  147. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
  148. path: "/stockRecord",
  149. },
  150. {
  151. id: "nav.store.doWorkbench",
  152. icon: <Description />,
  153. labelKey: "nav.store.doWorkbench",
  154. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
  155. path: "/doworkbench",
  156. },
  157. ],
  158. },
  159. {
  160. id: "nav.deliveryOrder",
  161. icon: <LocalShipping />,
  162. labelKey: "nav.deliveryOrder",
  163. path: "/do",
  164. requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN],
  165. },
  166. {
  167. id: "nav.scheduling",
  168. icon: <CalendarMonth />,
  169. labelKey: "nav.scheduling",
  170. path: "/ps",
  171. requiredAbility: [AUTH.FORECAST, AUTH.ADMIN],
  172. isHidden: false,
  173. },
  174. {
  175. id: "nav.jobOrderManagement",
  176. icon: <Factory />,
  177. labelKey: "nav.jobOrderManagement",
  178. path: "",
  179. requiredAbility: [AUTH.JOB_CREATE, AUTH.JOB_PICK, AUTH.JOB_PROD, AUTH.ADMIN],
  180. children: [
  181. {
  182. id: "nav.jobOrder.searchCreate",
  183. icon: <PostAdd />,
  184. labelKey: "nav.jobOrder.searchCreate",
  185. requiredAbility: [AUTH.JOB_CREATE, AUTH.ADMIN],
  186. path: "/jo",
  187. },
  188. {
  189. id: "nav.jobOrder.pickExecution",
  190. icon: <Inventory />,
  191. labelKey: "nav.jobOrder.pickExecution",
  192. requiredAbility: [AUTH.JOB_PICK, AUTH.JOB_MAT, AUTH.ADMIN],
  193. path: "/jodetail",
  194. },
  195. {
  196. id: "nav.jobOrder.productionProcess",
  197. icon: <Kitchen />,
  198. labelKey: "nav.jobOrder.productionProcess",
  199. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  200. path: "/productionProcess",
  201. },
  202. {
  203. id: "nav.jobOrder.bagUsage",
  204. icon: <Inventory2 />,
  205. labelKey: "nav.jobOrder.bagUsage",
  206. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  207. path: "/bag",
  208. },
  209. ],
  210. },
  211. {
  212. id: "nav.bagPrint",
  213. icon: <Print />,
  214. labelKey: "nav.bagPrint",
  215. path: "/bagPrint",
  216. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  217. isHidden: false,
  218. },
  219. {
  220. id: "nav.laserPrint",
  221. icon: <Print />,
  222. labelKey: "nav.laserPrint",
  223. path: "/laserPrint",
  224. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  225. isHidden: false,
  226. },
  227. {
  228. id: "nav.report",
  229. icon: <Assessment />,
  230. labelKey: "nav.report",
  231. path: "/report",
  232. requiredAbility: [AUTH.REPORT_MGMT, AUTH.ADMIN],
  233. isHidden: false,
  234. },
  235. {
  236. id: "nav.m18Sync",
  237. icon: <Sync />,
  238. labelKey: "nav.m18Sync",
  239. path: "/m18Syn",
  240. requiredAbility: [AUTH.ADMIN],
  241. isHidden: false,
  242. },
  243. {
  244. id: "nav.chartReports",
  245. icon: <ShowChart />,
  246. labelKey: "nav.chartReports",
  247. path: "",
  248. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  249. isHidden: false,
  250. children: [
  251. {
  252. id: "nav.chart.purchase",
  253. icon: <Storefront />,
  254. labelKey: "nav.chart.purchase",
  255. path: "/chart/purchase",
  256. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  257. },
  258. {
  259. id: "nav.chart.jobOrder",
  260. icon: <Assignment />,
  261. labelKey: "nav.chart.jobOrder",
  262. path: "/chart/joborder",
  263. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  264. },
  265. {
  266. id: "nav.chart.jobOrderBoard",
  267. icon: <ViewModule />,
  268. labelKey: "nav.chart.jobOrderBoard",
  269. path: "/chart/joborder/board",
  270. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  271. },
  272. {
  273. id: "nav.chart.delivery",
  274. icon: <LocalShipping />,
  275. labelKey: "nav.chart.delivery",
  276. path: "/chart/delivery",
  277. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  278. },
  279. {
  280. id: "nav.chart.warehouse",
  281. icon: <Warehouse />,
  282. labelKey: "nav.chart.warehouse",
  283. path: "/chart/warehouse",
  284. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  285. },
  286. {
  287. id: "nav.chart.forecast",
  288. icon: <TrendingUp />,
  289. labelKey: "nav.chart.forecast",
  290. path: "/chart/forecast",
  291. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  292. },
  293. ],
  294. },
  295. {
  296. id: "nav.settings",
  297. icon: <Settings />,
  298. labelKey: "nav.settings",
  299. path: "",
  300. requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
  301. children: [
  302. {
  303. id: "nav.settings.user",
  304. icon: <Person />,
  305. labelKey: "nav.settings.user",
  306. path: "/settings/user",
  307. requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
  308. },
  309. {
  310. id: "nav.settings.clientMonitor",
  311. icon: <Devices />,
  312. labelKey: "nav.settings.clientMonitor",
  313. path: "/settings/clientMonitor",
  314. requiredAbility: [AUTH.ADMIN, AUTH.TESTING],
  315. isHidden: !isMonitoringEnabled,
  316. },
  317. {
  318. id: "nav.settings.items",
  319. icon: <Category />,
  320. labelKey: "nav.settings.items",
  321. path: "/settings/items",
  322. },
  323. {
  324. id: "nav.settings.equipment",
  325. icon: <Build />,
  326. labelKey: "nav.settings.equipment",
  327. path: "/settings/equipment",
  328. },
  329. {
  330. id: "nav.settings.warehouse",
  331. icon: <Warehouse />,
  332. labelKey: "nav.settings.warehouse",
  333. path: "/settings/warehouse",
  334. },
  335. {
  336. id: "nav.settings.printer",
  337. icon: <Print />,
  338. labelKey: "nav.settings.printer",
  339. path: "/settings/printer",
  340. },
  341. {
  342. id: "nav.settings.priceInquiry",
  343. icon: <Assessment />,
  344. labelKey: "nav.settings.priceInquiry",
  345. path: "/settings/itemPrice",
  346. },
  347. {
  348. id: "nav.settings.qcItem",
  349. icon: <VerifiedUser />,
  350. labelKey: "nav.settings.qcItem",
  351. path: "/settings/qcItem",
  352. },
  353. {
  354. id: "nav.settings.qcCategory",
  355. icon: <Label />,
  356. labelKey: "nav.settings.qcCategory",
  357. path: "/settings/qcCategory",
  358. },
  359. {
  360. id: "nav.settings.qcItemAll",
  361. icon: <Checklist />,
  362. labelKey: "nav.settings.qcItemAll",
  363. path: "/settings/qcItemAll",
  364. },
  365. {
  366. id: "nav.settings.shopAndTruck",
  367. icon: <Storefront />,
  368. labelKey: "nav.settings.shopAndTruck",
  369. path: "/settings/shop/board",
  370. },
  371. {
  372. id: "nav.settings.deliveryOrderFloor",
  373. icon: <Layers />,
  374. labelKey: "nav.settings.deliveryOrderFloor",
  375. path: "/settings/deliveryOrderFloor",
  376. },
  377. {
  378. id: "nav.settings.demandForecast",
  379. icon: <TrendingUp />,
  380. labelKey: "nav.settings.demandForecast",
  381. path: "/settings/rss",
  382. },
  383. {
  384. id: "nav.settings.bomWeighting",
  385. icon: <ViewModule />,
  386. labelKey: "nav.settings.bomWeighting",
  387. path: "/settings/bomWeighting",
  388. },
  389. {
  390. id: "nav.settings.masterDataIssues",
  391. icon: <ReportProblem />,
  392. labelKey: "nav.settings.masterDataIssues",
  393. path: "/settings/masterDataIssues",
  394. requiredAbility: [AUTH.ADMIN],
  395. },
  396. {
  397. id: "nav.settings.qrCodeHandle",
  398. icon: <QrCodeIcon />,
  399. labelKey: "nav.settings.qrCodeHandle",
  400. path: "/settings/qrCodeHandle",
  401. },
  402. {
  403. id: "nav.settings.importTesting",
  404. icon: <Science />,
  405. labelKey: "nav.settings.importTesting",
  406. path: "/settings/m18ImportTesting",
  407. },
  408. {
  409. id: "nav.settings.importExcel",
  410. icon: <UploadFile />,
  411. labelKey: "nav.settings.importExcel",
  412. path: "/settings/importExcel",
  413. },
  414. {
  415. id: "nav.settings.importBom",
  416. icon: <UploadFile />,
  417. labelKey: "nav.settings.importBom",
  418. path: "/settings/importBom",
  419. },
  420. ],
  421. },
  422. ];
  423. const { t } = useTranslation("navigation");
  424. const pathname = usePathname();
  425. const abilitySet = new Set(abilities.map((a) => String(a).trim()));
  426. /** 採購入庫側欄紅點:TESTING / ADMIN / STOCK */
  427. const canSeePoAlerts =
  428. abilitySet.has(AUTH.TESTING) || abilitySet.has(AUTH.ADMIN) || abilitySet.has(AUTH.STOCK);
  429. /** 工單 QC/上架紅點:仍僅 TESTING */
  430. const canSeeJoFgAlerts = abilitySet.has(AUTH.TESTING);
  431. const canSeeMasterDataIssueBadge = abilitySet.has(AUTH.ADMIN);
  432. const [openItems, setOpenItems] = React.useState<string[]>([]);
  433. /** Keep parent sections expanded on deep links (e.g. /po/edit from nav red spot) so alerts stay visible. */
  434. React.useEffect(() => {
  435. const ensureOpen: string[] = [];
  436. if (pathname.startsWith("/chart")) {
  437. ensureOpen.push("nav.chartReports");
  438. }
  439. if (pathname === "/po" || pathname.startsWith("/po/")) {
  440. ensureOpen.push("nav.storeManagement");
  441. }
  442. if (pathname === "/productionProcess" || pathname.startsWith("/productionProcess/")) {
  443. ensureOpen.push("nav.jobOrderManagement");
  444. }
  445. if (
  446. pathname === "/jo/workbench" ||
  447. pathname.startsWith("/jo/workbench/") ||
  448. pathname === "/jodetail" ||
  449. pathname.startsWith("/jodetail/")
  450. ) {
  451. ensureOpen.push("nav.jobOrderManagement");
  452. }
  453. if (
  454. pathname === "/doworkbench" ||
  455. pathname.startsWith("/doworkbench/") ||
  456. pathname === "/doworkbenchsearch" ||
  457. pathname.startsWith("/doworkbenchsearch/")
  458. ) {
  459. ensureOpen.push("nav.storeManagement");
  460. }
  461. if (ensureOpen.length === 0) return;
  462. setOpenItems((prev) => {
  463. const set = new Set(prev);
  464. let changed = false;
  465. for (const id of ensureOpen) {
  466. if (!set.has(id)) {
  467. set.add(id);
  468. changed = true;
  469. }
  470. }
  471. return changed ? Array.from(set) : prev;
  472. });
  473. }, [pathname]);
  474. const toggleItem = (id: string) => {
  475. setOpenItems((prevOpenItems) =>
  476. prevOpenItems.includes(id)
  477. ? prevOpenItems.filter((item) => item !== id)
  478. : [...prevOpenItems, id],
  479. );
  480. };
  481. const selectedLeafPath = React.useMemo(() => {
  482. const leafPaths: string[] = [];
  483. const walk = (items: NavigationItem[]) => {
  484. for (const it of items) {
  485. if (it.isHidden) continue;
  486. if (!hasAbility(it.requiredAbility)) continue;
  487. if (it.path) leafPaths.push(it.path);
  488. if (it.children?.length) walk(it.children);
  489. }
  490. };
  491. walk(navigationItems);
  492. // Pick the most specific (longest) match to avoid double-highlighting
  493. const matches = leafPaths.filter((p) => {
  494. if (pathname === p) return true;
  495. if (!pathname.startsWith(p + "/")) return false;
  496. // `/doworkbench` must not claim `/doworkbenchsearch` (prefix without trailing slash)
  497. if (p === "/doworkbench" && pathname.startsWith("/doworkbenchsearch")) return false;
  498. // Shop sub-routes (detail, truckdetail, legacy tab page) share one nav entry → board
  499. if (
  500. p === "/settings/shop/board" &&
  501. (pathname === "/settings/shop" || pathname.startsWith("/settings/shop/"))
  502. ) {
  503. return true;
  504. }
  505. return true;
  506. });
  507. matches.sort((a, b) => b.length - a.length);
  508. return matches[0] ?? "";
  509. }, [hasAbility, navigationItems, pathname]);
  510. const renderNavigationItem = (item: NavigationItem) => {
  511. if (!hasAbility(item.requiredAbility)) {
  512. return null;
  513. }
  514. const isOpen = openItems.includes(item.id);
  515. const hasVisibleChildren = item.children?.some(child => hasAbility(child.requiredAbility));
  516. const isLeaf = Boolean(item.path);
  517. const isSelected = isLeaf && item.path
  518. ? item.path === selectedLeafPath
  519. : hasVisibleChildren && item.children?.some((c) => c.path && c.path === selectedLeafPath);
  520. const content = (
  521. <ListItemButton
  522. selected={isSelected}
  523. onClick={isLeaf ? undefined : () => toggleItem(item.id)}
  524. sx={{
  525. mx: 1,
  526. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  527. }}
  528. >
  529. <ListItemIcon sx={{ minWidth: 40 }}>{item.icon}</ListItemIcon>
  530. <ListItemText
  531. primary={t(item.labelKey)}
  532. primaryTypographyProps={{ fontWeight: isSelected ? 600 : 500 }}
  533. />
  534. </ListItemButton>
  535. );
  536. return (
  537. <Box key={item.id}>
  538. {isLeaf ? (
  539. <Link href={item.path!} style={{ textDecoration: "none", color: "inherit" }}>
  540. {content}
  541. </Link>
  542. ) : (
  543. content
  544. )}
  545. {item.children && isOpen && hasVisibleChildren && (
  546. <List sx={{ pl: 2, py: 0 }}>
  547. {item.children.map(
  548. (child) => !child.isHidden && hasAbility(child.requiredAbility) && (
  549. child.path === "/po" ? (
  550. <Box
  551. key={child.id}
  552. sx={{
  553. display: "flex",
  554. alignItems: "stretch",
  555. mx: 1,
  556. borderRadius: 1,
  557. overflow: "hidden",
  558. "&:hover": { bgcolor: "action.hover" },
  559. }}
  560. >
  561. <Box
  562. component={Link}
  563. href={child.path}
  564. sx={{
  565. flex: 1,
  566. minWidth: 0,
  567. textDecoration: "none",
  568. color: "inherit",
  569. display: "flex",
  570. }}
  571. >
  572. <ListItemButton
  573. selected={child.path === selectedLeafPath}
  574. sx={{
  575. flex: 1,
  576. py: 1,
  577. pr: 0.5,
  578. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  579. }}
  580. >
  581. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  582. <ListItemText
  583. primary={t(child.labelKey)}
  584. primaryTypographyProps={{
  585. fontWeight:
  586. child.path === selectedLeafPath ? 600 : 500,
  587. fontSize: "0.875rem",
  588. }}
  589. />
  590. </ListItemButton>
  591. </Box>
  592. <PurchaseStockInNavAlerts enabled={canSeePoAlerts} />
  593. </Box>
  594. ) : child.path === "/settings/masterDataIssues" ? (
  595. <Box
  596. key={child.id}
  597. component={Link}
  598. href={child.path}
  599. sx={{
  600. display: "block",
  601. mx: 1,
  602. borderRadius: 1,
  603. textDecoration: "none",
  604. color: "inherit",
  605. "&:hover": { bgcolor: "action.hover" },
  606. }}
  607. >
  608. <ListItemButton
  609. selected={child.path === selectedLeafPath}
  610. sx={{
  611. py: 1,
  612. pr: 1.5,
  613. display: "flex",
  614. alignItems: "center",
  615. gap: 0.5,
  616. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  617. }}
  618. >
  619. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  620. <ListItemText
  621. primary={t(child.labelKey)}
  622. sx={{ flex: 1, minWidth: 0, my: 0 }}
  623. primaryTypographyProps={{
  624. fontWeight:
  625. child.path === selectedLeafPath ? 600 : 500,
  626. fontSize: "0.875rem",
  627. }}
  628. />
  629. <MasterDataIssuesNavBadge enabled={canSeeMasterDataIssueBadge} />
  630. </ListItemButton>
  631. </Box>
  632. ) : child.path === "/productionProcess" ? (
  633. <Box
  634. key={child.id}
  635. sx={{
  636. display: "flex",
  637. alignItems: "stretch",
  638. mx: 1,
  639. borderRadius: 1,
  640. overflow: "hidden",
  641. "&:hover": { bgcolor: "action.hover" },
  642. }}
  643. >
  644. <Box
  645. component={Link}
  646. href={child.path}
  647. sx={{
  648. flex: 1,
  649. minWidth: 0,
  650. textDecoration: "none",
  651. color: "inherit",
  652. display: "flex",
  653. }}
  654. >
  655. <ListItemButton
  656. selected={child.path === selectedLeafPath}
  657. sx={{
  658. flex: 1,
  659. py: 1,
  660. pr: 0.5,
  661. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  662. }}
  663. >
  664. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  665. <ListItemText
  666. primary={t(child.labelKey)}
  667. primaryTypographyProps={{
  668. fontWeight:
  669. child.path === selectedLeafPath ? 600 : 500,
  670. fontSize: "0.875rem",
  671. }}
  672. />
  673. </ListItemButton>
  674. </Box>
  675. <JobOrderFgStockInNavAlerts enabled={canSeeJoFgAlerts} />
  676. </Box>
  677. ) : (
  678. <Box
  679. key={child.id}
  680. component={Link}
  681. href={child.path}
  682. sx={{ textDecoration: "none", color: "inherit" }}
  683. >
  684. <ListItemButton
  685. selected={child.path === selectedLeafPath}
  686. sx={{
  687. mx: 1,
  688. py: 1,
  689. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  690. }}
  691. >
  692. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  693. <ListItemText
  694. primary={t(child.labelKey)}
  695. primaryTypographyProps={{
  696. fontWeight:
  697. child.path === selectedLeafPath
  698. ? 600
  699. : 500,
  700. fontSize: "0.875rem",
  701. }}
  702. />
  703. </ListItemButton>
  704. </Box>
  705. )
  706. ),
  707. )}
  708. </List>
  709. )}
  710. </Box>
  711. );
  712. };
  713. if (status === "loading") {
  714. return <Box sx={{ width: NAVIGATION_CONTENT_WIDTH, p: 3 }}>Loading...</Box>;
  715. }
  716. return (
  717. <Box sx={{ width: NAVIGATION_CONTENT_WIDTH, height: "100%", display: "flex", flexDirection: "column" }}>
  718. <Box
  719. className="bg-gradient-to-br from-blue-500/15 via-slate-100 to-slate-50 dark:from-blue-500/20 dark:via-slate-800 dark:to-slate-900"
  720. sx={{
  721. mx: 1,
  722. mt: 1,
  723. mb: 1,
  724. px: 1.5,
  725. py: 2,
  726. flexShrink: 0,
  727. display: "flex",
  728. alignItems: "center",
  729. justifyContent: "flex-start",
  730. minHeight: 56,
  731. }}
  732. >
  733. <Stack direction="column" alignItems="flex-start" spacing={0.5}>
  734. <Logo height={42} />
  735. {process.env.NODE_ENV === "production" && (
  736. <Typography
  737. variant="body2"
  738. sx={{
  739. fontWeight: 700,
  740. color: "warning.dark",
  741. bgcolor: "warning.light",
  742. px: 1,
  743. py: 0.5,
  744. borderRadius: 1,
  745. border: "1px solid",
  746. borderColor: "warning.main",
  747. }}
  748. >
  749. {t("nav.productionServer")}
  750. </Typography>
  751. )}
  752. </Stack>
  753. </Box>
  754. <Box sx={{ borderTop: 1, borderColor: "divider" }} />
  755. <List component="nav" sx={{ flex: 1, overflow: "auto", py: 1, px: 0 }}>
  756. {navigationItems
  757. .filter(item => !item.isHidden)
  758. .map(renderNavigationItem)
  759. .filter(Boolean)}
  760. </List>
  761. </Box>
  762. );
  763. };
  764. export default NavigationContent;