| @@ -1,7 +1,7 @@ | |||
| import { APICommonResponse, ApiId, HttpMethod, makeParam, request } from "api"; | |||
| import { getUrl } from "./url"; | |||
| type 顧客マスタ = { | |||
| export type 顧客マスタ = { | |||
| id: number; | |||
| customer_id: string; | |||
| customer_name: string; | |||
| @@ -1,7 +1,7 @@ | |||
| import { APICommonResponse, ApiId, HttpMethod, makeParam, request } from "api"; | |||
| import { getUrl } from "./url"; | |||
| type 運営会社ログインユーザ = { | |||
| export type 運営会社ログインユーザ = { | |||
| id: string; | |||
| name: string; | |||
| email: string; | |||
| @@ -14,7 +14,7 @@ export type 顧客ログインユーザ新規登録Request = { | |||
| name: string; | |||
| email: string; | |||
| password: string; | |||
| customerCode: string; | |||
| customer_code: string; | |||
| }; | |||
| export type 顧客ログインユーザ新規登録Response = { | |||
| data: { | |||
| @@ -55,19 +55,21 @@ export const 店舗ログインユーザ新規登録 = async ( | |||
| return res; | |||
| }; | |||
| // -------顧客ログインユーザ新規登録--------------- | |||
| export type 顧客一覧取得Request = { | |||
| // -------顧客ログインユーザ一覧取得--------------- | |||
| export type 顧客ログインユーザ一覧取得Request = { | |||
| email?: string; | |||
| name?: string; | |||
| }; | |||
| export type 顧客一覧取得Response = { | |||
| export type 顧客ログインユーザ一覧取得Response = { | |||
| data: { | |||
| list: 運営会社ログインユーザ[]; | |||
| }; | |||
| } & APICommonResponse; | |||
| export const 顧客一覧取得 = async (param: 顧客一覧取得Request) => { | |||
| const res = await request<顧客一覧取得Response>({ | |||
| url: getUrl(ApiId.QRサービス券取得), | |||
| export const 顧客ログインユーザ一覧取得 = async ( | |||
| param: 顧客ログインユーザ一覧取得Request | |||
| ) => { | |||
| const res = await request<顧客ログインユーザ一覧取得Response>({ | |||
| url: getUrl(ApiId.顧客ログインユーザ一覧取得), | |||
| method: HttpMethod.GET, | |||
| data: new URLSearchParams(param), | |||
| }); | |||
| @@ -2,18 +2,24 @@ import { PageID as P } from "pages"; | |||
| import { UserRole } from "./UserRole"; | |||
| const 共通ルート = [P.LOGIN, P.LOGOUT]; | |||
| const 認証後共通ルート = [P.DASHBOARD_ENPTY, P.DASHBOARD_OVERVIEW]; | |||
| const 認可別許可ルート: { | |||
| [route: string]: P[]; | |||
| } = { | |||
| [UserRole.NONE]: [...共通ルート], | |||
| [UserRole.ADMIN]: [...共通ルート, P.DASHBOARD_OVERVIEW], | |||
| [UserRole.ADMIN]: [ | |||
| ...共通ルート, | |||
| ...認証後共通ルート, | |||
| P.ログインユーザ_顧客一覧, | |||
| P.ログインユーザ_顧客新規登録, | |||
| ], | |||
| [UserRole.CUSTOMER]: [...共通ルート, P.DASHBOARD_OVERVIEW], | |||
| [UserRole.CUSTOMER]: [...共通ルート, ...認証後共通ルート], | |||
| [UserRole.SHOP]: [ | |||
| ...共通ルート, | |||
| P.DASHBOARD_OVERVIEW, | |||
| ...認証後共通ルート, | |||
| P.サービス券発行用QRコード, | |||
| P.サービス券利用履歴, | |||
| ], | |||
| @@ -57,7 +57,12 @@ export default function TableHeadCustom({ | |||
| sx, | |||
| }: Props) { | |||
| return ( | |||
| <TableHead sx={sx}> | |||
| <TableHead | |||
| sx={{ | |||
| backgroundColor: "#b0e0e6", | |||
| ...sx, | |||
| }} | |||
| > | |||
| <TableRow> | |||
| {onSelectAllRows && ( | |||
| <TableCell padding="checkbox"> | |||
| @@ -47,7 +47,7 @@ export default function useNavigateCustom() { | |||
| // 同じURLで遷移要求があった場合、reload設定されていれば同じページを読み込みなおす | |||
| // 一旦、空白のページを経由する必要がある | |||
| if (currentUrl === newPath && option?.reload) { | |||
| navigate(getPath(PageID.NONE)); | |||
| navigate(getPath(PageID.DASHBOARD_ENPTY)); | |||
| setTimeout(() => { | |||
| navigate(newPath); | |||
| }, 50); | |||
| @@ -78,6 +78,21 @@ export default function Navigator(props: DrawerProps) { | |||
| label: "管理メニュー", | |||
| children: [], | |||
| }, | |||
| { | |||
| label: "ログインユーザ管理", | |||
| children: [ | |||
| { | |||
| label: "顧客一覧", | |||
| icon: <ArticleIcon />, | |||
| id: PageID.ログインユーザ_顧客一覧, | |||
| }, | |||
| { | |||
| label: "顧客新規登録", | |||
| icon: <ArticleIcon />, | |||
| id: PageID.ログインユーザ_顧客新規登録, | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| label: "QRサービス券", | |||
| children: [ | |||
| @@ -0,0 +1,3 @@ | |||
| export default function Empty() { | |||
| return null; | |||
| } | |||
| @@ -0,0 +1,87 @@ | |||
| import { Box, Grid, Stack, Typography } from "@mui/material"; | |||
| import { Dictionary } from "@types"; | |||
| import { | |||
| 運営会社ログインユーザ, | |||
| 顧客ログインユーザ一覧取得, | |||
| } from "api/login-user"; | |||
| import { FormProvider, RHFTextField } from "components/hook-form"; | |||
| import useAPICall from "hooks/useAPICall"; | |||
| import useSearchConditionContext from "hooks/useSearchConditionContext"; | |||
| import { UseTableReturn } from "hooks/useTable"; | |||
| import { isEqual } from "lodash"; | |||
| import { useEffect, useState } from "react"; | |||
| import { useForm } from "react-hook-form"; | |||
| type FormProps = { | |||
| name: string; | |||
| email: string; | |||
| }; | |||
| type CommonProps = { | |||
| table: UseTableReturn<運営会社ログインユーザ>; | |||
| }; | |||
| export default function SearchBox({ table }: CommonProps) { | |||
| const [lastSendSearchCondition, setLastSendSearchCondition] = | |||
| useState<object>({ default: false }); | |||
| const { condition, initialized, get, addCondition } = | |||
| useSearchConditionContext(); | |||
| const form = useForm<FormProps>({ | |||
| defaultValues: { | |||
| name: "", | |||
| email: "", | |||
| }, | |||
| }); | |||
| const { callAPI: call顧客一覧取得, makeSendData } = useAPICall({ | |||
| apiMethod: 顧客ログインユーザ一覧取得, | |||
| form, | |||
| backDrop: true, | |||
| onSuccess: ({ data }) => { | |||
| table.setRowData(data.list); | |||
| }, | |||
| }); | |||
| const handleBlur = () => { | |||
| addCondition(form.getValues()); | |||
| }; | |||
| const handleSubmit = async (data: FormProps) => { | |||
| addCondition(data); | |||
| }; | |||
| const fetch = async (data: Dictionary) => { | |||
| const sendData = makeSendData(data); | |||
| console.log({ sendData }); | |||
| if (!isEqual(sendData, lastSendSearchCondition)) { | |||
| setLastSendSearchCondition(sendData); | |||
| call顧客一覧取得(sendData); | |||
| } | |||
| }; | |||
| useEffect(() => { | |||
| console.log({ initialized, condition }); | |||
| if (initialized) { | |||
| fetch(condition); | |||
| } | |||
| }, [condition, initialized]); | |||
| return ( | |||
| <FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}> | |||
| <Box sx={{ p: 1, m: 1 }}> | |||
| <Grid container spacing={2} mt={-1}> | |||
| <Grid item xs={12} lg={6}> | |||
| <Stack> | |||
| <Typography>名前</Typography> | |||
| <RHFTextField name="name" onBlur={handleBlur} /> | |||
| </Stack> | |||
| </Grid> | |||
| <Grid item xs={12} lg={6}> | |||
| <Stack> | |||
| <Typography>Email</Typography> | |||
| <RHFTextField name="email" onBlur={handleBlur} /> | |||
| </Stack> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </FormProvider> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,93 @@ | |||
| import { | |||
| Box, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TablePagination, | |||
| TableRow, | |||
| } from "@mui/material"; | |||
| import { 運営会社ログインユーザ } from "api/login-user"; | |||
| import TableHeadCustom, { | |||
| HeadLabelProps, | |||
| } from "components/table/TableHeadCustom"; | |||
| import { UseTableReturn } from "hooks/useTable"; | |||
| type CommonProps = { | |||
| table: UseTableReturn<運営会社ログインユーザ>; | |||
| }; | |||
| export default function TableBox({ table }: CommonProps) { | |||
| const TABLE_HEAD: HeadLabelProps[] = [ | |||
| { id: "name", label: "名前", align: "left", needSort: false }, | |||
| { id: "email", label: "Email", align: "left", needSort: false }, | |||
| { id: "customer_name", label: "顧客名", align: "left", needSort: false }, | |||
| ]; | |||
| const { | |||
| order, | |||
| page, | |||
| sort, | |||
| rowsPerPage, | |||
| fetched, | |||
| fillteredRow, | |||
| isNotFound, | |||
| dataLength, | |||
| // | |||
| onSort, | |||
| onChangePage, | |||
| onChangeRowsPerPage, | |||
| // | |||
| setRowData, | |||
| // | |||
| ROWS_PER_PAGES, | |||
| } = table; | |||
| return ( | |||
| <> | |||
| <TableContainer> | |||
| <Table sx={{ minWidth: 1200 }} 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: 運営会社ログインユーザ; | |||
| }; | |||
| function Row({ data }: RowProps) { | |||
| return ( | |||
| <TableRow hover sx={{ cursor: "pointer" }}> | |||
| <TableCell>{data.name}</TableCell> | |||
| <TableCell>{data.email}</TableCell> | |||
| <TableCell> | |||
| {data.customer_name}({data.customer_code}) | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| import { Box } from "@mui/material"; | |||
| import { 運営会社ログインユーザ } from "api/login-user"; | |||
| import { SearchConditionContextProvider } from "contexts/SearchConditionContext"; | |||
| import useDashboard from "hooks/useDashBoard"; | |||
| import useTable from "hooks/useTable"; | |||
| import { PageID, TabID } from "pages"; | |||
| import { useEffect } from "react"; | |||
| import SearchBox from "./SearchBox"; | |||
| import TableBox from "./TableBox"; | |||
| export default function 顧客一覧() { | |||
| const { setHeaderTitle, setTabs } = useDashboard( | |||
| PageID.ログインユーザ_顧客一覧, | |||
| TabID.NONE | |||
| ); | |||
| useEffect(() => { | |||
| setHeaderTitle("ログインユーザ一覧"); | |||
| setTabs(null); | |||
| }, []); | |||
| return ( | |||
| <SearchConditionContextProvider> | |||
| <Page /> | |||
| </SearchConditionContextProvider> | |||
| ); | |||
| } | |||
| function Page() { | |||
| const table = useTable<運営会社ログインユーザ>(); | |||
| return ( | |||
| <Box> | |||
| <SearchBox table={table} /> | |||
| <TableBox table={table} /> | |||
| </Box> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,148 @@ | |||
| import { yupResolver } from "@hookform/resolvers/yup"; | |||
| import { Box, Button, Stack, Typography } from "@mui/material"; | |||
| import { 顧客マスタ, 顧客マスタ一覧取得 } from "api/customer"; | |||
| import { 顧客ログインユーザ新規登録 } from "api/login-user"; | |||
| import { | |||
| FormProvider, | |||
| RHFAutoComplete, | |||
| RHFTextField, | |||
| } from "components/hook-form"; | |||
| import { | |||
| AutoCompleteOption, | |||
| AutoCompleteOptionType, | |||
| getValue, | |||
| } from "components/hook-form/RHFAutoComplete"; | |||
| import StackRow from "components/stack/StackRow"; | |||
| import useAPICall from "hooks/useAPICall"; | |||
| import useDashboard from "hooks/useDashBoard"; | |||
| import useNavigateCustom from "hooks/useNavigateCustom"; | |||
| import useSnackbarCustom from "hooks/useSnackbarCustom"; | |||
| import { PageID, TabID } from "pages"; | |||
| import { useEffect, useMemo, useState } from "react"; | |||
| import { useForm } from "react-hook-form"; | |||
| import { getPath } from "routes/path"; | |||
| import 顧客マスタストア from "storage/cache/顧客マスタ"; | |||
| import { object, string } from "yup"; | |||
| type FormProps = { | |||
| name: string; | |||
| email: string; | |||
| password: string; | |||
| password_retype: string; | |||
| customer_code: AutoCompleteOptionType; | |||
| }; | |||
| export default function 顧客ログインユーザ新規登録_() { | |||
| const { setHeaderTitle, setTabs } = useDashboard( | |||
| PageID.ログインユーザ_顧客新規登録, | |||
| TabID.NONE | |||
| ); | |||
| const { success, error } = useSnackbarCustom(); | |||
| const { navigateWhenChanged } = useNavigateCustom(); | |||
| const [顧客マスタ, set顧客マスタ] = useState<顧客マスタ[]>([]); | |||
| const 顧客マスタOptions: AutoCompleteOption[] = useMemo(() => { | |||
| return 顧客マスタ.map((data) => { | |||
| return { | |||
| label: data.customer_name, | |||
| value: data.customer_id, | |||
| }; | |||
| }); | |||
| }, [顧客マスタ]); | |||
| const form = useForm<FormProps>({ | |||
| defaultValues: { | |||
| name: "", | |||
| email: "", | |||
| password: "", | |||
| password_retype: "", | |||
| customer_code: "", | |||
| }, | |||
| resolver: yupResolver( | |||
| object().shape({ | |||
| name: string().required("必須項目です"), | |||
| email: string().required("必須項目です"), | |||
| password: string().required("必須項目です"), | |||
| password_retype: string() | |||
| .required("必須項目です") | |||
| .test("retype", "入力が一致しません", function (value) { | |||
| return this.parent.password === value; | |||
| }), | |||
| customer_code: object().required("必須項目です"), | |||
| }) | |||
| ), | |||
| }); | |||
| const { callAPI: call顧客ログインユーザ新規登録 } = useAPICall({ | |||
| apiMethod: 顧客ログインユーザ新規登録, | |||
| form, | |||
| backDrop: true, | |||
| onSuccess: ({ data }, sendData) => { | |||
| success("登録しました"); | |||
| navigateWhenChanged( | |||
| getPath(PageID.ログインユーザ_顧客一覧), | |||
| new URLSearchParams({ email: sendData.email }) | |||
| ); | |||
| }, | |||
| onFailed: () => { | |||
| error("失敗しました"); | |||
| }, | |||
| }); | |||
| const handleSubmit = (data: FormProps) => { | |||
| const customer_code = getValue(data.customer_code); | |||
| call顧客ログインユーザ新規登録({ | |||
| ...data, | |||
| customer_code, | |||
| }); | |||
| }; | |||
| useEffect(() => { | |||
| setHeaderTitle("ログインユーザ登録"); | |||
| setTabs(null); | |||
| 顧客マスタストア.get().then((data) => { | |||
| if (data) { | |||
| set顧客マスタ(data); | |||
| } | |||
| }); | |||
| }, []); | |||
| return ( | |||
| <FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}> | |||
| <Box> | |||
| <Stack> | |||
| <Box> | |||
| <Typography>名称</Typography> | |||
| <RHFTextField name="name" /> | |||
| </Box> | |||
| <Box> | |||
| <Typography>Email</Typography> | |||
| <RHFTextField type="email" name="email" /> | |||
| </Box> | |||
| <Box> | |||
| <Typography>パスワード</Typography> | |||
| <RHFTextField type="password" name="password" /> | |||
| </Box> | |||
| <Box> | |||
| <Typography>パスワード(再入力)</Typography> | |||
| <RHFTextField type="password" name="password_retype" /> | |||
| </Box> | |||
| <Box> | |||
| <Typography>顧客</Typography> | |||
| <RHFAutoComplete | |||
| name="customer_code" | |||
| options={顧客マスタOptions} | |||
| size="small" | |||
| /> | |||
| </Box> | |||
| <StackRow> | |||
| <Button type="submit">登録</Button> | |||
| </StackRow> | |||
| </Stack> | |||
| </Box> | |||
| </FormProvider> | |||
| ); | |||
| } | |||
| @@ -2,15 +2,27 @@ let id = 0; | |||
| export const PageID = { | |||
| NONE: id++, | |||
| // 認証関連 ---------------------------------- | |||
| LOGIN: id++, | |||
| LOGOUT: id++, | |||
| // ダッシュボード系 START ---------------------------------- | |||
| DASHBOARD_ENPTY: id++, | |||
| DASHBOARD_OVERVIEW: id++, | |||
| // 顧客管理 | |||
| 顧客一覧: id++, | |||
| // ログインユーザー管理 | |||
| ログインユーザ_顧客一覧: id++, | |||
| ログインユーザ_顧客新規登録: id++, | |||
| ログインユーザ_店舗一覧: id++, | |||
| サービス券発行用QRコード: id++, | |||
| サービス券利用履歴: id++, | |||
| QRサービス券発行申請: id++, | |||
| // ダッシュボード系 END ---------------------------------- | |||
| PAGE_403: id++, | |||
| PAGE_404: id++, | |||
| @@ -29,7 +29,14 @@ const getTabId = (key: PathKey): TabID => { | |||
| }; | |||
| const PATHS_DASHBOARD = { | |||
| [makePathKey(PageID.DASHBOARD_ENPTY)]: "/dashboard/loading", | |||
| [makePathKey(PageID.DASHBOARD_OVERVIEW)]: "/dashboard", | |||
| [makePathKey(PageID.ログインユーザ_顧客一覧)]: | |||
| "/dashboard/login-user/customer/list", | |||
| [makePathKey(PageID.ログインユーザ_顧客新規登録)]: | |||
| "/dashboard/login-user/customer/register", | |||
| [makePathKey(PageID.ログインユーザ_店舗一覧)]: | |||
| "/dashboard/login-user/shop/list", | |||
| [makePathKey(PageID.サービス券発行用QRコード)]: "/dashboard/qrcode/generate", | |||
| [makePathKey(PageID.サービス券利用履歴)]: "/dashboard/qrcode/history", | |||
| @@ -12,6 +12,7 @@ export default function DashboardRoutes(): RouteObject[] { | |||
| const { currentRole } = useAuth(); | |||
| const children: RouteObject[] = useMemo(() => { | |||
| const Enpty = Loadable(lazy(() => import("pages/dashboard/empty"))); | |||
| const Dashboard = Loadable(lazy(() => import("pages/dashboard"))); | |||
| const サービス券発行用QRコード = Loadable( | |||
| @@ -20,12 +21,32 @@ export default function DashboardRoutes(): RouteObject[] { | |||
| const サービス券利用履歴 = Loadable( | |||
| lazy(() => import("pages/dashboard/qrcode/サービス券利用履歴")) | |||
| ); | |||
| const 顧客ログインユーザ一覧 = Loadable( | |||
| lazy(() => import("pages/dashboard/login-user/顧客ログインユーザ一覧")) | |||
| ); | |||
| const 顧客ログインユーザ新規登録 = Loadable( | |||
| lazy( | |||
| () => import("pages/dashboard/login-user/顧客ログインユーザ新規登録") | |||
| ) | |||
| ); | |||
| const allChildren = [ | |||
| { | |||
| pageId: PageID.DASHBOARD_ENPTY, | |||
| element: <Enpty />, | |||
| }, | |||
| { | |||
| pageId: PageID.DASHBOARD_OVERVIEW, | |||
| element: <Dashboard />, | |||
| }, | |||
| { | |||
| pageId: PageID.ログインユーザ_顧客一覧, | |||
| element: <顧客ログインユーザ一覧 />, | |||
| }, | |||
| { | |||
| pageId: PageID.ログインユーザ_顧客新規登録, | |||
| element: <顧客ログインユーザ新規登録 />, | |||
| }, | |||
| { | |||
| pageId: PageID.サービス券発行用QRコード, | |||
| element: <サービス券発行用QRコード />, | |||
| @@ -0,0 +1,15 @@ | |||
| import { 顧客マスタ, 顧客マスタ一覧取得 } from "api/customer"; | |||
| class 顧客マスタストア { | |||
| private list: 顧客マスタ[] | undefined = undefined; | |||
| async get() { | |||
| if (this.list === undefined) { | |||
| const res = await 顧客マスタ一覧取得({}); | |||
| this.list = res?.data.list ?? []; | |||
| } | |||
| return this.list; | |||
| } | |||
| } | |||
| export default new 顧客マスタストア(); | |||