Browse Source

認証機能拡充

develop
sosuke.iwabuchi 2 years ago
parent
commit
cfaad1641f
58 changed files with 3033 additions and 159 deletions
  1. +7
    -0
      .env
  2. +10
    -1
      package.json
  3. +2
    -0
      src/@types/index.ts
  4. +20
    -16
      src/App.tsx
  5. +200
    -0
      src/api/auth.ts
  6. +234
    -0
      src/api/index.ts
  7. +25
    -0
      src/api/url.ts
  8. +15
    -3
      src/codes/page.ts
  9. +83
    -0
      src/components/form/CheckBoxCustom.tsx
  10. +45
    -0
      src/components/form/DatePickerCustom.tsx
  11. +56
    -0
      src/components/form/InputAlert.tsx
  12. +95
    -0
      src/components/form/TextFieldCustom.tsx
  13. +36
    -0
      src/components/form/TextFieldEx.tsx
  14. +24
    -0
      src/components/hook-form/FormProvider.tsx
  15. +121
    -0
      src/components/hook-form/RHFAutoComplete.tsx
  16. +96
    -0
      src/components/hook-form/RHFCheckbox.tsx
  17. +106
    -0
      src/components/hook-form/RHFDatePicker.tsx
  18. +53
    -0
      src/components/hook-form/RHFRadioGroup.tsx
  19. +119
    -0
      src/components/hook-form/RHFSelect.tsx
  20. +29
    -0
      src/components/hook-form/RHFSwitch.tsx
  21. +66
    -0
      src/components/hook-form/RHFTextField.tsx
  22. +33
    -0
      src/components/hook-form/index.ts
  23. +96
    -0
      src/components/table/TableHeadCustom.tsx
  24. +1
    -0
      src/components/table/index.ts
  25. +3
    -0
      src/config.ts
  26. +126
    -0
      src/contexts/AuthContext.tsx
  27. +26
    -6
      src/contexts/DashboardLayoutContext.tsx
  28. +9
    -3
      src/contexts/PageContext.tsx
  29. +131
    -0
      src/contexts/SearchConditionContext.tsx
  30. +168
    -0
      src/hooks/useAPICall.ts
  31. +7
    -0
      src/hooks/useAuth.ts
  32. +6
    -4
      src/hooks/useDashBoard.ts
  33. +1
    -0
      src/hooks/useNavigateCustom.ts
  34. +6
    -0
      src/hooks/useSearchConditionContext.ts
  35. +26
    -0
      src/hooks/useSnackbarCustom.ts
  36. +177
    -0
      src/hooks/useTable.ts
  37. +48
    -0
      src/hooks/useURLSearchParams.ts
  38. +8
    -9
      src/index.tsx
  39. +11
    -6
      src/layouts/dashbord/index.tsx
  40. +33
    -37
      src/layouts/dashbord/navigator.tsx
  41. +3
    -3
      src/layouts/dashbord/tab/ContractTabs.tsx
  42. +7
    -7
      src/layouts/dashbord/tab/tabutil.tsx
  43. +26
    -0
      src/layouts/simple/index.tsx
  44. +80
    -0
      src/pages/auth/login.tsx
  45. +21
    -0
      src/pages/auth/logout.tsx
  46. +3
    -2
      src/pages/dashboard/contract/detail.tsx
  47. +175
    -4
      src/pages/dashboard/contract/list.tsx
  48. +17
    -0
      src/providers/CsrfTokenProvider.tsx
  49. +9
    -0
      src/providers/SnackbarProvider.tsx
  50. +32
    -21
      src/routes/index.tsx
  51. +57
    -32
      src/routes/path.ts
  52. +13
    -2
      src/theme/index.tsx
  53. +11
    -0
      src/types/common.ts
  54. +5
    -0
      src/types/contract.ts
  55. +20
    -0
      src/utils/axios.ts
  56. +24
    -0
      src/utils/datetime.ts
  57. +1
    -1
      tsconfig.json
  58. +171
    -2
      yarn.lock

+ 7
- 0
.env View File

@@ -0,0 +1,7 @@
GENERATE_SOURCEMAP=true

PORT=8080

# HOST
# ローカル
REACT_APP_HOST_API_KEY=http://localhost

+ 10
- 1
package.json View File

@@ -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",


+ 2
- 0
src/@types/index.ts View File

@@ -1,5 +1,7 @@
import { ReactNode } from "react";

export type DataUrl = string;

export type HasChildren = {
children: ReactNode;
};


+ 20
- 16
src/App.tsx View File

@@ -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;

+ 200
- 0
src/api/auth.ts View File

@@ -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;
// };

+ 234
- 0
src/api/index.ts View File

@@ -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;
}

+ 25
- 0
src/api/url.ts View File

@@ -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] ?? "");
};

+ 15
- 3
src/codes/page.ts View File

@@ -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];

+ 83
- 0
src/components/form/CheckBoxCustom.tsx View File

@@ -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}
/>
);
}

+ 45
- 0
src/components/form/DatePickerCustom.tsx View File

@@ -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);

+ 56
- 0
src/components/form/InputAlert.tsx View File

@@ -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);

+ 95
- 0
src/components/form/TextFieldCustom.tsx View File

@@ -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}
/>
);
}

+ 36
- 0
src/components/form/TextFieldEx.tsx View File

@@ -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);

+ 24
- 0
src/components/hook-form/FormProvider.tsx View File

@@ -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>
);
}

+ 121
- 0
src/components/hook-form/RHFAutoComplete.tsx View File

@@ -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,
},
}}
/>
)}
/>
);
}

+ 96
- 0
src/components/hook-form/RHFCheckbox.tsx View File

@@ -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>
);
}}
/>
);
}

+ 106
- 0
src/components/hook-form/RHFDatePicker.tsx View File

@@ -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);

+ 53
- 0
src/components/hook-form/RHFRadioGroup.tsx View File

@@ -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>
)}
/>
);
}

+ 119
- 0
src/components/hook-form/RHFSelect.tsx View File

@@ -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>
)}
/>
);
}

+ 29
- 0
src/components/hook-form/RHFSwitch.tsx View File

@@ -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}
/>
);
}

+ 66
- 0
src/components/hook-form/RHFTextField.tsx View File

@@ -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}
/>
)}
/>
);
}

+ 33
- 0
src/components/hook-form/index.ts View File

@@ -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;
}

+ 96
- 0
src/components/table/TableHeadCustom.tsx View File

@@ -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>
);
}

+ 1
- 0
src/components/table/index.ts View File

@@ -0,0 +1 @@
export { default as TableHeadCustom } from "./TableHeadCustom";

+ 3
- 0
src/config.ts View File

@@ -0,0 +1,3 @@
// API
// ----------------------------------------------------------------------
export const HOST_API = process.env.REACT_APP_HOST_API_KEY || "";

+ 126
- 0
src/contexts/AuthContext.tsx View File

@@ -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);

+ 26
- 6
src/contexts/DashboardLayoutContext.tsx View File

@@ -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}


+ 9
- 3
src/contexts/PageContext.tsx View File

@@ -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}


+ 131
- 0
src/contexts/SearchConditionContext.tsx View File

@@ -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>
);
}

+ 168
- 0
src/hooks/useAPICall.ts View File

@@ -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,
};
}

+ 7
- 0
src/hooks/useAuth.ts View File

@@ -0,0 +1,7 @@
import { AuthContext } from "contexts/AuthContext";
import { useContext } from "react";

export default function useAuth() {
const context = useContext(AuthContext);
return context;
}

+ 6
- 4
src/hooks/useDashBoard.ts View File

@@ -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
- 0
src/hooks/useNavigateCustom.ts View File

@@ -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();


+ 6
- 0
src/hooks/useSearchConditionContext.ts View File

@@ -0,0 +1,6 @@
import { useContext } from "react";
import { SearchConditionContext } from "contexts/SearchConditionContext";

export default function useSearchConditionContext() {
return useContext(SearchConditionContext);
}

+ 26
- 0
src/hooks/useSnackbarCustom.ts View File

@@ -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,
};
}

+ 177
- 0
src/hooks/useTable.ts View File

@@ -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,
};
}

+ 48
- 0
src/hooks/useURLSearchParams.ts View File

@@ -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,
};
}

+ 8
- 9
src/index.tsx View File

@@ -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


+ 11
- 6
src/layouts/dashbord/index.tsx View File

@@ -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 />


+ 33
- 37
src/layouts/dashbord/navigator.tsx View File

@@ -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);


+ 3
- 3
src/layouts/dashbord/tab/ContractTabs.tsx View File

@@ -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,
},
];



+ 7
- 7
src/layouts/dashbord/tab/tabutil.tsx View File

@@ -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,


+ 26
- 0
src/layouts/simple/index.tsx View File

@@ -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>
);
}

+ 80
- 0
src/pages/auth/login.tsx View File

@@ -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>
);
}

+ 21
- 0
src/pages/auth/logout.tsx View File

@@ -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;
}

+ 3
- 2
src/pages/dashboard/contract/detail.tsx View File

@@ -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(() => {


+ 175
- 4
src/pages/dashboard/contract/list.tsx View File

@@ -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>
);
}

+ 17
- 0
src/providers/CsrfTokenProvider.tsx View File

@@ -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);

+ 9
- 0
src/providers/SnackbarProvider.tsx View File

@@ -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>
);
}

+ 32
- 21
src/routes/index.tsx View File

@@ -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"))


+ 57
- 32
src/routes/path.ts View File

@@ -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 + "/", "");


+ 13
- 2
src/theme/index.tsx View File

@@ -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);

+ 11
- 0
src/types/common.ts View File

@@ -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;

+ 5
- 0
src/types/contract.ts View File

@@ -0,0 +1,5 @@
import { Data } from "./common";

export type Contract = {
name: string;
} & Data;

+ 20
- 0
src/utils/axios.ts View File

@@ -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;

+ 24
- 0
src/utils/datetime.ts View File

@@ -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
- 1
tsconfig.json View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "ES2022",
"lib": [
"dom",
"dom.iterable",


+ 171
- 2
yarn.lock View File

@@ -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"

Loading…
Cancel
Save