From 2dcb1c1f84fc703e59baceb4d020c4862a16e4a5 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 11 Sep 2023 08:31:03 +0000 Subject: [PATCH 1/7] =?UTF-8?q?Merged=20PR=20396:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=A2?= =?UTF-8?q?=E3=82=A4=E3=83=86=E3=83=A0=E5=8F=96=E5=BE=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2592: API実装(オプションアイテム取得)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2592) - オプションアイテム取得APIとテストを実装しました。 ## レビューポイント - リポジトリの取得ロジックは想定通りか - テストケースは適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/app.module.ts | 2 - dictation_server/src/common/test/modules.ts | 2 - .../features/accounts/accounts.controller.ts | 62 +-------- .../accounts/accounts.service.spec.ts | 131 ++++++++++++++++++ .../src/features/accounts/accounts.service.ts | 64 +++++++++ .../src/features/accounts/test/utility.ts | 32 ++++- .../option_items.repository.module.ts | 11 -- .../option_items.repository.service.ts | 7 - .../entity/option_item.entity.ts | 0 .../worktypes/worktypes.repository.module.ts | 3 +- .../worktypes/worktypes.repository.service.ts | 35 ++++- 11 files changed, 268 insertions(+), 81 deletions(-) delete mode 100644 dictation_server/src/repositories/option_items/option_items.repository.module.ts delete mode 100644 dictation_server/src/repositories/option_items/option_items.repository.service.ts rename dictation_server/src/repositories/{option_items => worktypes}/entity/option_item.entity.ts (100%) diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index e8b4375..a5cd069 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -41,7 +41,6 @@ import { UserGroupsRepositoryModule } from './repositories/user_groups/user_grou import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module'; import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module'; import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module'; -import { OptionItemsRepositoryModule } from './repositories/option_items/option_items.repository.module'; @Module({ imports: [ @@ -97,7 +96,6 @@ import { OptionItemsRepositoryModule } from './repositories/option_items/option_ AuthGuardsModule, SortCriteriaRepositoryModule, WorktypesRepositoryModule, - OptionItemsRepositoryModule, ], controllers: [ HealthController, diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 5788870..ed77ffc 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -30,7 +30,6 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat import { FilesService } from '../../features/files/files.service'; import { LicensesService } from '../../features/licenses/licenses.service'; import { TasksService } from '../../features/tasks/tasks.service'; -import { OptionItemsRepositoryModule } from '../../repositories/option_items/option_items.repository.module'; export const makeTestingModule = async ( datasource: DataSource, @@ -66,7 +65,6 @@ export const makeTestingModule = async ( AuthGuardsModule, SortCriteriaRepositoryModule, WorktypesRepositoryModule, - OptionItemsRepositoryModule, ], providers: [ AuthService, diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 7099620..f386fcf 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -815,63 +815,13 @@ export class AccountsController { const context = makeContext(userId); - console.log('id', id); - console.log(context.trackingId); + const optionItems = await this.accountService.getOptionItems( + context, + userId, + id, + ); - return { - optionItems: [ - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - { - itemLabel: '', - defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, - initialValue: '', - }, - ], - }; + return optionItems; } @Post('/worktypes/:id/option-items') diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 6932651..c9c9cf3 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -16,6 +16,7 @@ import { createLicense, createLicenseOrder, createLicenseSetExpiryDateAndStatus, + createOptionItems, createWorktype, getOptionItems, getSortCriteria, @@ -1505,6 +1506,7 @@ describe('AccountsService', () => { makeDefaultLicensesRepositoryMockValue(); const worktypesRepositoryMockValue = makeDefaultWorktypesRepositoryMockValue(); + const service = await makeAccountsServiceMock( accountsRepositoryMockValue, usersRepositoryMockValue, @@ -3863,6 +3865,135 @@ describe('updateWorktype', () => { }); }); +describe('getOptionItems', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + + it('指定WorktypeIDに紐づいたOptionItemを取得できる', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + const worktype = await createWorktype(source, account.id, 'worktype1'); + const optionItems = await createOptionItems(source, worktype.id); + + //作成したデータを確認 + { + expect(optionItems.length).toBe(10); + expect(optionItems[0].item_label).toBe(''); + expect(optionItems[0].default_value_type).toBe( + OPTION_ITEM_VALUE_TYPE.DEFAULT, + ); + expect(optionItems[0].initial_value).toBe(''); + } + + const resOptionItems = await service.getOptionItems( + context, + admin.external_id, + worktype.id, + ); + + //実行結果を確認 + { + expect(resOptionItems.optionItems.length).toBe(10); + expect(resOptionItems.optionItems[0].itemLabel).toBe(''); + expect(resOptionItems.optionItems[0].defaultValueType).toBe( + OPTION_ITEM_VALUE_TYPE.DEFAULT, + ); + expect(resOptionItems.optionItems[0].initialValue).toBe(''); + } + }); + + it('WorktypeIDが存在しない場合、400エラーとなること', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + const worktype = await createWorktype(source, account.id, 'worktype1'); + const optionItems = await createOptionItems(source, worktype.id); + + //作成したデータを確認 + { + expect(optionItems.length).toBe(10); + expect(optionItems[0].item_label).toBe(''); + expect(optionItems[0].default_value_type).toBe( + OPTION_ITEM_VALUE_TYPE.DEFAULT, + ); + expect(optionItems[0].initial_value).toBe(''); + } + + try { + await service.getOptionItems(context, admin.external_id, 999); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E011003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + const worktype = await createWorktype(source, account.id, 'worktype1'); + const optionItems = await createOptionItems(source, worktype.id); + + //作成したデータを確認 + { + expect(optionItems.length).toBe(10); + expect(optionItems[0].item_label).toBe(''); + expect(optionItems[0].default_value_type).toBe( + OPTION_ITEM_VALUE_TYPE.DEFAULT, + ); + expect(optionItems[0].initial_value).toBe(''); + } + + //DBアクセスに失敗するようにする + const worktypesService = module.get( + WorktypesRepositoryService, + ); + worktypesService.getOptionItems = jest.fn().mockRejectedValue('DB failed'); + + try { + await service.getWorktypes(context, admin.external_id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + describe('ライセンス発行キャンセル', () => { let source: DataSource = null; beforeEach(async () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 23a9fc2..7e61ef1 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -23,6 +23,7 @@ import { GetMyAccountResponse, GetTypistGroupResponse, GetWorktypesResponse, + GetOptionItemsResponse, GetPartnersResponse, } from './types/types'; import { @@ -1314,6 +1315,69 @@ export class AccountsService { } } + /** + * ワークタイプに紐づいたオプションアイテム一覧を取得します + * @param context + * @param externalId + * @param id Worktypeの内部ID + * @returns option items + */ + async getOptionItems( + context: Context, + externalId: string, + id: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getOptionItems.name} | params: { ` + + `externalId: ${externalId}, ` + + `id: ${id} };`, + ); + try { + // 外部IDをもとにユーザー情報を取得する + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(externalId); + + // オプションアイテム一覧を取得する + const optionItems = await this.worktypesRepository.getOptionItems( + accountId, + id, + ); + + return { + optionItems: optionItems.map((x) => ({ + itemLabel: x.item_label, + defaultValueType: x.default_value_type, + initialValue: x.initial_value, + })), + }; + } catch (e) { + this.logger.error(e); + if (e instanceof Error) { + switch (e.constructor) { + // 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す + case WorktypeIdNotFoundError: + throw new HttpException( + makeErrorResponse('E011003'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.getOptionItems.name}`, + ); + } + } + /** * パートナー一覧を取得します * @param context diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index 58b2ab4..32a2586 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -7,7 +7,8 @@ import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_cr import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity'; import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity'; import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity'; -import { OptionItem } from '../../../repositories/option_items/entity/option_item.entity'; +import { OptionItem } from '../../../repositories/worktypes/entity/option_item.entity'; +import { OPTION_ITEM_VALUE_TYPE } from '../../../constants'; /** * テスト ユーティリティ: すべてのソート条件を取得する @@ -153,6 +154,35 @@ export const getWorktypes = async ( }); }; +// オプションアイテムを作成する +export const createOptionItems = async ( + datasource: DataSource, + worktypeId: number, +): Promise => { + const optionItems = []; + + for (let i = 0; i < 10; i++) { + optionItems.push({ + worktype_id: worktypeId, + item_label: '', + default_value_type: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initial_value: '', + created_by: 'test_runner', + created_at: new Date(), + updated_by: 'updater', + updated_at: new Date(), + }); + } + + await datasource.getRepository(OptionItem).insert(optionItems); + + const items = datasource + .getRepository(OptionItem) + .find({ where: { worktype_id: worktypeId } }); + + return items; +}; + // オプションアイテムを取得する export const getOptionItems = async ( datasource: DataSource, diff --git a/dictation_server/src/repositories/option_items/option_items.repository.module.ts b/dictation_server/src/repositories/option_items/option_items.repository.module.ts deleted file mode 100644 index 17cf5f7..0000000 --- a/dictation_server/src/repositories/option_items/option_items.repository.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { OptionItem } from './entity/option_item.entity'; -import { OptionItemsRepositoryService } from './option_items.repository.service'; - -@Module({ - imports: [TypeOrmModule.forFeature([OptionItem])], - providers: [OptionItemsRepositoryService], - exports: [OptionItemsRepositoryService], -}) -export class OptionItemsRepositoryModule {} diff --git a/dictation_server/src/repositories/option_items/option_items.repository.service.ts b/dictation_server/src/repositories/option_items/option_items.repository.service.ts deleted file mode 100644 index 884b068..0000000 --- a/dictation_server/src/repositories/option_items/option_items.repository.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; - -@Injectable() -export class OptionItemsRepositoryService { - constructor(private dataSource: DataSource) {} -} diff --git a/dictation_server/src/repositories/option_items/entity/option_item.entity.ts b/dictation_server/src/repositories/worktypes/entity/option_item.entity.ts similarity index 100% rename from dictation_server/src/repositories/option_items/entity/option_item.entity.ts rename to dictation_server/src/repositories/worktypes/entity/option_item.entity.ts diff --git a/dictation_server/src/repositories/worktypes/worktypes.repository.module.ts b/dictation_server/src/repositories/worktypes/worktypes.repository.module.ts index df7a231..121ebab 100644 --- a/dictation_server/src/repositories/worktypes/worktypes.repository.module.ts +++ b/dictation_server/src/repositories/worktypes/worktypes.repository.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Worktype } from './entity/worktype.entity'; import { WorktypesRepositoryService } from './worktypes.repository.service'; +import { OptionItem } from './entity/option_item.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Worktype])], + imports: [TypeOrmModule.forFeature([Worktype, OptionItem])], providers: [WorktypesRepositoryService], exports: [WorktypesRepositoryService], }) diff --git a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts index 2f3920c..eea9fe2 100644 --- a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts +++ b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts @@ -11,7 +11,7 @@ import { WorktypeIdMaxCountError, WorktypeIdNotFoundError, } from './errors/types'; -import { OptionItem } from '../option_items/entity/option_item.entity'; +import { OptionItem } from './entity/option_item.entity'; @Injectable() export class WorktypesRepositoryService { @@ -135,4 +135,37 @@ export class WorktypesRepositoryService { await worktypeRepo.save(worktype); }); } + + /** + * オプションアイテム一覧を取得する + * @param accountId + * @param worktypeId worktypeの内部ID + * @returns option items + */ + async getOptionItems( + accountId: number, + worktypeId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const repoWorktype = entityManager.getRepository(Worktype); + const repoOptionItem = entityManager.getRepository(OptionItem); + + const worktype = await repoWorktype.findOne({ + where: { account_id: accountId, id: worktypeId }, + }); + + // ワークタイプが存在しない場合はエラー + if (!worktype) { + throw new WorktypeIdNotFoundError( + `Worktype is not found. id: ${worktypeId}`, + ); + } + + const optionItems = await repoOptionItem.find({ + where: { worktype_id: worktypeId }, + }); + + return optionItems; + }); + } } From 606ff6de9b1d84dbcd9b74051bb9223bbdba16f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B0=B4=E6=9C=AC=20=E7=A5=90=E5=B8=8C?= Date: Tue, 12 Sep 2023 01:14:26 +0000 Subject: [PATCH 2/7] =?UTF-8?q?Merged=20PR=20379:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E3=83=91=E3=83=BC=E3=83=88=E3=83=8A?= =?UTF-8?q?=E3=83=BC=E4=B8=80=E8=A6=A7=E7=94=BB=E9=9D=A2=E6=9C=AC=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2539: 画面実装(パートナー一覧画面本実装)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2539) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど パートナー一覧画面でパートナーの一覧が表示されるように実装 - このPull Requestでの対象/対象外 ・Add Accountボタンは前PBIのため対象外 ・Dealer Managementボタンの挙動は対象外 ・Delete Accountボタンの挙動は対象外 - 影響範囲(他の機能にも影響があるか) 特になし ## レビューポイント - 特にレビューしてほしい箇所 ・Dealer Management、Delete Accountボタンの表示制御 ・ページネーション - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task2539?csf=1&web=1&e=PNI5bw ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../src/features/partner/constants.ts | 1 + .../src/features/partner/index.ts | 1 + .../src/features/partner/operations.ts | 56 +++- .../src/features/partner/partnerSlice.ts | 37 ++- .../src/features/partner/selectors.ts | 20 ++ .../src/features/partner/state.ts | 12 +- .../src/pages/PartnerPage/index.tsx | 288 ++++++++++++++---- dictation_client/src/styles/app.module.scss | 14 +- 8 files changed, 354 insertions(+), 75 deletions(-) create mode 100644 dictation_client/src/features/partner/constants.ts 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 */} +
+ +
+
+
-
- -
+