FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

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