| @@ -5,8 +5,17 @@ export const PageID = { | |||
| LOGIN: id++, | |||
| LOGOUT: id++, | |||
| DASHBOARD_OVERVIEW: id++, | |||
| DASHBOARD_CONTRACT_LIST: id++, | |||
| DASHBOARD_CONTRACT_DETAIL: id++, | |||
| DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE: id++, | |||
| DASHBOARD_RECEIPT_ISSUING_ORDER_LIST: id++, | |||
| DASHBOARD_RECEIPT_ISSUING_ORDER_DETAIL: id++, | |||
| PAGE_403: id++, | |||
| PAGE_404: id++, | |||
| } as const; | |||
| export type PageID = (typeof PageID)[keyof typeof PageID]; | |||
| @@ -1,9 +1,18 @@ | |||
| import { HasChildren } from "@types"; | |||
| import { ResultCode } from "api"; | |||
| import { login as APILogin, logout as APILogout, me } from "api/auth"; | |||
| import { PageID } from "codes/page"; | |||
| import { UserRole } from "codes/user"; | |||
| import useAPICall from "hooks/useAPICall"; | |||
| import { createContext, memo, useEffect, useMemo, useState } from "react"; | |||
| import { | |||
| createContext, | |||
| memo, | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| } from "react"; | |||
| import { AUTH } from "routes/auth"; | |||
| type Auth = { | |||
| initialized: boolean; | |||
| @@ -19,6 +28,7 @@ type Auth = { | |||
| changeContractId: (contractId: string) => Promise<boolean>; | |||
| checkRole: (role?: UserRole) => boolean; | |||
| canAccess: (pageId: PageID) => boolean; | |||
| }; | |||
| export const AuthContext = createContext<Auth>({ | |||
| initialized: false, | |||
| @@ -32,6 +42,7 @@ export const AuthContext = createContext<Auth>({ | |||
| logout: () => {}, | |||
| changeContractId: async (contractId: string) => false, | |||
| checkRole: (role?: UserRole) => false, | |||
| canAccess: (pageId: PageID) => false, | |||
| }); | |||
| type Props = HasChildren; | |||
| @@ -93,10 +104,21 @@ function AuthContextProvider({ children }: Props) { | |||
| return false; | |||
| }; | |||
| const checkRole = (targetRole?: UserRole): boolean => { | |||
| if (targetRole === undefined) return true; | |||
| return targetRole <= role; | |||
| }; | |||
| const checkRole = useCallback( | |||
| (targetRole?: UserRole): boolean => { | |||
| if (targetRole === undefined) return true; | |||
| return targetRole <= role; | |||
| }, | |||
| [role] | |||
| ); | |||
| const canAccess = useCallback( | |||
| (pageId: PageID): boolean => { | |||
| const roles = AUTH[pageId] ?? []; | |||
| return roles.includes(role); | |||
| }, | |||
| [role] | |||
| ); | |||
| useEffect(() => { | |||
| callMe({}); | |||
| @@ -116,6 +138,7 @@ function AuthContextProvider({ children }: Props) { | |||
| logout, | |||
| changeContractId, | |||
| checkRole, | |||
| canAccess, | |||
| }} | |||
| > | |||
| {children} | |||
| @@ -23,8 +23,7 @@ function Copyright() { | |||
| function App() { | |||
| const [mobileOpen, setMobileOpen] = useState(false); | |||
| const { drawerWidth, innerHeight, innerWidth, contentsWidth, showDrawer } = | |||
| useDashboard(); | |||
| const { drawerWidth, innerWidth, contentsWidth, showDrawer } = useDashboard(); | |||
| const handleDrawerToggle = () => { | |||
| setMobileOpen(!mobileOpen); | |||
| @@ -12,7 +12,6 @@ import ListItemButton from "@mui/material/ListItemButton"; | |||
| import ListItemIcon from "@mui/material/ListItemIcon"; | |||
| import ListItemText from "@mui/material/ListItemText"; | |||
| import { PageID } from "codes/page"; | |||
| import { UserRole } from "codes/user"; | |||
| import useAuth from "hooks/useAuth"; | |||
| import useNavigateCustom from "hooks/useNavigateCustom"; | |||
| import usePage from "hooks/usePage"; | |||
| @@ -22,7 +21,6 @@ import { getPath } from "routes/path"; | |||
| type Group = { | |||
| label: string; | |||
| children: SubGroup[]; | |||
| role?: UserRole; | |||
| }; | |||
| type SubGroup = { | |||
| @@ -32,13 +30,11 @@ type SubGroup = { | |||
| // 子要素を持たない場合は設定 | |||
| id?: PageID; | |||
| role?: UserRole; | |||
| }; | |||
| type Child = { | |||
| label: string; | |||
| id: PageID; | |||
| role?: UserRole; | |||
| }; | |||
| const categories: Group[] = [ | |||
| @@ -48,7 +44,6 @@ const categories: Group[] = [ | |||
| { | |||
| label: "契約", | |||
| icon: <PeopleIcon />, | |||
| role: UserRole.SUPER_ADMIN, | |||
| children: [ | |||
| { | |||
| id: PageID.DASHBOARD_CONTRACT_LIST, | |||
| @@ -60,6 +55,20 @@ const categories: Group[] = [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| label: "領収証発行依頼", | |||
| icon: <PeopleIcon />, | |||
| children: [ | |||
| { | |||
| id: PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST, | |||
| label: "一覧", | |||
| }, | |||
| { | |||
| id: PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE, | |||
| label: "新規", | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -114,23 +123,24 @@ export default function Navigator(props: DrawerProps) { | |||
| function Group(group: Group) { | |||
| const { label, children } = group; | |||
| const { checkRole } = useAuth(); | |||
| if (!checkRole(group.role)) return null; | |||
| const elements = children.map((ele, index) => ( | |||
| <SubGroup {...ele} key={index} /> | |||
| )); | |||
| if (elements.length === 0) return null; | |||
| return ( | |||
| <Box sx={{ bgcolor: "#101F33" }}> | |||
| <ListItem sx={{ py: 2, px: 3 }}> | |||
| <ListItemText sx={{ color: "#fff" }}>{label}</ListItemText> | |||
| </ListItem> | |||
| {children.map((ele, index) => ( | |||
| <SubGroup {...ele} key={index} /> | |||
| ))} | |||
| {elements} | |||
| <Divider sx={{ mt: 2 }} /> | |||
| </Box> | |||
| ); | |||
| } | |||
| function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||
| function SubGroup({ icon, label, id, children }: SubGroup) { | |||
| const { pageId } = usePage(); | |||
| const { navigateWhenChanged } = useNavigateCustom(); | |||
| @@ -138,20 +148,15 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||
| const [open, setOpen] = React.useState(false); | |||
| const { checkRole } = useAuth(); | |||
| React.useEffect(() => { | |||
| setOpen(shouldOpen); | |||
| }, [shouldOpen]); | |||
| if (!checkRole(role)) return null; | |||
| // 子要素ありの場合 | |||
| if (elements && elements.length !== 0) { | |||
| const handleClick = () => { | |||
| setOpen(!open); | |||
| }; | |||
| return ( | |||
| <> | |||
| <ListItemButton onClick={handleClick} sx={item} selected={false}> | |||
| @@ -167,57 +172,59 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||
| </> | |||
| ); | |||
| } | |||
| // 子要素なしの場合 | |||
| const handleClick = () => { | |||
| if (id) { | |||
| const path = getPath(id); | |||
| navigateWhenChanged(path); | |||
| } | |||
| }; | |||
| const selected = id === pageId; | |||
| return ( | |||
| <ListItemButton onClick={handleClick} selected={selected} sx={item}> | |||
| <ListItemIcon>{icon}</ListItemIcon> | |||
| <ListItemText>{label}</ListItemText> | |||
| </ListItemButton> | |||
| ); | |||
| if (id !== undefined) { | |||
| const handleClick = () => { | |||
| if (id) { | |||
| const path = getPath(id); | |||
| navigateWhenChanged(path); | |||
| } | |||
| }; | |||
| const selected = id === pageId; | |||
| return ( | |||
| <ListItemButton onClick={handleClick} selected={selected} sx={item}> | |||
| <ListItemIcon>{icon}</ListItemIcon> | |||
| <ListItemText>{label}</ListItemText> | |||
| </ListItemButton> | |||
| ); | |||
| } | |||
| return null; | |||
| } | |||
| function useContents(children: Child[]) { | |||
| const { pageId } = usePage(); | |||
| const { navigateWhenChanged } = useNavigateCustom(); | |||
| const { checkRole } = useAuth(); | |||
| const { canAccess, initialized } = useAuth(); | |||
| const [shouldOpen, setShouldOpen] = React.useState(false); | |||
| const elements = React.useMemo(() => { | |||
| setShouldOpen(false); | |||
| return children.map(({ label, id, role }, index) => { | |||
| if (!checkRole(role)) return; | |||
| const selected = id === pageId; | |||
| if (selected) { | |||
| setShouldOpen(true); | |||
| } | |||
| const handleClick = () => { | |||
| const path = getPath(id); | |||
| navigateWhenChanged(path); | |||
| }; | |||
| return ( | |||
| <ListItemButton | |||
| selected={selected} | |||
| sx={{ ...item, pl: 4 }} | |||
| key={index} | |||
| onClick={handleClick} | |||
| > | |||
| <ListItemText primary={label} /> | |||
| </ListItemButton> | |||
| ); | |||
| }); | |||
| }, [pageId]); | |||
| return children | |||
| .filter(({ id }) => canAccess(id)) | |||
| .map(({ label, id }, index) => { | |||
| const selected = id === pageId; | |||
| if (selected) { | |||
| setShouldOpen(true); | |||
| } | |||
| const handleClick = () => { | |||
| const path = getPath(id); | |||
| navigateWhenChanged(path); | |||
| }; | |||
| return ( | |||
| <ListItemButton | |||
| selected={selected} | |||
| sx={{ ...item, pl: 4 }} | |||
| key={index} | |||
| onClick={handleClick} | |||
| > | |||
| <ListItemText primary={label} /> | |||
| </ListItemButton> | |||
| ); | |||
| }); | |||
| }, [pageId, initialized]); | |||
| return { | |||
| elements, | |||
| @@ -49,7 +49,7 @@ export default function Login() { | |||
| if (ret) { | |||
| success("ログイン成功"); | |||
| navigateWhenChanged(getPath(PageID.DASHBOARD_CONTRACT_LIST)); | |||
| navigateWhenChanged(getPath(PageID.DASHBOARD_OVERVIEW)); | |||
| } else { | |||
| error("ログイン失敗"); | |||
| setMessage("入力情報を確認してください"); | |||
| @@ -0,0 +1,5 @@ | |||
| import { Box } from "@mui/material"; | |||
| export default function Page403() { | |||
| return <Box>Un Authenticated.</Box>; | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| import { Box } from "@mui/material"; | |||
| export default function TestB() { | |||
| export default function Page404() { | |||
| return <Box>NotFound.</Box>; | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| import { Box } from "@mui/material"; | |||
| import { PageID, TabID } from "codes/page"; | |||
| import useDashboard from "hooks/useDashBoard"; | |||
| import { useEffect } from "react"; | |||
| export default function Overview() { | |||
| const { setHeaderTitle, setTabs } = useDashboard( | |||
| PageID.DASHBOARD_OVERVIEW, | |||
| TabID.NONE | |||
| ); | |||
| useEffect(() => { | |||
| setHeaderTitle("Dashboard"); | |||
| setTabs(null); | |||
| }, []); | |||
| return <Box></Box>; | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| import { Box } from "@mui/material"; | |||
| import { PageID, TabID } from "codes/page"; | |||
| import useDashboard from "hooks/useDashBoard"; | |||
| import { useEffect } from "react"; | |||
| export default function ReceiptIssuingOrderCreate() { | |||
| const { setHeaderTitle, setTabs } = useDashboard( | |||
| PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE, | |||
| TabID.NONE | |||
| ); | |||
| useEffect(() => { | |||
| setHeaderTitle("領収証発行依頼作成"); | |||
| setTabs(null); | |||
| }, []); | |||
| return <Box>Create</Box>; | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| import { PageID as P } from "codes/page"; | |||
| import { UserRole as R } from "codes/user"; | |||
| export const AUTH = { | |||
| [P.NONE]: setAuth("all"), | |||
| [P.LOGIN]: setAuth("all"), | |||
| [P.LOGOUT]: setAuth("all"), | |||
| [P.DASHBOARD_OVERVIEW]: setAuth("ge", R.NORMAL_ADMIN), | |||
| [P.DASHBOARD_CONTRACT_LIST]: setAuth("ge", R.SUPER_ADMIN), | |||
| [P.DASHBOARD_CONTRACT_DETAIL]: setAuth("ge", R.SUPER_ADMIN), | |||
| [P.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE]: setAuth("ge", R.NORMAL_ADMIN), | |||
| [P.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST]: setAuth("ge", R.NORMAL_ADMIN), | |||
| [P.DASHBOARD_RECEIPT_ISSUING_ORDER_DETAIL]: setAuth("ge", R.NORMAL_ADMIN), | |||
| [P.PAGE_403]: setAuth("all"), | |||
| [P.PAGE_404]: setAuth("all"), | |||
| }; | |||
| type Target = "ge" | "le" | "eq" | "all"; | |||
| type UserRoleKey = keyof typeof R; | |||
| function setAuth(target: Target, targetRole?: R): R[] { | |||
| const ret: R[] = []; | |||
| for (const key in R) { | |||
| const role = R[key as UserRoleKey]; | |||
| if (target === "all") { | |||
| ret.push(role); | |||
| continue; | |||
| } | |||
| if (targetRole === undefined) { | |||
| continue; | |||
| } | |||
| if (target === "ge" && role >= targetRole) { | |||
| ret.push(role); | |||
| continue; | |||
| } | |||
| if (target === "le" && role <= targetRole) { | |||
| ret.push(role); | |||
| continue; | |||
| } | |||
| if (target === "eq" && role === targetRole) { | |||
| ret.push(role); | |||
| continue; | |||
| } | |||
| } | |||
| return ret; | |||
| } | |||
| @@ -1,10 +1,12 @@ | |||
| import { PageID } from "codes/page"; | |||
| import LoadingScreen from "components/LoadingScreen"; | |||
| import DashboardLayout from "layouts/dashbord"; | |||
| import { ElementType, Suspense, lazy } from "react"; | |||
| import { RouteObject, useLocation, useRoutes } from "react-router-dom"; | |||
| import { PATH, getRoute } from "./path"; | |||
| import { PageID } from "codes/page"; | |||
| import SimpleLayout from "layouts/simple"; | |||
| import { ElementType, Suspense, lazy, useMemo } from "react"; | |||
| import { RouteObject, useRoutes } from "react-router-dom"; | |||
| import { getRoute } from "./path"; | |||
| import useAuth from "hooks/useAuth"; | |||
| import { UserRole } from "codes/user"; | |||
| const Loadable = (Component: ElementType) => (props: any) => { | |||
| return ( | |||
| @@ -28,41 +30,81 @@ const AuthRoutes = (): RouteObject => ({ | |||
| ], | |||
| }); | |||
| const DashboardRoutes = (): RouteObject => ({ | |||
| element: <DashboardLayout />, | |||
| children: [ | |||
| const DashboardRoutes = (): RouteObject => { | |||
| const { canAccess } = useAuth(); | |||
| const allChildren = [ | |||
| { | |||
| path: getRoute(PageID.DASHBOARD_CONTRACT_LIST), | |||
| pageId: PageID.DASHBOARD_OVERVIEW, | |||
| element: <Dashboard />, | |||
| target: UserRole.NORMAL_ADMIN, | |||
| }, | |||
| { | |||
| pageId: PageID.DASHBOARD_CONTRACT_LIST, | |||
| element: <ContractList />, | |||
| target: UserRole.SUPER_ADMIN, | |||
| }, | |||
| { | |||
| path: getRoute(PageID.DASHBOARD_CONTRACT_DETAIL), | |||
| pageId: PageID.DASHBOARD_CONTRACT_DETAIL, | |||
| element: <ContractDetail />, | |||
| target: UserRole.SUPER_ADMIN, | |||
| }, | |||
| ], | |||
| }); | |||
| { | |||
| pageId: PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE, | |||
| element: <ReceiptIssuingOrderCreate />, | |||
| target: UserRole.NORMAL_ADMIN, | |||
| }, | |||
| ]; | |||
| const children: RouteObject[] = useMemo(() => { | |||
| return allChildren | |||
| .filter(({ pageId }) => canAccess(pageId)) | |||
| .map(({ pageId, ...others }) => ({ | |||
| ...others, | |||
| path: getRoute(pageId), | |||
| })); | |||
| }, [canAccess]); | |||
| return { | |||
| element: <DashboardLayout />, | |||
| children, | |||
| }; | |||
| }; | |||
| export function Routes() { | |||
| const { initialized } = useAuth(); | |||
| return useRoutes([ | |||
| AuthRoutes(), | |||
| DashboardRoutes(), | |||
| { | |||
| path: "403", | |||
| element: <Page403 />, | |||
| }, | |||
| { | |||
| path: "*", | |||
| element: <Page404 />, | |||
| element: initialized ? <Page404 /> : <LoadingScreen />, | |||
| }, | |||
| ]); | |||
| } | |||
| const TestAPage = Loadable(lazy(() => import("pages/test/TestA"))); | |||
| const TestBPage = Loadable(lazy(() => import("pages/test/TestB"))); | |||
| // 認証関連 ------------------------------- | |||
| const Login = Loadable(lazy(() => import("pages/auth/login"))); | |||
| const Logout = Loadable(lazy(() => import("pages/auth/logout"))); | |||
| //ダッシュボード ---------------------------- | |||
| const Dashboard = Loadable(lazy(() => import("pages/dashboard"))); | |||
| // 契約関連 | |||
| const ContractList = Loadable( | |||
| lazy(() => import("pages/dashboard/contract/list")) | |||
| ); | |||
| const ContractDetail = Loadable( | |||
| lazy(() => import("pages/dashboard/contract/detail")) | |||
| ); | |||
| // 領収証発行依頼 | |||
| const ReceiptIssuingOrderCreate = Loadable( | |||
| lazy(() => import("pages/dashboard/receipt-issuing-order/create")) | |||
| ); | |||
| // その他 --------------------------------- | |||
| const Page403 = Loadable(lazy(() => import("pages/common/Page403"))); | |||
| const Page404 = Loadable(lazy(() => import("pages/common/Page404"))); | |||
| @@ -1,30 +1,6 @@ | |||
| import { Dictionary } from "@types"; | |||
| import { PageID, TabID } from "codes/page"; | |||
| import { get, isArray, isNumber, isString, replace } from "lodash"; | |||
| const DASHBOARD = "dashboard"; | |||
| export const PATH = { | |||
| login: "/login", | |||
| logout: "/logout", | |||
| dashboard: { | |||
| root: "/dashboard", | |||
| contract: "/contract", | |||
| }, | |||
| }; | |||
| // const makePath = (paths: string[]): string => { | |||
| // return "/" + paths.join("/"); | |||
| // }; | |||
| // const makeListPageCallback = (path: string) => { | |||
| // return (page: number) => { | |||
| // return [path, String(page)].join("/"); | |||
| // }; | |||
| // }; | |||
| // const makeDashboardPath = (paths: string[]): string => { | |||
| // return makePath([PATH.dashboard.root, ...paths]); | |||
| // }; | |||
| import { get, isArray, isString, replace } from "lodash"; | |||
| type PathKey = [PageID, TabID?] | PageID; | |||
| const makePathKey = (arg: PathKey): string => { | |||
| @@ -53,13 +29,30 @@ const getTabId = (key: PathKey): TabID => { | |||
| }; | |||
| const PATHS = { | |||
| [makePathKey(PageID.NONE)]: "/", | |||
| // 認証 | |||
| [makePathKey(PageID.LOGIN)]: "/login", | |||
| [makePathKey(PageID.LOGOUT)]: "/logout", | |||
| [makePathKey(PageID.DASHBOARD_OVERVIEW)]: "/dashboard", | |||
| // 契約関連 | |||
| [makePathKey(PageID.DASHBOARD_CONTRACT_LIST)]: | |||
| "/dashboard/contract/list/:page", | |||
| [makePathKey(PageID.DASHBOARD_CONTRACT_DETAIL)]: "/dashboard/contract/detail", | |||
| // 領収証発行依頼関連 | |||
| [makePathKey(PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE)]: | |||
| "/dashboard/receipt-issusing-order/create", | |||
| [makePathKey(PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST)]: | |||
| "/dashboard/receipt-issusing-order/list/:page", | |||
| [makePathKey(PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_DETAIL)]: | |||
| "/dashboard/receipt-issusing-order/detail", | |||
| // その他 | |||
| [makePathKey(PageID.PAGE_403)]: "403", | |||
| [makePathKey(PageID.PAGE_404)]: "404", | |||
| }; | |||
| export type PathOption = { | |||