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"))} +

+
+
+
+ + + + + + + + + + + + + {!isLoading && + partnerInfo.partners.length !== 0 && + partnerInfo.partners.map((x) => ( + // eslint-disable-next-line react/jsx-key + + + + + + + + + + + ))} +
{/** th is empty */} + {t(getTranslationID("partnerPage.label.name"))} + + {t(getTranslationID("partnerPage.label.category"))} + + {t(getTranslationID("partnerPage.label.accountId"))} + + {t(getTranslationID("partnerPage.label.country"))} + + + {t(getTranslationID("partnerPage.label.primaryAdmin"))} + + + {t(getTranslationID("partnerPage.label.email"))} + + + {t( + getTranslationID("partnerPage.label.dealerManagement") + )} + +
+ + {x.name}{tierNames[x.tier]}{x.accountId}{x.country}{x.primaryAdmin ?? "-"}{x.email ?? "-"} + +
+ {/** pagenation */} +
+ +
+
+
-
- -
+