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

NavigationContent.tsx 14 KiB

6ヶ月前
8ヶ月前
10ヶ月前
10ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3週間前
2ヶ月前
2ヶ月前
9ヶ月前
3週間前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import { useSession } from "next-auth/react";
  2. import Box from "@mui/material/Box";
  3. import React from "react";
  4. import List from "@mui/material/List";
  5. import ListItemButton from "@mui/material/ListItemButton";
  6. import ListItemText from "@mui/material/ListItemText";
  7. import ListItemIcon from "@mui/material/ListItemIcon";
  8. import Dashboard from "@mui/icons-material/Dashboard";
  9. import Storefront from "@mui/icons-material/Storefront";
  10. import LocalShipping from "@mui/icons-material/LocalShipping";
  11. import Assignment from "@mui/icons-material/Assignment";
  12. import Inventory from "@mui/icons-material/Inventory";
  13. import AssignmentTurnedIn from "@mui/icons-material/AssignmentTurnedIn";
  14. import ReportProblem from "@mui/icons-material/ReportProblem";
  15. import QrCodeIcon from "@mui/icons-material/QrCode";
  16. import ViewModule from "@mui/icons-material/ViewModule";
  17. import Description from "@mui/icons-material/Description";
  18. import CalendarMonth from "@mui/icons-material/CalendarMonth";
  19. import Factory from "@mui/icons-material/Factory";
  20. import PostAdd from "@mui/icons-material/PostAdd";
  21. import Kitchen from "@mui/icons-material/Kitchen";
  22. import Inventory2 from "@mui/icons-material/Inventory2";
  23. import Print from "@mui/icons-material/Print";
  24. import Assessment from "@mui/icons-material/Assessment";
  25. import ShowChart from "@mui/icons-material/ShowChart";
  26. import Settings from "@mui/icons-material/Settings";
  27. import Person from "@mui/icons-material/Person";
  28. import Group from "@mui/icons-material/Group";
  29. import Category from "@mui/icons-material/Category";
  30. import TrendingUp from "@mui/icons-material/TrendingUp";
  31. import Build from "@mui/icons-material/Build";
  32. import Warehouse from "@mui/icons-material/Warehouse";
  33. import VerifiedUser from "@mui/icons-material/VerifiedUser";
  34. import Label from "@mui/icons-material/Label";
  35. import Checklist from "@mui/icons-material/Checklist";
  36. import Science from "@mui/icons-material/Science";
  37. import UploadFile from "@mui/icons-material/UploadFile";
  38. import { useTranslation } from "react-i18next";
  39. import { usePathname } from "next/navigation";
  40. import Link from "next/link";
  41. import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
  42. import Logo from "../Logo";
  43. import { AUTH } from "../../authorities";
  44. interface NavigationItem {
  45. icon: React.ReactNode;
  46. label: string;
  47. path: string;
  48. children?: NavigationItem[];
  49. isHidden?: boolean | undefined;
  50. requiredAbility?: string | string[];
  51. }
  52. const NavigationContent: React.FC = () => {
  53. const { data: session, status } = useSession();
  54. const abilities = session?.user?.abilities ?? [];
  55. // Helper: check if user has required permission
  56. const hasAbility = (required?: string | string[]): boolean => {
  57. if (!required) return true; // no requirement → always show
  58. if (Array.isArray(required)) {
  59. return required.some(ability => abilities.includes(ability));
  60. }
  61. return abilities.includes(required);
  62. };
  63. const navigationItems: NavigationItem[] = [
  64. {
  65. icon: <Dashboard />,
  66. label: "Dashboard",
  67. path: "/dashboard",
  68. },
  69. {
  70. icon: <Storefront />,
  71. label: "Store Management",
  72. path: "",
  73. requiredAbility: [AUTH.PURCHASE, AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_FG, AUTH.STOCK_IN_BIND, AUTH.ADMIN],
  74. children: [
  75. {
  76. icon: <LocalShipping />,
  77. label: "Purchase Order",
  78. requiredAbility: [AUTH.PURCHASE, AUTH.ADMIN],
  79. path: "/po",
  80. },
  81. {
  82. icon: <Assignment />,
  83. label: "Pick Order",
  84. requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
  85. path: "/pickOrder",
  86. },
  87. {
  88. icon: <Inventory />,
  89. label: "View item In-out And inventory Ledger",
  90. requiredAbility: [AUTH.STOCK, AUTH.ADMIN],
  91. path: "/inventory",
  92. },
  93. {
  94. icon: <AssignmentTurnedIn />,
  95. label: "Stock Take Management",
  96. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
  97. path: "/stocktakemanagement",
  98. },
  99. {
  100. icon: <ReportProblem />,
  101. label: "Stock Issue",
  102. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN],
  103. path: "/stockIssue",
  104. },
  105. {
  106. icon: <QrCodeIcon />,
  107. label: "Put Away Scan",
  108. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.ADMIN],
  109. path: "/putAway",
  110. },
  111. {
  112. icon: <ViewModule />,
  113. label: "Finished Good Order",
  114. requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN],
  115. path: "/finishedGood",
  116. },
  117. {
  118. icon: <Description />,
  119. label: "Stock Record",
  120. requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN],
  121. path: "/stockRecord",
  122. },
  123. ],
  124. },
  125. {
  126. icon: <LocalShipping />,
  127. label: "Delivery Order",
  128. path: "/do",
  129. requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN],
  130. },
  131. {
  132. icon: <CalendarMonth />,
  133. label: "Scheduling",
  134. path: "/ps",
  135. requiredAbility: [AUTH.FORECAST, AUTH.ADMIN],
  136. isHidden: false,
  137. },
  138. {
  139. icon: <Factory />,
  140. label: "Management Job Order",
  141. path: "",
  142. requiredAbility: [AUTH.JOB_CREATE, AUTH.JOB_PICK, AUTH.JOB_PROD, AUTH.ADMIN],
  143. children: [
  144. {
  145. icon: <PostAdd />,
  146. label: "Search Job Order/ Create Job Order",
  147. requiredAbility: [AUTH.JOB_CREATE, AUTH.ADMIN],
  148. path: "/jo",
  149. },
  150. {
  151. icon: <Inventory />,
  152. label: "Job Order Pickexcution",
  153. requiredAbility: [AUTH.JOB_PICK, AUTH.JOB_MAT, AUTH.ADMIN],
  154. path: "/jodetail",
  155. },
  156. {
  157. icon: <Kitchen />,
  158. label: "Job Order Production Process",
  159. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  160. path: "/productionProcess",
  161. },
  162. {
  163. icon: <Inventory2 />,
  164. label: "Bag Usage",
  165. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  166. path: "/bag",
  167. },
  168. ],
  169. },
  170. {
  171. icon: <Print />,
  172. label: "打袋機",
  173. path: "/bagPrint",
  174. requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN],
  175. isHidden: false,
  176. },
  177. {
  178. icon: <Assessment />,
  179. label: "報告管理",
  180. path: "/report",
  181. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  182. isHidden: false,
  183. },
  184. {
  185. icon: <ShowChart />,
  186. label: "圖表報告",
  187. path: "",
  188. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  189. isHidden: false,
  190. children: [
  191. {
  192. icon: <Warehouse />,
  193. label: "庫存與倉儲",
  194. path: "/chart/warehouse",
  195. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  196. },
  197. {
  198. icon: <Storefront />,
  199. label: "採購",
  200. path: "/chart/purchase",
  201. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  202. },
  203. {
  204. icon: <LocalShipping />,
  205. label: "發貨與配送",
  206. path: "/chart/delivery",
  207. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  208. },
  209. {
  210. icon: <Assignment />,
  211. label: "工單",
  212. path: "/chart/joborder",
  213. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  214. },
  215. {
  216. icon: <TrendingUp />,
  217. label: "預測與計劃",
  218. path: "/chart/forecast",
  219. requiredAbility: [AUTH.TESTING, AUTH.ADMIN],
  220. },
  221. ],
  222. },
  223. {
  224. icon: <Settings />,
  225. label: "Settings",
  226. path: "",
  227. requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
  228. children: [
  229. {
  230. icon: <Person />,
  231. label: "User",
  232. path: "/settings/user",
  233. requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN],
  234. },
  235. //{
  236. // icon: <Group />,
  237. // label: "User Group",
  238. // path: "/settings/user",
  239. // requiredAbility: [AUTH.VIEW_GROUP, AUTH.ADMIN],
  240. //},
  241. {
  242. icon: <Category />,
  243. label: "Items",
  244. path: "/settings/items",
  245. },
  246. {
  247. icon: <Build />,
  248. label: "Equipment",
  249. path: "/settings/equipment",
  250. },
  251. {
  252. icon: <Assessment />,
  253. label: "Price Inquiry",
  254. path: "/settings/itemPrice",
  255. },
  256. {
  257. icon: <Warehouse />,
  258. label: "Warehouse",
  259. path: "/settings/warehouse",
  260. },
  261. {
  262. icon: <Print />,
  263. label: "Printer",
  264. path: "/settings/printer",
  265. },
  266. {
  267. icon: <VerifiedUser />,
  268. label: "QC Check Item",
  269. path: "/settings/qcItem",
  270. },
  271. {
  272. icon: <Label />,
  273. label: "QC Category",
  274. path: "/settings/qcCategory",
  275. },
  276. {
  277. icon: <Checklist />,
  278. label: "QC Item All",
  279. path: "/settings/qcItemAll",
  280. },
  281. {
  282. icon: <Storefront />,
  283. label: "ShopAndTruck",
  284. path: "/settings/shop",
  285. },
  286. {
  287. icon: <TrendingUp />,
  288. label: "Demand Forecast Setting",
  289. path: "/settings/rss",
  290. },
  291. //{
  292. // icon: <Person />,
  293. // label: "Customer",
  294. // path: "/settings/user",
  295. //},
  296. {
  297. icon: <ViewModule />,
  298. label: "BOM Weighting Score List",
  299. path: "/settings/bomWeighting",
  300. },
  301. {
  302. icon: <QrCodeIcon />,
  303. label: "QR Code Handle",
  304. path: "/settings/qrCodeHandle",
  305. },
  306. {
  307. icon: <Science />,
  308. label: "Import Testing",
  309. path: "/settings/m18ImportTesting",
  310. },
  311. {
  312. icon: <UploadFile />,
  313. label: "Import Excel",
  314. path: "/settings/importExcel",
  315. },
  316. {
  317. icon: <UploadFile />,
  318. label: "Import BOM",
  319. path: "/settings/importBom",
  320. },
  321. ],
  322. },
  323. ];
  324. const { t } = useTranslation("common");
  325. const pathname = usePathname();
  326. const [openItems, setOpenItems] = React.useState<string[]>([]);
  327. // Keep "圖表報告" expanded when on any chart sub-route
  328. React.useEffect(() => {
  329. if (pathname.startsWith("/chart/") && !openItems.includes("圖表報告")) {
  330. setOpenItems((prev) => [...prev, "圖表報告"]);
  331. }
  332. }, [pathname, openItems]);
  333. const toggleItem = (label: string) => {
  334. setOpenItems((prevOpenItems) =>
  335. prevOpenItems.includes(label)
  336. ? prevOpenItems.filter((item) => item !== label)
  337. : [...prevOpenItems, label],
  338. );
  339. };
  340. const renderNavigationItem = (item: NavigationItem) => {
  341. if (!hasAbility(item.requiredAbility)) {
  342. return null;
  343. }
  344. const isOpen = openItems.includes(item.label);
  345. const hasVisibleChildren = item.children?.some(child => hasAbility(child.requiredAbility));
  346. const isLeaf = Boolean(item.path);
  347. const isSelected = isLeaf && item.path
  348. ? pathname === item.path || pathname.startsWith(item.path + "/")
  349. : hasVisibleChildren && item.children?.some(
  350. (c) => c.path && (pathname === c.path || pathname.startsWith(c.path + "/"))
  351. );
  352. const content = (
  353. <ListItemButton
  354. selected={isSelected}
  355. onClick={isLeaf ? undefined : () => toggleItem(item.label)}
  356. sx={{
  357. mx: 1,
  358. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  359. }}
  360. >
  361. <ListItemIcon sx={{ minWidth: 40 }}>{item.icon}</ListItemIcon>
  362. <ListItemText
  363. primary={t(item.label)}
  364. primaryTypographyProps={{ fontWeight: isSelected ? 600 : 500 }}
  365. />
  366. </ListItemButton>
  367. );
  368. return (
  369. <Box key={`${item.label}-${item.path}`}>
  370. {isLeaf ? (
  371. <Link href={item.path!} style={{ textDecoration: "none", color: "inherit" }}>
  372. {content}
  373. </Link>
  374. ) : (
  375. content
  376. )}
  377. {item.children && isOpen && hasVisibleChildren && (
  378. <List sx={{ pl: 2, py: 0 }}>
  379. {item.children.map(
  380. (child) => !child.isHidden && hasAbility(child.requiredAbility) && (
  381. <Box
  382. key={`${child.label}-${child.path}`}
  383. component={Link}
  384. href={child.path}
  385. sx={{ textDecoration: "none", color: "inherit" }}
  386. >
  387. <ListItemButton
  388. selected={pathname === child.path || (!!child.path && pathname.startsWith(child.path + "/"))}
  389. sx={{
  390. mx: 1,
  391. py: 1,
  392. "&.Mui-selected .MuiListItemIcon-root": { color: "primary.main" },
  393. }}
  394. >
  395. <ListItemIcon sx={{ minWidth: 40 }}>{child.icon}</ListItemIcon>
  396. <ListItemText
  397. primary={t(child.label)}
  398. primaryTypographyProps={{
  399. fontWeight: pathname === child.path || (child.path && pathname.startsWith(child.path + "/")) ? 600 : 500,
  400. fontSize: "0.875rem",
  401. }}
  402. />
  403. </ListItemButton>
  404. </Box>
  405. ),
  406. )}
  407. </List>
  408. )}
  409. </Box>
  410. );
  411. };
  412. if (status === "loading") {
  413. return <Box sx={{ width: NAVIGATION_CONTENT_WIDTH, p: 3 }}>Loading...</Box>;
  414. }
  415. return (
  416. <Box sx={{ width: NAVIGATION_CONTENT_WIDTH, height: "100%", display: "flex", flexDirection: "column" }}>
  417. <Box
  418. 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"
  419. sx={{
  420. mx: 1,
  421. mt: 1,
  422. mb: 1,
  423. px: 1.5,
  424. py: 2,
  425. flexShrink: 0,
  426. display: "flex",
  427. alignItems: "center",
  428. justifyContent: "flex-start",
  429. minHeight: 56,
  430. }}
  431. >
  432. <Logo height={42} />
  433. </Box>
  434. <Box sx={{ borderTop: 1, borderColor: "divider" }} />
  435. <List component="nav" sx={{ flex: 1, overflow: "auto", py: 1, px: 0 }}>
  436. {navigationItems
  437. .filter(item => !item.isHidden)
  438. .map(renderNavigationItem)
  439. .filter(Boolean)}
  440. </List>
  441. </Box>
  442. );
  443. };
  444. export default NavigationContent;