| @@ -0,0 +1,7 @@ | |||||
| GENERATE_SOURCEMAP=true | |||||
| PORT=8080 | |||||
| # HOST | |||||
| # ローカル | |||||
| REACT_APP_HOST_API_KEY=http://localhost | |||||
| @@ -5,24 +5,33 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "@emotion/react": "^11.10.8", | "@emotion/react": "^11.10.8", | ||||
| "@emotion/styled": "^11.10.8", | "@emotion/styled": "^11.10.8", | ||||
| "@hookform/resolvers": "^3.1.0", | |||||
| "@mui/icons-material": "^5.11.16", | "@mui/icons-material": "^5.11.16", | ||||
| "@mui/lab": "^5.0.0-alpha.129", | |||||
| "@mui/material": "^5.12.2", | "@mui/material": "^5.12.2", | ||||
| "@testing-library/jest-dom": "^5.14.1", | "@testing-library/jest-dom": "^5.14.1", | ||||
| "@testing-library/react": "^13.0.0", | "@testing-library/react": "^13.0.0", | ||||
| "@testing-library/user-event": "^13.2.1", | "@testing-library/user-event": "^13.2.1", | ||||
| "@types/axios": "^0.14.0", | |||||
| "@types/date-fns": "^2.6.0", | |||||
| "@types/jest": "^27.0.1", | "@types/jest": "^27.0.1", | ||||
| "@types/lodash": "^4.14.194", | "@types/lodash": "^4.14.194", | ||||
| "@types/node": "^16.7.13", | "@types/node": "^16.7.13", | ||||
| "@types/react": "^18.0.0", | "@types/react": "^18.0.0", | ||||
| "@types/react-dom": "^18.0.0", | "@types/react-dom": "^18.0.0", | ||||
| "@types/react-router-dom": "^5.3.3", | "@types/react-router-dom": "^5.3.3", | ||||
| "axios": "^1.4.0", | |||||
| "date-fns": "^2.30.0", | |||||
| "lodash": "^4.17.21", | "lodash": "^4.17.21", | ||||
| "notistack": "^3.0.1", | |||||
| "react": "^18.2.0", | "react": "^18.2.0", | ||||
| "react-dom": "^18.2.0", | "react-dom": "^18.2.0", | ||||
| "react-hook-form": "^7.43.9", | |||||
| "react-router-dom": "^6.11.0", | "react-router-dom": "^6.11.0", | ||||
| "react-scripts": "5.0.1", | "react-scripts": "5.0.1", | ||||
| "typescript": "^4.4.2", | "typescript": "^4.4.2", | ||||
| "web-vitals": "^2.1.0" | |||||
| "web-vitals": "^2.1.0", | |||||
| "yup": "^1.1.1" | |||||
| }, | }, | ||||
| "scripts": { | "scripts": { | ||||
| "start": "react-scripts start", | "start": "react-scripts start", | ||||
| @@ -1,5 +1,7 @@ | |||||
| import { ReactNode } from "react"; | import { ReactNode } from "react"; | ||||
| export type DataUrl = string; | |||||
| export type HasChildren = { | export type HasChildren = { | ||||
| children: ReactNode; | children: ReactNode; | ||||
| }; | }; | ||||
| @@ -1,25 +1,29 @@ | |||||
| import { CssBaseline } from "@mui/material"; | import { CssBaseline } from "@mui/material"; | ||||
| import AuthContextProvider from "contexts/AuthContext"; | |||||
| import { PageContextProvider } from "contexts/PageContext"; | import { PageContextProvider } from "contexts/PageContext"; | ||||
| import { WindowSizeContextProvider } from "contexts/WindowSizeContext"; | import { WindowSizeContextProvider } from "contexts/WindowSizeContext"; | ||||
| import CsrfTokenProvider from "providers/CsrfTokenProvider"; | |||||
| import SnackbarProvider from "providers/SnackbarProvider"; | |||||
| import { BrowserRouter } from "react-router-dom"; | import { BrowserRouter } from "react-router-dom"; | ||||
| import { Routes } from "routes"; | import { Routes } from "routes"; | ||||
| import { AppThemeProvider } from "theme"; | |||||
| import AppThemeProvider from "theme"; | |||||
| function App() { | |||||
| export default function App() { | |||||
| return ( | return ( | ||||
| <> | |||||
| <AppThemeProvider> | |||||
| <PageContextProvider> | |||||
| <WindowSizeContextProvider> | |||||
| <BrowserRouter> | |||||
| <CssBaseline /> | |||||
| <Routes /> | |||||
| </BrowserRouter> | |||||
| </WindowSizeContextProvider> | |||||
| </PageContextProvider> | |||||
| </AppThemeProvider> | |||||
| </> | |||||
| <AuthContextProvider> | |||||
| <PageContextProvider> | |||||
| <WindowSizeContextProvider> | |||||
| <BrowserRouter> | |||||
| <AppThemeProvider> | |||||
| <SnackbarProvider> | |||||
| <CsrfTokenProvider /> | |||||
| <CssBaseline /> | |||||
| <Routes /> | |||||
| </SnackbarProvider> | |||||
| </AppThemeProvider> | |||||
| </BrowserRouter> | |||||
| </WindowSizeContextProvider> | |||||
| </PageContextProvider> | |||||
| </AuthContextProvider> | |||||
| ); | ); | ||||
| } | } | ||||
| export default App; | |||||
| @@ -0,0 +1,200 @@ | |||||
| import { UserRole } from "codes/user"; | |||||
| import { APICommonResponse, ApiId, HttpMethod, request } from "."; | |||||
| import { getUrl } from "./url"; | |||||
| type MeResponse = { | |||||
| data: { | |||||
| id: string; | |||||
| contract_id: string; | |||||
| role: UserRole; | |||||
| }; | |||||
| } & APICommonResponse; | |||||
| export const csrfToken = async () => { | |||||
| await request({ | |||||
| url: getUrl(ApiId.CSRF_TOKEN), | |||||
| method: HttpMethod.GET, | |||||
| }); | |||||
| }; | |||||
| export const me = async () => { | |||||
| const res = await request<MeResponse>({ | |||||
| url: getUrl(ApiId.ME), | |||||
| method: HttpMethod.GET, | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| export const login = async (param: { email: string; password: string }) => { | |||||
| const res = await request<MeResponse>({ | |||||
| url: getUrl(ApiId.LOGIN), | |||||
| method: HttpMethod.POST, | |||||
| data: param, | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| export const logout = async () => { | |||||
| const res = await request({ | |||||
| url: getUrl(ApiId.LOGOUT), | |||||
| method: HttpMethod.GET, | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| // export const getMe = async () => { | |||||
| // const res = await request<MeResponse>({ | |||||
| // url: getUrl(ApiId.ME), | |||||
| // method: HttpMethod.GET, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // export const login = async (email: string, password: string) => { | |||||
| // const data = new URLSearchParams({ | |||||
| // email, | |||||
| // password, | |||||
| // }); | |||||
| // const res = await request<MeResponse>({ | |||||
| // url: getUrl(ApiId.NORMAL_LOGIN), | |||||
| // method: HttpMethod.POST, | |||||
| // data, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // export const loginAdmin = async (email: string, password: string) => { | |||||
| // const data = new URLSearchParams({ | |||||
| // email, | |||||
| // password, | |||||
| // }); | |||||
| // const res = await request<MeResponse>({ | |||||
| // url: getUrl(ApiId.ADMIN_LOGIN), | |||||
| // method: HttpMethod.POST, | |||||
| // data, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // export const logout = async () => { | |||||
| // const res = await request({ | |||||
| // url: getUrl(ApiId.LOGOUT), | |||||
| // method: HttpMethod.POST, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // export type StartEmailVerifyParams = { | |||||
| // email: string; | |||||
| // for_entry?: boolean; | |||||
| // customer_code?: string; | |||||
| // parking_management_code?: string; | |||||
| // }; | |||||
| // export const startEmailVerify = async (data: StartEmailVerifyParams) => { | |||||
| // const sendData = new URLSearchParams(makeParam(data)); | |||||
| // const res = await request({ | |||||
| // url: getUrl(ApiId.EMAIL_VERIFY_START), | |||||
| // method: HttpMethod.POST, | |||||
| // data: sendData, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // export const verifyEmail = async ({ token }: { token: string }) => { | |||||
| // const sendData = new URLSearchParams(makeParam({ token })); | |||||
| // const res = await request<EmailVerifyResponse>({ | |||||
| // url: getUrl(ApiId.EMAIL_VERIFY), | |||||
| // method: HttpMethod.POST, | |||||
| // data: sendData, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // // パスワードリセット開始 | |||||
| // export type ResetPasswordStartParams = { | |||||
| // email: string; | |||||
| // customer_code?: string; | |||||
| // parking_management_code?: string; | |||||
| // }; | |||||
| // export const resetPasswordStart = async (data: ResetPasswordStartParams) => { | |||||
| // const sendData = new URLSearchParams(makeParam(data)); | |||||
| // const res = await request({ | |||||
| // url: getUrl(ApiId.RESET_PASSWORD_START), | |||||
| // method: HttpMethod.POST, | |||||
| // data: sendData, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // // パスワードリセットトークンチェック | |||||
| // export type ResetPasswordVerifyParams = { | |||||
| // token: string; | |||||
| // }; | |||||
| // export const resetPasswordVerify = async (data: ResetPasswordVerifyParams) => { | |||||
| // const sendData = new URLSearchParams(makeParam(data)); | |||||
| // const res = await request({ | |||||
| // url: getUrl(ApiId.RESET_PASSWORD_VERIFY), | |||||
| // method: HttpMethod.POST, | |||||
| // data: sendData, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // // パスワードリセット | |||||
| // export type ResetPasswordParams = { | |||||
| // password: string; | |||||
| // token: string; | |||||
| // }; | |||||
| // export const resetPassword = async (data: ResetPasswordParams) => { | |||||
| // const sendData = new URLSearchParams(makeParam(data)); | |||||
| // const res = await request({ | |||||
| // url: getUrl(ApiId.RESET_PASSWORD), | |||||
| // method: HttpMethod.POST, | |||||
| // data: sendData, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| // // 利用者登録 | |||||
| // export const RegisterUserParamKeyName = { | |||||
| // TOKEN: 'token', | |||||
| // PASSWORD: 'password', | |||||
| // FIRST_NAME: 'first_name', | |||||
| // LAST_NAME: 'last_name', | |||||
| // FIRST_NAME_KANA: 'first_name_kana', | |||||
| // LAST_NAME_KANA: 'last_name_kana', | |||||
| // ZIP_CODE: 'zip_code', | |||||
| // PREF_CODE: 'pref_code', | |||||
| // ADDRESS1: 'address1', | |||||
| // ADDRESS2: 'address2', | |||||
| // ADDRESS3: 'address3', | |||||
| // PHONE_NUMBER: 'phone_number', | |||||
| // CONFIRM_PRIVACY_POLICY: 'confirm_privacy_policy', | |||||
| // } as const; | |||||
| // export type RegisterUserParamKeyName = | |||||
| // typeof RegisterUserParamKeyName[keyof typeof RegisterUserParamKeyName]; | |||||
| // export type RegisterUserParam = { | |||||
| // [RegisterUserParamKeyName.TOKEN]: string; | |||||
| // [RegisterUserParamKeyName.PASSWORD]: string; | |||||
| // [RegisterUserParamKeyName.FIRST_NAME]: string; | |||||
| // [RegisterUserParamKeyName.LAST_NAME]: string; | |||||
| // [RegisterUserParamKeyName.FIRST_NAME_KANA]: string; | |||||
| // [RegisterUserParamKeyName.LAST_NAME_KANA]: string; | |||||
| // [RegisterUserParamKeyName.ZIP_CODE]: string; | |||||
| // [RegisterUserParamKeyName.PREF_CODE]: string; | |||||
| // [RegisterUserParamKeyName.ADDRESS1]: string; | |||||
| // [RegisterUserParamKeyName.ADDRESS2]: string; | |||||
| // [RegisterUserParamKeyName.ADDRESS3]: string; | |||||
| // [RegisterUserParamKeyName.PHONE_NUMBER]: string; | |||||
| // [RegisterUserParamKeyName.CONFIRM_PRIVACY_POLICY]: boolean; | |||||
| // }; | |||||
| // export const registerUser = async (data: RegisterUserParam) => { | |||||
| // const sendData = new URLSearchParams(makeParam(data)); | |||||
| // const res = await request({ | |||||
| // url: getUrl(ApiId.REGISTER_USER), | |||||
| // method: HttpMethod.POST, | |||||
| // data: sendData, | |||||
| // }); | |||||
| // return res; | |||||
| // }; | |||||
| @@ -0,0 +1,234 @@ | |||||
| import { AxiosError, AxiosResponse } from "axios"; | |||||
| import { format } from "date-fns"; | |||||
| import { Dictionary, get } from "lodash"; | |||||
| import { DataUrl } from "@types"; | |||||
| import { setFormErrorMessages } from "components/hook-form"; | |||||
| import axios from "utils/axios"; | |||||
| let id = 0; | |||||
| export const ApiId = { | |||||
| CSRF_TOKEN: id++, | |||||
| ME: id++, | |||||
| LOGIN: id++, | |||||
| LOGOUT: id++, | |||||
| } as const; | |||||
| export type ApiId = (typeof ApiId)[keyof typeof ApiId]; | |||||
| export const HttpMethod = { | |||||
| GET: "get", | |||||
| POST: "post", | |||||
| } as const; | |||||
| export type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod]; | |||||
| export const ResultCode = { | |||||
| SUCCESS: 0, | |||||
| FAILED: 1, | |||||
| UNAUTHORIZED: 2, | |||||
| EXCLUSIVE_ERROR: 3, | |||||
| }; | |||||
| export type ResultCode = (typeof ResultCode)[keyof typeof ResultCode]; | |||||
| export interface APICommonResponse { | |||||
| result: ResultCode; | |||||
| messages: { | |||||
| errors?: Dictionary<string>; | |||||
| general?: string; | |||||
| email_id?: number; | |||||
| }; | |||||
| } | |||||
| export type ImagesResponse = { | |||||
| data: { | |||||
| images: DataUrl[]; | |||||
| }; | |||||
| } & APICommonResponse; | |||||
| export const makeParam = <T extends object>(data: T): Dictionary<string> => { | |||||
| const res: Dictionary<string> = {}; | |||||
| Object.keys(data).map((key) => { | |||||
| const val = get(data, key); | |||||
| if (typeof val === "string" && val.length !== 0) { | |||||
| res[key] = val; | |||||
| } else if (typeof val === "number") { | |||||
| res[key] = String(val); | |||||
| } else if (typeof val === "boolean") { | |||||
| res[key] = val ? "1" : "0"; | |||||
| } else if (val instanceof Date) { | |||||
| res[key] = format(val, "yyyy-MM-dd"); | |||||
| } | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| export const makeFormData = <T extends object>(data: T): FormData => { | |||||
| const res = new FormData(); | |||||
| Object.keys(data).map((key) => { | |||||
| const val = get(data, key); | |||||
| if (typeof val === "string" && val.length !== 0) { | |||||
| res.append(key, val); | |||||
| } else if (typeof val === "number") { | |||||
| res.append(key, String(val)); | |||||
| } else if (typeof val === "boolean") { | |||||
| res.append(key, val ? "1" : "0"); | |||||
| } else if (val instanceof Date) { | |||||
| res.append(key, format(val, "yyyy-MM-dd")); | |||||
| } else if (val instanceof File) { | |||||
| res.append(key, val); | |||||
| } else if (Array.isArray(val)) { | |||||
| val.forEach((v) => { | |||||
| res.append(key + "[]", v); | |||||
| }); | |||||
| } else { | |||||
| console.log("undefined data", key, val); | |||||
| } | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| const isAxiosError = (error: any): error is AxiosError => { | |||||
| return !!error.isAxiosError; | |||||
| }; | |||||
| type RequestArgument = { | |||||
| url: string; | |||||
| method: HttpMethod; | |||||
| data?: URLSearchParams | FormData | object; | |||||
| multipart?: boolean; | |||||
| }; | |||||
| export const request = async <T extends APICommonResponse>({ | |||||
| url, | |||||
| method, | |||||
| data, | |||||
| multipart, | |||||
| }: RequestArgument): Promise<T | null> => { | |||||
| let response: AxiosResponse<T | any> | null = null; | |||||
| if (data instanceof URLSearchParams) { | |||||
| data.sort(); | |||||
| } | |||||
| try { | |||||
| if (multipart && data instanceof FormData) { | |||||
| response = await axios({ | |||||
| url, | |||||
| method: "post", | |||||
| data, | |||||
| headers: { | |||||
| "content-type": "multipart/form-data", | |||||
| }, | |||||
| }); | |||||
| console.log("RESPONSE", url, "multipart", data, response?.data); | |||||
| } else if (method === HttpMethod.GET) { | |||||
| let searchUrl = url; | |||||
| if (data instanceof URLSearchParams) { | |||||
| searchUrl += "?" + data.toString(); | |||||
| } | |||||
| response = await axios.get<T>(searchUrl); | |||||
| console.log("RESPONSE", searchUrl, method, response?.data); | |||||
| } else if (method === HttpMethod.POST) { | |||||
| response = await axios.post<T>(url, data); | |||||
| let sendData: Dictionary<String> = {}; | |||||
| if (data instanceof URLSearchParams) { | |||||
| data.forEach((val, key) => { | |||||
| sendData[key] = val; | |||||
| }); | |||||
| } else if (typeof data === "object" && !(data instanceof FormData)) { | |||||
| Object.keys(data).forEach((key) => { | |||||
| sendData[key] = get(data, key); | |||||
| }); | |||||
| } | |||||
| console.log("RESPONSE", url, method, sendData, response?.data); | |||||
| } else { | |||||
| return null; | |||||
| } | |||||
| if (response && response.data.result === ResultCode.SUCCESS) { | |||||
| return response.data; | |||||
| } | |||||
| return response?.data; | |||||
| } catch (e) { | |||||
| if (isAxiosError(e)) { | |||||
| if (e.response?.status === 401 || e.response?.status === 419) { | |||||
| window.location.reload(); | |||||
| } | |||||
| } | |||||
| if (typeof e === "object") { | |||||
| const message = get(e, "message"); | |||||
| if (message === "Unauthenticated.") { | |||||
| console.log("401!!!"); | |||||
| window.location.reload(); | |||||
| return null; | |||||
| } | |||||
| if (message === "CSRF token mismatch.") { | |||||
| console.log("419!!!"); | |||||
| window.location.reload(); | |||||
| return null; | |||||
| } | |||||
| if (message === "Service Unavailable") { | |||||
| console.log("503!!!"); | |||||
| window.location.reload(); | |||||
| return null; | |||||
| } | |||||
| } | |||||
| throw e; | |||||
| } | |||||
| }; | |||||
| export async function apiRequest< | |||||
| T extends APICommonResponse, | |||||
| U extends object | |||||
| >({ | |||||
| apiMethod, | |||||
| sendData, | |||||
| onSuccess, | |||||
| onFailed, | |||||
| onFinaly, | |||||
| errorSetter, | |||||
| setSending, | |||||
| }: { | |||||
| apiMethod: (sendData: U) => Promise<T | null>; | |||||
| sendData: U; | |||||
| onSuccess?: (res: T, sendData: U) => void; | |||||
| onFailed?: (res: APICommonResponse | null) => void; | |||||
| onFinaly?: (res: APICommonResponse | null) => void; | |||||
| errorSetter?: any; | |||||
| setSending?: (sending: boolean) => void; | |||||
| }) { | |||||
| if (setSending) { | |||||
| setSending(true); | |||||
| } | |||||
| const res = await apiMethod(sendData); | |||||
| if (setSending) { | |||||
| setSending(false); | |||||
| } | |||||
| if (res?.result === ResultCode.SUCCESS) { | |||||
| if (onSuccess) { | |||||
| onSuccess(res, sendData); | |||||
| } | |||||
| } else { | |||||
| if (res?.messages.errors) { | |||||
| if (errorSetter) { | |||||
| const errorCount = setFormErrorMessages( | |||||
| sendData, | |||||
| errorSetter, | |||||
| res.messages.errors | |||||
| ); | |||||
| console.log("FormErrorCount", errorCount); | |||||
| } | |||||
| } | |||||
| if (onFailed) { | |||||
| onFailed(res); | |||||
| } | |||||
| } | |||||
| if (onFinaly) { | |||||
| onFinaly(res); | |||||
| } | |||||
| return res; | |||||
| } | |||||
| @@ -0,0 +1,25 @@ | |||||
| import { ApiId } from "."; | |||||
| const urls = { | |||||
| [ApiId.CSRF_TOKEN]: "sanctum/csrf-cookie", | |||||
| [ApiId.ME]: "me", | |||||
| [ApiId.LOGIN]: "login", | |||||
| [ApiId.LOGOUT]: "logout", | |||||
| }; | |||||
| const prefixs = { | |||||
| [ApiId.CSRF_TOKEN]: "", | |||||
| }; | |||||
| const DEFAULT_API_URL_PREFIX = "api"; | |||||
| const getPrefix = (apiId: ApiId) => { | |||||
| return prefixs[apiId] ?? DEFAULT_API_URL_PREFIX; | |||||
| }; | |||||
| export const getUrl = (apiId: ApiId) => { | |||||
| let url = getPrefix(apiId); | |||||
| if (url.length !== 0) { | |||||
| url += "/"; | |||||
| } | |||||
| return url + (urls[apiId] ?? ""); | |||||
| }; | |||||
| @@ -1,8 +1,20 @@ | |||||
| let id = 0; | |||||
| export const PageID = { | export const PageID = { | ||||
| NONE: "NONE", | |||||
| NONE: id++, | |||||
| DASHBOARD_CONTRACT_LIST: "DASHBOARD_CONTRACT_LIST", | |||||
| DASHBOARD_CONTRACT_DETAIL: "DASHBOARD_CONTRACT_DETAIL", | |||||
| LOGIN: id++, | |||||
| LOGOUT: id++, | |||||
| DASHBOARD_CONTRACT_LIST: id++, | |||||
| DASHBOARD_CONTRACT_DETAIL: id++, | |||||
| } as const; | } as const; | ||||
| export type PageID = (typeof PageID)[keyof typeof PageID]; | export type PageID = (typeof PageID)[keyof typeof PageID]; | ||||
| id = 0; | |||||
| export const TabID = { | |||||
| NONE: id++, | |||||
| A: id++, | |||||
| } as const; | |||||
| export type TabID = (typeof TabID)[keyof typeof TabID]; | |||||
| @@ -0,0 +1,83 @@ | |||||
| import { Checkbox, CheckboxProps, FormControlLabel } from "@mui/material"; | |||||
| import { Dictionary } from "@types"; | |||||
| import { useMemo, useState } from "react"; | |||||
| export type CheckBoxCustomProps = { | |||||
| label: string; | |||||
| value: boolean; | |||||
| onFix?: () => void; | |||||
| onChangeValue?: (val: boolean) => void; | |||||
| messages?: Dictionary; | |||||
| readonly?: boolean; | |||||
| } & CheckboxProps; | |||||
| export default function CheckBoxCustom({ | |||||
| label, | |||||
| value, | |||||
| onFix, | |||||
| onChangeValue, | |||||
| messages, | |||||
| readonly, | |||||
| ...others | |||||
| }: CheckBoxCustomProps) { | |||||
| const [oldValue, setOldValue] = useState<string | null>(null); | |||||
| const inputProps = useMemo(() => { | |||||
| if (readonly) { | |||||
| return { | |||||
| style: { color: "rgb(50, 50, 50)" }, | |||||
| disabled: true, | |||||
| }; | |||||
| } else { | |||||
| return undefined; | |||||
| } | |||||
| }, [readonly]); | |||||
| const fix = (newValue: string) => { | |||||
| if (oldValue !== newValue) { | |||||
| setOldValue(newValue); | |||||
| if (onFix) { | |||||
| onFix(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| const handleChange = (e: any, val: boolean) => { | |||||
| if (onChangeValue) { | |||||
| onChangeValue(val); | |||||
| } | |||||
| }; | |||||
| const message = useMemo(() => { | |||||
| if (messages && others.name) { | |||||
| return messages[others.name] ?? ""; | |||||
| } else { | |||||
| return ""; | |||||
| } | |||||
| }, [messages]); | |||||
| const error = useMemo(() => { | |||||
| if (messages && others.name) { | |||||
| return ( | |||||
| messages[others.name] !== undefined && | |||||
| messages[others.name].length !== 0 | |||||
| ); | |||||
| } else { | |||||
| return false; | |||||
| } | |||||
| }, [messages]); | |||||
| return ( | |||||
| <FormControlLabel | |||||
| control={ | |||||
| <Checkbox | |||||
| checked={value} | |||||
| onChange={handleChange} | |||||
| inputProps={inputProps} | |||||
| {...others} | |||||
| /> | |||||
| } | |||||
| label={label} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,45 @@ | |||||
| import React from 'react'; | |||||
| import { TextField, TextFieldProps } from '@mui/material'; | |||||
| import { DatePicker } from '@mui/lab'; | |||||
| import { TextFieldCustomProps } from './TextFieldCustom'; | |||||
| import { isValid } from 'date-fns'; | |||||
| type DatePickerCustomProps = TextFieldCustomProps & { | |||||
| value: Date | null; | |||||
| onChangeDate: (val: Date | null) => void; | |||||
| }; | |||||
| const DatePickerCustom = ({ label, value, onChangeDate, ...others }: DatePickerCustomProps) => { | |||||
| const handleChange = (val: string | null) => { | |||||
| console.log({ handleChange: val }); | |||||
| if (onChangeDate) { | |||||
| if (val !== null) { | |||||
| const date = new Date(val); | |||||
| if (isValid(date)) { | |||||
| onChangeDate(date); | |||||
| } else { | |||||
| onChangeDate(null); | |||||
| } | |||||
| } else { | |||||
| onChangeDate(null); | |||||
| } | |||||
| } | |||||
| }; | |||||
| const handleRender = (params: TextFieldProps) => { | |||||
| return <TextField size="small" {...params} />; | |||||
| }; | |||||
| return ( | |||||
| <DatePicker | |||||
| label={label} | |||||
| inputFormat="yyyy/MM/dd" | |||||
| mask="____/__/__" | |||||
| value={value} | |||||
| onChange={handleChange} | |||||
| renderInput={handleRender} | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| export default React.memo(DatePickerCustom); | |||||
| @@ -0,0 +1,56 @@ | |||||
| import { Alert, SxProps } from "@mui/material"; | |||||
| import { APIErrorType } from "hooks/useAPICall"; | |||||
| import React, { useEffect, useMemo, useRef } from "react"; | |||||
| type Props = { | |||||
| error: APIErrorType; | |||||
| sx?: SxProps; | |||||
| getMessage?: (errpr: APIErrorType) => string | null; | |||||
| message?: string; | |||||
| errorScroll?: boolean; | |||||
| }; | |||||
| const InputAlert = ({ | |||||
| error, | |||||
| sx, | |||||
| getMessage, | |||||
| message: errorMessage, | |||||
| errorScroll, | |||||
| }: Props) => { | |||||
| const ref = useRef<HTMLDivElement>(null); | |||||
| const message = useMemo(() => { | |||||
| if (errorMessage) { | |||||
| return errorMessage; | |||||
| } | |||||
| if (getMessage) { | |||||
| const m = getMessage(error); | |||||
| if (m !== null) { | |||||
| return m; | |||||
| } | |||||
| } | |||||
| if (error === APIErrorType.INPUT) return "入力項目を確認してください。"; | |||||
| if (error === APIErrorType.SERVER) | |||||
| return "エラーが発生しております。しばらくお待ちください。"; | |||||
| if (error === APIErrorType.EXCLUSIVE) | |||||
| return "ページの期限が切れています。再度読込を行ってください"; | |||||
| return ""; | |||||
| }, [error, errorMessage, getMessage]); | |||||
| // エラー時に自動的にスクロール制御 | |||||
| useEffect(() => { | |||||
| if (error !== APIErrorType.NONE && errorScroll) { | |||||
| if (ref.current) { | |||||
| ref.current.scrollIntoView({ block: "center", behavior: "smooth" }); | |||||
| } | |||||
| } | |||||
| }, [error, ref]); | |||||
| if (message === "" && error === APIErrorType.NONE) return null; | |||||
| return ( | |||||
| <Alert severity="error" sx={sx} ref={ref}> | |||||
| {message} | |||||
| </Alert> | |||||
| ); | |||||
| }; | |||||
| export default React.memo(InputAlert); | |||||
| @@ -0,0 +1,95 @@ | |||||
| import React, { useMemo, useState } from "react"; | |||||
| import { TextField, TextFieldProps } from "@mui/material"; | |||||
| import { Dictionary } from "@types"; | |||||
| export type TextFieldCustomProps = { | |||||
| onFix?: () => void; | |||||
| onChangeValue?: (val: string) => void; | |||||
| messages?: Dictionary; | |||||
| readonly?: boolean; | |||||
| } & TextFieldProps; | |||||
| export default function TextFieldCustom({ | |||||
| onFix, | |||||
| onChangeValue, | |||||
| messages, | |||||
| readonly, | |||||
| ...others | |||||
| }: TextFieldCustomProps) { | |||||
| const [oldValue, setOldValue] = useState<string | null>(null); | |||||
| const inputProps = useMemo(() => { | |||||
| if (readonly) { | |||||
| return { | |||||
| style: { color: "rgb(50, 50, 50)" }, | |||||
| disabled: true, | |||||
| }; | |||||
| } else { | |||||
| return undefined; | |||||
| } | |||||
| }, [readonly]); | |||||
| const fix = (newValue: string) => { | |||||
| if (oldValue !== newValue) { | |||||
| setOldValue(newValue); | |||||
| if (onFix) { | |||||
| onFix(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| const handleEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||||
| if (e.key === "Enter") { | |||||
| if (e.target instanceof HTMLInputElement) { | |||||
| fix(e.target.value); | |||||
| } | |||||
| } | |||||
| }; | |||||
| const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => { | |||||
| if (!others.select) { | |||||
| fix(e.target.value); | |||||
| } | |||||
| }; | |||||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| if (onChangeValue) { | |||||
| onChangeValue(e.target.value); | |||||
| } | |||||
| if (others.select) { | |||||
| fix(e.target.value); | |||||
| } | |||||
| }; | |||||
| const message = useMemo(() => { | |||||
| if (messages && others.name) { | |||||
| return messages[others.name] ?? ""; | |||||
| } else { | |||||
| return ""; | |||||
| } | |||||
| }, [messages]); | |||||
| const error = useMemo(() => { | |||||
| if (messages && others.name) { | |||||
| return ( | |||||
| messages[others.name] !== undefined && | |||||
| messages[others.name].length !== 0 | |||||
| ); | |||||
| } else { | |||||
| return false; | |||||
| } | |||||
| }, [messages]); | |||||
| return ( | |||||
| <TextField | |||||
| size="small" | |||||
| onKeyDown={handleEnter} | |||||
| onBlur={handleBlur} | |||||
| onChange={handleChange} | |||||
| helperText={message} | |||||
| error={error} | |||||
| inputProps={inputProps} | |||||
| {...others} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,36 @@ | |||||
| import React, { useMemo, useState } from 'react'; | |||||
| import { TextField, TextFieldProps } from '@mui/material'; | |||||
| export type TextFieldExProps = { | |||||
| readOnly?: boolean; | |||||
| } & TextFieldProps; | |||||
| const TextFieldEx = ({ readOnly, ...others }: TextFieldExProps) => { | |||||
| if (readOnly) { | |||||
| const props: any = {}; | |||||
| if (typeof others.value === 'string' && others.value.length === 0) { | |||||
| props.value = ' '; | |||||
| } | |||||
| return ( | |||||
| <TextField | |||||
| {...others} | |||||
| sx={{ | |||||
| input: { | |||||
| WebkitTextFillColor: 'black !important', | |||||
| }, | |||||
| textarea: { | |||||
| WebkitTextFillColor: 'black !important', | |||||
| }, | |||||
| }} | |||||
| disabled | |||||
| variant="standard" | |||||
| {...props} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| return <TextField {...others} />; | |||||
| }; | |||||
| export default React.memo(TextFieldEx); | |||||
| @@ -0,0 +1,24 @@ | |||||
| import { Button } from '@mui/material'; | |||||
| import { ReactNode } from 'react'; | |||||
| // form | |||||
| import { FormProvider as Form, UseFormReturn } from 'react-hook-form'; | |||||
| // ---------------------------------------------------------------------- | |||||
| type Props = { | |||||
| children: ReactNode; | |||||
| methods: UseFormReturn<any>; | |||||
| onSubmit?: VoidFunction; | |||||
| }; | |||||
| export default function FormProvider({ children, onSubmit, methods }: Props) { | |||||
| return ( | |||||
| <Form {...methods}> | |||||
| <form onSubmit={onSubmit}> | |||||
| {children} | |||||
| {/* エンターでsubmitできるようにする */} | |||||
| <Button type="submit" sx={{ display: 'none' }} /> | |||||
| </form> | |||||
| </Form> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,121 @@ | |||||
| import { useFormContext, Controller } from 'react-hook-form'; | |||||
| import { Autocomplete, TextField, TextFieldProps } from '@mui/material'; | |||||
| import React, { useEffect, useMemo } from 'react'; | |||||
| import TextFieldEx from '../form/TextFieldEx'; | |||||
| // ---------------------------------------------------------------------- | |||||
| export type AutoCompleteOption = { | |||||
| label: string; | |||||
| value: string; | |||||
| }; | |||||
| export type AutoCompleteOptionType = AutoCompleteOption | string | null; | |||||
| export const getValue = (option: AutoCompleteOptionType): string => { | |||||
| if (option === null) { | |||||
| return ''; | |||||
| } | |||||
| if (typeof option === 'object') { | |||||
| return option.value; | |||||
| } | |||||
| if (typeof option === 'string') { | |||||
| return option; | |||||
| } | |||||
| return ''; | |||||
| }; | |||||
| type IProps = { | |||||
| name: string; | |||||
| /** | |||||
| * undefined の場合は、オプション確定前と単断する | |||||
| */ | |||||
| options?: AutoCompleteOption[]; | |||||
| onFix?: VoidFunction; | |||||
| readOnly?: boolean; | |||||
| }; | |||||
| type Props = IProps & TextFieldProps; | |||||
| export type RHFAutoCompleteProps = Props; | |||||
| export const getAutoCompleteOption = ( | |||||
| options: AutoCompleteOption[], | |||||
| value: string | |||||
| ): AutoCompleteOptionType => { | |||||
| return options.find((option) => option.value === value) ?? null; | |||||
| }; | |||||
| export default function RHFAutoComplete({ name, options, onFix, readOnly, ...other }: Props) { | |||||
| const { control, watch, setValue } = useFormContext(); | |||||
| const value: AutoCompleteOption | string | null = watch(name); | |||||
| const valueStr = useMemo(() => { | |||||
| if (value === null) return ''; | |||||
| if (value === undefined) return ''; | |||||
| if (typeof value === 'string') { | |||||
| return value; | |||||
| } else { | |||||
| return value.label ?? ''; | |||||
| } | |||||
| }, [value]); | |||||
| // string型からAutoCompleteOptionへ変換してフォームへセットする | |||||
| useEffect(() => { | |||||
| if (typeof value === 'string' && options) { | |||||
| if (value === '') { | |||||
| setValue(name, null); | |||||
| } else { | |||||
| const val = getAutoCompleteOption(options, value); | |||||
| if (val !== null) { | |||||
| setValue(name, val); | |||||
| } | |||||
| } | |||||
| } | |||||
| }, [value, options]); | |||||
| if (readOnly) { | |||||
| return <TextFieldEx readOnly {...other} value={valueStr} variant="standard" />; | |||||
| } | |||||
| if (typeof value === 'string') return null; | |||||
| return ( | |||||
| <Controller | |||||
| name={name} | |||||
| control={control} | |||||
| render={({ field: { onChange, onBlur, value, ref }, fieldState }) => ( | |||||
| <Autocomplete | |||||
| options={options ?? []} | |||||
| fullWidth | |||||
| autoComplete | |||||
| includeInputInList | |||||
| noOptionsText="候補がありません" | |||||
| getOptionLabel={(option) => option?.label ?? ''} | |||||
| isOptionEqualToValue={(option, value) => { | |||||
| // if (typeof value !== 'object') return false; | |||||
| return option.value === value.value; | |||||
| }} | |||||
| onChange={(e, item) => { | |||||
| onChange(item); | |||||
| if (onFix) { | |||||
| onFix(); | |||||
| } | |||||
| }} | |||||
| value={value} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| fullWidth | |||||
| error={fieldState.invalid} | |||||
| helperText={fieldState.error?.message} | |||||
| {...other} | |||||
| /> | |||||
| )} | |||||
| ChipProps={{ | |||||
| style: { | |||||
| margin: 0, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,96 @@ | |||||
| // form | |||||
| import { useFormContext, Controller } from 'react-hook-form'; | |||||
| // @mui | |||||
| import { Checkbox, FormControlLabel, FormGroup, FormControlLabelProps } from '@mui/material'; | |||||
| // ---------------------------------------------------------------------- | |||||
| interface RHFCheckboxProps extends Omit<FormControlLabelProps, 'control'> { | |||||
| name: string; | |||||
| readOnly?: boolean; | |||||
| } | |||||
| export function RHFCheckbox({ name, readOnly, ...other }: RHFCheckboxProps) { | |||||
| const { control, watch } = useFormContext(); | |||||
| const formValue: boolean = watch(name); | |||||
| if (readOnly) { | |||||
| return ( | |||||
| <FormControlLabel | |||||
| control={ | |||||
| <Checkbox | |||||
| disableRipple | |||||
| disableTouchRipple | |||||
| disableFocusRipple | |||||
| checked={formValue} | |||||
| sx={{ | |||||
| cursor: 'default', | |||||
| }} | |||||
| /> | |||||
| } | |||||
| sx={{ | |||||
| cursor: 'default', | |||||
| }} | |||||
| {...other} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <FormControlLabel | |||||
| control={ | |||||
| <Controller | |||||
| name={name} | |||||
| control={control} | |||||
| render={({ field }) => <Checkbox {...field} checked={field.value} />} | |||||
| /> | |||||
| } | |||||
| {...other} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| // ---------------------------------------------------------------------- | |||||
| interface RHFMultiCheckboxProps extends Omit<FormControlLabelProps, 'control' | 'label'> { | |||||
| name: string; | |||||
| options: { | |||||
| label: string; | |||||
| value: any; | |||||
| }[]; | |||||
| } | |||||
| export function RHFMultiCheckbox({ name, options, ...other }: RHFMultiCheckboxProps) { | |||||
| const { control } = useFormContext(); | |||||
| return ( | |||||
| <Controller | |||||
| name={name} | |||||
| control={control} | |||||
| render={({ field }) => { | |||||
| const onSelected = (option: string) => | |||||
| field.value.includes(option) | |||||
| ? field.value.filter((value: string) => value !== option) | |||||
| : [...field.value, option]; | |||||
| return ( | |||||
| <FormGroup> | |||||
| {options.map((option) => ( | |||||
| <FormControlLabel | |||||
| key={option.value} | |||||
| control={ | |||||
| <Checkbox | |||||
| checked={field.value.includes(option.value)} | |||||
| onChange={() => field.onChange(onSelected(option.value))} | |||||
| /> | |||||
| } | |||||
| label={option.label} | |||||
| {...other} | |||||
| /> | |||||
| ))} | |||||
| </FormGroup> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,106 @@ | |||||
| // form | |||||
| import { | |||||
| Controller, | |||||
| ControllerFieldState, | |||||
| ControllerRenderProps, | |||||
| FieldValues, | |||||
| useFormContext, | |||||
| } from "react-hook-form"; | |||||
| // @mui | |||||
| import ClearIcon from "@mui/icons-material/Clear"; | |||||
| import { DatePicker } from "@mui/lab"; | |||||
| import { | |||||
| IconButton, | |||||
| InputAdornment, | |||||
| TextField, | |||||
| TextFieldProps, | |||||
| } from "@mui/material"; | |||||
| import React, { useMemo } from "react"; | |||||
| import RHFTextField from "./RHFTextField"; | |||||
| // ---------------------------------------------------------------------- | |||||
| type IProps = { | |||||
| name: string; | |||||
| readOnly?: boolean; | |||||
| datePickerProps?: any; | |||||
| }; | |||||
| type Props = IProps & TextFieldProps; | |||||
| export type RHFDatePickerProps = Props; | |||||
| const RHFDatePicker = ({ | |||||
| name, | |||||
| readOnly, | |||||
| datePickerProps, | |||||
| ...other | |||||
| }: Props) => { | |||||
| const { control, watch, setValue } = useFormContext(); | |||||
| const value: Date | null = watch(name); | |||||
| const handleClear = () => { | |||||
| setValue(name, null); | |||||
| }; | |||||
| const icon = useMemo(() => { | |||||
| if (value !== null) { | |||||
| return ( | |||||
| <InputAdornment position="end"> | |||||
| <IconButton onClick={handleClear} edge="end"> | |||||
| <ClearIcon /> | |||||
| </IconButton> | |||||
| </InputAdornment> | |||||
| ); | |||||
| } else { | |||||
| return null; | |||||
| } | |||||
| }, [value]); | |||||
| const iconMerge = (ele: React.ReactNode) => { | |||||
| return ( | |||||
| <> | |||||
| {icon} | |||||
| {ele} | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| if (readOnly) { | |||||
| return <RHFTextField name={name} readOnly {...other} variant="standard" />; | |||||
| } | |||||
| const render = ({ | |||||
| field, | |||||
| fieldState, | |||||
| }: { | |||||
| field: ControllerRenderProps<FieldValues, string>; | |||||
| fieldState: ControllerFieldState; | |||||
| }) => { | |||||
| return ( | |||||
| <DatePicker | |||||
| inputFormat="yyyy/MM/dd" | |||||
| mask="____/__/__" | |||||
| renderInput={(params: any) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| fullWidth | |||||
| error={fieldState.invalid} | |||||
| helperText={fieldState.error?.message} | |||||
| InputProps={{ | |||||
| ...params.InputProps, | |||||
| endAdornment: iconMerge(params.InputProps?.endAdornment), | |||||
| }} | |||||
| {...other} | |||||
| /> | |||||
| )} | |||||
| {...datePickerProps} | |||||
| {...field} | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| return <Controller name={name} control={control} render={render} />; | |||||
| }; | |||||
| export default React.memo(RHFDatePicker); | |||||
| @@ -0,0 +1,53 @@ | |||||
| // form | |||||
| import { useFormContext, Controller } from 'react-hook-form'; | |||||
| // @mui | |||||
| import { | |||||
| Radio, | |||||
| RadioGroup, | |||||
| FormHelperText, | |||||
| RadioGroupProps, | |||||
| FormControlLabel, | |||||
| } from '@mui/material'; | |||||
| // ---------------------------------------------------------------------- | |||||
| type IProps = { | |||||
| name: string; | |||||
| options: { | |||||
| label: string; | |||||
| value: any; | |||||
| }[]; | |||||
| }; | |||||
| type Props = IProps & RadioGroupProps; | |||||
| export default function RHFRadioGroup({ name, options, ...other }: Props) { | |||||
| const { control } = useFormContext(); | |||||
| return ( | |||||
| <Controller | |||||
| name={name} | |||||
| control={control} | |||||
| render={({ field, fieldState: { error } }) => ( | |||||
| <div> | |||||
| <RadioGroup {...field} row {...other}> | |||||
| {options.map((option) => ( | |||||
| <FormControlLabel | |||||
| key={option.value} | |||||
| value={option.value} | |||||
| control={<Radio />} | |||||
| label={option.label} | |||||
| /> | |||||
| ))} | |||||
| </RadioGroup> | |||||
| {!!error && ( | |||||
| <FormHelperText error sx={{ px: 2 }}> | |||||
| {error.message} | |||||
| </FormHelperText> | |||||
| )} | |||||
| </div> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,119 @@ | |||||
| import { useFormContext, Controller } from "react-hook-form"; | |||||
| import { MenuItem, TextField, TextFieldProps } from "@mui/material"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import TextFieldEx from "../form/TextFieldEx"; | |||||
| import { Dictionary } from "@types"; | |||||
| // ---------------------------------------------------------------------- | |||||
| export type SelectOptionProps = { | |||||
| value: string; | |||||
| label: string; | |||||
| }; | |||||
| export const makeOptions = (list: Dictionary[]): SelectOptionProps[] => { | |||||
| const ret: SelectOptionProps[] = []; | |||||
| list.forEach((dic) => { | |||||
| const value = Object.keys(dic)[0]; | |||||
| const label = dic[value]; | |||||
| ret.push({ | |||||
| value, | |||||
| label, | |||||
| }); | |||||
| }); | |||||
| return ret; | |||||
| }; | |||||
| type IProps = { | |||||
| name: string; | |||||
| children?: any; | |||||
| readOnly?: boolean; | |||||
| options?: SelectOptionProps[]; | |||||
| onFix?: VoidFunction; | |||||
| }; | |||||
| type Props = IProps & TextFieldProps; | |||||
| export type RHFSelectProps = Props; | |||||
| export default function RHFSelect({ | |||||
| name, | |||||
| children, | |||||
| readOnly, | |||||
| options, | |||||
| onFix, | |||||
| ...other | |||||
| }: Props) { | |||||
| const { control, watch } = useFormContext(); | |||||
| const formValue: string = watch(name); | |||||
| const [isMounted, setIsMounted] = useState(false); | |||||
| const getOptionElements = useCallback(() => { | |||||
| if (options) { | |||||
| if (options.length === 0) { | |||||
| return [ | |||||
| <MenuItem key="disabled" disabled> | |||||
| 候補がありません | |||||
| </MenuItem>, | |||||
| ]; | |||||
| } else { | |||||
| return options.map((option, index) => { | |||||
| return ( | |||||
| <MenuItem value={option.value} key={index}> | |||||
| {option.label} | |||||
| </MenuItem> | |||||
| ); | |||||
| }); | |||||
| } | |||||
| } | |||||
| return []; | |||||
| }, [options]); | |||||
| const getLabel = useMemo(() => { | |||||
| if (!options) return ""; | |||||
| return ( | |||||
| options.find((option) => { | |||||
| return option.value === formValue; | |||||
| })?.label ?? " " | |||||
| ); | |||||
| }, [formValue, options]); | |||||
| useEffect(() => { | |||||
| if (isMounted && onFix) { | |||||
| onFix(); | |||||
| } | |||||
| setIsMounted(true); | |||||
| }, [formValue]); | |||||
| if (readOnly) { | |||||
| return ( | |||||
| <TextFieldEx readOnly {...other} value={getLabel} variant="standard" /> | |||||
| ); | |||||
| } | |||||
| if (!options) { | |||||
| return null; | |||||
| } | |||||
| return ( | |||||
| <Controller | |||||
| name={name} | |||||
| control={control} | |||||
| render={({ field, fieldState: { error } }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| select | |||||
| fullWidth | |||||
| error={!!error} | |||||
| helperText={error?.message} | |||||
| {...other} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| SelectProps={{}} | |||||
| > | |||||
| {children} | |||||
| {getOptionElements()} | |||||
| </TextField> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,29 @@ | |||||
| // form | |||||
| import { useFormContext, Controller } from 'react-hook-form'; | |||||
| // @mui | |||||
| import { Switch, FormControlLabel, FormControlLabelProps } from '@mui/material'; | |||||
| // ---------------------------------------------------------------------- | |||||
| type IProps = Omit<FormControlLabelProps, 'control'>; | |||||
| interface Props extends IProps { | |||||
| name: string; | |||||
| } | |||||
| export default function RHFSwitch({ name, ...other }: Props) { | |||||
| const { control } = useFormContext(); | |||||
| return ( | |||||
| <FormControlLabel | |||||
| control={ | |||||
| <Controller | |||||
| name={name} | |||||
| control={control} | |||||
| render={({ field }) => <Switch {...field} checked={field.value} />} | |||||
| /> | |||||
| } | |||||
| {...other} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,66 @@ | |||||
| import { useFormContext, Controller } from "react-hook-form"; | |||||
| import { TextField, TextFieldProps } from "@mui/material"; | |||||
| import { useMemo } from "react"; | |||||
| import { formatDateStr } from "utils/datetime"; | |||||
| import TextFieldEx from "../form/TextFieldEx"; | |||||
| // ---------------------------------------------------------------------- | |||||
| type IProps = { | |||||
| name: string; | |||||
| readOnly?: boolean; | |||||
| }; | |||||
| type Props = IProps & TextFieldProps; | |||||
| export type RHFTextFieldProps = Props; | |||||
| export default function RHFTextField({ | |||||
| name, | |||||
| readOnly, | |||||
| size: fieldSize = "small", | |||||
| ...other | |||||
| }: Props) { | |||||
| const { control, watch } = useFormContext(); | |||||
| const value = watch(name); | |||||
| const valueStr = useMemo(() => { | |||||
| if (typeof value === "string") { | |||||
| if (value === "") { | |||||
| return " "; | |||||
| } else { | |||||
| return value; | |||||
| } | |||||
| } | |||||
| if (value instanceof Date) { | |||||
| return formatDateStr(value); | |||||
| } | |||||
| if (readOnly) { | |||||
| return " "; | |||||
| } | |||||
| return ""; | |||||
| }, [value]); | |||||
| if (readOnly) { | |||||
| return ( | |||||
| <TextFieldEx readOnly {...other} value={valueStr} variant="standard" /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <Controller | |||||
| name={name} | |||||
| control={control} | |||||
| render={({ field, fieldState: { error } }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| fullWidth | |||||
| error={!!error} | |||||
| helperText={error?.message} | |||||
| {...other} | |||||
| size={fieldSize} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,33 @@ | |||||
| import { Dictionary } from "@types"; | |||||
| export * from "./RHFCheckbox"; | |||||
| export { default as FormProvider } from "./FormProvider"; | |||||
| export { default as RHFSwitch } from "./RHFSwitch"; | |||||
| export { default as RHFSelect } from "./RHFSelect"; | |||||
| export { default as RHFTextField } from "./RHFTextField"; | |||||
| export { default as RHFRadioGroup } from "./RHFRadioGroup"; | |||||
| export { default as RHFAutoComplete } from "./RHFAutoComplete"; | |||||
| /** | |||||
| * | |||||
| * @param formData object | |||||
| * @param setter RHFの関数setError | |||||
| * @param messages Dictionary | |||||
| */ | |||||
| export function setFormErrorMessages( | |||||
| formData: object, | |||||
| setter: any, | |||||
| messages: Dictionary | |||||
| ) { | |||||
| let count = 0; | |||||
| const keys = Object.keys(formData); | |||||
| Object.keys(messages).forEach((name) => { | |||||
| if (keys.includes(name)) { | |||||
| setter(name, { message: messages[name] }); | |||||
| count++; | |||||
| } | |||||
| }); | |||||
| return count; | |||||
| } | |||||
| @@ -0,0 +1,96 @@ | |||||
| // @mui | |||||
| import { Theme } from '@mui/material/styles'; | |||||
| import { | |||||
| Box, | |||||
| SxProps, | |||||
| Checkbox, | |||||
| TableRow, | |||||
| TableCell, | |||||
| TableHead, | |||||
| TableSortLabel, | |||||
| } from '@mui/material'; | |||||
| // ---------------------------------------------------------------------- | |||||
| const visuallyHidden = { | |||||
| border: 0, | |||||
| margin: -1, | |||||
| padding: 0, | |||||
| width: '1px', | |||||
| height: '1px', | |||||
| overflow: 'hidden', | |||||
| position: 'absolute', | |||||
| whiteSpace: 'nowrap', | |||||
| clip: 'rect(0 0 0 0)', | |||||
| } as const; | |||||
| // ---------------------------------------------------------------------- | |||||
| type Props = { | |||||
| order?: 'asc' | 'desc'; | |||||
| orderBy?: string; | |||||
| headLabel: any[]; | |||||
| rowCount?: number; | |||||
| numSelected?: number; | |||||
| onSort?: (id: string) => void; | |||||
| onSelectAllRows?: (checked: boolean) => void; | |||||
| sx?: SxProps<Theme>; | |||||
| }; | |||||
| export default function TableHeadCustom({ | |||||
| order, | |||||
| orderBy, | |||||
| rowCount = 0, | |||||
| headLabel, | |||||
| numSelected = 0, | |||||
| onSort, | |||||
| onSelectAllRows, | |||||
| sx, | |||||
| }: Props) { | |||||
| return ( | |||||
| <TableHead sx={sx}> | |||||
| <TableRow> | |||||
| {onSelectAllRows && ( | |||||
| <TableCell padding="checkbox"> | |||||
| <Checkbox | |||||
| indeterminate={numSelected > 0 && numSelected < rowCount} | |||||
| checked={rowCount > 0 && numSelected === rowCount} | |||||
| onChange={(event: React.ChangeEvent<HTMLInputElement>) => | |||||
| onSelectAllRows(event.target.checked) | |||||
| } | |||||
| /> | |||||
| </TableCell> | |||||
| )} | |||||
| {headLabel.map((headCell) => ( | |||||
| <TableCell | |||||
| key={headCell.id} | |||||
| align={headCell.align || 'left'} | |||||
| sortDirection={orderBy === headCell.id ? order : false} | |||||
| sx={{ width: headCell.width, minWidth: headCell.minWidth }} | |||||
| > | |||||
| {onSort && headCell.needSort !== false ? ( | |||||
| <TableSortLabel | |||||
| hideSortIcon | |||||
| active={orderBy === headCell.id} | |||||
| direction={orderBy === headCell.id ? order : 'asc'} | |||||
| onClick={() => onSort(headCell.id)} | |||||
| sx={{ textTransform: 'capitalize' }} | |||||
| > | |||||
| {headCell.label} | |||||
| {orderBy === headCell.id ? ( | |||||
| <Box sx={{ ...visuallyHidden }}> | |||||
| {order === 'desc' ? 'sorted descending' : 'sorted ascending'} | |||||
| </Box> | |||||
| ) : null} | |||||
| </TableSortLabel> | |||||
| ) : ( | |||||
| headCell.label | |||||
| )} | |||||
| </TableCell> | |||||
| ))} | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| export { default as TableHeadCustom } from "./TableHeadCustom"; | |||||
| @@ -0,0 +1,3 @@ | |||||
| // API | |||||
| // ---------------------------------------------------------------------- | |||||
| export const HOST_API = process.env.REACT_APP_HOST_API_KEY || ""; | |||||
| @@ -0,0 +1,126 @@ | |||||
| import { HasChildren } from "@types"; | |||||
| import { ResultCode } from "api"; | |||||
| import { login as APILogin, logout as APILogout, me } from "api/auth"; | |||||
| import { UserRole } from "codes/user"; | |||||
| import useAPICall from "hooks/useAPICall"; | |||||
| import { createContext, memo, useEffect, useMemo, useState } from "react"; | |||||
| type Auth = { | |||||
| initialized: boolean; | |||||
| authenticated: boolean; | |||||
| role: UserRole; | |||||
| contractId: string | null; | |||||
| login: (email: string, password: string) => Promise<boolean>; | |||||
| logout: VoidFunction; | |||||
| changeContractId: (contractId: string) => Promise<boolean>; | |||||
| checkRole: (role?: UserRole) => boolean; | |||||
| }; | |||||
| export const AuthContext = createContext<Auth>({ | |||||
| initialized: false, | |||||
| authenticated: false, | |||||
| role: UserRole.NONE, | |||||
| contractId: null, | |||||
| login: async (email: string, password: string) => false, | |||||
| logout: () => {}, | |||||
| changeContractId: async (contractId: string) => false, | |||||
| checkRole: (role?: UserRole) => false, | |||||
| }); | |||||
| type Props = HasChildren; | |||||
| function AuthContextProvider({ children }: Props) { | |||||
| const [initialized, setInitialized] = useState(false); | |||||
| const [role, setRole] = useState<UserRole>(UserRole.NONE); | |||||
| const [contractId, setContractId] = useState<string | null>(null); | |||||
| const authenticated = useMemo(() => { | |||||
| return role !== UserRole.NONE; | |||||
| }, [role]); | |||||
| const { callAPI: callMe } = useAPICall({ | |||||
| apiMethod: me, | |||||
| onSuccess: (res) => { | |||||
| setContractId(res.data.contract_id); | |||||
| setRole(res.data.role); | |||||
| setInitialized(true); | |||||
| }, | |||||
| onFailed: () => { | |||||
| clear(); | |||||
| setInitialized(true); | |||||
| }, | |||||
| }); | |||||
| const { callAPI: callLogin } = useAPICall({ | |||||
| apiMethod: APILogin, | |||||
| onSuccess: (res) => { | |||||
| setContractId(res.data.contract_id); | |||||
| setRole(res.data.role); | |||||
| }, | |||||
| }); | |||||
| const { callAPI: callLogout } = useAPICall({ | |||||
| apiMethod: APILogout, | |||||
| onSuccess: () => { | |||||
| clear(); | |||||
| }, | |||||
| }); | |||||
| const clear = () => { | |||||
| setRole(UserRole.NONE); | |||||
| setContractId(null); | |||||
| }; | |||||
| const login = async (email: string, password: string) => { | |||||
| const res = await callLogin({ email, password }); | |||||
| if (!res) return false; | |||||
| return res.result === ResultCode.SUCCESS; | |||||
| }; | |||||
| const logout = () => { | |||||
| callLogout({}); | |||||
| console.info("ログアウト"); | |||||
| }; | |||||
| const changeContractId = async (contractId: string) => { | |||||
| console.error("未実装 成り代わり"); | |||||
| return false; | |||||
| }; | |||||
| const checkRole = (targetRole?: UserRole): boolean => { | |||||
| if (targetRole === undefined) return true; | |||||
| return targetRole < role; | |||||
| }; | |||||
| useEffect(() => { | |||||
| callMe({}); | |||||
| }, []); | |||||
| return ( | |||||
| <AuthContext.Provider | |||||
| value={{ | |||||
| // Value | |||||
| initialized, | |||||
| authenticated, | |||||
| role, | |||||
| contractId, | |||||
| // Func | |||||
| login, | |||||
| logout, | |||||
| changeContractId, | |||||
| checkRole, | |||||
| }} | |||||
| > | |||||
| {children} | |||||
| </AuthContext.Provider> | |||||
| ); | |||||
| } | |||||
| export default memo(AuthContextProvider); | |||||
| @@ -1,6 +1,7 @@ | |||||
| import { HasChildren } from "@types"; | import { HasChildren } from "@types"; | ||||
| import { PageID } from "codes/page"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| import usePage from "hooks/usePage"; | import usePage from "hooks/usePage"; | ||||
| import useResponsive from "hooks/useResponsive"; | |||||
| import useWindowSize from "hooks/useWindowSize"; | import useWindowSize from "hooks/useWindowSize"; | ||||
| import { ReactNode, createContext, useMemo, useState } from "react"; | import { ReactNode, createContext, useMemo, useState } from "react"; | ||||
| @@ -14,7 +15,12 @@ type ContextProps = { | |||||
| tabs: ReactNode | null; | tabs: ReactNode | null; | ||||
| setTabs: (tabs: ReactNode | null) => void; | setTabs: (tabs: ReactNode | null) => void; | ||||
| pageId: PageID; | pageId: PageID; | ||||
| setPageId: (tabs: PageID) => void; | |||||
| setPageId: (pageId: PageID) => void; | |||||
| tabId: TabID; | |||||
| setTabId: (tabId: TabID) => void; | |||||
| showDrawer: boolean; | |||||
| overBreakPoint: boolean; | |||||
| }; | }; | ||||
| const contextInit: ContextProps = { | const contextInit: ContextProps = { | ||||
| headerTitle: "", | headerTitle: "", | ||||
| @@ -26,7 +32,12 @@ const contextInit: ContextProps = { | |||||
| tabs: null, | tabs: null, | ||||
| setTabs: (tabs: ReactNode | null) => {}, | setTabs: (tabs: ReactNode | null) => {}, | ||||
| pageId: PageID.NONE, | pageId: PageID.NONE, | ||||
| setPageId: (tabs: PageID) => {}, | |||||
| setPageId: (pageId: PageID) => {}, | |||||
| tabId: TabID.NONE, | |||||
| setTabId: (tabId: TabID) => {}, | |||||
| showDrawer: false, | |||||
| overBreakPoint: false, | |||||
| }; | }; | ||||
| export const DashboardLayoutContext = createContext(contextInit); | export const DashboardLayoutContext = createContext(contextInit); | ||||
| @@ -38,11 +49,15 @@ export function DashboardLayoutContextProvider({ children }: Props) { | |||||
| const [tabs, setTabs] = useState<ReactNode | null>(null); | const [tabs, setTabs] = useState<ReactNode | null>(null); | ||||
| const { width: innerWidth, height: innerHeight } = useWindowSize(); | const { width: innerWidth, height: innerHeight } = useWindowSize(); | ||||
| const { pageId, setPageId } = usePage(); | |||||
| const { pageId, setPageId, tabId, setTabId } = usePage(); | |||||
| const overBreakPoint = !!useResponsive("up", "sm"); | |||||
| const showDrawer = useMemo(() => overBreakPoint, [overBreakPoint]); | |||||
| const contentsWidth = useMemo(() => { | const contentsWidth = useMemo(() => { | ||||
| return innerWidth - drawerWidth; | |||||
| }, [drawerWidth, innerWidth]); | |||||
| return innerWidth - (showDrawer ? drawerWidth : 0); | |||||
| }, [drawerWidth, innerWidth, showDrawer]); | |||||
| return ( | return ( | ||||
| <DashboardLayoutContext.Provider | <DashboardLayoutContext.Provider | ||||
| @@ -57,6 +72,11 @@ export function DashboardLayoutContextProvider({ children }: Props) { | |||||
| setTabs, | setTabs, | ||||
| pageId, | pageId, | ||||
| setPageId, | setPageId, | ||||
| tabId, | |||||
| setTabId, | |||||
| showDrawer, | |||||
| overBreakPoint, | |||||
| }} | }} | ||||
| > | > | ||||
| {children} | {children} | ||||
| @@ -1,27 +1,33 @@ | |||||
| import { HasChildren } from "@types"; | import { HasChildren } from "@types"; | ||||
| import { PageID } from "codes/page"; | |||||
| import useWindowSize from "hooks/useWindowSize"; | |||||
| import { ReactNode, createContext, useMemo, useState } from "react"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| import { createContext, useState } from "react"; | |||||
| type ContextProps = { | type ContextProps = { | ||||
| pageId: PageID; | pageId: PageID; | ||||
| tabId: TabID; | |||||
| setPageId: (pageId: PageID) => void; | setPageId: (pageId: PageID) => void; | ||||
| setTabId: (tabId: TabID) => void; | |||||
| }; | }; | ||||
| const contextInit: ContextProps = { | const contextInit: ContextProps = { | ||||
| pageId: PageID.NONE, | pageId: PageID.NONE, | ||||
| tabId: TabID.NONE, | |||||
| setPageId: (pageId: PageID) => {}, | setPageId: (pageId: PageID) => {}, | ||||
| setTabId: (tabId: TabID) => {}, | |||||
| }; | }; | ||||
| export const PageContext = createContext(contextInit); | export const PageContext = createContext(contextInit); | ||||
| type Props = HasChildren; | type Props = HasChildren; | ||||
| export function PageContextProvider({ children }: Props) { | export function PageContextProvider({ children }: Props) { | ||||
| const [pageId, setPageId] = useState<PageID>(PageID.NONE); | const [pageId, setPageId] = useState<PageID>(PageID.NONE); | ||||
| const [tabId, setTabId] = useState<TabID>(TabID.NONE); | |||||
| return ( | return ( | ||||
| <PageContext.Provider | <PageContext.Provider | ||||
| value={{ | value={{ | ||||
| pageId, | pageId, | ||||
| tabId, | |||||
| setPageId, | setPageId, | ||||
| setTabId, | |||||
| }} | }} | ||||
| > | > | ||||
| {children} | {children} | ||||
| @@ -0,0 +1,131 @@ | |||||
| import { cloneDeep, isEqual, unset } from "lodash"; | |||||
| import { ReactNode, createContext, useEffect, useMemo, useState } from "react"; | |||||
| // form | |||||
| import { useLocation } from "react-router"; | |||||
| import { Dictionary } from "@types"; | |||||
| import useNavigateCustom from "hooks/useNavigateCustom"; | |||||
| // ---------------------------------------------------------------------- | |||||
| const _condition: Dictionary = {}; | |||||
| const initialState = { | |||||
| initialized: false, | |||||
| condition: _condition, | |||||
| get: (key: string): string => "", | |||||
| initializeCondition: () => {}, | |||||
| addCondition: (condition: Dictionary) => {}, | |||||
| clearCondition: () => {}, | |||||
| }; | |||||
| export const SearchConditionContext = createContext(initialState); | |||||
| type Props = { | |||||
| children: ReactNode; | |||||
| }; | |||||
| export function SearchConditionContextProvider({ children }: Props) { | |||||
| const [condition, _setCondition] = useState<Dictionary>({}); | |||||
| const [initialized, setInitialized] = useState(false); | |||||
| const { pathname, search } = useLocation(); | |||||
| const { navigateWhenChanged } = useNavigateCustom(); | |||||
| const setCondition = (after: Dictionary, message?: any) => { | |||||
| if (message) { | |||||
| console.log("Contidion Change", { after, message }); | |||||
| } | |||||
| _setCondition(after); | |||||
| }; | |||||
| const initializeCondition = () => { | |||||
| const after: Dictionary = {}; | |||||
| const urlParam = new URLSearchParams(search); | |||||
| for (const [key, value] of urlParam.entries()) { | |||||
| after[key] = value; | |||||
| } | |||||
| if (!isEqual(after, condition)) { | |||||
| console.log("initialCondition", { before: condition, after }); | |||||
| setCondition(after, "initializeCondition"); | |||||
| } | |||||
| setInitialized(true); | |||||
| }; | |||||
| const get = (key: string) => { | |||||
| return condition[key] ?? ""; | |||||
| }; | |||||
| const getCondition = useMemo(() => { | |||||
| return cloneDeep(condition); | |||||
| }, [condition]); | |||||
| const addCondition = (additional: Dictionary) => { | |||||
| if (!initialized) return; | |||||
| const before = cloneDeep(condition); | |||||
| const after = cloneDeep(condition); | |||||
| Object.keys(additional).forEach((key) => { | |||||
| unset(after, key); | |||||
| if (additional[key] !== "") { | |||||
| after[key] = additional[key]; | |||||
| } | |||||
| }); | |||||
| if (!isEqual(before, after)) { | |||||
| console.log("addCondition", { additional, before, after }); | |||||
| setCondition(after, "addCondition"); | |||||
| } | |||||
| }; | |||||
| const searchParams = useMemo(() => { | |||||
| const params = new URLSearchParams(); | |||||
| if (!initialized) return params; | |||||
| Object.keys(condition).forEach((key) => { | |||||
| params.append(key, condition[key]); | |||||
| }); | |||||
| params.sort(); | |||||
| return params; | |||||
| }, [condition]); | |||||
| const applyToURL = () => { | |||||
| if (!initialized) return; | |||||
| const params = searchParams; | |||||
| const searchStr = params.toString(); | |||||
| const url = pathname + (searchStr ? "?" + searchStr : ""); | |||||
| navigateWhenChanged(pathname, condition, "applyToURL"); | |||||
| }; | |||||
| const clearCondition = () => { | |||||
| setCondition({}, "clearCondition"); | |||||
| setInitialized(false); | |||||
| console.log("clearCondition"); | |||||
| }; | |||||
| useEffect(() => { | |||||
| if (initialized) { | |||||
| console.log("call applyToURL", { condition, initialized }); | |||||
| applyToURL(); | |||||
| } | |||||
| }, [condition, initialized]); | |||||
| useEffect(() => { | |||||
| initializeCondition(); | |||||
| }, [pathname, search]); | |||||
| return ( | |||||
| <SearchConditionContext.Provider | |||||
| value={{ | |||||
| condition: getCondition, | |||||
| initialized, | |||||
| get, | |||||
| initializeCondition, | |||||
| addCondition, | |||||
| clearCondition, | |||||
| }} | |||||
| > | |||||
| {children} | |||||
| </SearchConditionContext.Provider> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,168 @@ | |||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import { APICommonResponse, apiRequest, ResultCode } from "api"; | |||||
| import { UseFormReturn } from "react-hook-form"; | |||||
| import { Dictionary } from "@types"; | |||||
| import { useSnackbar } from "notistack"; | |||||
| export const APIErrorType = { | |||||
| NONE: "none", | |||||
| INPUT: "input", | |||||
| SERVER: "server", | |||||
| EXCLUSIVE: "exclusive", | |||||
| } as const; | |||||
| export type APIErrorType = (typeof APIErrorType)[keyof typeof APIErrorType]; | |||||
| export default function useAPICall< | |||||
| T extends object, | |||||
| U extends APICommonResponse | |||||
| >({ | |||||
| apiMethod, | |||||
| onSuccess, | |||||
| onFailed, | |||||
| form, | |||||
| successMessage = false, | |||||
| failedMessage = false, | |||||
| }: { | |||||
| apiMethod: (sendData: T) => Promise<U | null>; | |||||
| onSuccess?: (res: U, sendData: T) => void; | |||||
| onFailed?: (res: APICommonResponse | null) => void; | |||||
| form?: UseFormReturn<any>; | |||||
| successMessage?: ((res: U) => string) | boolean; | |||||
| failedMessage?: ((res: APICommonResponse | null) => string) | boolean; | |||||
| }) { | |||||
| const [sending, setSending] = useState(false); | |||||
| const [sendData, setSendData] = useState<T | null>(null); | |||||
| const [result, setResult] = useState<ResultCode | null>(null); | |||||
| const [errors, setErrors] = useState<Dictionary>({}); | |||||
| const [validationError, setValidationError] = useState(false); | |||||
| const [exclusiveError, setExclusiveError] = useState(false); | |||||
| const [generalErrorMessage, setGeneralErrorMessage] = useState(""); | |||||
| const { enqueueSnackbar } = useSnackbar(); | |||||
| const clearErrors = () => { | |||||
| setResult(null); | |||||
| setErrors({}); | |||||
| setSendData(null); | |||||
| setValidationError(false); | |||||
| setExclusiveError(false); | |||||
| setGeneralErrorMessage(""); | |||||
| }; | |||||
| // 入力項目に対してエラーレスポンスがあるか、ないかでエラーのタイプを設定する | |||||
| const errorMode = useMemo<APIErrorType>(() => { | |||||
| if (generalErrorMessage !== "") return APIErrorType.INPUT; | |||||
| if (exclusiveError) return APIErrorType.EXCLUSIVE; | |||||
| if (validationError) return APIErrorType.INPUT; | |||||
| if (result === null) return APIErrorType.NONE; | |||||
| if (result === ResultCode.SUCCESS) return APIErrorType.NONE; | |||||
| if (sendData === null) return APIErrorType.NONE; | |||||
| const messageKeys = Object.keys(errors); | |||||
| if (messageKeys.length === 0) return APIErrorType.SERVER; | |||||
| if (sendData) { | |||||
| const sendDataKeys = Object.keys(sendData); | |||||
| const find = messageKeys.find((key: string) => { | |||||
| return sendDataKeys.includes(key); | |||||
| }); | |||||
| if (find) { | |||||
| return APIErrorType.INPUT; | |||||
| } | |||||
| } | |||||
| return APIErrorType.SERVER; | |||||
| }, [result, errors, validationError, exclusiveError, generalErrorMessage]); | |||||
| const getSuccessMessageStr = useCallback( | |||||
| (res: U) => { | |||||
| if (typeof successMessage === "boolean") { | |||||
| return successMessage ? "成功しました" : ""; | |||||
| } | |||||
| return successMessage(res); | |||||
| }, | |||||
| [successMessage] | |||||
| ); | |||||
| const getFailedMessageStr = useCallback( | |||||
| (res: APICommonResponse | null) => { | |||||
| if (typeof failedMessage === "boolean") { | |||||
| return failedMessage ? "失敗しました" : ""; | |||||
| } | |||||
| return failedMessage(res); | |||||
| }, | |||||
| [failedMessage] | |||||
| ); | |||||
| const successCallback = (res: U, sendData: T) => { | |||||
| setResult(ResultCode.SUCCESS); | |||||
| const message = getSuccessMessageStr(res); | |||||
| if (message) { | |||||
| enqueueSnackbar(message); | |||||
| } | |||||
| if (onSuccess) { | |||||
| onSuccess(res, sendData); | |||||
| } | |||||
| }; | |||||
| const failedCallback = (res: APICommonResponse | null) => { | |||||
| if (res?.result === ResultCode.EXCLUSIVE_ERROR) { | |||||
| setExclusiveError(true); | |||||
| } | |||||
| if (res) { | |||||
| setResult(res.result); | |||||
| } | |||||
| if (res?.messages.errors) { | |||||
| console.log("seterrors", res.messages.errors); | |||||
| setErrors(res.messages.errors); | |||||
| } | |||||
| if (res?.messages.general) { | |||||
| setGeneralErrorMessage(res.messages.general); | |||||
| } | |||||
| const message = getFailedMessageStr(res); | |||||
| if (message) { | |||||
| enqueueSnackbar(message, { variant: "error" }); | |||||
| } | |||||
| if (onFailed) { | |||||
| onFailed(res); | |||||
| } | |||||
| }; | |||||
| const handleValidationError = (error: any) => { | |||||
| console.log(error); | |||||
| setValidationError(true); | |||||
| }; | |||||
| const callAPI = async (sendData: T) => { | |||||
| if (form) { | |||||
| form.clearErrors(); | |||||
| } | |||||
| clearErrors(); | |||||
| setSendData(sendData); | |||||
| const res = await apiRequest({ | |||||
| apiMethod, | |||||
| onSuccess: successCallback, | |||||
| onFailed: failedCallback, | |||||
| sendData, | |||||
| setSending, | |||||
| errorSetter: form?.setError, | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| const makeSendData = (data: T) => { | |||||
| return data; | |||||
| }; | |||||
| return { | |||||
| callAPI, | |||||
| sending, | |||||
| errorMode, | |||||
| generalErrorMessage, | |||||
| handleValidationError, | |||||
| makeSendData, | |||||
| }; | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| import { AuthContext } from "contexts/AuthContext"; | |||||
| import { useContext } from "react"; | |||||
| export default function useAuth() { | |||||
| const context = useContext(AuthContext); | |||||
| return context; | |||||
| } | |||||
| @@ -1,14 +1,16 @@ | |||||
| import { useContext, useEffect } from "react"; | import { useContext, useEffect } from "react"; | ||||
| import { DashboardLayoutContext } from "contexts/DashboardLayoutContext"; | import { DashboardLayoutContext } from "contexts/DashboardLayoutContext"; | ||||
| import { PageID } from "codes/page"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| export default function useDashboard(pageId?: PageID) { | |||||
| export default function useDashboard(pageId?: PageID, tabId?: TabID) { | |||||
| const context = useContext(DashboardLayoutContext); | const context = useContext(DashboardLayoutContext); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (pageId) { | if (pageId) { | ||||
| context.setPageId(pageId ?? PageID.NONE); | |||||
| // console.log("CURRENT", pageId); | |||||
| context.setPageId(pageId); | |||||
| } | |||||
| if (tabId) { | |||||
| context.setTabId(tabId); | |||||
| } | } | ||||
| }, []); | }, []); | ||||
| @@ -1,5 +1,6 @@ | |||||
| import { useLocation, useNavigate } from "react-router"; | import { useLocation, useNavigate } from "react-router"; | ||||
| import { Dictionary } from "@types"; | import { Dictionary } from "@types"; | ||||
| import { PageID } from "codes/page"; | |||||
| export default function useNavigateCustom() { | export default function useNavigateCustom() { | ||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| @@ -0,0 +1,6 @@ | |||||
| import { useContext } from "react"; | |||||
| import { SearchConditionContext } from "contexts/SearchConditionContext"; | |||||
| export default function useSearchConditionContext() { | |||||
| return useContext(SearchConditionContext); | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| import { OptionsObject, useSnackbar } from "notistack"; | |||||
| export default function useSnackbarCustom() { | |||||
| const { enqueueSnackbar } = useSnackbar(); | |||||
| const info = (message: string, option?: OptionsObject) => { | |||||
| enqueueSnackbar(message, { variant: "info", ...option }); | |||||
| }; | |||||
| const success = (message: string, option?: OptionsObject) => { | |||||
| enqueueSnackbar(message, { variant: "success", ...option }); | |||||
| }; | |||||
| const warn = (message: string, option?: OptionsObject) => { | |||||
| enqueueSnackbar(message, { variant: "warning", ...option }); | |||||
| }; | |||||
| const error = (message: string, option?: OptionsObject) => { | |||||
| enqueueSnackbar(message, { variant: "error", ...option }); | |||||
| }; | |||||
| return { | |||||
| enqueueSnackbar, | |||||
| info, | |||||
| success, | |||||
| warn, | |||||
| error, | |||||
| }; | |||||
| } | |||||
| @@ -0,0 +1,177 @@ | |||||
| import { ceil, max } from "lodash"; | |||||
| import { useEffect, useMemo, useState } from "react"; | |||||
| import { useParams } from "react-router"; | |||||
| import useNavigateCustom from "./useNavigateCustom"; | |||||
| import useSearchConditionContext from "./useSearchConditionContext"; | |||||
| import useURLSearchParams from "./useURLSearchParams"; | |||||
| import usePage from "./usePage"; | |||||
| import { getListPagePath } from "routes/path"; | |||||
| // ---------------------------------------------------------------------- | |||||
| export type UseTableReturn<T extends object> = { | |||||
| order: "asc" | "desc"; | |||||
| page: number; | |||||
| sort: string; | |||||
| rowsPerPage: number; | |||||
| fetched: boolean; | |||||
| row: T[]; | |||||
| fillteredRow: T[]; | |||||
| isNotFound: boolean; | |||||
| dataLength: number; | |||||
| // | |||||
| onSort: (id: string) => void; | |||||
| onChangePage: (event: unknown, page: number) => void; | |||||
| onChangeRowsPerPage: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||||
| // | |||||
| setRowData: (data: T[]) => void; | |||||
| // | |||||
| ROWS_PER_PAGES: number[]; | |||||
| }; | |||||
| export default function useTable<T extends object>(): UseTableReturn<T> { | |||||
| const ROWS_PER_PAGES = [20, 50, 100]; | |||||
| const ORDER = "order"; | |||||
| const SORT = "sort"; | |||||
| const ROWS = "rows"; | |||||
| const { pageId } = usePage(); | |||||
| const { page: urlPage } = useParams(); | |||||
| const { navigateWhenChanged } = useNavigateCustom(); | |||||
| const { addCondition, initialized, get } = useSearchConditionContext(); | |||||
| const { search } = useURLSearchParams(); | |||||
| const [sort, setSort] = useState(""); | |||||
| const [order, setOrder] = useState<"asc" | "desc">("asc"); | |||||
| const [rowsPerPage, setRowsPerPage] = useState(ROWS_PER_PAGES[0]); | |||||
| const [page, setPage] = useState(0); | |||||
| const [data, setData] = useState<T[]>([]); | |||||
| const [fetched, setFetched] = useState(false); | |||||
| const [requestPage, setRequestPage] = useState<number | null>(null); | |||||
| const paging = (page: number) => { | |||||
| return getListPagePath(pageId, page); | |||||
| }; | |||||
| // 表示する行 | |||||
| const fillteredRow = useMemo(() => { | |||||
| if (requestPage !== page) { | |||||
| // console.log("fillteredRow DIFFs", { requestPage, page }); | |||||
| return []; | |||||
| } | |||||
| return data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | |||||
| }, [page, requestPage, rowsPerPage, data]); | |||||
| const isNotFound = useMemo(() => { | |||||
| if (data.length !== 0) return false; | |||||
| if (!fetched) return false; | |||||
| return true; | |||||
| }, [fetched, data]); | |||||
| const dataLength = useMemo(() => { | |||||
| return data.length; | |||||
| }, [data]); | |||||
| const maxPage = useMemo(() => { | |||||
| return max([ceil(dataLength / rowsPerPage) - 1, 0]) ?? 0; | |||||
| }, [dataLength, rowsPerPage]); | |||||
| const onSort = (id: string) => { | |||||
| const isAsc = sort === id && order === "asc"; | |||||
| if (id !== "") { | |||||
| const tmpOrder = isAsc ? "desc" : "asc"; | |||||
| setOrder(tmpOrder); | |||||
| setSort(id); | |||||
| addCondition({ | |||||
| [SORT]: id, | |||||
| [ORDER]: tmpOrder, | |||||
| }); | |||||
| } | |||||
| }; | |||||
| const onChangePage = (event: unknown, page: number) => { | |||||
| setRequestPage(page); | |||||
| }; | |||||
| const onChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | |||||
| const newRowsPerPage = parseInt(event.target.value, 10); | |||||
| setRowsPerPage(newRowsPerPage); | |||||
| setPage(0); | |||||
| addCondition({ | |||||
| rows: String(newRowsPerPage), | |||||
| }); | |||||
| }; | |||||
| // 一度ページを0にする | |||||
| // 下記のページ遷移制御によって、requestPageのと調整を行う | |||||
| const setRowData = (data: T[]) => { | |||||
| setPage(0); | |||||
| setData(data); | |||||
| setFetched(true); | |||||
| }; | |||||
| // 主にページ遷移を制御する | |||||
| // リストデータ数や1ページあたりの表示数によっては、現在のページが最大ページを超えてしまう可能性があるので | |||||
| // 一度、requestPageにページ番号をためておき、最大ページを超えないように調整する | |||||
| useEffect(() => { | |||||
| if (requestPage !== null && fetched) { | |||||
| const newPage = requestPage <= maxPage ? requestPage : maxPage; | |||||
| if (page !== newPage) { | |||||
| setPage(newPage); | |||||
| } | |||||
| if (requestPage !== newPage) { | |||||
| setRequestPage(newPage); | |||||
| } | |||||
| navigateWhenChanged(paging(newPage), search, "useTable.paging"); | |||||
| } | |||||
| }, [requestPage, maxPage, fetched, page]); | |||||
| // クエリパラメータから各初期値を設定する | |||||
| useEffect(() => { | |||||
| if (initialized) { | |||||
| setSort(get(SORT)); | |||||
| const order = get(ORDER); | |||||
| if (order === "asc" || order === "desc") { | |||||
| setOrder(order); | |||||
| } | |||||
| const rows = Number(get(ROWS)); | |||||
| setRowsPerPage(ROWS_PER_PAGES.includes(rows) ? rows : ROWS_PER_PAGES[0]); | |||||
| } | |||||
| }, [initialized]); | |||||
| useEffect(() => { | |||||
| setRequestPage(Number(urlPage ?? "0")); | |||||
| }, [urlPage]); | |||||
| return { | |||||
| order, | |||||
| page, | |||||
| sort, | |||||
| rowsPerPage, | |||||
| fetched, | |||||
| row: data, | |||||
| fillteredRow, | |||||
| isNotFound, | |||||
| dataLength, | |||||
| // | |||||
| onSort, | |||||
| onChangePage, | |||||
| onChangeRowsPerPage, | |||||
| // | |||||
| setRowData, | |||||
| // | |||||
| ROWS_PER_PAGES, | |||||
| }; | |||||
| } | |||||
| @@ -0,0 +1,48 @@ | |||||
| import { useEffect, useState } from 'react'; | |||||
| import { useLocation, useNavigate } from 'react-router'; | |||||
| export default function useURLSearchParams() { | |||||
| const navigate = useNavigate(); | |||||
| const { pathname, search } = useLocation(); | |||||
| const [urlParam, setUrlParam] = useState(new URLSearchParams(search)); | |||||
| const [needApply, setNeedApply] = useState(false); | |||||
| const setParam = () => { | |||||
| const path = pathname; | |||||
| const url = path + '?' + urlParam.toString(); | |||||
| const current = pathname + search; | |||||
| if (url !== current) { | |||||
| navigate(url); | |||||
| } | |||||
| setNeedApply(false); | |||||
| }; | |||||
| const appendAll = (list: { key: string; value: string | null | undefined }[]) => { | |||||
| list.forEach(({ key, value }) => { | |||||
| urlParam.delete(key); | |||||
| if (value) { | |||||
| urlParam.append(key, value); | |||||
| } | |||||
| }); | |||||
| urlParam.sort(); | |||||
| setNeedApply(true); | |||||
| }; | |||||
| useEffect(() => { | |||||
| setUrlParam(new URLSearchParams(search)); | |||||
| }, [search]); | |||||
| useEffect(() => { | |||||
| if (needApply) { | |||||
| setParam(); | |||||
| } | |||||
| }, [needApply]); | |||||
| return { | |||||
| search, | |||||
| urlParam, | |||||
| appendAll, | |||||
| }; | |||||
| } | |||||
| @@ -1,16 +1,15 @@ | |||||
| import React from 'react'; | |||||
| import ReactDOM from 'react-dom/client'; | |||||
| import './index.css'; | |||||
| import App from './App'; | |||||
| import reportWebVitals from './reportWebVitals'; | |||||
| import ReactDOM from "react-dom/client"; | |||||
| import App from "./App"; | |||||
| import "./index.css"; | |||||
| import reportWebVitals from "./reportWebVitals"; | |||||
| const root = ReactDOM.createRoot( | const root = ReactDOM.createRoot( | ||||
| document.getElementById('root') as HTMLElement | |||||
| document.getElementById("root") as HTMLElement | |||||
| ); | ); | ||||
| root.render( | root.render( | ||||
| <React.StrictMode> | |||||
| <App /> | |||||
| </React.StrictMode> | |||||
| // <React.StrictMode> | |||||
| <App /> | |||||
| // </React.StrictMode> | |||||
| ); | ); | ||||
| // If you want to start measuring performance in your app, pass a function | // If you want to start measuring performance in your app, pass a function | ||||
| @@ -23,9 +23,7 @@ function Copyright() { | |||||
| function App() { | function App() { | ||||
| const [mobileOpen, setMobileOpen] = useState(false); | const [mobileOpen, setMobileOpen] = useState(false); | ||||
| const isSmUp = useResponsive("up", "sm"); | |||||
| const { drawerWidth, innerHeight, innerWidth, contentsWidth } = | |||||
| const { drawerWidth, innerHeight, innerWidth, contentsWidth, showDrawer } = | |||||
| useDashboard(); | useDashboard(); | ||||
| const handleDrawerToggle = () => { | const handleDrawerToggle = () => { | ||||
| @@ -34,7 +32,7 @@ function App() { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log({ drawerWidth, innerWidth, contentsWidth }); | console.log({ drawerWidth, innerWidth, contentsWidth }); | ||||
| }, []); | |||||
| }, [drawerWidth, innerWidth, contentsWidth]); | |||||
| return ( | return ( | ||||
| <Box sx={{ display: "flex", minHeight: "100vh" }}> | <Box sx={{ display: "flex", minHeight: "100vh" }}> | ||||
| @@ -42,7 +40,7 @@ function App() { | |||||
| component="nav" | component="nav" | ||||
| sx={{ width: { sm: drawerWidth }, flexShrink: { md: 0 } }} | sx={{ width: { sm: drawerWidth }, flexShrink: { md: 0 } }} | ||||
| > | > | ||||
| {isSmUp ? null : ( | |||||
| {showDrawer && ( | |||||
| <Navigator | <Navigator | ||||
| PaperProps={{ style: { width: drawerWidth } }} | PaperProps={{ style: { width: drawerWidth } }} | ||||
| variant="temporary" | variant="temporary" | ||||
| @@ -55,7 +53,14 @@ function App() { | |||||
| sx={{ display: { sm: "block", xs: "none" } }} | sx={{ display: { sm: "block", xs: "none" } }} | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| <Box sx={{ flex: 1, display: "flex", flexDirection: "column" }}> | |||||
| <Box | |||||
| sx={{ | |||||
| flex: 1, | |||||
| display: "flex", | |||||
| flexDirection: "column", | |||||
| maxWidth: contentsWidth, | |||||
| }} | |||||
| > | |||||
| <Header onDrawerToggle={handleDrawerToggle} /> | <Header onDrawerToggle={handleDrawerToggle} /> | ||||
| <Box component="main" sx={{ flex: 1, py: 6, px: 4 }}> | <Box component="main" sx={{ flex: 1, py: 6, px: 4 }}> | ||||
| <Outlet /> | <Outlet /> | ||||
| @@ -1,34 +1,28 @@ | |||||
| import * as React from "react"; | |||||
| import { ExpandLess, ExpandMore } from "@mui/icons-material"; | |||||
| import HomeIcon from "@mui/icons-material/Home"; | |||||
| import PeopleIcon from "@mui/icons-material/People"; | |||||
| import SettingsIcon from "@mui/icons-material/Settings"; | |||||
| import { Collapse } from "@mui/material"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Divider from "@mui/material/Divider"; | import Divider from "@mui/material/Divider"; | ||||
| import Drawer, { DrawerProps } from "@mui/material/Drawer"; | import Drawer, { DrawerProps } from "@mui/material/Drawer"; | ||||
| import List from "@mui/material/List"; | import List from "@mui/material/List"; | ||||
| import Box from "@mui/material/Box"; | |||||
| import ListItem from "@mui/material/ListItem"; | import ListItem from "@mui/material/ListItem"; | ||||
| import ListItemButton from "@mui/material/ListItemButton"; | import ListItemButton from "@mui/material/ListItemButton"; | ||||
| import ListItemIcon from "@mui/material/ListItemIcon"; | import ListItemIcon from "@mui/material/ListItemIcon"; | ||||
| import ListItemText from "@mui/material/ListItemText"; | import ListItemText from "@mui/material/ListItemText"; | ||||
| import HomeIcon from "@mui/icons-material/Home"; | |||||
| import PeopleIcon from "@mui/icons-material/People"; | |||||
| import DnsRoundedIcon from "@mui/icons-material/DnsRounded"; | |||||
| import PermMediaOutlinedIcon from "@mui/icons-material/PhotoSizeSelectActual"; | |||||
| import PublicIcon from "@mui/icons-material/Public"; | |||||
| import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet"; | |||||
| import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; | |||||
| import TimerIcon from "@mui/icons-material/Timer"; | |||||
| import SettingsIcon from "@mui/icons-material/Settings"; | |||||
| import PhonelinkSetupIcon from "@mui/icons-material/PhonelinkSetup"; | |||||
| import { UserRole } from "codes/user"; | |||||
| import { useLocation } from "react-router-dom"; | |||||
| import { PATH, getPath } from "routes/path"; | |||||
| import usePage from "hooks/usePage"; | |||||
| import { PageID } from "codes/page"; | import { PageID } from "codes/page"; | ||||
| import { ExpandLess, ExpandMore } from "@mui/icons-material"; | |||||
| import { Collapse } from "@mui/material"; | |||||
| import { UserRole } from "codes/user"; | |||||
| import useAuth from "hooks/useAuth"; | |||||
| import useNavigateCustom from "hooks/useNavigateCustom"; | import useNavigateCustom from "hooks/useNavigateCustom"; | ||||
| import usePage from "hooks/usePage"; | |||||
| import * as React from "react"; | |||||
| import { getPath } from "routes/path"; | |||||
| type Group = { | type Group = { | ||||
| label: string; | label: string; | ||||
| children: SubGroup[]; | children: SubGroup[]; | ||||
| role?: UserRole; | |||||
| }; | }; | ||||
| type SubGroup = { | type SubGroup = { | ||||
| @@ -36,7 +30,7 @@ type SubGroup = { | |||||
| icon: React.ReactNode; | icon: React.ReactNode; | ||||
| children?: Child[]; | children?: Child[]; | ||||
| // 子要素を持たない場合は下記は必須 | |||||
| // 子要素を持たない場合は設定 | |||||
| id?: PageID; | id?: PageID; | ||||
| role?: UserRole; | role?: UserRole; | ||||
| }; | }; | ||||
| @@ -54,6 +48,7 @@ const categories: Group[] = [ | |||||
| { | { | ||||
| label: "契約", | label: "契約", | ||||
| icon: <PeopleIcon />, | icon: <PeopleIcon />, | ||||
| role: UserRole.SUPER_ADMIN, | |||||
| children: [ | children: [ | ||||
| { | { | ||||
| id: PageID.DASHBOARD_CONTRACT_LIST, | id: PageID.DASHBOARD_CONTRACT_LIST, | ||||
| @@ -65,24 +60,14 @@ const categories: Group[] = [ | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| // { id: "Database", icon: <DnsRoundedIcon />, navigate: "contract" }, | |||||
| // { id: "Storage", icon: <PermMediaOutlinedIcon /> }, | |||||
| // { id: "Hosting", icon: <PublicIcon /> }, | |||||
| // { id: "Functions", icon: <SettingsEthernetIcon /> }, | |||||
| // { | |||||
| // id: "Machine learning", | |||||
| // icon: <SettingsInputComponentIcon />, | |||||
| // }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| // { | |||||
| // id: "Quality", | |||||
| // children: [ | |||||
| // { id: "Analytics", icon: <SettingsIcon /> }, | |||||
| // { id: "Performance", icon: <TimerIcon /> }, | |||||
| // { id: "Test Lab", icon: <PhonelinkSetupIcon /> }, | |||||
| // ], | |||||
| // }, | |||||
| { | |||||
| label: "アカウント", | |||||
| children: [ | |||||
| { label: "ログアウト", icon: <SettingsIcon />, id: PageID.LOGOUT }, | |||||
| ], | |||||
| }, | |||||
| ]; | ]; | ||||
| const item = { | const item = { | ||||
| @@ -129,6 +114,9 @@ export default function Navigator(props: DrawerProps) { | |||||
| function Group(group: Group) { | function Group(group: Group) { | ||||
| const { label, children } = group; | const { label, children } = group; | ||||
| const { checkRole } = useAuth(); | |||||
| if (!checkRole(group.role)) return null; | |||||
| return ( | return ( | ||||
| <Box sx={{ bgcolor: "#101F33" }}> | <Box sx={{ bgcolor: "#101F33" }}> | ||||
| <ListItem sx={{ py: 2, px: 3 }}> | <ListItem sx={{ py: 2, px: 3 }}> | ||||
| @@ -150,15 +138,20 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||||
| const [open, setOpen] = React.useState(false); | const [open, setOpen] = React.useState(false); | ||||
| const { checkRole } = useAuth(); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| setOpen(shouldOpen); | setOpen(shouldOpen); | ||||
| }, [shouldOpen]); | }, [shouldOpen]); | ||||
| if (!checkRole(role)) return null; | |||||
| // 子要素ありの場合 | // 子要素ありの場合 | ||||
| if (elements && elements.length !== 0) { | if (elements && elements.length !== 0) { | ||||
| const handleClick = () => { | const handleClick = () => { | ||||
| setOpen(!open); | setOpen(!open); | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <ListItemButton onClick={handleClick} sx={item} selected={false}> | <ListItemButton onClick={handleClick} sx={item} selected={false}> | ||||
| @@ -184,7 +177,7 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||||
| }; | }; | ||||
| const selected = id === pageId; | const selected = id === pageId; | ||||
| return ( | return ( | ||||
| <ListItemButton onClick={handleClick} selected={selected}> | |||||
| <ListItemButton onClick={handleClick} selected={selected} sx={item}> | |||||
| <ListItemIcon>{icon}</ListItemIcon> | <ListItemIcon>{icon}</ListItemIcon> | ||||
| <ListItemText>{label}</ListItemText> | <ListItemText>{label}</ListItemText> | ||||
| </ListItemButton> | </ListItemButton> | ||||
| @@ -194,12 +187,15 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||||
| function useContents(children: Child[]) { | function useContents(children: Child[]) { | ||||
| const { pageId } = usePage(); | const { pageId } = usePage(); | ||||
| const { navigateWhenChanged } = useNavigateCustom(); | const { navigateWhenChanged } = useNavigateCustom(); | ||||
| const { checkRole } = useAuth(); | |||||
| const [shouldOpen, setShouldOpen] = React.useState(false); | const [shouldOpen, setShouldOpen] = React.useState(false); | ||||
| const elements = React.useMemo(() => { | const elements = React.useMemo(() => { | ||||
| setShouldOpen(false); | setShouldOpen(false); | ||||
| return children.map(({ label, id }, index) => { | |||||
| return children.map(({ label, id, role }, index) => { | |||||
| if (!checkRole(role)) return; | |||||
| const selected = id === pageId; | const selected = id === pageId; | ||||
| if (selected) { | if (selected) { | ||||
| setShouldOpen(true); | setShouldOpen(true); | ||||
| @@ -1,16 +1,16 @@ | |||||
| import { Tabs } from "@mui/material"; | import { Tabs } from "@mui/material"; | ||||
| import { TabProps, useTab } from "./tabutil"; | import { TabProps, useTab } from "./tabutil"; | ||||
| import { PageID } from "codes/page"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| import { getPath } from "routes/path"; | import { getPath } from "routes/path"; | ||||
| const tabs: TabProps[] = [ | const tabs: TabProps[] = [ | ||||
| { | { | ||||
| label: "一覧", | label: "一覧", | ||||
| pageId: PageID.DASHBOARD_CONTRACT_LIST, | |||||
| tabId: TabID.NONE, | |||||
| }, | }, | ||||
| { | { | ||||
| label: "詳細", | label: "詳細", | ||||
| pageId: PageID.DASHBOARD_CONTRACT_DETAIL, | |||||
| tabId: TabID.A, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -1,4 +1,4 @@ | |||||
| import { PageID } from "codes/page"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| import usePage from "hooks/usePage"; | import usePage from "hooks/usePage"; | ||||
| import { useMemo } from "react"; | import { useMemo } from "react"; | ||||
| import { Tab } from "."; | import { Tab } from "."; | ||||
| @@ -6,15 +6,15 @@ import { getPath } from "routes/path"; | |||||
| export type TabProps = { | export type TabProps = { | ||||
| label: string; | label: string; | ||||
| pageId: PageID; | |||||
| tabId: TabID; | |||||
| }; | }; | ||||
| export function useTab(tabs: TabProps[]) { | export function useTab(tabs: TabProps[]) { | ||||
| const { pageId } = usePage(); | |||||
| const { pageId, tabId } = usePage(); | |||||
| const elements = useMemo(() => { | const elements = useMemo(() => { | ||||
| return tabs.map(({ label, pageId: elementPageId }, index) => { | |||||
| const path = getPath(elementPageId); | |||||
| return tabs.map(({ label, tabId: elementTabId }, index) => { | |||||
| const path = getPath([pageId, tabId]); | |||||
| return <Tab {...{ label, navigate: path, key: index }} />; | return <Tab {...{ label, navigate: path, key: index }} />; | ||||
| }); | }); | ||||
| }, [tabs]); | }, [tabs]); | ||||
| @@ -22,10 +22,10 @@ export function useTab(tabs: TabProps[]) { | |||||
| const getTabIndex = useMemo(() => { | const getTabIndex = useMemo(() => { | ||||
| return ( | return ( | ||||
| tabs.findIndex((tab) => { | tabs.findIndex((tab) => { | ||||
| return tab.pageId === pageId; | |||||
| return tab.tabId === tabId; | |||||
| }) ?? 0 | }) ?? 0 | ||||
| ); | ); | ||||
| }, [pageId, tabs]); | |||||
| }, [tabId, tabs]); | |||||
| return { | return { | ||||
| elements, | elements, | ||||
| @@ -0,0 +1,26 @@ | |||||
| import { AppBar, Box, Grid } from "@mui/material"; | |||||
| import { Outlet } from "react-router-dom"; | |||||
| export default function SimpleLayout() { | |||||
| return ( | |||||
| <Box> | |||||
| <AppBar | |||||
| component="div" | |||||
| color="primary" | |||||
| position="static" | |||||
| elevation={0} | |||||
| sx={{ zIndex: 0 }} | |||||
| > | |||||
| <Grid container> | |||||
| <Grid item xs /> | |||||
| <Grid item xs={5} textAlign="center"> | |||||
| EasyReceipt | |||||
| </Grid> | |||||
| <Grid item xs /> | |||||
| </Grid> | |||||
| </AppBar> | |||||
| <Outlet /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| import { yupResolver } from "@hookform/resolvers/yup"; | |||||
| import { LoadingButton } from "@mui/lab"; | |||||
| import { AppBar, Box, Grid, Stack, Typography } from "@mui/material"; | |||||
| import { PageID } from "codes/page"; | |||||
| import InputAlert from "components/form/InputAlert"; | |||||
| import { FormProvider, RHFTextField } from "components/hook-form"; | |||||
| import useAuth from "hooks/useAuth"; | |||||
| import useNavigateCustom from "hooks/useNavigateCustom"; | |||||
| import useSnackbarCustom from "hooks/useSnackbarCustom"; | |||||
| import { useState } from "react"; | |||||
| import { useForm } from "react-hook-form"; | |||||
| import { getPath } from "routes/path"; | |||||
| import * as Yup from "yup"; | |||||
| type FormProps = { | |||||
| email: string; | |||||
| password: string; | |||||
| }; | |||||
| const LoginSchema = Yup.object().shape({ | |||||
| email: Yup.string().required("必須項目です"), | |||||
| password: Yup.string().required("必須項目です"), | |||||
| }); | |||||
| export default function Login() { | |||||
| const { success, error } = useSnackbarCustom(); | |||||
| const [message, setMessage] = useState(""); | |||||
| const [sending, setSending] = useState(false); | |||||
| const { login } = useAuth(); | |||||
| const { navigateWhenChanged } = useNavigateCustom(); | |||||
| const form = useForm<FormProps>({ | |||||
| defaultValues: { | |||||
| email: "", | |||||
| password: "", | |||||
| }, | |||||
| resolver: yupResolver(LoginSchema), | |||||
| }); | |||||
| const handleSubmit = async (data: FormProps) => { | |||||
| setMessage(""); | |||||
| setSending(true); | |||||
| const ret = await login(data.email, data.password); | |||||
| setSending(false); | |||||
| if (ret) { | |||||
| success("ログイン成功"); | |||||
| navigateWhenChanged(getPath(PageID.DASHBOARD_CONTRACT_LIST)); | |||||
| } else { | |||||
| error("ログイン失敗"); | |||||
| setMessage("入力情報を確認してください"); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}> | |||||
| <Box sx={{ p: 3, pt: 5, mx: "auto", maxWidth: 500 }} textAlign="center"> | |||||
| <Stack spacing={3}> | |||||
| <Typography variant="h5">ログイン</Typography> | |||||
| <InputAlert error="none" message={message} /> | |||||
| <RHFTextField name="email" label="email" size="small" /> | |||||
| <RHFTextField | |||||
| name="password" | |||||
| type="password" | |||||
| label="password" | |||||
| size="small" | |||||
| /> | |||||
| <LoadingButton loading={sending} type="submit" variant="contained"> | |||||
| ログイン | |||||
| </LoadingButton> | |||||
| </Stack> | |||||
| </Box> | |||||
| </FormProvider> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| import { PageID } from "codes/page"; | |||||
| import useAuth from "hooks/useAuth"; | |||||
| import useNavigateCustom from "hooks/useNavigateCustom"; | |||||
| import useSnackbarCustom from "hooks/useSnackbarCustom"; | |||||
| import { useEffect } from "react"; | |||||
| import { getPath } from "routes/path"; | |||||
| export default function Logout() { | |||||
| const { logout } = useAuth(); | |||||
| const { info, success } = useSnackbarCustom(); | |||||
| const { navigateWhenChanged } = useNavigateCustom(); | |||||
| useEffect(() => { | |||||
| logout(); | |||||
| info("ログアウトしました"); | |||||
| navigateWhenChanged(getPath(PageID.LOGIN)); | |||||
| }, []); | |||||
| return null; | |||||
| } | |||||
| @@ -1,12 +1,13 @@ | |||||
| import { Box } from "@mui/material"; | import { Box } from "@mui/material"; | ||||
| import { PageID } from "codes/page"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| import useDashboard from "hooks/useDashBoard"; | import useDashboard from "hooks/useDashBoard"; | ||||
| import ContractTabs from "layouts/dashbord/tab/ContractTabs"; | import ContractTabs from "layouts/dashbord/tab/ContractTabs"; | ||||
| import { useEffect } from "react"; | import { useEffect } from "react"; | ||||
| export default function ContractDetail() { | export default function ContractDetail() { | ||||
| const { setHeaderTitle, setTabs } = useDashboard( | const { setHeaderTitle, setTabs } = useDashboard( | ||||
| PageID.DASHBOARD_CONTRACT_DETAIL | |||||
| PageID.DASHBOARD_CONTRACT_DETAIL, | |||||
| TabID.A | |||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -1,17 +1,188 @@ | |||||
| import { Box } from "@mui/material"; | |||||
| import { PageID } from "codes/page"; | |||||
| import { | |||||
| Box, | |||||
| Grid, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TablePagination, | |||||
| TableRow, | |||||
| TextField, | |||||
| } from "@mui/material"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| import { TableHeadCustom } from "components/table"; | |||||
| import { SearchConditionContextProvider } from "contexts/SearchConditionContext"; | |||||
| import useDashboard from "hooks/useDashBoard"; | import useDashboard from "hooks/useDashBoard"; | ||||
| import useTable, { UseTableReturn } from "hooks/useTable"; | |||||
| import ContractTabs from "layouts/dashbord/tab/ContractTabs"; | import ContractTabs from "layouts/dashbord/tab/ContractTabs"; | ||||
| import { useEffect } from "react"; | import { useEffect } from "react"; | ||||
| import { Contract } from "types/contract"; | |||||
| export default function ContractList() { | export default function ContractList() { | ||||
| const { setHeaderTitle, setTabs } = useDashboard( | const { setHeaderTitle, setTabs } = useDashboard( | ||||
| PageID.DASHBOARD_CONTRACT_LIST | |||||
| PageID.DASHBOARD_CONTRACT_LIST, | |||||
| TabID.NONE | |||||
| ); | ); | ||||
| const table = useTable<Contract>(); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setHeaderTitle("契約者一覧"); | setHeaderTitle("契約者一覧"); | ||||
| setTabs(<ContractTabs />); | setTabs(<ContractTabs />); | ||||
| }, []); | }, []); | ||||
| return <Box>ContractList</Box>; | |||||
| return ( | |||||
| <SearchConditionContextProvider> | |||||
| <Page table={table} /> | |||||
| </SearchConditionContextProvider> | |||||
| ); | |||||
| } | |||||
| type CommonProps = { | |||||
| table: UseTableReturn<Contract>; | |||||
| }; | |||||
| function Page({ table }: CommonProps) { | |||||
| const { | |||||
| order, | |||||
| page, | |||||
| sort, | |||||
| rowsPerPage, | |||||
| fetched, | |||||
| fillteredRow, | |||||
| isNotFound, | |||||
| dataLength, | |||||
| // | |||||
| onSort, | |||||
| onChangePage, | |||||
| onChangeRowsPerPage, | |||||
| // | |||||
| setRowData, | |||||
| // | |||||
| ROWS_PER_PAGES, | |||||
| } = table; | |||||
| return ( | |||||
| <Box> | |||||
| <SearchBox table={table} /> | |||||
| <TableBox table={table} /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| function SearchBox({ table }: CommonProps) { | |||||
| return ( | |||||
| <Box sx={{ p: 1, m: 1 }}> | |||||
| <Grid container spacing={1}> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField label="a" fullWidth size="small" /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| function TableBox({ table }: CommonProps) { | |||||
| const TABLE_HEAD = [ | |||||
| { id: "id", label: "ID", align: "left" }, | |||||
| { id: "name", label: "名前", align: "left" }, | |||||
| { id: "emply", label: "---", align: "left" }, | |||||
| ]; | |||||
| const { | |||||
| order, | |||||
| page, | |||||
| sort, | |||||
| rowsPerPage, | |||||
| fetched, | |||||
| fillteredRow, | |||||
| isNotFound, | |||||
| dataLength, | |||||
| // | |||||
| onSort, | |||||
| onChangePage, | |||||
| onChangeRowsPerPage, | |||||
| // | |||||
| setRowData, | |||||
| // | |||||
| ROWS_PER_PAGES, | |||||
| } = table; | |||||
| useEffect(() => { | |||||
| setRowData([ | |||||
| { id: "iwabuchi", name: "hei" }, | |||||
| { id: "iwabuchi", name: "hei1" }, | |||||
| ]); | |||||
| }, []); | |||||
| return ( | |||||
| <> | |||||
| <TableContainer | |||||
| sx={{ | |||||
| // minWidth: 800, | |||||
| position: "relative", | |||||
| }} | |||||
| > | |||||
| <Table size="small"> | |||||
| <TableHeadCustom | |||||
| order={order} | |||||
| orderBy={sort} | |||||
| headLabel={TABLE_HEAD} | |||||
| rowCount={1} | |||||
| numSelected={0} | |||||
| onSort={onSort} | |||||
| /> | |||||
| <TableBody> | |||||
| {fillteredRow.map((row, index) => ( | |||||
| <Row data={row} key={index} /> | |||||
| ))} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <Box sx={{ position: "relative" }}> | |||||
| <TablePagination | |||||
| rowsPerPageOptions={ROWS_PER_PAGES} | |||||
| component="div" | |||||
| count={dataLength} | |||||
| rowsPerPage={rowsPerPage} | |||||
| page={page} | |||||
| onPageChange={onChangePage} | |||||
| onRowsPerPageChange={onChangeRowsPerPage} | |||||
| /> | |||||
| </Box> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| type RowProps = { | |||||
| data: Contract; | |||||
| }; | |||||
| function Row({ data }: RowProps) { | |||||
| return ( | |||||
| <TableRow hover sx={{ cursor: "pointer" }}> | |||||
| <TableCell>{data.id}</TableCell> | |||||
| <TableCell>{data.name}</TableCell> | |||||
| <TableCell>{data.created_at}</TableCell> | |||||
| </TableRow> | |||||
| ); | |||||
| } | } | ||||
| @@ -0,0 +1,17 @@ | |||||
| import { csrfToken } from "api/auth"; | |||||
| import { memo, useEffect, useState } from "react"; | |||||
| function CsrfTokenProvider() { | |||||
| const [done, setDone] = useState(false); | |||||
| useEffect(() => { | |||||
| if (!done) { | |||||
| setDone(true); | |||||
| csrfToken(); | |||||
| } | |||||
| }, []); | |||||
| return null; | |||||
| } | |||||
| export default memo(CsrfTokenProvider); | |||||
| @@ -0,0 +1,9 @@ | |||||
| import { HasChildren } from "@types"; | |||||
| import { SnackbarProvider as NotistackProvider } from "notistack"; | |||||
| type Props = HasChildren; | |||||
| export default function SnackbarProvider({ children }: Props) { | |||||
| return ( | |||||
| <NotistackProvider autoHideDuration={1000}>{children}</NotistackProvider> | |||||
| ); | |||||
| } | |||||
| @@ -1,9 +1,10 @@ | |||||
| import LoadingScreen from "components/LoadingScreen"; | import LoadingScreen from "components/LoadingScreen"; | ||||
| import DashboardLayout from "layouts/dashbord"; | import DashboardLayout from "layouts/dashbord"; | ||||
| import { ElementType, Suspense, lazy } from "react"; | import { ElementType, Suspense, lazy } from "react"; | ||||
| import { useLocation, useRoutes } from "react-router-dom"; | |||||
| import { RouteObject, useLocation, useRoutes } from "react-router-dom"; | |||||
| import { PATH, getRoute } from "./path"; | import { PATH, getRoute } from "./path"; | ||||
| import { PageID } from "codes/page"; | import { PageID } from "codes/page"; | ||||
| import SimpleLayout from "layouts/simple"; | |||||
| const Loadable = (Component: ElementType) => (props: any) => { | const Loadable = (Component: ElementType) => (props: any) => { | ||||
| return ( | return ( | ||||
| @@ -13,39 +14,49 @@ const Loadable = (Component: ElementType) => (props: any) => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| export function Routes() { | |||||
| return useRoutes([ | |||||
| const AuthRoutes = (): RouteObject => ({ | |||||
| element: <SimpleLayout />, | |||||
| children: [ | |||||
| { | { | ||||
| path: "testa", | |||||
| element: <TestAPage />, | |||||
| path: getRoute(PageID.LOGIN), | |||||
| element: <Login />, | |||||
| }, | }, | ||||
| { | { | ||||
| path: "testb", | |||||
| element: <TestBPage />, | |||||
| path: getRoute(PageID.LOGOUT), | |||||
| element: <Logout />, | |||||
| }, | }, | ||||
| ], | |||||
| }); | |||||
| const DashboardRoutes = (): RouteObject => ({ | |||||
| element: <DashboardLayout />, | |||||
| children: [ | |||||
| { | { | ||||
| path: "*", | |||||
| element: <Page404 />, | |||||
| path: getRoute(PageID.DASHBOARD_CONTRACT_LIST), | |||||
| element: <ContractList />, | |||||
| }, | |||||
| { | |||||
| path: getRoute(PageID.DASHBOARD_CONTRACT_DETAIL), | |||||
| element: <ContractDetail />, | |||||
| }, | }, | ||||
| ], | |||||
| }); | |||||
| export function Routes() { | |||||
| return useRoutes([ | |||||
| AuthRoutes(), | |||||
| DashboardRoutes(), | |||||
| { | { | ||||
| path: PATH.dashboard.root, | |||||
| element: <DashboardLayout />, | |||||
| children: [ | |||||
| { | |||||
| path: getRoute(PageID.DASHBOARD_CONTRACT_LIST, PATH.dashboard.root), | |||||
| element: <ContractList />, | |||||
| }, | |||||
| { | |||||
| path: getRoute(PageID.DASHBOARD_CONTRACT_DETAIL, PATH.dashboard.root), | |||||
| element: <ContractDetail />, | |||||
| }, | |||||
| ], | |||||
| path: "*", | |||||
| element: <Page404 />, | |||||
| }, | }, | ||||
| ]); | ]); | ||||
| } | } | ||||
| const TestAPage = Loadable(lazy(() => import("pages/test/TestA"))); | const TestAPage = Loadable(lazy(() => import("pages/test/TestA"))); | ||||
| const TestBPage = Loadable(lazy(() => import("pages/test/TestB"))); | 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 ContractList = Loadable( | const ContractList = Loadable( | ||||
| lazy(() => import("pages/dashboard/contract/list")) | lazy(() => import("pages/dashboard/contract/list")) | ||||
| @@ -1,54 +1,75 @@ | |||||
| import { Dictionary } from "@types"; | import { Dictionary } from "@types"; | ||||
| import { PageID } from "codes/page"; | |||||
| import { get, isNumber, isString, replace } from "lodash"; | |||||
| import { PageID, TabID } from "codes/page"; | |||||
| import { get, isArray, isNumber, isString, replace } from "lodash"; | |||||
| const DASHBOARD = "dashboard"; | const DASHBOARD = "dashboard"; | ||||
| const PREFIX = { | |||||
| list: "list", | |||||
| detail: "detail", | |||||
| }; | |||||
| export const PATH = { | export const PATH = { | ||||
| login: "/login", | |||||
| logout: "/logout", | |||||
| dashboard: { | dashboard: { | ||||
| root: "dashboard", | |||||
| contract: "contract", | |||||
| root: "/dashboard", | |||||
| contract: "/contract", | |||||
| }, | }, | ||||
| }; | }; | ||||
| const makePath = (paths: string[]): string => { | |||||
| return "/" + paths.join("/"); | |||||
| // 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]); | |||||
| // }; | |||||
| type PathKey = [PageID, TabID?] | PageID; | |||||
| const makePathKey = (arg: PathKey): string => { | |||||
| if (isArray(arg)) { | |||||
| const tabStr = arg[1] !== undefined ? "/" + String(arg[1]) : ""; | |||||
| return String(arg[0]) + tabStr; | |||||
| } else { | |||||
| return String(arg); | |||||
| } | |||||
| }; | }; | ||||
| const makeListPageCallback = (path: string) => { | |||||
| return (page: number) => { | |||||
| return [path, "/", PREFIX.list, String(page)].join("/"); | |||||
| }; | |||||
| }; | |||||
| const makeDashboardPath = (paths: string[]): string => { | |||||
| return makePath([PATH.dashboard.root, ...paths]); | |||||
| const getPageId = (key: PathKey): PageID => { | |||||
| if (isArray(key)) { | |||||
| return key[0]; | |||||
| } else { | |||||
| return key; | |||||
| } | |||||
| }; | }; | ||||
| export const PATH_DASHBOARD = { | |||||
| contract: { | |||||
| list: makeListPageCallback(makeDashboardPath([PATH.dashboard.contract])), | |||||
| detail: makeDashboardPath([PATH.dashboard.contract, "detail"]), | |||||
| }, | |||||
| sms: { | |||||
| list: makePath([DASHBOARD, "sms"]), | |||||
| }, | |||||
| const getTabId = (key: PathKey): TabID => { | |||||
| if (isArray(key)) { | |||||
| return key[1] ?? TabID.NONE; | |||||
| } else { | |||||
| return TabID.NONE; | |||||
| } | |||||
| }; | }; | ||||
| const PATHS = { | const PATHS = { | ||||
| [PageID.DASHBOARD_CONTRACT_LIST]: "/dashboard/contract/list/:page", | |||||
| [PageID.DASHBOARD_CONTRACT_DETAIL]: "/dashboard/contract/detail", | |||||
| // 認証 | |||||
| [makePathKey(PageID.LOGIN)]: "/login", | |||||
| [makePathKey(PageID.LOGOUT)]: "/logout", | |||||
| [makePathKey(PageID.DASHBOARD_CONTRACT_LIST)]: | |||||
| "/dashboard/contract/list/:page", | |||||
| [makePathKey(PageID.DASHBOARD_CONTRACT_DETAIL)]: "/dashboard/contract/detail", | |||||
| }; | }; | ||||
| export type PathOption = { | export type PathOption = { | ||||
| page?: number; | page?: number; | ||||
| query?: Dictionary; | query?: Dictionary; | ||||
| }; | }; | ||||
| export function getPath(pageId: PageID, option?: PathOption) { | |||||
| export function getPath(key: PathKey, option?: PathOption) { | |||||
| const pageId = getPageId(key); | |||||
| const tabId = getTabId(key); | |||||
| let path = getRoute(pageId); | let path = getRoute(pageId); | ||||
| // ページ番号解決 | // ページ番号解決 | ||||
| @@ -67,9 +88,13 @@ export function getPath(pageId: PageID, option?: PathOption) { | |||||
| return path; | return path; | ||||
| } | } | ||||
| export function getRoute(pageId: PageID, exclude?: string): string { | |||||
| let path = get(PATHS, pageId); | |||||
| if (!path) throw new Error("ルート未定義:" + pageId); | |||||
| export function getListPagePath(key: PathKey, page: number): string { | |||||
| return getPath(key, { page }); | |||||
| } | |||||
| export function getRoute(key: PathKey, exclude?: string): string { | |||||
| let path = get(PATHS, makePathKey(key)); | |||||
| if (!path) throw new Error("ルート未定義:" + makePathKey(key)); | |||||
| if (exclude) { | if (exclude) { | ||||
| path = replace(path, "/" + exclude + "/", ""); | path = replace(path, "/" + exclude + "/", ""); | ||||
| @@ -1,5 +1,6 @@ | |||||
| import { ThemeProvider, createTheme } from "@mui/material"; | import { ThemeProvider, createTheme } from "@mui/material"; | ||||
| import { HasChildren } from "@types"; | import { HasChildren } from "@types"; | ||||
| import { memo, useMemo } from "react"; | |||||
| let theme = createTheme({ | let theme = createTheme({ | ||||
| palette: { | palette: { | ||||
| @@ -43,6 +44,13 @@ theme = { | |||||
| }, | }, | ||||
| }, | }, | ||||
| }, | }, | ||||
| MuiTableHead: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| backgroundColor: "#D1E6D6", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiButton: { | MuiButton: { | ||||
| styleOverrides: { | styleOverrides: { | ||||
| root: { | root: { | ||||
| @@ -145,6 +153,9 @@ theme = { | |||||
| }; | }; | ||||
| type Props = HasChildren; | type Props = HasChildren; | ||||
| export function AppThemeProvider({ children }: Props) { | |||||
| return <ThemeProvider theme={theme}>{children}</ThemeProvider>; | |||||
| function AppThemeProvider({ children }: Props) { | |||||
| const t = useMemo(() => theme, []); | |||||
| return <ThemeProvider theme={t}>{children}</ThemeProvider>; | |||||
| } | } | ||||
| export default memo(AppThemeProvider); | |||||
| @@ -0,0 +1,11 @@ | |||||
| export type Data = { | |||||
| id: string; | |||||
| updated_id?: string; | |||||
| updated_at?: string; | |||||
| created_at?: string; | |||||
| updated_by?: string; | |||||
| created_by?: string; | |||||
| }; | |||||
| export type HistoryData = { | |||||
| history_id: string; | |||||
| } & Data; | |||||
| @@ -0,0 +1,5 @@ | |||||
| import { Data } from "./common"; | |||||
| export type Contract = { | |||||
| name: string; | |||||
| } & Data; | |||||
| @@ -0,0 +1,20 @@ | |||||
| import axios from "axios"; | |||||
| // config | |||||
| import { HOST_API } from "config"; | |||||
| // ---------------------------------------------------------------------- | |||||
| const axiosInstance = axios.create({ | |||||
| baseURL: HOST_API, | |||||
| withCredentials: true, | |||||
| }); | |||||
| axiosInstance.interceptors.response.use( | |||||
| (response) => response, | |||||
| (error) => | |||||
| Promise.reject( | |||||
| (error.response && error.response.data) || "Something went wrong" | |||||
| ) | |||||
| ); | |||||
| export default axiosInstance; | |||||
| @@ -0,0 +1,24 @@ | |||||
| import { format, parseISO } from 'date-fns'; | |||||
| type Input = Date | string | null | undefined; | |||||
| export const formatDateStr = (source: Input) => { | |||||
| return formatToStr(source, 'yyyy/MM/dd'); | |||||
| }; | |||||
| export const formatDateTimeStr = (source: Date | string | null | undefined) => { | |||||
| return formatToStr(source, 'yyyy/MM/dd HH:mm:ss'); | |||||
| }; | |||||
| const formatToStr = (source: Input, formatStr: string) => { | |||||
| if (source === null || source === undefined) return ''; | |||||
| if (source instanceof Date) return format(source, formatStr); | |||||
| return format(parseISO(source), formatStr); | |||||
| }; | |||||
| export const now = () => { | |||||
| return new Date(); | |||||
| }; | |||||
| export const nowStr = (): string => { | |||||
| return formatDateTimeStr(now()); | |||||
| }; | |||||
| @@ -1,6 +1,6 @@ | |||||
| { | { | ||||
| "compilerOptions": { | "compilerOptions": { | ||||
| "target": "es5", | |||||
| "target": "ES2022", | |||||
| "lib": [ | "lib": [ | ||||
| "dom", | "dom", | ||||
| "dom.iterable", | "dom.iterable", | ||||
| @@ -1334,6 +1334,11 @@ | |||||
| resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" | resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" | ||||
| integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== | integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== | ||||
| "@hookform/resolvers@^3.1.0": | |||||
| version "3.1.0" | |||||
| resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.1.0.tgz#ff83ef4aa6078173201da131ceea4c3583b67034" | |||||
| integrity sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA== | |||||
| "@humanwhocodes/config-array@^0.11.8": | "@humanwhocodes/config-array@^0.11.8": | ||||
| version "0.11.8" | version "0.11.8" | ||||
| resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" | resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" | ||||
| @@ -1669,6 +1674,20 @@ | |||||
| prop-types "^15.8.1" | prop-types "^15.8.1" | ||||
| react-is "^18.2.0" | react-is "^18.2.0" | ||||
| "@mui/base@5.0.0-alpha.128": | |||||
| version "5.0.0-alpha.128" | |||||
| resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.128.tgz#8ce4beb971ac989df0b1d3b2bd3e9274dbfa604f" | |||||
| integrity sha512-wub3wxNN+hUp8hzilMlXX3sZrPo75vsy1cXEQpqdTfIFlE9HprP1jlulFiPg5tfPst2OKmygXr2hhmgvAKRrzQ== | |||||
| dependencies: | |||||
| "@babel/runtime" "^7.21.0" | |||||
| "@emotion/is-prop-valid" "^1.2.0" | |||||
| "@mui/types" "^7.2.4" | |||||
| "@mui/utils" "^5.12.3" | |||||
| "@popperjs/core" "^2.11.7" | |||||
| clsx "^1.2.1" | |||||
| prop-types "^15.8.1" | |||||
| react-is "^18.2.0" | |||||
| "@mui/core-downloads-tracker@^5.12.2": | "@mui/core-downloads-tracker@^5.12.2": | ||||
| version "5.12.2" | version "5.12.2" | ||||
| resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.12.2.tgz#4a0186d25b01d693171366e1c00de0e7c8c35f6a" | resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.12.2.tgz#4a0186d25b01d693171366e1c00de0e7c8c35f6a" | ||||
| @@ -1681,6 +1700,20 @@ | |||||
| dependencies: | dependencies: | ||||
| "@babel/runtime" "^7.21.0" | "@babel/runtime" "^7.21.0" | ||||
| "@mui/lab@^5.0.0-alpha.129": | |||||
| version "5.0.0-alpha.129" | |||||
| resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.129.tgz#e940aeef995175586e058cad36e801502730b670" | |||||
| integrity sha512-niv2mFgSTgdrRJXbWoX9pIivhe80BaFXfdWajXe1bS8VYH3Y5WyJpk8KiU3rbHyJswbFEGd8N6EBBrq11X8yMA== | |||||
| dependencies: | |||||
| "@babel/runtime" "^7.21.0" | |||||
| "@mui/base" "5.0.0-alpha.128" | |||||
| "@mui/system" "^5.12.3" | |||||
| "@mui/types" "^7.2.4" | |||||
| "@mui/utils" "^5.12.3" | |||||
| clsx "^1.2.1" | |||||
| prop-types "^15.8.1" | |||||
| react-is "^18.2.0" | |||||
| "@mui/material@^5.12.2": | "@mui/material@^5.12.2": | ||||
| version "5.12.2" | version "5.12.2" | ||||
| resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.12.2.tgz#c3fcc94e523d9e673e2e045dfad04d12ab454a80" | resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.12.2.tgz#c3fcc94e523d9e673e2e045dfad04d12ab454a80" | ||||
| @@ -1708,6 +1741,15 @@ | |||||
| "@mui/utils" "^5.12.0" | "@mui/utils" "^5.12.0" | ||||
| prop-types "^15.8.1" | prop-types "^15.8.1" | ||||
| "@mui/private-theming@^5.12.3": | |||||
| version "5.12.3" | |||||
| resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.12.3.tgz#f5e4704e25d9d91b906561cae573cda8f3801e10" | |||||
| integrity sha512-o1e7Z1Bp27n4x2iUHhegV4/Jp6H3T6iBKHJdLivS5GbwsuAE/5l4SnZ+7+K+e5u9TuhwcAKZLkjvqzkDe8zqfA== | |||||
| dependencies: | |||||
| "@babel/runtime" "^7.21.0" | |||||
| "@mui/utils" "^5.12.3" | |||||
| prop-types "^15.8.1" | |||||
| "@mui/styled-engine@^5.12.0": | "@mui/styled-engine@^5.12.0": | ||||
| version "5.12.0" | version "5.12.0" | ||||
| resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.12.0.tgz#44640cad961adcc9413ae32116237cd1c8f7ddb0" | resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.12.0.tgz#44640cad961adcc9413ae32116237cd1c8f7ddb0" | ||||
| @@ -1718,6 +1760,16 @@ | |||||
| csstype "^3.1.2" | csstype "^3.1.2" | ||||
| prop-types "^15.8.1" | prop-types "^15.8.1" | ||||
| "@mui/styled-engine@^5.12.3": | |||||
| version "5.12.3" | |||||
| resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.12.3.tgz#3307643d52c81947a624cdd0437536cc8109c4f0" | |||||
| integrity sha512-AhZtiRyT8Bjr7fufxE/mLS+QJ3LxwX1kghIcM2B2dvJzSSg9rnIuXDXM959QfUVIM3C8U4x3mgVoPFMQJvc4/g== | |||||
| dependencies: | |||||
| "@babel/runtime" "^7.21.0" | |||||
| "@emotion/cache" "^11.10.8" | |||||
| csstype "^3.1.2" | |||||
| prop-types "^15.8.1" | |||||
| "@mui/system@^5.12.1": | "@mui/system@^5.12.1": | ||||
| version "5.12.1" | version "5.12.1" | ||||
| resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.12.1.tgz#8452bc03159f0a6725b96bde1dee1316e308231b" | resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.12.1.tgz#8452bc03159f0a6725b96bde1dee1316e308231b" | ||||
| @@ -1732,6 +1784,20 @@ | |||||
| csstype "^3.1.2" | csstype "^3.1.2" | ||||
| prop-types "^15.8.1" | prop-types "^15.8.1" | ||||
| "@mui/system@^5.12.3": | |||||
| version "5.12.3" | |||||
| resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.12.3.tgz#306b3cdffa3046067640219c1e5dd7e3dae38ff9" | |||||
| integrity sha512-JB/6sypHqeJCqwldWeQ1MKkijH829EcZAKKizxbU2MJdxGG5KSwZvTBa5D9qiJUA1hJFYYupjiuy9ZdJt6rV6w== | |||||
| dependencies: | |||||
| "@babel/runtime" "^7.21.0" | |||||
| "@mui/private-theming" "^5.12.3" | |||||
| "@mui/styled-engine" "^5.12.3" | |||||
| "@mui/types" "^7.2.4" | |||||
| "@mui/utils" "^5.12.3" | |||||
| clsx "^1.2.1" | |||||
| csstype "^3.1.2" | |||||
| prop-types "^15.8.1" | |||||
| "@mui/types@^7.2.4": | "@mui/types@^7.2.4": | ||||
| version "7.2.4" | version "7.2.4" | ||||
| resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328" | resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328" | ||||
| @@ -1748,6 +1814,17 @@ | |||||
| prop-types "^15.8.1" | prop-types "^15.8.1" | ||||
| react-is "^18.2.0" | react-is "^18.2.0" | ||||
| "@mui/utils@^5.12.3": | |||||
| version "5.12.3" | |||||
| resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.12.3.tgz#3fa3570dac7ec66bb9cc84ab7c16ab6e1b7200f2" | |||||
| integrity sha512-D/Z4Ub3MRl7HiUccid7sQYclTr24TqUAQFFlxHQF8FR177BrCTQ0JJZom7EqYjZCdXhwnSkOj2ph685MSKNtIA== | |||||
| dependencies: | |||||
| "@babel/runtime" "^7.21.0" | |||||
| "@types/prop-types" "^15.7.5" | |||||
| "@types/react-is" "^16.7.1 || ^17.0.0" | |||||
| prop-types "^15.8.1" | |||||
| react-is "^18.2.0" | |||||
| "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": | "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": | ||||
| version "5.1.1-v1" | version "5.1.1-v1" | ||||
| resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" | resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" | ||||
| @@ -2040,6 +2117,13 @@ | |||||
| resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" | resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" | ||||
| integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== | integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== | ||||
| "@types/axios@^0.14.0": | |||||
| version "0.14.0" | |||||
| resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46" | |||||
| integrity sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ== | |||||
| dependencies: | |||||
| axios "*" | |||||
| "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": | "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": | ||||
| version "7.20.0" | version "7.20.0" | ||||
| resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" | resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" | ||||
| @@ -2103,6 +2187,13 @@ | |||||
| dependencies: | dependencies: | ||||
| "@types/node" "*" | "@types/node" "*" | ||||
| "@types/date-fns@^2.6.0": | |||||
| version "2.6.0" | |||||
| resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1" | |||||
| integrity sha512-9DSw2ZRzV0Tmpa6PHHJbMcZn79HHus+BBBohcOaDzkK/G3zMjDUDYjJIWBFLbkh+1+/IOS0A59BpQfdr37hASg== | |||||
| dependencies: | |||||
| date-fns "*" | |||||
| "@types/eslint-scope@^3.7.3": | "@types/eslint-scope@^3.7.3": | ||||
| version "3.7.4" | version "3.7.4" | ||||
| resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" | resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" | ||||
| @@ -2952,6 +3043,15 @@ axe-core@^4.6.2: | |||||
| resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" | resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" | ||||
| integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== | integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== | ||||
| axios@*, axios@^1.4.0: | |||||
| version "1.4.0" | |||||
| resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" | |||||
| integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== | |||||
| dependencies: | |||||
| follow-redirects "^1.15.0" | |||||
| form-data "^4.0.0" | |||||
| proxy-from-env "^1.1.0" | |||||
| axobject-query@^3.1.1: | axobject-query@^3.1.1: | ||||
| version "3.1.1" | version "3.1.1" | ||||
| resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" | resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" | ||||
| @@ -3369,7 +3469,7 @@ cliui@^7.0.2: | |||||
| strip-ansi "^6.0.0" | strip-ansi "^6.0.0" | ||||
| wrap-ansi "^7.0.0" | wrap-ansi "^7.0.0" | ||||
| clsx@^1.2.1: | |||||
| clsx@^1.1.0, clsx@^1.2.1: | |||||
| version "1.2.1" | version "1.2.1" | ||||
| resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" | resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" | ||||
| integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== | integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== | ||||
| @@ -3798,6 +3898,13 @@ data-urls@^2.0.0: | |||||
| whatwg-mimetype "^2.3.0" | whatwg-mimetype "^2.3.0" | ||||
| whatwg-url "^8.0.0" | whatwg-url "^8.0.0" | ||||
| date-fns@*, date-fns@^2.30.0: | |||||
| version "2.30.0" | |||||
| resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" | |||||
| integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== | |||||
| dependencies: | |||||
| "@babel/runtime" "^7.21.0" | |||||
| debug@2.6.9, debug@^2.6.0: | debug@2.6.9, debug@^2.6.0: | ||||
| version "2.6.9" | version "2.6.9" | ||||
| resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | ||||
| @@ -4771,7 +4878,7 @@ flatted@^3.1.0: | |||||
| resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" | resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" | ||||
| integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== | integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== | ||||
| follow-redirects@^1.0.0: | |||||
| follow-redirects@^1.0.0, follow-redirects@^1.15.0: | |||||
| version "1.15.2" | version "1.15.2" | ||||
| resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" | ||||
| integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== | integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== | ||||
| @@ -4811,6 +4918,15 @@ form-data@^3.0.0: | |||||
| combined-stream "^1.0.8" | combined-stream "^1.0.8" | ||||
| mime-types "^2.1.12" | mime-types "^2.1.12" | ||||
| form-data@^4.0.0: | |||||
| version "4.0.0" | |||||
| resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" | |||||
| integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== | |||||
| dependencies: | |||||
| asynckit "^0.4.0" | |||||
| combined-stream "^1.0.8" | |||||
| mime-types "^2.1.12" | |||||
| forwarded@0.2.0: | forwarded@0.2.0: | ||||
| version "0.2.0" | version "0.2.0" | ||||
| resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" | ||||
| @@ -5012,6 +5128,11 @@ globby@^11.0.4, globby@^11.1.0: | |||||
| merge2 "^1.4.1" | merge2 "^1.4.1" | ||||
| slash "^3.0.0" | slash "^3.0.0" | ||||
| goober@^2.0.33: | |||||
| version "2.1.13" | |||||
| resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c" | |||||
| integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ== | |||||
| gopd@^1.0.1: | gopd@^1.0.1: | ||||
| version "1.0.1" | version "1.0.1" | ||||
| resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" | resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" | ||||
| @@ -6715,6 +6836,14 @@ normalize-url@^6.0.1: | |||||
| resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" | resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" | ||||
| integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== | integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== | ||||
| notistack@^3.0.1: | |||||
| version "3.0.1" | |||||
| resolved "https://registry.yarnpkg.com/notistack/-/notistack-3.0.1.tgz#daf59888ab7e2c30a1fa8f71f9cba2978773236e" | |||||
| integrity sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA== | |||||
| dependencies: | |||||
| clsx "^1.1.0" | |||||
| goober "^2.0.33" | |||||
| npm-run-path@^4.0.1: | npm-run-path@^4.0.1: | ||||
| version "4.0.1" | version "4.0.1" | ||||
| resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" | ||||
| @@ -7691,6 +7820,11 @@ prop-types@^15.6.2, prop-types@^15.8.1: | |||||
| object-assign "^4.1.1" | object-assign "^4.1.1" | ||||
| react-is "^16.13.1" | react-is "^16.13.1" | ||||
| property-expr@^2.0.5: | |||||
| version "2.0.5" | |||||
| resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" | |||||
| integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== | |||||
| proxy-addr@~2.0.7: | proxy-addr@~2.0.7: | ||||
| version "2.0.7" | version "2.0.7" | ||||
| resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" | ||||
| @@ -7699,6 +7833,11 @@ proxy-addr@~2.0.7: | |||||
| forwarded "0.2.0" | forwarded "0.2.0" | ||||
| ipaddr.js "1.9.1" | ipaddr.js "1.9.1" | ||||
| proxy-from-env@^1.1.0: | |||||
| version "1.1.0" | |||||
| resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" | |||||
| integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== | |||||
| psl@^1.1.33: | psl@^1.1.33: | ||||
| version "1.9.0" | version "1.9.0" | ||||
| resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" | resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" | ||||
| @@ -7815,6 +7954,11 @@ react-error-overlay@^6.0.11: | |||||
| resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" | resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" | ||||
| integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== | integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== | ||||
| react-hook-form@^7.43.9: | |||||
| version "7.43.9" | |||||
| resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d" | |||||
| integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ== | |||||
| react-is@^16.13.1, react-is@^16.7.0: | react-is@^16.13.1, react-is@^16.7.0: | ||||
| version "16.13.1" | version "16.13.1" | ||||
| resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | ||||
| @@ -8885,6 +9029,11 @@ thunky@^1.0.2: | |||||
| resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" | resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" | ||||
| integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== | integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== | ||||
| tiny-case@^1.0.3: | |||||
| version "1.0.3" | |||||
| resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" | |||||
| integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== | |||||
| tmpl@1.0.5: | tmpl@1.0.5: | ||||
| version "1.0.5" | version "1.0.5" | ||||
| resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" | resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" | ||||
| @@ -8907,6 +9056,11 @@ toidentifier@1.0.1: | |||||
| resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" | ||||
| integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== | ||||
| toposort@^2.0.2: | |||||
| version "2.0.2" | |||||
| resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" | |||||
| integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== | |||||
| tough-cookie@^4.0.0: | tough-cookie@^4.0.0: | ||||
| version "4.1.2" | version "4.1.2" | ||||
| resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" | ||||
| @@ -9002,6 +9156,11 @@ type-fest@^0.21.3: | |||||
| resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" | ||||
| integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== | integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== | ||||
| type-fest@^2.19.0: | |||||
| version "2.19.0" | |||||
| resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" | |||||
| integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== | |||||
| type-is@~1.6.18: | type-is@~1.6.18: | ||||
| version "1.6.18" | version "1.6.18" | ||||
| resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" | ||||
| @@ -9686,3 +9845,13 @@ yocto-queue@^0.1.0: | |||||
| version "0.1.0" | version "0.1.0" | ||||
| resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" | ||||
| integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== | ||||
| yup@^1.1.1: | |||||
| version "1.1.1" | |||||
| resolved "https://registry.yarnpkg.com/yup/-/yup-1.1.1.tgz#49dbcf5ae7693ed0a36ed08a9e9de0a09ac18e6b" | |||||
| integrity sha512-KfCGHdAErqFZWA5tZf7upSUnGKuTOnsI3hUsLr7fgVtx+DK04NPV01A68/FslI4t3s/ZWpvXJmgXhd7q6ICnag== | |||||
| dependencies: | |||||
| property-expr "^2.0.5" | |||||
| tiny-case "^1.0.3" | |||||
| toposort "^2.0.2" | |||||
| type-fest "^2.19.0" | |||||