| @@ -0,0 +1,7 @@ | |||
| GENERATE_SOURCEMAP=true | |||
| PORT=8080 | |||
| # HOST | |||
| # ローカル | |||
| REACT_APP_HOST_API_KEY=http://localhost | |||
| @@ -5,24 +5,33 @@ | |||
| "dependencies": { | |||
| "@emotion/react": "^11.10.8", | |||
| "@emotion/styled": "^11.10.8", | |||
| "@hookform/resolvers": "^3.1.0", | |||
| "@mui/icons-material": "^5.11.16", | |||
| "@mui/lab": "^5.0.0-alpha.129", | |||
| "@mui/material": "^5.12.2", | |||
| "@testing-library/jest-dom": "^5.14.1", | |||
| "@testing-library/react": "^13.0.0", | |||
| "@testing-library/user-event": "^13.2.1", | |||
| "@types/axios": "^0.14.0", | |||
| "@types/date-fns": "^2.6.0", | |||
| "@types/jest": "^27.0.1", | |||
| "@types/lodash": "^4.14.194", | |||
| "@types/node": "^16.7.13", | |||
| "@types/react": "^18.0.0", | |||
| "@types/react-dom": "^18.0.0", | |||
| "@types/react-router-dom": "^5.3.3", | |||
| "axios": "^1.4.0", | |||
| "date-fns": "^2.30.0", | |||
| "lodash": "^4.17.21", | |||
| "notistack": "^3.0.1", | |||
| "react": "^18.2.0", | |||
| "react-dom": "^18.2.0", | |||
| "react-hook-form": "^7.43.9", | |||
| "react-router-dom": "^6.11.0", | |||
| "react-scripts": "5.0.1", | |||
| "typescript": "^4.4.2", | |||
| "web-vitals": "^2.1.0" | |||
| "web-vitals": "^2.1.0", | |||
| "yup": "^1.1.1" | |||
| }, | |||
| "scripts": { | |||
| "start": "react-scripts start", | |||
| @@ -1,5 +1,7 @@ | |||
| import { ReactNode } from "react"; | |||
| export type DataUrl = string; | |||
| export type HasChildren = { | |||
| children: ReactNode; | |||
| }; | |||
| @@ -1,25 +1,29 @@ | |||
| import { CssBaseline } from "@mui/material"; | |||
| import AuthContextProvider from "contexts/AuthContext"; | |||
| import { PageContextProvider } from "contexts/PageContext"; | |||
| import { WindowSizeContextProvider } from "contexts/WindowSizeContext"; | |||
| import CsrfTokenProvider from "providers/CsrfTokenProvider"; | |||
| import SnackbarProvider from "providers/SnackbarProvider"; | |||
| import { BrowserRouter } from "react-router-dom"; | |||
| import { Routes } from "routes"; | |||
| import { AppThemeProvider } from "theme"; | |||
| import AppThemeProvider from "theme"; | |||
| function App() { | |||
| export default function App() { | |||
| 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 = { | |||
| 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; | |||
| 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 { PageID } from "codes/page"; | |||
| import { PageID, TabID } from "codes/page"; | |||
| import usePage from "hooks/usePage"; | |||
| import useResponsive from "hooks/useResponsive"; | |||
| import useWindowSize from "hooks/useWindowSize"; | |||
| import { ReactNode, createContext, useMemo, useState } from "react"; | |||
| @@ -14,7 +15,12 @@ type ContextProps = { | |||
| tabs: ReactNode | null; | |||
| setTabs: (tabs: ReactNode | null) => void; | |||
| pageId: PageID; | |||
| setPageId: (tabs: PageID) => void; | |||
| setPageId: (pageId: PageID) => void; | |||
| tabId: TabID; | |||
| setTabId: (tabId: TabID) => void; | |||
| showDrawer: boolean; | |||
| overBreakPoint: boolean; | |||
| }; | |||
| const contextInit: ContextProps = { | |||
| headerTitle: "", | |||
| @@ -26,7 +32,12 @@ const contextInit: ContextProps = { | |||
| tabs: null, | |||
| setTabs: (tabs: ReactNode | null) => {}, | |||
| pageId: PageID.NONE, | |||
| setPageId: (tabs: PageID) => {}, | |||
| setPageId: (pageId: PageID) => {}, | |||
| tabId: TabID.NONE, | |||
| setTabId: (tabId: TabID) => {}, | |||
| showDrawer: false, | |||
| overBreakPoint: false, | |||
| }; | |||
| export const DashboardLayoutContext = createContext(contextInit); | |||
| @@ -38,11 +49,15 @@ export function DashboardLayoutContextProvider({ children }: Props) { | |||
| const [tabs, setTabs] = useState<ReactNode | null>(null); | |||
| 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(() => { | |||
| return innerWidth - drawerWidth; | |||
| }, [drawerWidth, innerWidth]); | |||
| return innerWidth - (showDrawer ? drawerWidth : 0); | |||
| }, [drawerWidth, innerWidth, showDrawer]); | |||
| return ( | |||
| <DashboardLayoutContext.Provider | |||
| @@ -57,6 +72,11 @@ export function DashboardLayoutContextProvider({ children }: Props) { | |||
| setTabs, | |||
| pageId, | |||
| setPageId, | |||
| tabId, | |||
| setTabId, | |||
| showDrawer, | |||
| overBreakPoint, | |||
| }} | |||
| > | |||
| {children} | |||
| @@ -1,27 +1,33 @@ | |||
| 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 = { | |||
| pageId: PageID; | |||
| tabId: TabID; | |||
| setPageId: (pageId: PageID) => void; | |||
| setTabId: (tabId: TabID) => void; | |||
| }; | |||
| const contextInit: ContextProps = { | |||
| pageId: PageID.NONE, | |||
| tabId: TabID.NONE, | |||
| setPageId: (pageId: PageID) => {}, | |||
| setTabId: (tabId: TabID) => {}, | |||
| }; | |||
| export const PageContext = createContext(contextInit); | |||
| type Props = HasChildren; | |||
| export function PageContextProvider({ children }: Props) { | |||
| const [pageId, setPageId] = useState<PageID>(PageID.NONE); | |||
| const [tabId, setTabId] = useState<TabID>(TabID.NONE); | |||
| return ( | |||
| <PageContext.Provider | |||
| value={{ | |||
| pageId, | |||
| tabId, | |||
| setPageId, | |||
| setTabId, | |||
| }} | |||
| > | |||
| {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 { 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); | |||
| useEffect(() => { | |||
| 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 { Dictionary } from "@types"; | |||
| import { PageID } from "codes/page"; | |||
| export default function useNavigateCustom() { | |||
| 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( | |||
| document.getElementById('root') as HTMLElement | |||
| document.getElementById("root") as HTMLElement | |||
| ); | |||
| 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 | |||
| @@ -23,9 +23,7 @@ function Copyright() { | |||
| function App() { | |||
| const [mobileOpen, setMobileOpen] = useState(false); | |||
| const isSmUp = useResponsive("up", "sm"); | |||
| const { drawerWidth, innerHeight, innerWidth, contentsWidth } = | |||
| const { drawerWidth, innerHeight, innerWidth, contentsWidth, showDrawer } = | |||
| useDashboard(); | |||
| const handleDrawerToggle = () => { | |||
| @@ -34,7 +32,7 @@ function App() { | |||
| useEffect(() => { | |||
| console.log({ drawerWidth, innerWidth, contentsWidth }); | |||
| }, []); | |||
| }, [drawerWidth, innerWidth, contentsWidth]); | |||
| return ( | |||
| <Box sx={{ display: "flex", minHeight: "100vh" }}> | |||
| @@ -42,7 +40,7 @@ function App() { | |||
| component="nav" | |||
| sx={{ width: { sm: drawerWidth }, flexShrink: { md: 0 } }} | |||
| > | |||
| {isSmUp ? null : ( | |||
| {showDrawer && ( | |||
| <Navigator | |||
| PaperProps={{ style: { width: drawerWidth } }} | |||
| variant="temporary" | |||
| @@ -55,7 +53,14 @@ function App() { | |||
| sx={{ display: { sm: "block", xs: "none" } }} | |||
| /> | |||
| </Box> | |||
| <Box sx={{ flex: 1, display: "flex", flexDirection: "column" }}> | |||
| <Box | |||
| sx={{ | |||
| flex: 1, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| maxWidth: contentsWidth, | |||
| }} | |||
| > | |||
| <Header onDrawerToggle={handleDrawerToggle} /> | |||
| <Box component="main" sx={{ flex: 1, py: 6, px: 4 }}> | |||
| <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 Drawer, { DrawerProps } from "@mui/material/Drawer"; | |||
| import List from "@mui/material/List"; | |||
| import Box from "@mui/material/Box"; | |||
| import ListItem from "@mui/material/ListItem"; | |||
| import ListItemButton from "@mui/material/ListItemButton"; | |||
| import ListItemIcon from "@mui/material/ListItemIcon"; | |||
| 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 { 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 usePage from "hooks/usePage"; | |||
| import * as React from "react"; | |||
| import { getPath } from "routes/path"; | |||
| type Group = { | |||
| label: string; | |||
| children: SubGroup[]; | |||
| role?: UserRole; | |||
| }; | |||
| type SubGroup = { | |||
| @@ -36,7 +30,7 @@ type SubGroup = { | |||
| icon: React.ReactNode; | |||
| children?: Child[]; | |||
| // 子要素を持たない場合は下記は必須 | |||
| // 子要素を持たない場合は設定 | |||
| id?: PageID; | |||
| role?: UserRole; | |||
| }; | |||
| @@ -54,6 +48,7 @@ const categories: Group[] = [ | |||
| { | |||
| label: "契約", | |||
| icon: <PeopleIcon />, | |||
| role: UserRole.SUPER_ADMIN, | |||
| children: [ | |||
| { | |||
| 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 = { | |||
| @@ -129,6 +114,9 @@ export default function Navigator(props: DrawerProps) { | |||
| function Group(group: Group) { | |||
| const { label, children } = group; | |||
| const { checkRole } = useAuth(); | |||
| if (!checkRole(group.role)) return null; | |||
| return ( | |||
| <Box sx={{ bgcolor: "#101F33" }}> | |||
| <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 { checkRole } = useAuth(); | |||
| React.useEffect(() => { | |||
| setOpen(shouldOpen); | |||
| }, [shouldOpen]); | |||
| if (!checkRole(role)) return null; | |||
| // 子要素ありの場合 | |||
| if (elements && elements.length !== 0) { | |||
| const handleClick = () => { | |||
| setOpen(!open); | |||
| }; | |||
| return ( | |||
| <> | |||
| <ListItemButton onClick={handleClick} sx={item} selected={false}> | |||
| @@ -184,7 +177,7 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||
| }; | |||
| const selected = id === pageId; | |||
| return ( | |||
| <ListItemButton onClick={handleClick} selected={selected}> | |||
| <ListItemButton onClick={handleClick} selected={selected} sx={item}> | |||
| <ListItemIcon>{icon}</ListItemIcon> | |||
| <ListItemText>{label}</ListItemText> | |||
| </ListItemButton> | |||
| @@ -194,12 +187,15 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) { | |||
| function useContents(children: Child[]) { | |||
| const { pageId } = usePage(); | |||
| const { navigateWhenChanged } = useNavigateCustom(); | |||
| const { checkRole } = useAuth(); | |||
| const [shouldOpen, setShouldOpen] = React.useState(false); | |||
| const elements = React.useMemo(() => { | |||
| setShouldOpen(false); | |||
| return children.map(({ label, id }, index) => { | |||
| return children.map(({ label, id, role }, index) => { | |||
| if (!checkRole(role)) return; | |||
| const selected = id === pageId; | |||
| if (selected) { | |||
| setShouldOpen(true); | |||
| @@ -1,16 +1,16 @@ | |||
| import { Tabs } from "@mui/material"; | |||
| import { TabProps, useTab } from "./tabutil"; | |||
| import { PageID } from "codes/page"; | |||
| import { PageID, TabID } from "codes/page"; | |||
| import { getPath } from "routes/path"; | |||
| const tabs: TabProps[] = [ | |||
| { | |||
| label: "一覧", | |||
| pageId: PageID.DASHBOARD_CONTRACT_LIST, | |||
| tabId: TabID.NONE, | |||
| }, | |||
| { | |||
| 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 { useMemo } from "react"; | |||
| import { Tab } from "."; | |||
| @@ -6,15 +6,15 @@ import { getPath } from "routes/path"; | |||
| export type TabProps = { | |||
| label: string; | |||
| pageId: PageID; | |||
| tabId: TabID; | |||
| }; | |||
| export function useTab(tabs: TabProps[]) { | |||
| const { pageId } = usePage(); | |||
| const { pageId, tabId } = usePage(); | |||
| 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 }} />; | |||
| }); | |||
| }, [tabs]); | |||
| @@ -22,10 +22,10 @@ export function useTab(tabs: TabProps[]) { | |||
| const getTabIndex = useMemo(() => { | |||
| return ( | |||
| tabs.findIndex((tab) => { | |||
| return tab.pageId === pageId; | |||
| return tab.tabId === tabId; | |||
| }) ?? 0 | |||
| ); | |||
| }, [pageId, tabs]); | |||
| }, [tabId, tabs]); | |||
| return { | |||
| 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 { PageID } from "codes/page"; | |||
| import { PageID, TabID } from "codes/page"; | |||
| import useDashboard from "hooks/useDashBoard"; | |||
| import ContractTabs from "layouts/dashbord/tab/ContractTabs"; | |||
| import { useEffect } from "react"; | |||
| export default function ContractDetail() { | |||
| const { setHeaderTitle, setTabs } = useDashboard( | |||
| PageID.DASHBOARD_CONTRACT_DETAIL | |||
| PageID.DASHBOARD_CONTRACT_DETAIL, | |||
| TabID.A | |||
| ); | |||
| 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 useTable, { UseTableReturn } from "hooks/useTable"; | |||
| import ContractTabs from "layouts/dashbord/tab/ContractTabs"; | |||
| import { useEffect } from "react"; | |||
| import { Contract } from "types/contract"; | |||
| export default function ContractList() { | |||
| const { setHeaderTitle, setTabs } = useDashboard( | |||
| PageID.DASHBOARD_CONTRACT_LIST | |||
| PageID.DASHBOARD_CONTRACT_LIST, | |||
| TabID.NONE | |||
| ); | |||
| const table = useTable<Contract>(); | |||
| useEffect(() => { | |||
| setHeaderTitle("契約者一覧"); | |||
| 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 DashboardLayout from "layouts/dashbord"; | |||
| 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 { PageID } from "codes/page"; | |||
| import SimpleLayout from "layouts/simple"; | |||
| const Loadable = (Component: ElementType) => (props: any) => { | |||
| 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 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( | |||
| lazy(() => import("pages/dashboard/contract/list")) | |||
| @@ -1,54 +1,75 @@ | |||
| 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 PREFIX = { | |||
| list: "list", | |||
| detail: "detail", | |||
| }; | |||
| export const PATH = { | |||
| login: "/login", | |||
| logout: "/logout", | |||
| 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 = { | |||
| [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 = { | |||
| page?: number; | |||
| 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); | |||
| // ページ番号解決 | |||
| @@ -67,9 +88,13 @@ export function getPath(pageId: PageID, option?: PathOption) { | |||
| 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) { | |||
| path = replace(path, "/" + exclude + "/", ""); | |||
| @@ -1,5 +1,6 @@ | |||
| import { ThemeProvider, createTheme } from "@mui/material"; | |||
| import { HasChildren } from "@types"; | |||
| import { memo, useMemo } from "react"; | |||
| let theme = createTheme({ | |||
| palette: { | |||
| @@ -43,6 +44,13 @@ theme = { | |||
| }, | |||
| }, | |||
| }, | |||
| MuiTableHead: { | |||
| styleOverrides: { | |||
| root: { | |||
| backgroundColor: "#D1E6D6", | |||
| }, | |||
| }, | |||
| }, | |||
| MuiButton: { | |||
| styleOverrides: { | |||
| root: { | |||
| @@ -145,6 +153,9 @@ theme = { | |||
| }; | |||
| 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": { | |||
| "target": "es5", | |||
| "target": "ES2022", | |||
| "lib": [ | |||
| "dom", | |||
| "dom.iterable", | |||
| @@ -1334,6 +1334,11 @@ | |||
| resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" | |||
| 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": | |||
| version "0.11.8" | |||
| resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" | |||
| @@ -1669,6 +1674,20 @@ | |||
| prop-types "^15.8.1" | |||
| 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": | |||
| version "5.12.2" | |||
| resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.12.2.tgz#4a0186d25b01d693171366e1c00de0e7c8c35f6a" | |||
| @@ -1681,6 +1700,20 @@ | |||
| dependencies: | |||
| "@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": | |||
| version "5.12.2" | |||
| resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.12.2.tgz#c3fcc94e523d9e673e2e045dfad04d12ab454a80" | |||
| @@ -1708,6 +1741,15 @@ | |||
| "@mui/utils" "^5.12.0" | |||
| 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": | |||
| version "5.12.0" | |||
| resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.12.0.tgz#44640cad961adcc9413ae32116237cd1c8f7ddb0" | |||
| @@ -1718,6 +1760,16 @@ | |||
| csstype "^3.1.2" | |||
| 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": | |||
| version "5.12.1" | |||
| resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.12.1.tgz#8452bc03159f0a6725b96bde1dee1316e308231b" | |||
| @@ -1732,6 +1784,20 @@ | |||
| csstype "^3.1.2" | |||
| 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": | |||
| version "7.2.4" | |||
| resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328" | |||
| @@ -1748,6 +1814,17 @@ | |||
| prop-types "^15.8.1" | |||
| 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": | |||
| 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" | |||
| @@ -2040,6 +2117,13 @@ | |||
| resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" | |||
| 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": | |||
| version "7.20.0" | |||
| resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" | |||
| @@ -2103,6 +2187,13 @@ | |||
| dependencies: | |||
| "@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": | |||
| version "3.7.4" | |||
| 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" | |||
| 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: | |||
| version "3.1.1" | |||
| 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" | |||
| wrap-ansi "^7.0.0" | |||
| clsx@^1.2.1: | |||
| clsx@^1.1.0, clsx@^1.2.1: | |||
| version "1.2.1" | |||
| resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" | |||
| integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== | |||
| @@ -3798,6 +3898,13 @@ data-urls@^2.0.0: | |||
| whatwg-mimetype "^2.3.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: | |||
| version "2.6.9" | |||
| 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" | |||
| 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" | |||
| resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" | |||
| integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== | |||
| @@ -4811,6 +4918,15 @@ form-data@^3.0.0: | |||
| combined-stream "^1.0.8" | |||
| 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: | |||
| version "0.2.0" | |||
| 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" | |||
| 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: | |||
| version "1.0.1" | |||
| 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" | |||
| 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: | |||
| version "4.0.1" | |||
| 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" | |||
| 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: | |||
| version "2.0.7" | |||
| 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" | |||
| 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: | |||
| version "1.9.0" | |||
| 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" | |||
| 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: | |||
| version "16.13.1" | |||
| 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" | |||
| 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: | |||
| version "1.0.5" | |||
| 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" | |||
| 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: | |||
| version "4.1.2" | |||
| 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" | |||
| 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: | |||
| version "1.6.18" | |||
| 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" | |||
| resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" | |||
| 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" | |||