| @@ -9,6 +9,7 @@ export type ScheduleType = "all" | "rough" | "detailed" | "manual"; | |||||
| export interface RoughProdScheduleResult { | export interface RoughProdScheduleResult { | ||||
| id: number; | id: number; | ||||
| scheduleAt: number[]; | scheduleAt: number[]; | ||||
| produceAt: number[]; | |||||
| schedulePeriod: number[]; | schedulePeriod: number[]; | ||||
| schedulePeriodTo: number[]; | schedulePeriodTo: number[]; | ||||
| totalEstProdCount: number; | totalEstProdCount: number; | ||||
| @@ -80,6 +81,7 @@ export interface RoughProdScheduleLineResultByBomByDate { | |||||
| // Detailed | // Detailed | ||||
| export interface DetailedProdScheduleResult { | export interface DetailedProdScheduleResult { | ||||
| id: number; | id: number; | ||||
| produceAt: number[]; | |||||
| scheduleAt: number[]; | scheduleAt: number[]; | ||||
| totalEstProdCount: number; | totalEstProdCount: number; | ||||
| totalFGType: number; | totalFGType: number; | ||||
| @@ -0,0 +1,7 @@ | |||||
| export const [VIEW_USER, VIEW_DO, MAINTAIN_USER, VIEW_GROUP, MAINTAIN_GROUP] = [ | |||||
| "VIEW_USER", | |||||
| "VIEW_DO", | |||||
| "MAINTAIN_USER", | |||||
| "VIEW_GROUP", | |||||
| "MAINTAIN_GROUP", | |||||
| ]; | |||||
| @@ -1,6 +0,0 @@ | |||||
| export const [VIEW_USER, MAINTAIN_USER, VIEW_GROUP, MAINTAIN_GROUP] = [ | |||||
| "VIEW_USER", | |||||
| "MAINTAIN_USER", | |||||
| "VIEW_GROUP", | |||||
| "MAINTAIN_GROUP", | |||||
| ]; | |||||
| @@ -1,3 +1,4 @@ | |||||
| import { useSession } from "next-auth/react"; | |||||
| import Divider from "@mui/material/Divider"; | import Divider from "@mui/material/Divider"; | ||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||
| import React, { useEffect } from "react"; | import React, { useEffect } from "react"; | ||||
| @@ -24,6 +25,15 @@ import { usePathname } from "next/navigation"; | |||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | ||||
| import Logo from "../Logo"; | import Logo from "../Logo"; | ||||
| import { | |||||
| VIEW_USER, | |||||
| VIEW_DO, | |||||
| MAINTAIN_USER, | |||||
| VIEW_GROUP, | |||||
| MAINTAIN_GROUP, | |||||
| // Add more authorities as needed, e.g.: | |||||
| // VIEW_PO, MAINTAIN_PO, VIEW_INVENTORY, etc. | |||||
| } from "../../authorities"; | |||||
| interface NavigationItem { | interface NavigationItem { | ||||
| icon: React.ReactNode; | icon: React.ReactNode; | ||||
| @@ -31,9 +41,22 @@ interface NavigationItem { | |||||
| path: string; | path: string; | ||||
| children?: NavigationItem[]; | children?: NavigationItem[]; | ||||
| isHidden?: true | undefined; | isHidden?: true | undefined; | ||||
| requiredAbility?: string | string[]; | |||||
| } | } | ||||
| const NavigationContent: React.FC = () => { | const NavigationContent: React.FC = () => { | ||||
| const { data: session, status } = useSession(); | |||||
| const abilities = session?.user?.abilities ?? []; | |||||
| // Helper: check if user has required permission | |||||
| const hasAbility = (required?: string | string[]): boolean => { | |||||
| if (!required) return true; // no requirement → always show | |||||
| if (Array.isArray(required)) { | |||||
| return required.some(ability => abilities.includes(ability)); | |||||
| } | |||||
| return abilities.includes(required); | |||||
| }; | |||||
| const navigationItems: NavigationItem[] = [ | const navigationItems: NavigationItem[] = [ | ||||
| { | { | ||||
| icon: <Dashboard />, | icon: <Dashboard />, | ||||
| @@ -108,49 +131,12 @@ const NavigationContent: React.FC = () => { | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| // { | |||||
| // icon: <RequestQuote />, | |||||
| // label: "Production", | |||||
| // path: "", | |||||
| // children: [ | |||||
| // { | |||||
| // icon: <RequestQuote />, | |||||
| // label: "Job Order", | |||||
| // path: "", | |||||
| // }, | |||||
| // { | |||||
| // icon: <RequestQuote />, | |||||
| // label: "Job Order Traceablity ", | |||||
| // path: "", | |||||
| // }, | |||||
| // { | |||||
| // icon: <RequestQuote />, | |||||
| // label: "Work Order", | |||||
| // path: "", | |||||
| // }, | |||||
| // { | |||||
| // icon: <RequestQuote />, | |||||
| // label: "Work Order Traceablity ", | |||||
| // path: "", | |||||
| // }, | |||||
| // ], | |||||
| // }, | |||||
| // { | |||||
| // icon: <RequestQuote />, | |||||
| // label: "Quality Control Log", | |||||
| // path: "", | |||||
| // children: [ | |||||
| // { | |||||
| // icon: <RequestQuote />, | |||||
| // label: "Quality Control Log", | |||||
| // path: "", | |||||
| // }, | |||||
| // ], | |||||
| // }, | |||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "Delivery", | label: "Delivery", | ||||
| path: "", | path: "", | ||||
| //requiredAbility: VIEW_DO, | |||||
| requiredAbility: VIEW_USER, | |||||
| children: [ | children: [ | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| @@ -248,16 +234,19 @@ const NavigationContent: React.FC = () => { | |||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "Settings", | label: "Settings", | ||||
| path: "", | path: "", | ||||
| requiredAbility: [VIEW_USER, VIEW_GROUP], | |||||
| children: [ | children: [ | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "User", | label: "User", | ||||
| path: "/settings/user", | path: "/settings/user", | ||||
| requiredAbility: VIEW_USER, | |||||
| }, | }, | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "User Group", | label: "User Group", | ||||
| path: "/settings/user", | path: "/settings/user", | ||||
| requiredAbility: VIEW_GROUP, | |||||
| }, | }, | ||||
| // { | // { | ||||
| // icon: <RequestQuote />, | // icon: <RequestQuote />, | ||||
| @@ -360,7 +349,12 @@ const NavigationContent: React.FC = () => { | |||||
| }; | }; | ||||
| const renderNavigationItem = (item: NavigationItem) => { | const renderNavigationItem = (item: NavigationItem) => { | ||||
| if (!hasAbility(item.requiredAbility)) { | |||||
| return null; | |||||
| } | |||||
| const isOpen = openItems.includes(item.label); | const isOpen = openItems.includes(item.label); | ||||
| const hasVisibleChildren = item.children?.some(child => hasAbility(child.requiredAbility)); | |||||
| return ( | return ( | ||||
| <Box | <Box | ||||
| @@ -376,7 +370,7 @@ const NavigationContent: React.FC = () => { | |||||
| <ListItemIcon>{item.icon}</ListItemIcon> | <ListItemIcon>{item.icon}</ListItemIcon> | ||||
| <ListItemText primary={t(item.label)} /> | <ListItemText primary={t(item.label)} /> | ||||
| </ListItemButton> | </ListItemButton> | ||||
| {item.children && isOpen && ( | |||||
| {item.children && isOpen && hasVisibleChildren && ( | |||||
| <List sx={{ pl: 2 }}> | <List sx={{ pl: 2 }}> | ||||
| {item.children.map( | {item.children.map( | ||||
| (child) => !child.isHidden && renderNavigationItem(child), | (child) => !child.isHidden && renderNavigationItem(child), | ||||
| @@ -387,6 +381,10 @@ const NavigationContent: React.FC = () => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| if (status === "loading") { | |||||
| return <Box sx={{ width: NAVIGATION_CONTENT_WIDTH, p: 3 }}>Loading...</Box>; | |||||
| } | |||||
| return ( | return ( | ||||
| <Box sx={{ width: NAVIGATION_CONTENT_WIDTH }}> | <Box sx={{ width: NAVIGATION_CONTENT_WIDTH }}> | ||||
| <Box sx={{ p: 3, display: "flex" }}> | <Box sx={{ p: 3, display: "flex" }}> | ||||
| @@ -397,7 +395,9 @@ const NavigationContent: React.FC = () => { | |||||
| </Box> | </Box> | ||||
| <Divider /> | <Divider /> | ||||
| <List component="nav"> | <List component="nav"> | ||||
| {navigationItems.map((item) => renderNavigationItem(item))} | |||||
| {navigationItems | |||||
| .map(renderNavigationItem) | |||||
| .filter(Boolean)} | |||||
| {/* {navigationItems.map(({ icon, label, path }, index) => { | {/* {navigationItems.map(({ icon, label, path }, index) => { | ||||
| return ( | return ( | ||||
| <Box | <Box | ||||
| @@ -1,12 +1,38 @@ | |||||
| import { AuthOptions, Session } from "next-auth"; | |||||
| // config/authConfig.ts (or wherever your authOptions live) | |||||
| import { AuthOptions } from "next-auth"; | |||||
| import CredentialsProvider from "next-auth/providers/credentials"; | import CredentialsProvider from "next-auth/providers/credentials"; | ||||
| import { LOGIN_API_PATH } from "./api"; | import { LOGIN_API_PATH } from "./api"; | ||||
| export interface SessionWithTokens extends Session { | |||||
| accessToken: string | null; | |||||
| refreshToken?: string; | |||||
| abilities: string[]; | |||||
| id?: string | null; | |||||
| // Extend the built-in types | |||||
| declare module "next-auth" { | |||||
| interface Session { | |||||
| accessToken: string | null; | |||||
| refreshToken?: string; | |||||
| abilities: string[]; | |||||
| id?: string; | |||||
| user: { | |||||
| name?: string | null; | |||||
| email?: string | null; | |||||
| image?: string | null; | |||||
| abilities: string[]; // add abilities to user object too if you need it client-side | |||||
| }; | |||||
| } | |||||
| interface User { | |||||
| id?: string; | |||||
| accessToken: string | null; | |||||
| refreshToken?: string; | |||||
| abilities: string[]; | |||||
| } | |||||
| } | |||||
| declare module "next-auth/jwt" { | |||||
| interface JWT { | |||||
| id?: string; | |||||
| accessToken: string | null; | |||||
| refreshToken?: string; | |||||
| abilities: string[]; | |||||
| } | |||||
| } | } | ||||
| export const authOptions: AuthOptions = { | export const authOptions: AuthOptions = { | ||||
| @@ -19,18 +45,26 @@ export const authOptions: AuthOptions = { | |||||
| username: { label: "Username", type: "text" }, | username: { label: "Username", type: "text" }, | ||||
| password: { label: "Password", type: "password" }, | password: { label: "Password", type: "password" }, | ||||
| }, | }, | ||||
| async authorize(credentials, req) { | |||||
| async authorize(credentials) { | |||||
| if (!credentials?.username || !credentials?.password) return null; | |||||
| const res = await fetch(LOGIN_API_PATH, { | const res = await fetch(LOGIN_API_PATH, { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(credentials), | body: JSON.stringify(credentials), | ||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| }); | }); | ||||
| if (!res.ok) return null; | |||||
| const user = await res.json(); | const user = await res.json(); | ||||
| if (res.ok && user) { | |||||
| return user; | |||||
| // Important: next-auth expects the user object returned here | |||||
| // to be serializable and contain the fields you want in token/session | |||||
| // Ensure your backend returns: { id, accessToken, abilities, ...other fields } | |||||
| if (user && user.abilities) { | |||||
| return user; // this will be passed to jwt callback as `user` | |||||
| } | } | ||||
| return null; | return null; | ||||
| }, | }, | ||||
| }), | }), | ||||
| @@ -39,25 +73,36 @@ export const authOptions: AuthOptions = { | |||||
| signIn: "/login", | signIn: "/login", | ||||
| }, | }, | ||||
| callbacks: { | callbacks: { | ||||
| jwt(params) { | |||||
| // Add the data from user to the token | |||||
| const { token, user } = params; | |||||
| const newToken = { ...token, ...user }; | |||||
| return newToken; | |||||
| // Persist custom fields into the JWT token | |||||
| async jwt({ token, user }) { | |||||
| // First sign-in: `user` is available | |||||
| if (user) { | |||||
| token.id = user.id ?? token.sub; // fallback to sub if no id | |||||
| token.accessToken = user.accessToken; | |||||
| token.refreshToken = user.refreshToken; | |||||
| token.abilities = user.abilities ?? []; | |||||
| } | |||||
| // On subsequent calls (token refresh, session access), user is not present | |||||
| // so we just return the existing token with custom fields preserved | |||||
| return token; | |||||
| }, | }, | ||||
| session({ session, token }) { | |||||
| const sessionWithToken: SessionWithTokens = { | |||||
| ...session, | |||||
| // Add the data from the token to the session | |||||
| id: token.id as string | undefined, | |||||
| accessToken: token.accessToken as string | null, | |||||
| refreshToken: token.refreshToken as string | undefined, | |||||
| abilities: token.abilities as string[], | |||||
| }; | |||||
| if (sessionWithToken.user) { | |||||
| sessionWithToken.user.abilities = token.abilities as string[]; | |||||
| // Expose custom fields to the client session | |||||
| async session({ session, token }) { | |||||
| session.id = token.id as string | undefined; | |||||
| session.accessToken = token.accessToken as string | null; | |||||
| session.refreshToken = token.refreshToken as string | undefined; | |||||
| session.abilities = token.abilities as string[]; | |||||
| // Also add abilities to session.user for easier client-side access | |||||
| if (session.user) { | |||||
| session.user.abilities = token.abilities as string[]; | |||||
| } | } | ||||
| return sessionWithToken; | |||||
| return session; | |||||
| }, | }, | ||||
| }, | }, | ||||
| }; | }; | ||||
| export default authOptions; | |||||