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

NavigationContent.tsx 21 KiB

1ヶ月前
3週間前
6ヶ月前
9ヶ月前
10ヶ月前
10ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1日前
2ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3週間前
3週間前
1ヶ月前
4週間前
2ヶ月前
2ヶ月前
4週間前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
10ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  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 { useTranslation } from "react-i18next";
  42. import { usePathname } from "next/navigation";
  43. import Link from "next/link";
  44. import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
  45. import Logo from "../Logo";
  46. import { AUTH } from "../../authorities";
  47. import PurchaseStockInNavAlerts from "./PurchaseStockInNavAlerts";
  48. import JobOrderFgStockInNavAlerts from "./JobOrderFgStockInNavAlerts";
  49. interface NavigationItem {
  50. icon: React.ReactNode;
  51. label: string;
  52. path: string;
  53. children?: NavigationItem[];
  54. isHidden?: boolean | undefined;
  55. requiredAbility?: string | string[];
  56. }
  57. const NavigationContent: React.FC = () => {
  58. const { data: session, status } = useSession();
  59. const abilities = session?.user?.abilities ?? [];
  60. // Helper: check if user has required permission
  61. const hasAbility = (required?: string | string[]): boolean => {
  62. if (!required) return true; // no requirement → always show
  63. if (Array.isArray(required)) {
  64. return required.some(ability => abilities.includes(ability));
  65. }
  66. return abilities.includes(required);
  67. };
  68. const navigationItems: NavigationItem[] = [
  69. {
  70. icon: <Dashboard />,
  71. label: "Dashboard",
  72. path: "/dashboard",
  73. },
  74. {
  75. icon: <Storefront />,
  76. label: "Store Management",
  77. path: "",
  78. requiredAbility: [AUTH.PURCHASE, AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_FG, AUTH.STOCK_IN_BIND, AUTH.ADMIN],
  79. children: [
  80. {
  81. icon: <LocalShipping />,
  82. label: "Purchase Order",
  83. requiredAbility: [AUTH.PURCHASE, AUTH.ADMIN],
  84. path: "/po",
  85. },
  86. {
  87. icon: <Assignment />,
  88. label: "Pick Order",
  89. requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
  90. path: "/pickOrder",
  91. },
  92. {
  93. icon: <Inventory />,
  94. label: "View item In-out And inventory Ledger",
  95. requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
  96. path: "/inventory",
  97. },
  98. {
  99. icon: <AssignmentTurnedIn />,
  100. label: "Stock Take Management",
  101. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
  102. path: "/stocktakemanagement",
  103. },
  104. {
  105. icon: <ReportProblem />,
  106. label: "Stock Issue",
  107. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
  108. path: "/stockIssue",
  109. },
  110. {
  111. icon: <QrCodeIcon />,
  112. label: "Put Away Scan",
  113. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.ADMIN],
  114. path: "/putAway",
  115. },
  116. {
  117. icon: <ViewModule />,
  118. label: "Finished Good Order",
  119. requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN],
  120. path: "/finishedGood",
  121. },
  122. {
  123. icon: <ViewModule />,
  124. label: "Finished Good Management",
  125. requiredAbility: [AUTH.ADMIN],
  126. path: "/finishedGood/management",
  127. },
  128. {
  129. icon: <Description />,
  130. label: "Stock Record",
  131. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
  132. path: "/stockRecord",
  133. },
  134. /*
  135. {
  136. icon: <Description />,
  137. label: "Do Workbench",
  138. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
  139. path: "/doworkbench",
  140. },
  141. */
  142. ],
  143. },
  144. {
  145. icon: <LocalShipping />,
  146. label: "Delivery Order",
  147. path: "/do",
  148. requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN],
  149. },
  150. {
  151. icon: <CalendarMonth />,
  152. label: "Scheduling",
  153. path: "/ps",
  154. requiredAbility: [AUTH.FORECAST, AUTH.ADMIN],
  155. isHidden: false,
  156. },
  157. {
  158. icon: <Factory />,
  159. label: "Management Job Order",
  160. path: "",
  161. requiredAbility: [AUTH.JOB_CREATE, AUTH.JOB_PICK, AUTH.JOB_PROD, AUTH.ADMIN],
  162. children: [
  163. {
  164. icon: <PostAdd />,
  165. label: "Search Job Order/ Create Job Order",
  166. requiredAbility: [AUTH.JOB_CREATE, AUTH.ADMIN],
  167. path: "/jo",
  168. },
  169. {
  170. icon: <Inventory />,
  171. label: "Job Order Pickexcution",
  172. requiredAbility: [AUTH.JOB_PICK, AUTH.JOB_MAT, AUTH.ADMIN],
  173. path: "/jodetail",
  174. },
  175. {
  176. icon: <Kitchen />,
  177. label: "Job Order Production Process",
  178. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  179. path: "/productionProcess",
  180. },
  181. {
  182. icon: <Inventory2 />,
  183. label: "Bag Usage",
  184. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  185. path: "/bag",
  186. },
  187. ],
  188. },
  189. {
  190. icon: <Print />,
  191. label: "打袋機",
  192. path: "/bagPrint",
  193. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  194. isHidden: false,
  195. },
  196. {
  197. icon: <Print />,
  198. label: "檸檬機(激光機)",
  199. path: "/laserPrint",
  200. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  201. isHidden: false,
  202. },
  203. {
  204. icon: <Assessment />,
  205. label: "報告管理",
  206. path: "/report",
  207. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  208. isHidden: false,
  209. },
  210. {
  211. icon: <Sync />,
  212. label: "M18 Sync",
  213. path: "/m18Syn",
  214. requiredAbility: [AUTH.ADMIN],
  215. isHidden: false,
  216. },
  217. {
  218. icon: <ShowChart />,
  219. label: "圖表報告",
  220. path: "",
  221. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  222. isHidden: false,
  223. children: [
  224. {
  225. icon: <Storefront />,
  226. label: "採購",
  227. path: "/chart/purchase",
  228. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  229. },
  230. {
  231. icon: <Assignment />,
  232. label: "工單",
  233. path: "/chart/joborder",
  234. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  235. },
  236. {
  237. icon: <ViewModule />,
  238. label: "工單即時看板",
  239. path: "/chart/joborder/board",
  240. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  241. },
  242. {
  243. icon: <LocalShipping />,
  244. label: "發貨與配送",
  245. path: "/chart/delivery",
  246. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  247. },
  248. {
  249. icon: <Warehouse />,
  250. label: "庫存與倉儲",
  251. path: "/chart/warehouse",
  252. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  253. },
  254. {
  255. icon: <TrendingUp />,
  256. label: "預測與計劃",
  257. path: "/chart/forecast",
  258. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  259. },
  260. ],
  261. },
  262. {
  263. icon: <Settings />,
  264. label: "Settings",
  265. path: "",
  266. requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
  267. children: [
  268. {
  269. icon: <Person />,
  270. label: "User",
  271. path: "/settings/user",
  272. requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
  273. },
  274. //{
  275. // icon: <Group />,
  276. // label: "User Group",
  277. // path: "/settings/user",
  278. // requiredAbility: [AUTH.VIEW_GROUP, AUTH.ADMIN],
  279. //},
  280. {
  281. icon: <Category />,
  282. label: "Items",
  283. path: "/settings/items",
  284. },
  285. {
  286. icon: <Build />,
  287. label: "Equipment",
  288. path: "/settings/equipment",
  289. },
  290. {
  291. icon: <Warehouse />,
  292. label: "Warehouse",
  293. path: "/settings/warehouse",
  294. },
  295. {
  296. icon: <Print />,
  297. label: "Printer",
  298. path: "/settings/printer",
  299. },
  300. {
  301. icon: <Assessment />,
  302. label: "Price Inquiry",
  303. path: "/settings/itemPrice",
  304. },
  305. {
  306. icon: <VerifiedUser />,
  307. label: "QC Check Item",
  308. path: "/settings/qcItem",
  309. },
  310. {
  311. icon: <Label />,
  312. label: "QC Category",
  313. path: "/settings/qcCategory",
  314. },
  315. {
  316. icon: <Checklist />,
  317. label: "QC Item All",
  318. path: "/settings/qcItemAll",
  319. },
  320. {
  321. icon: <Storefront />,
  322. label: "ShopAndTruck",
  323. path: "/settings/shop",
  324. },
  325. {
  326. icon: <TrendingUp />,
  327. label: "Demand Forecast Setting",
  328. path: "/settings/rss",
  329. },
  330. //{
  331. // icon: <Person />,
  332. // label: "Customer",
  333. // path: "/settings/user",
  334. //},
  335. {
  336. icon: <ViewModule />,
  337. label: "BOM Weighting Score List",
  338. path: "/settings/bomWeighting",
  339. },
  340. {
  341. icon: <QrCodeIcon />,
  342. label: "QR Code Handle",
  343. path: "/settings/qrCodeHandle",
  344. },
  345. {
  346. icon: <Science />,
  347. label: "Import Testing",
  348. path: "/settings/m18ImportTesting",
  349. },
  350. {
  351. icon: <UploadFile />,
  352. label: "Import Excel",
  353. path: "/settings/importExcel",
  354. },
  355. {
  356. icon: <UploadFile />,
  357. label: "Import BOM",
  358. path: "/settings/importBom",
  359. },
  360. ],
  361. },
  362. ];
  363. const { t } = useTranslation("common");
  364. const pathname = usePathname();
  365. const abilitySet = new Set(abilities.map((a) => String(a).trim()));
  366. /** 採購入庫側欄紅點:TESTING / ADMIN / STOCK */
  367. const canSeePoAlerts =
  368. abilitySet.has(AUTH.TESTING) || abilitySet.has(AUTH.ADMIN) || abilitySet.has(AUTH.STOCK);
  369. /** 工單 QC/上架紅點:仍僅 TESTING */
  370. const canSeeJoFgAlerts = abilitySet.has(AUTH.TESTING);
  371. const [openItems, setOpenItems] = React.useState<string[]>([]);
  372. /** Keep parent sections expanded on deep links (e.g. /po/edit from nav red spot) so alerts stay visible. */
  373. React.useEffect(() => {
  374. const ensureOpen: string[] = [];
  375. if (pathname.startsWith("/chart")) {
  376. ensureOpen.push("圖表報告");
  377. }
  378. if (pathname === "/po" || pathname.startsWith("/po/")) {
  379. ensureOpen.push("Store Management");
  380. }
  381. if (pathname === "/productionProcess" || pathname.startsWith("/productionProcess/")) {
  382. ensureOpen.push("Management Job Order");
  383. }
  384. if (ensureOpen.length === 0) return;
  385. setOpenItems((prev) => {
  386. const set = new Set(prev);
  387. let changed = false;
  388. for (const label of ensureOpen) {
  389. if (!set.has(label)) {
  390. set.add(label);
  391. changed = true;
  392. }
  393. }
  394. return changed ? Array.from(set) : prev;
  395. });
  396. }, [pathname]);
  397. const toggleItem = (label: string) => {
  398. setOpenItems((prevOpenItems) =>
  399. prevOpenItems.includes(label)
  400. ? prevOpenItems.filter((item) => item !== label)
  401. : [...prevOpenItems, label],
  402. );
  403. };
  404. const selectedLeafPath = React.useMemo(() => {
  405. const leafPaths: string[] = [];
  406. const walk = (items: NavigationItem[]) => {
  407. for (const it of items) {
  408. if (it.isHidden) continue;
  409. if (!hasAbility(it.requiredAbility)) continue;
  410. if (it.path) leafPaths.push(it.path);
  411. if (it.children?.length) walk(it.children);
  412. }
  413. };
  414. walk(navigationItems);
  415. // Pick the most specific (longest) match to avoid double-highlighting
  416. const matches = leafPaths.filter((p) => pathname === p || pathname.startsWith(p + "/"));
  417. matches.sort((a, b) => b.length - a.length);
  418. return matches[0] ?? "";
  419. }, [hasAbility, navigationItems, pathname]);
  420. const renderNavigationItem = (item: NavigationItem) => {
  421. if (!hasAbility(item.requiredAbility)) {
  422. return null;
  423. }
  424. const isOpen = openItems.includes(item.label);
  425. const hasVisibleChildren = item.children?.some(child => hasAbility(child.requiredAbility));
  426. const isLeaf = Boolean(item.path);
  427. const isSelected = isLeaf && item.path
  428. ? item.path === selectedLeafPath
  429. : hasVisibleChildren && item.children?.some((c) => c.path && c.path === selectedLeafPath);
  430. const content = (
  431. <ListItemButton
  432. selected={isSelected}
  433. onClick={isLeaf ? undefined : () => toggleItem(item.label)}
  434. sx={{
  435. mx: 1,
  436. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  437. }}
  438. >
  439. <ListItemIcon sx={{ minWidth: 40 }}>{item.icon}</ListItemIcon>
  440. <ListItemText
  441. primary={t(item.label)}
  442. primaryTypographyProps={{ fontWeight: isSelected ? 600 : 500 }}
  443. />
  444. </ListItemButton>
  445. );
  446. return (
  447. <Box key={`${item.label}-${item.path}`}>
  448. {isLeaf ? (
  449. <Link href={item.path!} style={{ textDecoration: "none", color: "inherit" }}>
  450. {content}
  451. </Link>
  452. ) : (
  453. content
  454. )}
  455. {item.children && isOpen && hasVisibleChildren && (
  456. <List sx={{ pl: 2, py: 0 }}>
  457. {item.children.map(
  458. (child) => !child.isHidden && hasAbility(child.requiredAbility) && (
  459. child.path === "/po" ? (
  460. <Box
  461. key={`${child.label}-${child.path}`}
  462. sx={{
  463. display: "flex",
  464. alignItems: "stretch",
  465. mx: 1,
  466. borderRadius: 1,
  467. overflow: "hidden",
  468. "&:hover": { bgcolor: "action.hover" },
  469. }}
  470. >
  471. <Box
  472. component={Link}
  473. href={child.path}
  474. sx={{
  475. flex: 1,
  476. minWidth: 0,
  477. textDecoration: "none",
  478. color: "inherit",
  479. display: "flex",
  480. }}
  481. >
  482. <ListItemButton
  483. selected={child.path === selectedLeafPath}
  484. sx={{
  485. flex: 1,
  486. py: 1,
  487. pr: 0.5,
  488. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  489. }}
  490. >
  491. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  492. <ListItemText
  493. primary={t(child.label)}
  494. primaryTypographyProps={{
  495. fontWeight:
  496. child.path === selectedLeafPath ? 600 : 500,
  497. fontSize: "0.875rem",
  498. }}
  499. />
  500. </ListItemButton>
  501. </Box>
  502. <PurchaseStockInNavAlerts enabled={canSeePoAlerts} />
  503. </Box>
  504. ) : child.path === "/productionProcess" ? (
  505. <Box
  506. key={`${child.label}-${child.path}`}
  507. sx={{
  508. display: "flex",
  509. alignItems: "stretch",
  510. mx: 1,
  511. borderRadius: 1,
  512. overflow: "hidden",
  513. "&:hover": { bgcolor: "action.hover" },
  514. }}
  515. >
  516. <Box
  517. component={Link}
  518. href={child.path}
  519. sx={{
  520. flex: 1,
  521. minWidth: 0,
  522. textDecoration: "none",
  523. color: "inherit",
  524. display: "flex",
  525. }}
  526. >
  527. <ListItemButton
  528. selected={child.path === selectedLeafPath}
  529. sx={{
  530. flex: 1,
  531. py: 1,
  532. pr: 0.5,
  533. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  534. }}
  535. >
  536. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  537. <ListItemText
  538. primary={t(child.label)}
  539. primaryTypographyProps={{
  540. fontWeight:
  541. child.path === selectedLeafPath ? 600 : 500,
  542. fontSize: "0.875rem",
  543. }}
  544. />
  545. </ListItemButton>
  546. </Box>
  547. <JobOrderFgStockInNavAlerts enabled={canSeeJoFgAlerts} />
  548. </Box>
  549. ) : (
  550. <Box
  551. key={`${child.label}-${child.path}`}
  552. component={Link}
  553. href={child.path}
  554. sx={{ textDecoration: "none", color: "inherit" }}
  555. >
  556. <ListItemButton
  557. selected={child.path === selectedLeafPath}
  558. sx={{
  559. mx: 1,
  560. py: 1,
  561. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  562. }}
  563. >
  564. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  565. <ListItemText
  566. primary={t(child.label)}
  567. primaryTypographyProps={{
  568. fontWeight:
  569. child.path === selectedLeafPath
  570. ? 600
  571. : 500,
  572. fontSize: "0.875rem",
  573. }}
  574. />
  575. </ListItemButton>
  576. </Box>
  577. )
  578. ),
  579. )}
  580. </List>
  581. )}
  582. </Box>
  583. );
  584. };
  585. if (status === "loading") {
  586. return <Box sx={{ width: NAVIGATION_CONTENT_WIDTH, p: 3 }}>Loading...</Box>;
  587. }
  588. return (
  589. <Box sx={{ width: NAVIGATION_CONTENT_WIDTH, height: "100%", display: "flex", flexDirection: "column" }}>
  590. <Box
  591. 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"
  592. sx={{
  593. mx: 1,
  594. mt: 1,
  595. mb: 1,
  596. px: 1.5,
  597. py: 2,
  598. flexShrink: 0,
  599. display: "flex",
  600. alignItems: "center",
  601. justifyContent: "flex-start",
  602. minHeight: 56,
  603. }}
  604. >
  605. <Stack direction="column" alignItems="flex-start" spacing={0.5}>
  606. <Logo height={42} />
  607. {process.env.NODE_ENV === "production" && (
  608. <Typography
  609. variant="body2"
  610. sx={{
  611. fontWeight: 700,
  612. color: "warning.dark",
  613. bgcolor: "warning.light",
  614. px: 1,
  615. py: 0.5,
  616. borderRadius: 1,
  617. border: "1px solid",
  618. borderColor: "warning.main",
  619. }}
  620. >
  621. 正式服務器 Production Server
  622. </Typography>
  623. )}
  624. </Stack>
  625. </Box>
  626. <Box sx={{ borderTop: 1, borderColor: "divider" }} />
  627. <List component="nav" sx={{ flex: 1, overflow: "auto", py: 1, px: 0 }}>
  628. {navigationItems
  629. .filter(item => !item.isHidden)
  630. .map(renderNavigationItem)
  631. .filter(Boolean)}
  632. </List>
  633. </Box>
  634. );
  635. };
  636. export default NavigationContent;