diff --git a/dictation_client/src/features/license/partnerLicense/operations.ts b/dictation_client/src/features/license/partnerLicense/operations.ts index 69005a1..30d9fd3 100644 --- a/dictation_client/src/features/license/partnerLicense/operations.ts +++ b/dictation_client/src/features/license/partnerLicense/operations.ts @@ -28,25 +28,21 @@ export const getMyAccountAsync = createAsyncThunk< const { configuration, accessToken } = state.auth; const config = new Configuration(configuration); const accountsApi = new AccountsApi(config); - try { const getMyAccountResponse = await accountsApi.getMyAccount({ headers: { authorization: `Bearer ${accessToken}` }, }); - // accountIDを返す return getMyAccountResponse.data.account; } catch (e) { // e ⇒ errorObjectに変換" const error = createErrorObject(e); - thunkApi.dispatch( openSnackbar({ level: "error", message: getTranslationID("common.message.internalServerError"), }) ); - return thunkApi.rejectWithValue({ error }); } }); @@ -59,7 +55,7 @@ export const getPartnerLicenseAsync = createAsyncThunk< { limit: number; offset: number; - accountId: number; + accountId: number | undefined; }, { // rejectした時の返却値の型 @@ -76,11 +72,18 @@ export const getPartnerLicenseAsync = createAsyncThunk< const accountsApi = new AccountsApi(config); try { + const getMyAccountResponse = await accountsApi.getMyAccount({ + headers: { authorization: `Bearer ${accessToken}` }, + }); + + const accountId = + args.accountId ?? getMyAccountResponse.data.account.accountId; + const getPartnerLicenseResponse = await accountsApi.getPartnerLicenses( { limit: args.limit, offset: args.offset, - accountId: args.accountId, + accountId, }, { headers: { authorization: `Bearer ${accessToken}` } } ); diff --git a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts index 436778d..f31d96a 100644 --- a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts +++ b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts @@ -1,10 +1,11 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { PartnerLicensesState } from "./state"; -import { PartnerLicenseInfo } from "../../../api/api"; +import { PartnerLicensesState, HierarchicalElement } from "./state"; import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations"; +import { ACCOUNTS_VIEW_LIMIT } from "./constants"; const initialState: PartnerLicensesState = { domain: { + myAccountInfo: { accountId: 0 }, total: 0, ownPartnerLicense: { accountId: 0, @@ -16,11 +17,14 @@ const initialState: PartnerLicensesState = { issueRequesting: 0, }, childrenPartnerLicenses: [], - acountInfo: { accountId: 0 }, + totalPage: 0, }, apps: { - limit: 1, + limit: ACCOUNTS_VIEW_LIMIT, offset: 0, + currentPage: 0, + hierarchicalElements: [], + isLoading: true, }, }; @@ -28,43 +32,70 @@ export const partnerLicenseSlice = createSlice({ name: "partnerLicense", initialState, reducers: { - changeTotal: (state, action: PayloadAction<{ total: number }>) => { - const { total } = action.payload; - state.domain.total = total; - }, - changeOwnPartnerLicense: ( - state, - action: PayloadAction<{ ownPartnerLicense: PartnerLicenseInfo }> - ) => { - const { ownPartnerLicense } = action.payload; - state.domain.ownPartnerLicense = ownPartnerLicense; - }, - changeChildrenPartnerLicenses: ( + pushHierarchicalElement: ( state, action: PayloadAction<{ - childrenPartnerLicenses: PartnerLicenseInfo[]; + hierarchicalElement: HierarchicalElement; }> ) => { - const { childrenPartnerLicenses } = action.payload; - state.domain.childrenPartnerLicenses = childrenPartnerLicenses; + const { hierarchicalElement } = action.payload; + state.apps.hierarchicalElements.push(hierarchicalElement); + }, + popHierarchicalElement: (state) => { + state.apps.hierarchicalElements.pop(); + }, + spliceHierarchicalElement: ( + state, + action: PayloadAction<{ + deleteCount: number; + }> + ) => { + const { deleteCount } = action.payload; + state.apps.hierarchicalElements.splice(-deleteCount); + }, + 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(getMyAccountAsync.pending, (state) => { + state.apps.isLoading = true; + }); builder.addCase(getMyAccountAsync.fulfilled, (state, action) => { - state.domain.acountInfo = action.payload; + state.domain.myAccountInfo = action.payload; + state.apps.isLoading = false; + }); + builder.addCase(getMyAccountAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(getPartnerLicenseAsync.pending, (state) => { + state.apps.isLoading = true; }); builder.addCase(getPartnerLicenseAsync.fulfilled, (state, action) => { state.domain.total = action.payload.total; state.domain.ownPartnerLicense = action.payload.ownPartnerLicense; state.domain.childrenPartnerLicenses = action.payload.childrenPartnerLicenses; + state.apps.isLoading = false; + }); + builder.addCase(getPartnerLicenseAsync.rejected, (state) => { + state.apps.isLoading = false; }); }, }); export const { - changeTotal, - changeOwnPartnerLicense, - changeChildrenPartnerLicenses, + pushHierarchicalElement, + popHierarchicalElement, + spliceHierarchicalElement, + savePageInfo, } = partnerLicenseSlice.actions; export default partnerLicenseSlice.reducer; diff --git a/dictation_client/src/features/license/partnerLicense/selectors.ts b/dictation_client/src/features/license/partnerLicense/selectors.ts index b4497cc..28b9f5b 100644 --- a/dictation_client/src/features/license/partnerLicense/selectors.ts +++ b/dictation_client/src/features/license/partnerLicense/selectors.ts @@ -1,10 +1,30 @@ +import { ceil, floor } from "lodash"; import { RootState } from "../../../app/store"; +import { ACCOUNTS_VIEW_LIMIT } from "./constants"; +export const selectMyAccountInfo = (state: RootState) => + state.partnerLicense.domain.myAccountInfo; export const selectTotal = (state: RootState) => state.partnerLicense.domain.total; export const selectOwnPartnerLicense = (state: RootState) => state.partnerLicense.domain.ownPartnerLicense; export const selectChildrenPartnerLicenses = (state: RootState) => state.partnerLicense.domain.childrenPartnerLicenses; -export const selectAccountInfo = (state: RootState) => - state.partnerLicense.domain.acountInfo; +export const selectHierarchicalElements = (state: RootState) => + state.partnerLicense.apps.hierarchicalElements; +export const selectTotalPage = (state: RootState) => { + const { total } = state.partnerLicense.domain; + const page = ceil(total / ACCOUNTS_VIEW_LIMIT); + return page; +}; +export const selectIsLoading = (state: RootState) => + state.partnerLicense.apps.isLoading; +export const selectLimit = (state: RootState) => + state.partnerLicense.apps.limit; +export const selectOffset = (state: RootState) => + state.partnerLicense.apps.offset; +export const selectCurrentPage = (state: RootState) => { + const { limit, offset } = state.partnerLicense.apps; + const page = floor(offset / limit) + 1; + return page; +}; diff --git a/dictation_client/src/features/license/partnerLicense/state.ts b/dictation_client/src/features/license/partnerLicense/state.ts index 0914690..e08066f 100644 --- a/dictation_client/src/features/license/partnerLicense/state.ts +++ b/dictation_client/src/features/license/partnerLicense/state.ts @@ -6,13 +6,22 @@ export interface PartnerLicensesState { } export interface Domain { + myAccountInfo: Account; total: number; ownPartnerLicense: PartnerLicenseInfo; childrenPartnerLicenses: PartnerLicenseInfo[]; - acountInfo: Account; + totalPage: number; } export interface Apps { limit: number; offset: number; + currentPage: number; + hierarchicalElements: HierarchicalElement[]; + isLoading: boolean; +} + +export interface HierarchicalElement { + accountId: number; + companyName: string; } diff --git a/dictation_client/src/pages/LicenseOrderHistoryPage/index.tsx b/dictation_client/src/pages/LicenseOrderHistoryPage/index.tsx index 81c8be7..271f6e2 100644 --- a/dictation_client/src/pages/LicenseOrderHistoryPage/index.tsx +++ b/dictation_client/src/pages/LicenseOrderHistoryPage/index.tsx @@ -1,34 +1,45 @@ -import React from "react"; +import React, { useCallback } from "react"; import Footer from "components/footer"; import Header from "components/header"; import styles from "styles/app.module.scss"; import { UpdateTokenTimer } from "components/auth/updateTokenTimer"; -const PartnerLicense: React.FC = (): JSX.Element => ( - // 表示確認用の仮画面 -
-
- -
-
-
-

Order History

-
-
- -
-
-); +interface LicenseOrderHistoryProps { + onClose: () => void; +} +export const LicenseOrderHistory: React.FC = ( + props +): JSX.Element => { + const { onClose } = props; -export default PartnerLicense; + // ポップアップを閉じる処理 + const closeScreen = useCallback(() => { + onClose(); + }, [onClose]); + + // 表示確認用の仮画面 + return ( +
+
+ +
+
+
+

Order History

+
+
+
    +
  • + +
  • +
+
+
+
+ ); +}; + +export default LicenseOrderHistory; diff --git a/dictation_client/src/pages/LicensePage/partnerLicense.tsx b/dictation_client/src/pages/LicensePage/partnerLicense.tsx index be2335a..524702e 100644 --- a/dictation_client/src/pages/LicensePage/partnerLicense.tsx +++ b/dictation_client/src/pages/LicensePage/partnerLicense.tsx @@ -1,32 +1,42 @@ import React, { useCallback, useState, useEffect } from "react"; -import { useMsal } from "@azure/msal-react"; import Footer from "components/footer"; import Header from "components/header"; import styles from "styles/app.module.scss"; import { UpdateTokenTimer } from "components/auth/updateTokenTimer"; -import { clearToken } from "features/auth"; import { useDispatch, useSelector } from "react-redux"; import { AppDispatch } from "app/store"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; +import { PartnerLicenseInfo } from "api"; import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup"; import postAdd from "../../assets/images/post_add.svg"; import history from "../../assets/images/history.svg"; +import returnLabel from "../../assets/images/undo.svg"; import { isApproveTier } from "../../features/auth/utils"; import { TIERS } from "../../components/auth/constants"; import { - getMyAccountAsync, getPartnerLicenseAsync, ACCOUNTS_VIEW_LIMIT, + selectMyAccountInfo, selectTotal, selectOwnPartnerLicense, selectChildrenPartnerLicenses, - selectAccountInfo, + selectHierarchicalElements, + selectTotalPage, + selectIsLoading, + selectOffset, + selectCurrentPage, + pushHierarchicalElement, + popHierarchicalElement, + spliceHierarchicalElement, + savePageInfo, + getMyAccountAsync, } from "../../features/license/partnerLicense"; import { LicenseOrderPopup } from "./licenseOrderPopup"; +import LicenseSummary from "./licenseSummary"; +import { LicenseOrderHistory } from "../LicenseOrderHistoryPage"; const PartnerLicense: React.FC = (): JSX.Element => { - const { instance } = useMsal(); const dispatch: AppDispatch = useDispatch(); const [t] = useTranslation(); @@ -34,6 +44,8 @@ const PartnerLicense: React.FC = (): JSX.Element => { const [isCardLicenseIssuePopupOpen, setIsCardLicenseIssuePopupOpen] = useState(false); const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false); + const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] = + useState(false); // 階層表示用 const tierNames: { [key: number]: string } = { @@ -58,48 +70,138 @@ const PartnerLicense: React.FC = (): JSX.Element => { }, [setIslicenseOrderPopupOpen]); // apiからの値取得関係 - const myAccountInfo = useSelector(selectAccountInfo); + const myAccountInfo = useSelector(selectMyAccountInfo); const total = useSelector(selectTotal); + const totalPage = useSelector(selectTotalPage); const ownPartnerLicenseInfo = useSelector(selectOwnPartnerLicense); const childrenPartnerLicensesInfo = useSelector( selectChildrenPartnerLicenses ); + const hierarchicalElements = useSelector(selectHierarchicalElements); + const isLoading = useSelector(selectIsLoading); + + // ページネーション制御用 + const currentPage = useSelector(selectCurrentPage); + const offset = useSelector(selectOffset); + // ページネーションのボタンクリック時のアクション + const movePage = (targetOffset: number) => { + dispatch( + savePageInfo({ limit: ACCOUNTS_VIEW_LIMIT, offset: targetOffset }) + ); + }; + + // パンくずリスト内のアカウント押下時 + const onClickBreadCrumbList = (id: number) => { + const clickLevel = hierarchicalElements.findIndex( + (param) => param.accountId === id + ); + const nowLevel = hierarchicalElements.length - 1; + const deleteCount = nowLevel - clickLevel; + // 階層を上がった分だけ表示アカウント管理用の配列の最後からパラメータを削除する + if (deleteCount > 0) { + dispatch(spliceHierarchicalElement({ deleteCount })); + movePage(0); + } + }; + + // 行要素クリック時イベント + const handleRowClick = (value: PartnerLicenseInfo) => { + dispatch( + pushHierarchicalElement({ + hierarchicalElement: { + accountId: value.accountId, + companyName: value.companyName, + }, + }) + ); + movePage(0); + }; + + // returnボタンクリック時イベント + const returnClick = () => { + dispatch(popHierarchicalElement()); + movePage(0); + }; // ログイン時に階層をチェック const isTier1 = isApproveTier([TIERS.TIER1]); const isTier2ToTier4 = isApproveTier([TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]); - // パラメーターを入れておく空配列 - // eslint-disable-next-line react-hooks/exhaustive-deps - const hierarchicalElements: HierarchicalElements[] = []; + // licenseDetailsボタン押下時 + // TODO 本PIBでは対象外のため、遷移先の内容の正しさは対象外 + const onClickViewDetails = () => ; + + // orderHistoryボタン押下時 + // TODO 本PIBでは画面遷移までで、遷移先の内容の正しさは対象外 + const onClickOrderHistory = useCallback(() => { + setIslicenseOrderHistoryOpen(true); + }, [setIslicenseOrderHistoryOpen]); // マウント時のみ実行 useEffect(() => { dispatch(getMyAccountAsync()); - dispatch( - getPartnerLicenseAsync({ - limit: ACCOUNTS_VIEW_LIMIT, - offset: 0, - accountId: myAccountInfo.accountId, - }) - ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // 画面遷移時に実行 - // TODO 仮実装なので挙動は未検証 + // 自アカウントID取得時に実行 useEffect(() => { - if (hierarchicalElements.length === 1) { - dispatch(getMyAccountAsync()); + if (myAccountInfo.accountId !== 0) { + dispatch( + getPartnerLicenseAsync({ + limit: ACCOUNTS_VIEW_LIMIT, + offset, + accountId: myAccountInfo.accountId, + }) + ); } - dispatch( - getPartnerLicenseAsync({ - limit: ACCOUNTS_VIEW_LIMIT, - offset: 0, - accountId: myAccountInfo.accountId, - }) - ); - }, [dispatch, hierarchicalElements, myAccountInfo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [myAccountInfo]); + + // 現在の表示階層に合わせたボタン制御用 + const [buttonLabel, setButtonLabel] = useState(""); + + // パンくずリスト用stateに自アカウントを追加 + useEffect(() => { + if ( + hierarchicalElements.length === 0 && + ownPartnerLicenseInfo.accountId !== 0 + ) { + dispatch( + pushHierarchicalElement({ + hierarchicalElement: { + accountId: ownPartnerLicenseInfo.accountId, + companyName: ownPartnerLicenseInfo.companyName, + }, + }) + ); + } + // 表内のボタン表示判定 + if (hierarchicalElements.length === 1 && ownPartnerLicenseInfo.tier !== 4) { + setButtonLabel( + t(getTranslationID("partnerLicense.label.orderHistoryButton")) + ); + } else if (ownPartnerLicenseInfo.tier === 4) { + setButtonLabel(t(getTranslationID("partnerLicense.label.viewDetails"))); + } else { + setButtonLabel(""); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ownPartnerLicenseInfo]); + + // 表内の情報取得処理 + useEffect(() => { + if (hierarchicalElements.length !== 0) { + dispatch( + getPartnerLicenseAsync({ + limit: ACCOUNTS_VIEW_LIMIT, + offset, + accountId: + hierarchicalElements[hierarchicalElements.length - 1].accountId, + }) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hierarchicalElements, currentPage]); return ( <> @@ -119,232 +221,279 @@ const PartnerLicense: React.FC = (): JSX.Element => { }} /> )} - -
-
- -
-
-
-

- {t(getTranslationID("partnerLicense.label.title"))} -

+ {islicenseOrderHistoryOpen && ( + { + setIslicenseOrderHistoryOpen(false); + }} + /> + )} + {!islicenseOrderHistoryOpen && ( +
+
+ +
+
+
+

+ {t(getTranslationID("partnerLicense.label.title"))} +

+
-
-
-
-

{t(getTranslationID("partnerLicense.label.subTitle"))}

-
-
- -
{" "} -
-
+ + +