FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

457 regels
14 KiB

  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;