| @@ -9,6 +9,7 @@ export type ScheduleType = "all" | "rough" | "detailed" | "manual"; | |||
| export interface RoughProdScheduleResult { | |||
| id: number; | |||
| scheduleAt: number[]; | |||
| produceAt: number[]; | |||
| schedulePeriod: number[]; | |||
| schedulePeriodTo: number[]; | |||
| totalEstProdCount: number; | |||
| @@ -80,6 +81,7 @@ export interface RoughProdScheduleLineResultByBomByDate { | |||
| // Detailed | |||
| export interface DetailedProdScheduleResult { | |||
| id: number; | |||
| produceAt: number[]; | |||
| scheduleAt: number[]; | |||
| totalEstProdCount: 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 Box from "@mui/material/Box"; | |||
| import React, { useEffect } from "react"; | |||
| @@ -24,6 +25,15 @@ import { usePathname } from "next/navigation"; | |||
| import Link from "next/link"; | |||
| import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | |||
| 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 { | |||
| icon: React.ReactNode; | |||
| @@ -31,9 +41,22 @@ interface NavigationItem { | |||
| path: string; | |||
| children?: NavigationItem[]; | |||
| isHidden?: true | undefined; | |||
| requiredAbility?: string | string[]; | |||
| } | |||
| 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[] = [ | |||
| { | |||
| 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 />, | |||
| label: "Delivery", | |||
| path: "", | |||
| //requiredAbility: VIEW_DO, | |||
| requiredAbility: VIEW_USER, | |||
| children: [ | |||
| { | |||
| icon: <RequestQuote />, | |||
| @@ -248,16 +234,19 @@ const NavigationContent: React.FC = () => { | |||
| icon: <RequestQuote />, | |||
| label: "Settings", | |||
| path: "", | |||
| requiredAbility: [VIEW_USER, VIEW_GROUP], | |||
| children: [ | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "User", | |||
| path: "/settings/user", | |||
| requiredAbility: VIEW_USER, | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "User Group", | |||
| path: "/settings/user", | |||
| requiredAbility: VIEW_GROUP, | |||
| }, | |||
| // { | |||
| // icon: <RequestQuote />, | |||
| @@ -360,7 +349,12 @@ const NavigationContent: React.FC = () => { | |||
| }; | |||
| const renderNavigationItem = (item: NavigationItem) => { | |||
| if (!hasAbility(item.requiredAbility)) { | |||
| return null; | |||
| } | |||
| const isOpen = openItems.includes(item.label); | |||
| const hasVisibleChildren = item.children?.some(child => hasAbility(child.requiredAbility)); | |||
| return ( | |||
| <Box | |||
| @@ -376,7 +370,7 @@ const NavigationContent: React.FC = () => { | |||
| <ListItemIcon>{item.icon}</ListItemIcon> | |||
| <ListItemText primary={t(item.label)} /> | |||
| </ListItemButton> | |||
| {item.children && isOpen && ( | |||
| {item.children && isOpen && hasVisibleChildren && ( | |||
| <List sx={{ pl: 2 }}> | |||
| {item.children.map( | |||
| (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 ( | |||
| <Box sx={{ width: NAVIGATION_CONTENT_WIDTH }}> | |||
| <Box sx={{ p: 3, display: "flex" }}> | |||
| @@ -397,7 +395,9 @@ const NavigationContent: React.FC = () => { | |||
| </Box> | |||
| <Divider /> | |||
| <List component="nav"> | |||
| {navigationItems.map((item) => renderNavigationItem(item))} | |||
| {navigationItems | |||
| .map(renderNavigationItem) | |||
| .filter(Boolean)} | |||
| {/* {navigationItems.map(({ icon, label, path }, index) => { | |||
| return ( | |||
| <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 { 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 = { | |||
| @@ -19,18 +45,26 @@ export const authOptions: AuthOptions = { | |||
| username: { label: "Username", type: "text" }, | |||
| 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, { | |||
| method: "POST", | |||
| body: JSON.stringify(credentials), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| if (!res.ok) return null; | |||
| 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; | |||
| }, | |||
| }), | |||
| @@ -39,25 +73,36 @@ export const authOptions: AuthOptions = { | |||
| signIn: "/login", | |||
| }, | |||
| 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; | |||