diff --git a/dictation_client/src/features/partner/constants.ts b/dictation_client/src/features/partner/constants.ts
new file mode 100644
index 0000000..fd3b330
--- /dev/null
+++ b/dictation_client/src/features/partner/constants.ts
@@ -0,0 +1 @@
+export const LIMIT_PARTNER_VIEW_NUM = 15;
diff --git a/dictation_client/src/features/partner/index.ts b/dictation_client/src/features/partner/index.ts
index 3de17ae..d6ac63e 100644
--- a/dictation_client/src/features/partner/index.ts
+++ b/dictation_client/src/features/partner/index.ts
@@ -2,3 +2,4 @@ export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./partnerSlice";
+export * from "./constants";
diff --git a/dictation_client/src/features/partner/operations.ts b/dictation_client/src/features/partner/operations.ts
index abb662b..e4fed0c 100644
--- a/dictation_client/src/features/partner/operations.ts
+++ b/dictation_client/src/features/partner/operations.ts
@@ -3,7 +3,11 @@ import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
-import { AccountsApi, CreatePartnerAccountRequest } from "../../api/api";
+import {
+ AccountsApi,
+ CreatePartnerAccountRequest,
+ GetPartnersResponse,
+} from "../../api/api";
import { Configuration } from "../../api/configuration";
export const createPartnerAccountAsync = createAsyncThunk<
@@ -62,3 +66,53 @@ export const createPartnerAccountAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
+
+// パートナー一覧取得APIからパートナーのアカウント情報をもらう
+export const getPartnerInfoAsync = createAsyncThunk<
+ // 正常時の戻り値の型
+ GetPartnersResponse,
+ {
+ // パラメータ
+ limit: number;
+ offset: number;
+ },
+ {
+ // rejectした時の返却値の型
+ rejectValue: {
+ error: ErrorObject;
+ };
+ }
+>("partner/getPartnerInfoAsync", async (args, thunkApi) => {
+ const { limit, offset } = args;
+ const { getState } = thunkApi;
+ const state = getState() as RootState;
+ const { configuration, accessToken } = state.auth;
+ const config = new Configuration(configuration);
+ const accountsApi = new AccountsApi(config);
+
+ try {
+ const res = await accountsApi.getPartners(limit, offset, {
+ headers: { authorization: `Bearer ${accessToken}` },
+ });
+ const ret = {
+ partners: res.data.partners,
+ total: res.data.total,
+ };
+ return ret;
+ } catch (e) {
+ const error = createErrorObject(e);
+ const errorMessage =
+ error.code === "E000108"
+ ? getTranslationID("common.message.permissionDeniedError")
+ : getTranslationID("common.message.internalServerError");
+
+ thunkApi.dispatch(
+ openSnackbar({
+ level: "error",
+ message: errorMessage,
+ })
+ );
+
+ return thunkApi.rejectWithValue({ error });
+ }
+});
diff --git a/dictation_client/src/features/partner/partnerSlice.ts b/dictation_client/src/features/partner/partnerSlice.ts
index 6f73872..8a29d5f 100644
--- a/dictation_client/src/features/partner/partnerSlice.ts
+++ b/dictation_client/src/features/partner/partnerSlice.ts
@@ -1,8 +1,15 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { PartnerState } from "./state";
-import { createPartnerAccountAsync } from "./operations";
+import { createPartnerAccountAsync, getPartnerInfoAsync } from "./operations";
+import { LIMIT_PARTNER_VIEW_NUM } from "./constants";
const initialState: PartnerState = {
+ domain: {
+ getPartnersInfo: {
+ total: 0,
+ partners: [],
+ },
+ },
apps: {
addPartner: {
companyName: "",
@@ -10,6 +17,8 @@ const initialState: PartnerState = {
adminName: "",
email: "",
},
+ limit: LIMIT_PARTNER_VIEW_NUM,
+ offset: 0,
isLoading: false,
},
};
@@ -37,6 +46,20 @@ export const partnerSlice = createSlice({
cleanupAddPartner: (state) => {
state.apps.addPartner = initialState.apps.addPartner;
},
+ cleanupApps: (state) => {
+ state.domain = initialState.domain;
+ },
+ savePageInfo: (
+ state,
+ action: PayloadAction<{
+ limit: number;
+ offset: number;
+ }>
+ ) => {
+ const { limit, offset } = action.payload;
+ state.apps.limit = limit;
+ state.apps.offset = offset;
+ },
},
extraReducers: (builder) => {
builder.addCase(createPartnerAccountAsync.pending, (state) => {
@@ -48,6 +71,17 @@ export const partnerSlice = createSlice({
builder.addCase(createPartnerAccountAsync.rejected, (state) => {
state.apps.isLoading = false;
});
+ builder.addCase(getPartnerInfoAsync.pending, (state) => {
+ state.apps.isLoading = true;
+ });
+ builder.addCase(getPartnerInfoAsync.fulfilled, (state, action) => {
+ state.domain.getPartnersInfo.total = action.payload.total;
+ state.domain.getPartnersInfo.partners = action.payload.partners;
+ state.apps.isLoading = false;
+ });
+ builder.addCase(getPartnerInfoAsync.rejected, (state) => {
+ state.apps.isLoading = false;
+ });
},
});
export const {
@@ -56,5 +90,6 @@ export const {
changeCompany,
changeCountry,
cleanupAddPartner,
+ savePageInfo,
} = partnerSlice.actions;
export default partnerSlice.reducer;
diff --git a/dictation_client/src/features/partner/selectors.ts b/dictation_client/src/features/partner/selectors.ts
index 00d1cda..cbfbab6 100644
--- a/dictation_client/src/features/partner/selectors.ts
+++ b/dictation_client/src/features/partner/selectors.ts
@@ -1,4 +1,5 @@
import { RootState } from "app/store";
+import { ceil, floor } from "lodash";
export const selectInputValidationErrors = (state: RootState) => {
// 必須項目のチェック
@@ -33,3 +34,22 @@ export const selectEmail = (state: RootState) =>
state.partner.apps.addPartner.email;
export const selectIsLoading = (state: RootState) =>
state.partner.apps.isLoading;
+
+export const selectPartnersInfo = (state: RootState) =>
+ state.partner.domain.getPartnersInfo;
+export const selectTotal = (state: RootState) =>
+ state.partner.domain.getPartnersInfo.total;
+export const seletctLimit = (state: RootState) => state.partner.apps.limit;
+export const selectOffset = (state: RootState) => state.partner.apps.offset;
+export const selectTotalPage = (state: RootState) => {
+ const { limit } = state.partner.apps;
+ const { total } = state.partner.domain.getPartnersInfo;
+ const page = ceil(total / limit);
+ return page;
+};
+
+export const selectCurrentPage = (state: RootState) => {
+ const { limit, offset } = state.partner.apps;
+ const page = floor(offset / limit) + 1;
+ return page;
+};
diff --git a/dictation_client/src/features/partner/state.ts b/dictation_client/src/features/partner/state.ts
index 6cc9844..6ff5dba 100644
--- a/dictation_client/src/features/partner/state.ts
+++ b/dictation_client/src/features/partner/state.ts
@@ -1,10 +1,20 @@
-import { CreatePartnerAccountRequest } from "../../api/api";
+import {
+ CreatePartnerAccountRequest,
+ GetPartnersResponse,
+} from "../../api/api";
export interface PartnerState {
+ domain: Domain;
apps: Apps;
}
+export interface Domain {
+ getPartnersInfo: GetPartnersResponse;
+}
+
export interface Apps {
+ limit: number;
+ offset: number;
addPartner: CreatePartnerAccountRequest;
isLoading: boolean;
}
diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx
index e7efb50..03d43c4 100644
--- a/dictation_client/src/pages/PartnerPage/index.tsx
+++ b/dictation_client/src/pages/PartnerPage/index.tsx
@@ -1,44 +1,89 @@
-import { useMsal } from "@azure/msal-react";
+/* eslint-disable jsx-a11y/control-has-associated-label */
import { AppDispatch } from "app/store";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
-import { clearToken } from "features/auth";
-import React, { useCallback, useState } from "react";
-import { useDispatch } from "react-redux";
+import React, { useCallback, useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
import styles from "styles/app.module.scss";
-import { loadAccessToken, isApproveTier } from "features/auth/utils";
-import postAdd from "../../assets/images/post_add.svg";
-import { decodeToken } from "../../common/decodeToken";
+import { isApproveTier } from "features/auth/utils";
+import {
+ LIMIT_PARTNER_VIEW_NUM,
+ selectCurrentPage,
+ selectIsLoading,
+ selectOffset,
+ selectTotal,
+ selectTotalPage,
+ getPartnerInfoAsync,
+ selectPartnersInfo,
+} from "features/partner/index";
+import { savePageInfo } from "features/partner/partnerSlice";
+import { getTranslationID } from "translation";
+import { useTranslation } from "react-i18next";
+import personAdd from "../../assets/images/person_add.svg";
import { TIERS } from "../../components/auth/constants";
import { AddPartnerAccountPopup } from "./addPartnerAccountPopup";
+import checkFill from "../../assets/images/check_fill.svg";
const PartnerPage: React.FC = (): JSX.Element => {
- const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
const [isPopupOpen, setIsPopupOpen] = useState(false);
+ const [t] = useTranslation();
+ const total = useSelector(selectTotal);
+ const totalPage = useSelector(selectTotalPage);
+ const offset = useSelector(selectOffset);
+ const currentPage = useSelector(selectCurrentPage);
+ const isLoading = useSelector(selectIsLoading);
- /* XXX 本実装の際に消す想定です。
- POデモ時に階層情報を表示するための実装です。 */
- const getUserTier = () => {
- const jwt = loadAccessToken(); // トークンを取得
- const token = jwt && decodeToken(jwt); // トークンをデコード
+ // apiからの値取得関係
+ const partnerInfo = useSelector(selectPartnersInfo);
- if (token && token.tier) {
- return token.tier.toString(); // ユーザーの階層情報を取得
- }
-
- return "error!"; // 階層情報が見つからない場合はerror!を返す
+ // 階層表示用
+ const tierNames: { [key: number]: string } = {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 1: t(getTranslationID("common.label.tier1")),
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 2: t(getTranslationID("common.label.tier2")),
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 3: t(getTranslationID("common.label.tier3")),
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 4: t(getTranslationID("common.label.tier4")),
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 5: t(getTranslationID("common.label.tier5")),
};
- /* XXX 本実装の際に消す想定です。
- ログインしているアカウントの階層を確認するために実装 */
- const userTier = getUserTier();
// 第1~3階層にボタンを表示する
- const isVisible = isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3]);
+ const isVisibleButton = isApproveTier([
+ TIERS.TIER1,
+ TIERS.TIER2,
+ TIERS.TIER3,
+ ]);
+
+ // 第4階層でdealerManagementを表示
+ const isVisibleDealerManagement = isApproveTier([TIERS.TIER4]);
+
const onOpen = useCallback(() => {
setIsPopupOpen(true);
}, [setIsPopupOpen]);
+
+ // パートナー取得APIを呼び出す
+ useEffect(() => {
+ dispatch(
+ getPartnerInfoAsync({
+ limit: LIMIT_PARTNER_VIEW_NUM,
+ offset,
+ })
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [dispatch, currentPage]);
+
+ // ページネーションのボタンクリック時のアクション
+ const movePage = (targetOffset: number) => {
+ dispatch(
+ savePageInfo({ limit: LIMIT_PARTNER_VIEW_NUM, offset: targetOffset })
+ );
+ };
+
// HTML
return (
<>
@@ -52,52 +97,167 @@ const PartnerPage: React.FC = (): JSX.Element => {
-
- -
- {isVisible && (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-
-
- Add Account
-
- )}
-
-
-
+
+
+ {t(getTranslationID("partnerPage.label.title"))}
+
+
+
+
+
+
+ {/** pagenation */}
+
+
+
-
-
-
+
>
);
};
+
export default PartnerPage;
diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss
index e42918c..7ed4158 100644
--- a/dictation_client/src/styles/app.module.scss
+++ b/dictation_client/src/styles/app.module.scss
@@ -2165,8 +2165,7 @@ tr.isSelected .menuInTable li a {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
- center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@@ -2177,8 +2176,8 @@ tr.isSelected .menuInTable li a {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
- right center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
+ center;
background-size: 1.3rem;
}
.formChange > p {
@@ -2337,8 +2336,7 @@ tr.isSelected .menuInTable li a {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
- center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@@ -2349,8 +2347,8 @@ tr.isSelected .menuInTable li a {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
- right center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
+ center;
background-size: 1.3rem;
}
.formChange > p {