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

NavigationContent.tsx 13 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ヶ月前
4週間前
4週間前
2ヶ月前
2ヶ月前
4週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
9ヶ月前
3週間前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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;