diff --git a/dictation_client/openapitools.json b/dictation_client/openapitools.json index 3015568..4053ae8 100644 --- a/dictation_client/openapitools.json +++ b/dictation_client/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.0.0" + "version": "7.0.1" } } diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index 248c80b..1cb6298 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -19,6 +19,7 @@ import PartnerPage from "pages/PartnerPage"; import WorkflowPage from "pages/WorkflowPage"; import TypistGroupSettingPage from "pages/TypistGroupSettingPage"; import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage"; +import AccountPage from "pages/AccountPage"; const AppRouter: React.FC = () => ( @@ -53,7 +54,7 @@ const AppRouter: React.FC = () => ( {/* XXX ヘッダーの挙動確認のため仮のページを作成 */} } />} + element={} />} /> + ) => { + const { parentAccountId } = action.payload; + state.apps.updateAccountInfo.parentAccountId = parentAccountId; + }, + changeDealerPermission: ( + state, + action: PayloadAction<{ delegationPermission: boolean }> + ) => { + const { delegationPermission } = action.payload; + state.apps.updateAccountInfo.delegationPermission = delegationPermission; + }, + changePrimaryAdministrator: ( + state, + action: PayloadAction<{ primaryAdminUserId: number }> + ) => { + const { primaryAdminUserId } = action.payload; + state.apps.updateAccountInfo.primaryAdminUserId = primaryAdminUserId; + }, + changeSecondryAdministrator: ( + state, + action: PayloadAction<{ secondryAdminUserId: number | undefined }> + ) => { + const { secondryAdminUserId } = action.payload; + state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId; + }, + cleanupApps: (state) => { + state.domain = initialState.domain; + }, + }, + extraReducers: (builder) => { + builder.addCase(getAccountRelationsAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(getAccountRelationsAsync.fulfilled, (state, action) => { + state.domain.getAccountInfo = action.payload.accountInfo; + state.domain.dealers = action.payload.dealers.dealers; + state.domain.users = action.payload.users.users; + state.apps.updateAccountInfo.parentAccountId = + action.payload.accountInfo.account.parentAccountId; + state.apps.updateAccountInfo.delegationPermission = + action.payload.accountInfo.account.delegationPermission; + if (action.payload.accountInfo.account.primaryAdminUserId) + state.apps.updateAccountInfo.primaryAdminUserId = + action.payload.accountInfo.account.primaryAdminUserId; + state.apps.updateAccountInfo.secondryAdminUserId = + action.payload.accountInfo.account.secondryAdminUserId; + state.apps.isLoading = false; + }); + builder.addCase(getAccountRelationsAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateAccountInfoAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateAccountInfoAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateAccountInfoAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + }, +}); +export const { + changeDealer, + changeDealerPermission, + changePrimaryAdministrator, + changeSecondryAdministrator, + cleanupApps, +} = accountSlice.actions; +export default accountSlice.reducer; diff --git a/dictation_client/src/features/account/index.ts b/dictation_client/src/features/account/index.ts new file mode 100644 index 0000000..30af2f3 --- /dev/null +++ b/dictation_client/src/features/account/index.ts @@ -0,0 +1,5 @@ +export * from "./state"; +export * from "./operations"; +export * from "./accountSlice"; +export * from "./selectors"; +export * from "./types"; diff --git a/dictation_client/src/features/account/operations.ts b/dictation_client/src/features/account/operations.ts new file mode 100644 index 0000000..b1d4164 --- /dev/null +++ b/dictation_client/src/features/account/operations.ts @@ -0,0 +1,99 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import type { RootState } from "app/store"; +import { ErrorObject, createErrorObject } from "common/errors"; +import { getTranslationID } from "translation"; +import { openSnackbar } from "features/ui/uiSlice"; +import { AccountsApi, UpdateAccountInfoRequest, UsersApi } from "../../api/api"; +import { Configuration } from "../../api/configuration"; +import { ViewAccountRelationsInfo } from "./types"; + +export const getAccountRelationsAsync = createAsyncThunk< + // 正常時の戻り値の型 + ViewAccountRelationsInfo, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/getAccountRelationsAsync", async (_args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + const usersApi = new UsersApi(config); + + try { + const accountInfo = await accountsApi.getMyAccount({ + headers: { authorization: `Bearer ${accessToken}` }, + }); + const dealers = await accountsApi.getDealers(); + const users = await usersApi.getUsers({ + headers: { authorization: `Bearer ${accessToken}` }, + }); + return { + accountInfo: accountInfo.data, + dealers: dealers.data, + users: users.data, + }; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); + +export const updateAccountInfoAsync = createAsyncThunk< + { + /* Empty Object */ + }, + UpdateAccountInfoRequest, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/updateAccountInfoAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + await accountApi.me(args, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + return {}; + } catch (e) { + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + if (error.code === "E010502") { + errorMessage = getTranslationID( + "accountPage.message.updateAccountFailedError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/account/selectors.ts b/dictation_client/src/features/account/selectors.ts new file mode 100644 index 0000000..23a0bfe --- /dev/null +++ b/dictation_client/src/features/account/selectors.ts @@ -0,0 +1,17 @@ +import { Dealer } from "api/api"; +import { RootState } from "app/store"; + +export const selectAccountInfo = (state: RootState) => + state.account.domain.getAccountInfo; +export const selectAllDealers = (state: RootState) => + state.account.domain.dealers; +export const selectDealers = (state: RootState) => { + const { dealers } = state.account.domain; + const { country } = state.account.domain.getAccountInfo.account; + return dealers.filter((x: Dealer) => x.country === country); +}; +export const selectUsers = (state: RootState) => state.account.domain.users; +export const selectIsLoading = (state: RootState) => + state.account.apps.isLoading; +export const selectUpdateAccountInfo = (state: RootState) => + state.account.apps.updateAccountInfo; diff --git a/dictation_client/src/features/account/state.ts b/dictation_client/src/features/account/state.ts new file mode 100644 index 0000000..6d3cbe7 --- /dev/null +++ b/dictation_client/src/features/account/state.ts @@ -0,0 +1,22 @@ +import { + UpdateAccountInfoRequest, + GetMyAccountResponse, + Dealer, + User, +} from "../../api/api"; + +export interface AccountState { + domain: Domain; + apps: Apps; +} + +export interface Domain { + getAccountInfo: GetMyAccountResponse; + dealers: Dealer[]; + users: User[]; +} + +export interface Apps { + updateAccountInfo: UpdateAccountInfoRequest; + isLoading: boolean; +} diff --git a/dictation_client/src/features/account/types.ts b/dictation_client/src/features/account/types.ts new file mode 100644 index 0000000..c88875f --- /dev/null +++ b/dictation_client/src/features/account/types.ts @@ -0,0 +1,11 @@ +import { + GetMyAccountResponse, + GetDealersResponse, + GetUsersResponse, +} from "../../api/api"; + +export interface ViewAccountRelationsInfo { + accountInfo: GetMyAccountResponse; + dealers: GetDealersResponse; + users: GetUsersResponse; +} diff --git a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts index 259eb02..8fd549a 100644 --- a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts +++ b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts @@ -6,7 +6,13 @@ import { ACCOUNTS_VIEW_LIMIT } from "./constants"; const initialState: PartnerLicensesState = { domain: { - myAccountInfo: { accountId: 0, companyName: "" }, + myAccountInfo: { + accountId: 0, + companyName: "", + tier: 0, + country: "", + delegationPermission: false, + }, total: 0, ownPartnerLicense: { accountId: 0, diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx new file mode 100644 index 0000000..771a92e --- /dev/null +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -0,0 +1,354 @@ +import { AppDispatch } from "app/store"; +import { UpdateTokenTimer } from "components/auth/updateTokenTimer"; +import Footer from "components/footer"; +import Header from "components/header"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import styles from "styles/app.module.scss"; +import { + changeDealer, + changeDealerPermission, + changePrimaryAdministrator, + changeSecondryAdministrator, + getAccountRelationsAsync, + selectAccountInfo, + selectDealers, + selectIsLoading, + selectUpdateAccountInfo, + selectUsers, + updateAccountInfoAsync, +} from "features/account/index"; +import { useTranslation } from "react-i18next"; +import { getTranslationID } from "translation"; +import { TIERS } from "components/auth/constants"; +import { isApproveTier } from "features/auth/utils"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +const AccountPage: React.FC = (): JSX.Element => { + const dispatch: AppDispatch = useDispatch(); + const [t] = useTranslation(); + const viewInfo = useSelector(selectAccountInfo); + const dealers = useSelector(selectDealers); + const users = useSelector(selectUsers); + const isLoading = useSelector(selectIsLoading); + const updateAccountInfo = useSelector(selectUpdateAccountInfo); + + // ユーザーが第5階層であるかどうかを判定する + const isTier5 = isApproveTier([TIERS.TIER5]); + + // 階層表示用 + 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")), + }; + + // 画面起動時 + useEffect(() => { + dispatch(getAccountRelationsAsync()); + }, [dispatch]); + + const [isPushSaveChangesButton, setIsPushSaveChangesButton] = + useState(false); + const [isEmptyPrimaryAdmin, setIsEmptyPrimaryAdmin] = + useState(false); + // SaveChanges押下時 + const onSaveChangesButton = useCallback(async () => { + setIsPushSaveChangesButton(true); + if (!updateAccountInfo.primaryAdminUserId) { + setIsEmptyPrimaryAdmin(true); + return; + } + await dispatch(updateAccountInfoAsync(updateAccountInfo)); + dispatch(getAccountRelationsAsync()); + setIsEmptyPrimaryAdmin(false); + setIsPushSaveChangesButton(false); + }, [dispatch, updateAccountInfo]); + + return ( +
+
+ +
+
+
+

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

+
+ +
+
+ + +
+
+

+ {t( + getTranslationID("accountPage.label.accountInformation") + )} +

+
+ {t(getTranslationID("accountPage.label.companyName"))} +
+
{viewInfo.account.companyName}
+
{t(getTranslationID("accountPage.label.accountID"))}
+
{viewInfo.account.accountId}
+
+ {t(getTranslationID("accountPage.label.yourCategory"))} +
+
{tierNames[viewInfo.account.tier]}
+
+ {t(getTranslationID("accountPage.label.yourCountry"))} +
+
{viewInfo.account.country}
+
{t(getTranslationID("accountPage.label.yourDealer"))}
+ {isTier5 && !viewInfo.account.parentAccountId && ( +
+ +
+ )} + {(!isTier5 || viewInfo.account.parentAccountId) && ( +
+ {dealers.find( + (x) => x.id === viewInfo.account.parentAccountId + )?.name || "-"} +
+ )} +
+ {t(getTranslationID("accountPage.label.dealerManagement"))} +
+ {isTier5 && ( +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+ )} + {!isTier5 &&
-
} +
+
+ +
+
+

+ {t( + getTranslationID( + "accountPage.label.administratorInformation" + ) + )} +

+
+ {t( + getTranslationID("accountPage.label.primaryAdministrator") + )} +
+
+ { + users.find( + (x) => x.id === viewInfo.account.primaryAdminUserId + )?.name + } +
+
+ {t(getTranslationID("accountPage.label.emailAddress"))} +
+
+ + {isPushSaveChangesButton && isEmptyPrimaryAdmin && ( + + {" "} + {t( + getTranslationID("signupPage.message.inputEmptyError") + )} + + )} +
+
+ {t( + getTranslationID( + "accountPage.label.secondaryAdministrator" + ) + )} +
+
+ { + users.find( + (x) => x.id === viewInfo.account.secondryAdminUserId + )?.name + } +
+
+ {t(getTranslationID("accountPage.label.emailAddress"))} +
+
+ +
+
+
+
+ + Loading +
+
+ + +
+
+
+
+ ); +}; + +export default AccountPage; diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 7ed4158..df592f4 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -537,6 +537,9 @@ h3 + .brCrumb .tlIcon { letter-spacing: 0.07rem; font-weight: normal; } +.formList dt.formTitle.alignCenter { + text-align: center; +} .formList dt.overLine { padding: 0 4% 0; line-height: 1.4; @@ -722,6 +725,26 @@ h3 + .brCrumb .tlIcon { .formSubmit.isActive:hover { background: rgba(0, 94, 184, 0.7); } +.formButtonFul { + min-width: 15rem; + padding: 0.8rem 0.8rem; + border: 1px #999999 solid; + background: #ffffff; + font-size: 16px; + line-height: 1.4; + letter-spacing: 0.04rem; + font-weight: normal; + cursor: pointer; + border-radius: 0.3rem; + position: relative; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -webkit-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} +.formButtonFul:hover { + background: #f0f0f0; +} .formButton { padding: 0.8rem 0.8rem; border: 1px #999999 solid; @@ -741,6 +764,29 @@ h3 + .brCrumb .tlIcon { .formButton:hover { background: #f0f0f0; } +.formDelete { + min-width: 15rem; + padding: 0.8rem 2rem; + border: 1px #999999 solid; + color: #ffffff; + font-size: 16px; + line-height: 1.4; + letter-spacing: 0.04rem; + font-weight: normal; + background: #e60000; + border: 1px #e60000 solid; + opacity: 1; + border-radius: 0.3rem; + position: relative; + cursor: pointer; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -webkit-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} +.formDelete:hover { + background: rgba(230, 0, 0, 0.7); +} .formBack { width: 15rem; padding: 0.8rem 2rem; @@ -780,6 +826,11 @@ h3 + .brCrumb .tlIcon { .formDone { width: 100px; } +.formTrash { + width: 50px; + filter: brightness(0) saturate(100%) invert(10%) sepia(97%) saturate(7447%) + hue-rotate(17deg) brightness(95%) contrast(117%); +} .listVertical { width: 600px; @@ -797,6 +848,7 @@ h3 + .brCrumb .tlIcon { .listVertical dt, .listVertical dd { padding: 0.8rem 4%; + background: #ffffff; font-size: 16px; line-height: 1.4; letter-spacing: 0.04rem; @@ -814,6 +866,9 @@ h3 + .brCrumb .tlIcon { .listVertical dd { width: 42%; text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .boxFlex { @@ -875,7 +930,7 @@ h3 + .brCrumb .tlIcon { height: 100%; background: rgba(0, 0, 0, 0.6); position: fixed; - z-index: 5; + z-index: 6; } .modal.isShow .modalBox { display: block; @@ -1074,6 +1129,7 @@ h3 + .brCrumb .tlIcon { border-radius: 0.2rem; opacity: 0.4; pointer-events: none; + background: #ffffff; } .pagenationNav a.isActive { opacity: 1; @@ -1157,6 +1213,9 @@ _:-ms-lang(x)::-ms-backdrop, width: 100%; margin-bottom: 1rem; } +.table tr:not(.tableHeader) { + background: #ffffff; +} .table tr:nth-child(2n + 3) { background: #f5f5f5; } @@ -1735,6 +1794,10 @@ _:-ms-lang(x)::-ms-backdrop, tr.isSelected .menuInTable li a { color: #ffffff; } +tr.isSelected .menuInTable li a.isDisable { + pointer-events: none; + opacity: 0.3; +} .icCheckCircle { width: 20px; @@ -1746,6 +1809,64 @@ tr.isSelected .menuInTable li a { vertical-align: bottom; } +.wrap.manage .header, +.wrap.manage .main { + background: #def5fd; +} + +.manageInfo { + width: 800px; + display: flex; + justify-content: flex-start; + padding: 0.2rem 1.5rem; + color: #0084b2; + border: 1px #0084b2 solid; + border-radius: 0.3rem; + background: #def5fd; + position: absolute; + top: 0.2rem; + left: 50%; + transform: translateX(-50%); + box-sizing: border-box; + z-index: 5; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -webkit-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} +.manage .txNormal { + width: calc(800px - 3rem - 3.3rem); + font-size: 16px; + line-height: 1.7; + letter-spacing: 0; + font-weight: normal; + padding-top: 0.5rem; + line-height: 1.4; +} +.manage .txNormal span { + display: inline-block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; +} +.manageIcon { + width: 1.5rem; + margin-right: 1rem; + vertical-align: middle; + filter: brightness(0) saturate(100%) invert(31%) sepia(75%) saturate(1954%) + hue-rotate(172deg) brightness(90%) contrast(101%); +} +.manageIconClose { + width: 1.5rem; + filter: brightness(0) saturate(100%) invert(31%) sepia(75%) saturate(1954%) + hue-rotate(172deg) brightness(90%) contrast(101%); +} +.manageIconClose:hover { + cursor: pointer; +} + .license .listVertical dd img[src*="circle"] { filter: brightness(0) saturate(100%) invert(58%) sepia(41%) saturate(5814%) hue-rotate(143deg) brightness(96%) contrast(101%); @@ -1987,6 +2108,12 @@ tr.isSelected .menuInTable li a { padding-bottom: 2rem; vertical-align: top; } +.dictation .table.dictation td .menuInTable li:nth-child(3) { + border-right: none; +} +.dictation .table.dictation td .menuInTable li a.mnBack { + margin-left: 3rem; +} .dictation .table.dictation td:has(img[alt="encrypted"]) { text-align: center; } @@ -2127,6 +2254,44 @@ tr.isSelected .menuInTable li a { display: none; } +.formList.property .formTitle { + padding: 1rem 4% 0; + line-height: 1.2; +} +.formList.property dt:not(.formTitle) { + width: 30%; + padding: 0 4% 0 4%; + font-size: 0.9rem; +} +.formList.property dt:not(.formTitle):nth-of-type(odd) { + background: #f0f0f0; +} +.formList.property dt:not(.formTitle):nth-of-type(odd) + dd { + background: #f0f0f0; +} +.formList.property dd { + width: 58%; + padding: 0.2rem 4% 0.2rem 0; + margin-bottom: 0; + white-space: pre-line; + word-wrap: break-word; + line-height: 1.2; +} +.formList.property dd img { + height: 1.1rem; +} +.formList.property dd.full { + width: 100%; + padding: 0.2rem 4% 0.2rem 4%; +} +.formList.property dd.full .buttonText { + padding: 0 0 0.8rem; +} +.formList.property dd.full .buttonText img { + vertical-align: text-bottom; + margin-right: 0.5rem; +} + .formList dd.formChange { display: flex; flex-wrap: wrap; @@ -2234,12 +2399,6 @@ tr.isSelected .menuInTable li a { .partners .table.partner.role4 { margin-top: 3rem; } -.partners .table.partner.role4 td { - padding-bottom: 0.7rem; -} -.partners .table.partner.role4 .menuInTable { - display: none; -} .partners .table.partner:not(.role4) tr th:last-of-type, .partners .table.partner:not(.role4) tr td:last-of-type { display: none; @@ -2415,8 +2574,6 @@ tr.isSelected .menuInTable li a { margin-top: 5rem; padding: 0 2rem; font-size: 14px; - position: absolute; - bottom: 3rem; } .borderTop { diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index 30434e7..66481cb 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -209,5 +209,6 @@ declare const classNames: { readonly txNormal: "txNormal"; readonly txIcon: "txIcon"; readonly txWswrap: "txWswrap"; + readonly required: "required"; }; export = classNames;