Merge branch 'develop' into main

This commit is contained in:
makabe 2023-09-21 10:53:01 +09:00
commit 19f1997bd6
49 changed files with 2303 additions and 75 deletions

View File

@ -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"
}
}

View File

@ -24,7 +24,7 @@ const App = (): JSX.Element => {
(e: AxiosError) => {
if (e?.response?.status === 401) {
dispatch(clearToken());
instance.logout({
instance.logoutRedirect({
postLogoutRedirectUri: "/?logout=true",
});
}

View File

@ -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 = () => (
<Routes>
@ -53,7 +54,7 @@ const AppRouter: React.FC = () => (
{/* XXX ヘッダーの挙動確認のため仮のページを作成 */}
<Route
path="/account"
element={<RouteAuthGuard component={<SamplePage />} />}
element={<RouteAuthGuard component={<AccountPage />} />}
/>
<Route
path="/dictations"

View File

@ -1 +1 @@
7.0.0
7.0.1

View File

@ -54,6 +54,48 @@ export interface Account {
* @memberof Account
*/
'companyName': string;
/**
*
* @type {number}
* @memberof Account
*/
'tier': number;
/**
*
* @type {string}
* @memberof Account
*/
'country': string;
/**
*
* @type {number}
* @memberof Account
*/
'parentAccountId'?: number;
/**
*
* @type {boolean}
* @memberof Account
*/
'delegationPermission': boolean;
/**
*
* @type {number}
* @memberof Account
*/
'primaryAdminUserId'?: number;
/**
*
* @type {number}
* @memberof Account
*/
'secondryAdminUserId'?: number;
/**
*
* @type {string}
* @memberof Account
*/
'parentAccountName'?: string;
}
/**
*
@ -824,7 +866,7 @@ export interface GetRelationsResponse {
* @type {string}
* @memberof GetRelationsResponse
*/
'encryptionPassword': string | null;
'encryptionPassword'?: string;
/**
* WorkTypeIDWorkTypeIDから一つ指定activeWorktypeがなければ空文字を返却する
* @type {string}
@ -969,7 +1011,7 @@ export interface GetWorktypesResponse {
* @type {number}
* @memberof GetWorktypesResponse
*/
'acrive'?: number;
'active'?: number;
}
/**
*
@ -1307,12 +1349,6 @@ export interface PostUpdateUserRequest {
* @interface PostWorktypeOptionItem
*/
export interface PostWorktypeOptionItem {
/**
*
* @type {number}
* @memberof PostWorktypeOptionItem
*/
'id': number;
/**
*
* @type {string}
@ -1682,7 +1718,7 @@ export interface UpdateAccountInfoRequest {
* @type {number}
* @memberof UpdateAccountInfoRequest
*/
'parentAccountId': number;
'parentAccountId'?: number;
/**
*
* @type {boolean}
@ -1700,7 +1736,7 @@ export interface UpdateAccountInfoRequest {
* @type {number}
* @memberof UpdateAccountInfoRequest
*/
'secondryAdminUserId': number;
'secondryAdminUserId'?: number;
}
/**
*

View File

@ -15,6 +15,7 @@ import partner from "features/partner/partnerSlice";
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
import typistGroup from "features/workflow/typistGroup/typistGroupSlice";
import worktype from "features/workflow/worktype/worktypeSlice";
import account from "features/account/accountSlice";
export const store = configureStore({
reducer: {
@ -34,6 +35,7 @@ export const store = configureStore({
partner,
typistGroup,
worktype,
account,
},
});

View File

@ -36,6 +36,7 @@ export const errorCodes = [
"E010302", // authorId重複エラー
"E010401", // PONumber重複エラー
"E010501", // アカウント不在エラー
"E010502", // アカウント情報変更不可エラー
"E010601", // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
"E010602", // タスク変更権限不足エラー
"E010603", // タスク不在エラー

View File

@ -23,7 +23,7 @@ export const RouteAuthGuard = (props: RouteAuthGuardProps) => {
if (!isAuth || isExpired) {
dispatch(clearToken());
// B2Cからもログアウトする
instance.logout({
instance.logoutRedirect({
postLogoutRedirectUri: "/?logout=true",
});
}

View File

@ -0,0 +1,107 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AccountState } from "./state";
import { updateAccountInfoAsync, getAccountRelationsAsync } from "./operations";
const initialState: AccountState = {
domain: {
getAccountInfo: {
account: {
accountId: 0,
companyName: "",
tier: 0,
country: "",
delegationPermission: false,
},
},
dealers: [],
users: [],
},
apps: {
updateAccountInfo: {
parentAccountId: undefined,
delegationPermission: false,
primaryAdminUserId: 0,
secondryAdminUserId: undefined,
},
isLoading: false,
},
};
export const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
changeDealer: (
state,
action: PayloadAction<{ parentAccountId: number | undefined }>
) => {
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;

View File

@ -0,0 +1,5 @@
export * from "./state";
export * from "./operations";
export * from "./accountSlice";
export * from "./selectors";
export * from "./types";

View File

@ -0,0 +1,105 @@
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}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
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 });
}
});

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,11 @@
import {
GetMyAccountResponse,
GetDealersResponse,
GetUsersResponse,
} from "../../api/api";
export interface ViewAccountRelationsInfo {
accountInfo: GetMyAccountResponse;
dealers: GetDealersResponse;
users: GetUsersResponse;
}

View File

@ -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,

View File

@ -284,3 +284,61 @@ export const editOptionItemsAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const updateActiveWorktypeAsync = createAsyncThunk<
{
// return empty
},
{ id?: number | undefined },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/updateActiveWorktypeAsync", 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 { id } = args;
try {
await accountsApi.activeWorktype(
{ id },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let errorMessage = getTranslationID("common.message.internalServerError");
// ActiveWorktypeの保存に失敗した場合
if (error.code === "E011003") {
errorMessage = getTranslationID(
"worktypeIdSetting.message.updateActiveWorktypeFailedError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -70,3 +70,6 @@ export const selectHasErrorOptionItems = (state: RootState) => {
// isOptionItemsLoadingを取得する
export const selectIsOptionItemsLoading = (state: RootState) =>
state.worktype.apps.isOptionItemsLoading;
export const selectActiveWorktypeId = (state: RootState) =>
state.worktype.apps.activeWorktypeId;

View File

@ -15,6 +15,7 @@ export interface Apps {
worktypeId: string;
description?: string;
optionItems?: OptionItem[];
activeWorktypeId?: number | undefined;
}
export interface Domain {

View File

@ -6,6 +6,7 @@ import {
editWorktypeAsync,
getOptionItemsAsync,
listWorktypesAsync,
updateActiveWorktypeAsync,
} from "./operations";
import { OptionItem, isOptionItemDefaultValueType } from "./types";
import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants";
@ -20,6 +21,7 @@ const initialState: WorktypeState = {
worktypeId: "",
description: undefined,
optionItems: undefined,
activeWorktypeId: undefined,
},
domain: {},
};
@ -82,9 +84,10 @@ export const worktypeSlice = createSlice({
state.apps.isLoading = true;
});
builder.addCase(listWorktypesAsync.fulfilled, (state, action) => {
// TODO:Active WorktypeIDも取得する
const { worktypes } = action.payload;
const { worktypes, active } = action.payload;
state.domain.worktypes = worktypes;
state.apps.activeWorktypeId = active;
state.apps.isLoading = false;
});
builder.addCase(listWorktypesAsync.rejected, (state) => {
@ -137,6 +140,15 @@ export const worktypeSlice = createSlice({
builder.addCase(editOptionItemsAsync.rejected, (state) => {
state.apps.isOptionItemsLoading = false;
});
builder.addCase(updateActiveWorktypeAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateActiveWorktypeAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateActiveWorktypeAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {

View File

@ -0,0 +1,350 @@
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<boolean>(false);
const [isEmptyPrimaryAdmin, setIsEmptyPrimaryAdmin] =
useState<boolean>(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 (
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("accountPage.label.title"))}
</h1>
</div>
<section className={styles.account}>
<div className={styles.boxFlex}>
<ul className={`${styles.menuAction} ${styles.box100}`}>
<li>
<a
href="account_setting.html"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img
src="images/file_delete.svg"
alt=""
className={styles.menuIcon}
/>
{t(getTranslationID("accountPage.label.fileDeleteSetting"))}
</a>
</li>
</ul>
<div className={styles.marginRgt3}>
<dl className={styles.listVertical}>
<h4 className={styles.listHeader}>
{t(
getTranslationID("accountPage.label.accountInformation")
)}
</h4>
<dt>
{t(getTranslationID("accountPage.label.companyName"))}
</dt>
<dd>{viewInfo.account.companyName}</dd>
<dt>{t(getTranslationID("accountPage.label.accountID"))}</dt>
<dd>{viewInfo.account.accountId}</dd>
<dt>
{t(getTranslationID("accountPage.label.yourCategory"))}
</dt>
<dd>{tierNames[viewInfo.account.tier]}</dd>
<dt>
{t(getTranslationID("accountPage.label.yourCountry"))}
</dt>
<dd>{viewInfo.account.country}</dd>
<dt>{t(getTranslationID("accountPage.label.yourDealer"))}</dt>
{isTier5 && !viewInfo.account.parentAccountName && (
<dd className={styles.form}>
<select
className={`${styles.formInput} ${styles.required}`}
onChange={(event) => {
dispatch(
changeDealer({
parentAccountId:
dealers.find(
(x) => x.name === event.target.value
)?.id || undefined,
})
);
}}
>
<option value="">
{t(
getTranslationID("accountPage.label.selectDealer")
)}
</option>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<option value="Blank" />
{dealers.map((x) => (
<option key={x.name} value={x.name}>
{x.name}
</option>
))}
</select>
</dd>
)}
{(!isTier5 || viewInfo.account.parentAccountName) && (
<dd>{viewInfo.account.parentAccountName ?? "-"}</dd>
)}
<dt>
{t(getTranslationID("accountPage.label.dealerManagement"))}
</dt>
{isTier5 && (
<dd>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label>
<input
type="checkbox"
className={styles.formCheck}
checked={updateAccountInfo.delegationPermission}
onChange={(e) => {
dispatch(
changeDealerPermission({
delegationPermission: e.target.checked,
})
);
}}
/>
</label>
</dd>
)}
{!isTier5 && <dd>-</dd>}
</dl>
</div>
<div>
<dl className={styles.listVertical}>
<h4 className={styles.listHeader}>
{t(
getTranslationID(
"accountPage.label.administratorInformation"
)
)}
</h4>
<dt>
{t(
getTranslationID("accountPage.label.primaryAdministrator")
)}
</dt>
<dd>
{
users.find(
(x) => x.id === viewInfo.account.primaryAdminUserId
)?.name
}
</dd>
<dt>
{t(getTranslationID("accountPage.label.emailAddress"))}
</dt>
<dd className={styles.form}>
<select
name=""
className={`${styles.formInput} ${styles.required}`}
onChange={(event) => {
dispatch(
changePrimaryAdministrator({
primaryAdminUserId:
users.find((x) => x.email === event.target.value)
?.id || 0,
})
);
}}
>
<option
value={
users.find(
(x) => x.id === viewInfo.account.primaryAdminUserId
)?.email || undefined
}
>
{
users.find(
(x) => x.id === viewInfo.account.primaryAdminUserId
)?.email
}
</option>
{users.map((x) => (
<option key={x.email} value={x.email}>
{x.email}
</option>
))}
</select>
{isPushSaveChangesButton && isEmptyPrimaryAdmin && (
<span className={styles.formError}>
{" "}
{t(
getTranslationID("signupPage.message.inputEmptyError")
)}
</span>
)}
</dd>
<dt>
{t(
getTranslationID(
"accountPage.label.secondaryAdministrator"
)
)}
</dt>
<dd>
{
users.find(
(x) => x.id === viewInfo.account.secondryAdminUserId
)?.name
}
</dd>
<dt>
{t(getTranslationID("accountPage.label.emailAddress"))}
</dt>
<dd className={styles.form}>
<select
name=""
className={`${styles.formInput} ${styles.required}`}
onChange={(event) => {
dispatch(
changeSecondryAdministrator({
secondryAdminUserId:
users.find((x) => x.email === event.target.value)
?.id ?? undefined,
})
);
}}
>
<option
value={
viewInfo.account.secondryAdminUserId
? users.find(
(x) =>
x.id === viewInfo.account.secondryAdminUserId
)?.email
: undefined
}
>
{viewInfo.account.secondryAdminUserId
? users.find(
(x) =>
x.id === viewInfo.account.secondryAdminUserId
)?.email
: t(
getTranslationID(
"accountPage.label.selectSecondaryAdministrator"
)
)}
</option>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<option value="Blank" />
{users.map((x) => (
<option key={x.email} value={x.email}>
{x.email}
</option>
))}
</select>
</dd>
</dl>
</div>
<div className={`${styles.box100} ${styles.alignLeft} `}>
<input
type="submit"
name="submit"
value={t(getTranslationID("accountPage.label.saveChanges"))}
className={`${styles.formSubmit} ${
!isLoading ? styles.isActive : ""
}
`}
onClick={onSaveChangesButton}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</div>
</div>
<ul className={styles.linkBottom}>
<li>
<a href="" className={styles.linkTx}>
{t(getTranslationID("accountPage.label.deleteAccount"))}
</a>
</li>
</ul>
</section>
</div>
</main>
<Footer />
</div>
);
};
export default AccountPage;

View File

@ -16,7 +16,7 @@ const LicensePage: React.FC = (): JSX.Element => {
const redirectToTopPage = useCallback(() => {
dispatch(clearToken());
instance.logout({
instance.logoutRedirect({
postLogoutRedirectUri: "/",
});
}, [dispatch, instance]);

View File

@ -25,7 +25,7 @@ const LoginPage: React.FC = (): JSX.Element => {
// ログイン失敗した場合、B2Cをログアウトしてからエラーページに遷移する
if (meta.requestStatus === "rejected") {
instance.logout({
instance.logoutRedirect({
postLogoutRedirectUri: "/AuthError",
});
}

View File

@ -24,7 +24,6 @@ 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";
import checkOutline from "../../assets/images/check_outline.svg";
const PartnerPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();

View File

@ -20,7 +20,7 @@ const SamplePage: React.FC = (): JSX.Element => {
type="button"
className={styles.buttonText}
onClick={() => {
instance.logout({ postLogoutRedirectUri: "/" });
instance.logoutRedirect({ postLogoutRedirectUri: "/" });
dispatch(clearToken());
}}
>

View File

@ -14,8 +14,10 @@ import {
changeWorktypeId,
changeDescription,
listWorktypesAsync,
updateActiveWorktypeAsync,
selectIsLoading,
selectWorktypes,
selectActiveWorktypeId,
} from "features/workflow/worktype";
import { AppDispatch } from "app/store";
import { AddWorktypeIdPopup } from "./addWorktypeIdPopup";
@ -27,6 +29,8 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
const [t] = useTranslation();
const isLoading = useSelector(selectIsLoading);
const worktypes = useSelector(selectWorktypes);
const activeWorktypeId = useSelector(selectActiveWorktypeId);
const [selectedRow, setSelectedRow] = useState<number>(NaN);
// 追加Popupの表示制御
const [isShowAddPopup, setIsShowAddPopup] = useState<boolean>(false);
@ -34,10 +38,53 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
const [isShowEditPopup, setIsShowEditPopup] = useState<boolean>(false);
const [isShowEditOptionItemPopup, setIsShowEditOptionItemPopup] =
useState<boolean>(false);
// ActiveWorktypeIDのセレクトボックス表示の値
const [selectedActiveWorktypeId, setSelectedActiveWorktypeId] = useState<
number | undefined
>(undefined);
// 初期表示
useEffect(() => {
dispatch(listWorktypesAsync());
}, [dispatch]);
// APIから取得したactiveWorktypeIdを画面表示に反映
useEffect(() => {
setSelectedActiveWorktypeId(activeWorktypeId);
}, [activeWorktypeId]);
// ActiveWorktypeIDのセレクトボックス変更時セレクトボックスに表示するActiveWorktypeIDの変更時
useEffect(() => {
// 画面表示の変更後にダイアログを表示するため、setTimeoutを使用
const timeout = setTimeout(async () => {
if (selectedActiveWorktypeId === activeWorktypeId) {
return;
}
// ダイアログ確認
if (
// eslint-disable-next-line no-alert
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
setSelectedActiveWorktypeId(activeWorktypeId);
return;
}
const { meta } = await dispatch(
updateActiveWorktypeAsync({ id: selectedActiveWorktypeId })
);
if (meta.requestStatus === "fulfilled") {
dispatch(listWorktypesAsync());
} else {
setSelectedActiveWorktypeId(activeWorktypeId);
}
}, 0);
return () => {
clearTimeout(timeout);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedActiveWorktypeId]);
return (
<>
<AddWorktypeIdPopup
@ -109,7 +156,18 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
"worktypeIdSetting.label.activeWorktypeId"
)
)}:`}
<select name="Active Worktype" className={styles.formInput}>
<select
name="Active Worktype"
className={styles.formInput}
value={selectedActiveWorktypeId ?? ""}
onChange={(e) => {
const { value } = e.target;
const active = value === "" ? undefined : Number(value);
setSelectedActiveWorktypeId(active);
}}
>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<option value="" />
{worktypes?.map((worktype) => (
<option key={worktype.id} value={worktype.id}>
{worktype.worktypeId}

View File

@ -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;
@ -677,6 +680,7 @@ h3 + .brCrumb .tlIcon {
line-height: 1.4;
letter-spacing: 0;
font-weight: normal;
white-space: pre-line;
}
.formError {
display: block;
@ -686,6 +690,7 @@ h3 + .brCrumb .tlIcon {
line-height: 1.4;
letter-spacing: 0;
font-weight: normal;
white-space: pre-line;
}
.formConfirm {
width: 350px;
@ -722,6 +727,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 +766,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 +828,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 +850,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 +868,9 @@ h3 + .brCrumb .tlIcon {
.listVertical dd {
width: 42%;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.boxFlex {
@ -875,7 +932,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;
@ -986,8 +1043,8 @@ h3 + .brCrumb .tlIcon {
left: calc(50% - 25px);
}
.modal .form .tableWrap {
max-height: 60vh;
overflow-y: scroll;
max-height: 100%;
overflow-x: hidden;
margin-bottom: 1rem;
}
.modal .form .table {
@ -1074,6 +1131,7 @@ h3 + .brCrumb .tlIcon {
border-radius: 0.2rem;
opacity: 0.4;
pointer-events: none;
background: #ffffff;
}
.pagenationNav a.isActive {
opacity: 1;
@ -1157,6 +1215,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 +1796,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 +1811,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 +2110,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 +2256,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;
@ -2165,7 +2332,8 @@ 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,
@ -2176,8 +2344,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 {
@ -2234,12 +2402,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;
@ -2336,7 +2498,8 @@ 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,
@ -2347,8 +2510,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 {
@ -2415,8 +2578,6 @@ tr.isSelected .menuInTable li a {
margin-top: 5rem;
padding: 0 2rem;
font-size: 14px;
position: absolute;
bottom: 3rem;
}
.borderTop {

View File

@ -209,5 +209,6 @@ declare const classNames: {
readonly txNormal: "txNormal";
readonly txIcon: "txIcon";
readonly txWswrap: "txWswrap";
readonly required: "required";
};
export = classNames;

View File

@ -409,7 +409,8 @@
"worktypeIDLimitError": "(de)Worktype IDが登録件数の上限に達しているため追加できません。",
"optionItemInvalidError": "(de)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(de)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(de)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください"
"optionItemIncorrectError": "(de)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(de)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"partnerPage": {
@ -426,5 +427,29 @@
"partners": "(de)partners",
"deleteAccount": "(de)Delete Account"
}
},
"accountPage": {
"label": {
"title": "(de)Account",
"fileDeleteSetting": "(de)File Delete Setting",
"accountInformation": "(de)Account Information",
"companyName": "(de)Company Name",
"accountID": "(de)Account ID",
"yourCategory": "(de)Your Category",
"yourCountry": "(de)Your Country",
"yourDealer": "(de)Your DealerUpper layer",
"selectDealer": "(de)Select Dealer",
"dealerManagement": "(de)Dealer Management",
"administratorInformation": "(de)Administrator Information",
"primaryAdministrator": "(de)Primary Administrator",
"secondaryAdministrator": "(de)Secondary Administrator",
"emailAddress": "(de)E-mail address",
"selectSecondaryAdministrator": "(de)Select Secondary Administrator",
"saveChanges": "(de)Save Changes",
"deleteAccount": "(de)Delete Account"
},
"message": {
"updateAccountFailedError": "(de)アカウント情報の保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -409,7 +409,8 @@
"worktypeIDLimitError": "Worktype IDが登録件数の上限に達しているため追加できません。",
"optionItemInvalidError": "Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください"
"optionItemIncorrectError": "入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"partnerPage": {
@ -426,5 +427,29 @@
"partners": "partners",
"deleteAccount": "Delete Account"
}
},
"accountPage": {
"label": {
"title": "Account",
"fileDeleteSetting": "File Delete Setting",
"accountInformation": "Account Information",
"companyName": "Company Name",
"accountID": "Account ID",
"yourCategory": "Your Category",
"yourCountry": "Your Country",
"yourDealer": "Your DealerUpper layer",
"selectDealer": "Select Dealer",
"dealerManagement": "Dealer Management",
"administratorInformation": "Administrator Information",
"primaryAdministrator": "Primary Administrator",
"secondaryAdministrator": "Secondary Administrator",
"emailAddress": "E-mail address",
"selectSecondaryAdministrator": "Select Secondary Administrator",
"saveChanges": "Save Changes",
"deleteAccount": "Delete Account"
},
"message": {
"updateAccountFailedError": "アカウント情報の保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -409,7 +409,8 @@
"worktypeIDLimitError": "(es)Worktype IDが登録件数の上限に達しているため追加できません。",
"optionItemInvalidError": "(es)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(es)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(es)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください"
"optionItemIncorrectError": "(es)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(es)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"partnerPage": {
@ -426,5 +427,29 @@
"partners": "(es)partners",
"deleteAccount": "(es)Delete Account"
}
},
"accountPage": {
"label": {
"title": "(es)Account",
"fileDeleteSetting": "(es)File Delete Setting",
"accountInformation": "(es)Account Information",
"companyName": "(es)Company Name",
"accountID": "(es)Account ID",
"yourCategory": "(es)Your Category",
"yourCountry": "(es)Your Country",
"yourDealer": "(es)Your DealerUpper layer",
"selectDealer": "(es)Select Dealer",
"dealerManagement": "(es)Dealer Management",
"administratorInformation": "(es)Administrator Information",
"primaryAdministrator": "(es)Primary Administrator",
"secondaryAdministrator": "(es)Secondary Administrator",
"emailAddress": "(es)E-mail address",
"selectSecondaryAdministrator": "(es)Select Secondary Administrator",
"saveChanges": "(es)Save Changes",
"deleteAccount": "(es)Delete Account"
},
"message": {
"updateAccountFailedError": "(es)アカウント情報の保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -409,7 +409,8 @@
"worktypeIDLimitError": "(fr)Worktype IDが登録件数の上限に達しているため追加できません。",
"optionItemInvalidError": "(fr)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(fr)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(fr)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください"
"optionItemIncorrectError": "(fr)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(fr)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
}
},
"partnerPage": {
@ -426,5 +427,29 @@
"partners": "(fr)partners",
"deleteAccount": "(fr)Delete Account"
}
},
"accountPage": {
"label": {
"title": "(fr)Account",
"fileDeleteSetting": "(fr)File Delete Setting",
"accountInformation": "(fr)Account Information",
"companyName": "(fr)Company Name",
"accountID": "(fr)Account ID",
"yourCategory": "(fr)Your Category",
"yourCountry": "(fr)Your Country",
"yourDealer": "(fr)Your DealerUpper layer",
"selectDealer": "(fr)Select Dealer",
"dealerManagement": "(fr)Dealer Management",
"administratorInformation": "(fr)Administrator Information",
"primaryAdministrator": "(fr)Primary Administrator",
"secondaryAdministrator": "(fr)Secondary Administrator",
"emailAddress": "(fr)E-mail address",
"selectSecondaryAdministrator": "(fr)Select Secondary Administrator",
"saveChanges": "(fr)Save Changes",
"deleteAccount": "(fr)Delete Account"
},
"message": {
"updateAccountFailedError": "(fr)アカウント情報の保存に失敗しました。画面を更新し、再度実行してください"
}
}
}
}

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE `template_files` DROP COLUMN `deleted_at`;
-- +migrate Down
ALTER TABLE `template_files` ADD COLUMN `deleted_at` TIMESTAMP COMMENT '削除時刻' AFTER `file_name`;

View File

@ -226,7 +226,7 @@
"security": [{ "bearer": [] }]
},
"post": {
"operationId": "me",
"operationId": "updateAccountInfo",
"summary": "",
"parameters": [],
"requestBody": {
@ -1181,6 +1181,51 @@
"security": [{ "bearer": [] }]
}
},
"/accounts/delete": {
"post": {
"operationId": "deleteAccount",
"summary": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/DeleteAccountRequest" }
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateAccountInfoResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "DBアクセスに失敗しログインできる状態で処理が終了した場合",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["accounts"],
"security": [{ "bearer": [] }]
}
},
"/users/confirm": {
"post": {
"operationId": "confirmUser",
@ -1849,6 +1894,100 @@
"security": [{ "bearer": [] }]
}
},
"/files/template/upload-location": {
"get": {
"operationId": "uploadTemplateLocation",
"summary": "",
"description": "ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TemplateUploadLocationResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["files"],
"security": [{ "bearer": [] }]
}
},
"/files/template/upload-finished": {
"post": {
"operationId": "uploadTemplateFinished",
"summary": "",
"description": "アップロードが完了したテンプレートファイルの情報を登録します",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TemplateUploadFinishedRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TemplateUploadFinishedReqponse"
}
}
}
},
"400": {
"description": "不正なパラメータ",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["files"],
"security": [{ "bearer": [] }]
}
},
"/tasks": {
"get": {
"operationId": "getTasks",
@ -2671,6 +2810,44 @@
"security": [{ "bearer": [] }]
}
},
"/templates": {
"get": {
"operationId": "getTemplates",
"summary": "",
"description": "アカウント内のテンプレートファイルの一覧を取得します",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetTemplatesResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["templates"],
"security": [{ "bearer": [] }]
}
},
"/notification/register": {
"post": {
"operationId": "register",
@ -2843,7 +3020,8 @@
"parentAccountId": { "type": "number" },
"delegationPermission": { "type": "boolean" },
"primaryAdminUserId": { "type": "number" },
"secondryAdminUserId": { "type": "number" }
"secondryAdminUserId": { "type": "number" },
"parentAccountName": { "type": "string" }
},
"required": [
"accountId",
@ -3263,9 +3441,16 @@
"description": "セカンダリ管理者ID"
}
},
"required": ["delegationPermission"]
"required": ["delegationPermission", "primaryAdminUserId"]
},
"UpdateAccountInfoResponse": { "type": "object", "properties": {} },
"DeleteAccountRequest": {
"type": "object",
"properties": {
"accountId": { "type": "number", "description": "アカウントID" }
},
"required": ["accountId"]
},
"ConfirmRequest": {
"type": "object",
"properties": { "token": { "type": "string" } },
@ -3602,6 +3787,36 @@
"properties": { "url": { "type": "string" } },
"required": ["url"]
},
"TemplateUploadLocationResponse": {
"type": "object",
"properties": { "url": { "type": "string" } },
"required": ["url"]
},
"TemplateFile": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "テンプレートファイルのファイル名"
},
"url": {
"type": "string",
"description": "テンプレートファイルのURL"
}
},
"required": ["name", "url"]
},
"TemplateUploadFinishedRequest": {
"type": "object",
"properties": {
"templateFile": {
"description": "テンプレートファイルのファイル情報",
"allOf": [{ "$ref": "#/components/schemas/TemplateFile" }]
}
},
"required": ["templateFile"]
},
"TemplateUploadFinishedReqponse": { "type": "object", "properties": {} },
"Assignee": {
"type": "object",
"properties": {
@ -3808,6 +4023,17 @@
"required": ["poNumber"]
},
"CancelOrderResponse": { "type": "object", "properties": {} },
"GetTemplatesResponse": {
"type": "object",
"properties": {
"templates": {
"description": "テンプレートファイルの一覧",
"type": "array",
"items": { "$ref": "#/components/schemas/TemplateFile" }
}
},
"required": ["templates"]
},
"RegisterRequest": {
"type": "object",
"properties": {

View File

@ -28,6 +28,8 @@ import { NotificationModule } from './features/notification/notification.module'
import { FilesModule } from './features/files/files.module';
import { FilesController } from './features/files/files.controller';
import { FilesService } from './features/files/files.service';
import { TemplatesModule } from './features/templates/templates.module';
import { TemplatesController } from './features/templates/templates.controller';
import { TasksService } from './features/tasks/tasks.service';
import { TasksController } from './features/tasks/tasks.controller';
import { TasksModule } from './features/tasks/tasks.module';
@ -41,6 +43,7 @@ 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 { TemplatesService } from './features/templates/templates.service';
@Module({
imports: [
@ -65,6 +68,7 @@ import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.re
FilesModule,
TasksModule,
UsersModule,
TemplatesModule,
SendGridModule,
LicensesModule,
AccountsRepositoryModule,
@ -106,6 +110,7 @@ import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.re
TasksController,
UsersController,
LicensesController,
TemplatesController,
],
providers: [
AuthService,
@ -115,6 +120,7 @@ import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.re
FilesService,
TasksService,
LicensesService,
TemplatesService,
],
})
export class AppModule {

View File

@ -36,6 +36,7 @@ export const ErrorCodes = [
'E010302', // authorId重複エラー
'E010401', // PONumber重複エラー
'E010501', // アカウント不在エラー
'E010502', // アカウント情報変更不可エラー
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
'E010602', // タスク変更権限不足エラー
'E010603', // タスク不在エラー

View File

@ -25,6 +25,7 @@ export const errors: Errors = {
E010302: 'This AuthorId already used Error',
E010401: 'This PoNumber already used Error',
E010501: 'Account not Found Error.',
E010502: 'Account information cannot be changed Error.',
E010601: 'Task is not Editable Error',
E010602: 'No task edit permissions Error',
E010603: 'Task not found Error.',

View File

@ -60,6 +60,8 @@ import {
PostActiveWorktypeResponse,
UpdateAccountInfoRequest,
UpdateAccountInfoResponse,
DeleteAccountRequest,
DeleteAccountResponse,
} from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards';
@ -909,9 +911,7 @@ export class AccountsController {
const context = makeContext(userId);
console.log('id', id);
console.log(context.trackingId);
await this.accountService.updateActiveWorktype(context, userId, id);
return {};
}
@ -985,7 +985,7 @@ export class AccountsController {
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'me' })
@ApiOperation({ operationId: 'updateAccountInfo' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
@ -1004,19 +1004,61 @@ export class AccountsController {
secondryAdminUserId,
} = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const { userId, tier } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
// 仮。API実装で本実装
// await this.accountService.updateAccountInfo(
// context,
// userId,
// parentAccountId,
// delegationPermission,
// primaryAdminUserId,
// secondryAdminUserId,
// );
await this.accountService.updateAccountInfo(
context,
userId,
tier,
delegationPermission,
primaryAdminUserId,
parentAccountId,
secondryAdminUserId,
);
return;
}
@Post('/delete')
@ApiResponse({
status: HttpStatus.OK,
type: UpdateAccountInfoResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'DBアクセスに失敗しログインできる状態で処理が終了した場合',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'deleteAccount' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN],
}),
)
async deleteAccount(
@Req() req: Request,
@Body() body: DeleteAccountRequest,
): Promise<DeleteAccountResponse> {
const { accountId } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(userId);
/* TODO
await this.accountService.deleteAccount(
context,
accountId
);
*/
return;
}
}

View File

@ -65,6 +65,7 @@ import {
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import { AdB2cUser } from '../../gateways/adb2c/types/types';
import { Worktype } from '../../repositories/worktypes/entity/worktype.entity';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
describe('createAccount', () => {
let source: DataSource = null;
@ -4322,6 +4323,221 @@ describe('updateOptionItems', () => {
});
});
describe('updateActiveWorktype', () => {
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('アカウントのActiveWorktypeIDを指定WorktypeIDに更新できるNULL⇒ID設定', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype = await createWorktype(source, account.id, 'worktype1');
//作成したデータを確認
{
const beforeAccount = await getAccount(source, account.id);
expect(beforeAccount.active_worktype_id).toBe(null);
}
await service.updateActiveWorktype(context, admin.external_id, worktype.id);
//実行結果を確認
{
const { active_worktype_id } = await getAccount(source, account.id);
expect(active_worktype_id).toBe(worktype.id);
}
});
it('アカウントのActiveWorktypeIDを指定WorktypeIDに更新できる別のWorkTypeIDを設定', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype1 = await createWorktype(
source,
account.id,
'worktype1',
'description1',
true,
);
const worktype2 = await createWorktype(source, account.id, 'worktype2');
//作成したデータを確認
{
const beforeAccount = await getAccount(source, account.id);
expect(beforeAccount.active_worktype_id).toBe(worktype1.id);
}
await service.updateActiveWorktype(
context,
admin.external_id,
worktype2.id,
);
//実行結果を確認
{
const { active_worktype_id } = await getAccount(source, account.id);
expect(active_worktype_id).toBe(worktype2.id);
}
});
it('アカウントのActiveWorktypeIDをNULLに更新できるWorkTypeID⇒NULL', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const worktype1 = await createWorktype(
source,
account.id,
'worktype1',
'description1',
true,
);
//作成したデータを確認
{
const beforeAccount = await getAccount(source, account.id);
expect(beforeAccount.active_worktype_id).toBe(worktype1.id);
}
await service.updateActiveWorktype(context, admin.external_id, undefined);
//実行結果を確認
{
const { active_worktype_id } = await getAccount(source, account.id);
expect(active_worktype_id).toBe(null);
}
});
it('自アカウント内に指定されたIDのWorktypeIDが存在しない場合、400エラーとなることWorkTypeIDが存在しない場合', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
await createWorktype(source, account.id, 'worktype1');
//作成したデータを確認
{
const beforeAccount = await getAccount(source, account.id);
expect(beforeAccount.active_worktype_id).toBe(null);
}
try {
await service.updateActiveWorktype(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('自アカウント内に指定されたIDのWorktypeIDが存在しない場合、400エラーとなることWorkTypeIDが別アカウントの場合', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { account: otherAccount } = await makeTestAccount(source, {
tier: 5,
});
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
await createWorktype(source, account.id, 'worktype1');
await createWorktype(source, otherAccount.id, 'worktype2');
//作成したデータを確認
{
const beforeAccount = await getAccount(source, account.id);
const worktype1 = await getWorktypes(source, account.id);
const worktype2 = await getWorktypes(source, otherAccount.id);
expect(beforeAccount.active_worktype_id).toBe(null);
expect(worktype1.length).toBe(1);
expect(worktype1[0].custom_worktype_id).toBe('worktype1');
expect(worktype2.length).toBe(1);
expect(worktype2[0].custom_worktype_id).toBe('worktype2');
}
try {
await service.updateActiveWorktype(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>(AccountsService);
const context = makeContext(admin.external_id);
await createWorktype(source, account.id, 'worktype1');
//作成したデータを確認
{
const beforeAccount = await getAccount(source, account.id);
expect(beforeAccount.active_worktype_id).toBe(null);
}
//DBアクセスに失敗するようにする
const accountsRepositoryService = module.get<AccountsRepositoryService>(
AccountsRepositoryService,
);
accountsRepositoryService.updateActiveWorktypeId = jest
.fn()
.mockRejectedValue('DB failed');
try {
await service.updateActiveWorktype(context, admin.external_id, 999);
} 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 () => {
@ -4758,6 +4974,179 @@ describe('パートナー一覧取得', () => {
});
});
describe('アカウント情報更新', () => {
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('アカウント情報を更新する(第五階層が実行/セカンダリ管理者ユーザがnull', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
await service.updateAccountInfo(
makeContext('trackingId'),
tier5Accounts.admin.external_id,
tier5Accounts.account.tier,
true,
tier5Accounts.admin.id,
tier4Accounts[0].account.id,
undefined,
);
// DB内が想定通りになっているか確認
const account = await getAccount(source, tier5Accounts.account.id);
expect(account.parent_account_id).toBe(tier4Accounts[0].account.id);
expect(account.delegation_permission).toBe(true);
expect(account.primary_admin_user_id).toBe(tier5Accounts.admin.id);
expect(account.secondary_admin_user_id).toBe(null);
});
it('アカウント情報を更新する(第五階層以外が実行)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const { tier3Accounts: tier3Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const adduser = await makeTestUser(source, {
account_id: tier4Accounts[0].account.id,
external_id: 'typist-user-external-id',
role: 'typist',
});
await service.updateAccountInfo(
makeContext('trackingId'),
tier4Accounts[0].users[0].external_id,
tier4Accounts[0].account.tier,
false,
tier4Accounts[0].users[0].id,
tier3Accounts[0].account.id,
adduser.id,
);
// DB内が想定通りになっているか確認
const account = await getAccount(source, tier4Accounts[0].account.id);
expect(account.parent_account_id).toBe(tier3Accounts[0].account.id);
expect(account.delegation_permission).toBe(false);
expect(account.primary_admin_user_id).toBe(tier4Accounts[0].users[0].id);
expect(account.secondary_admin_user_id).toBe(adduser.id);
});
it('アカウント情報を更新する(ディーラーアカウントが未入力)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const { tier3Accounts: tier3Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const adduser = await makeTestUser(source, {
account_id: tier4Accounts[0].account.id,
external_id: 'typist-user-external-id',
role: 'typist',
});
await service.updateAccountInfo(
makeContext('trackingId'),
tier4Accounts[0].users[0].external_id,
tier4Accounts[0].account.tier,
false,
tier4Accounts[0].users[0].id,
undefined,
adduser.id,
);
// DB内が想定通りになっているか確認
const account = await getAccount(source, tier4Accounts[0].account.id);
expect(account.parent_account_id).toBe(null);
expect(account.delegation_permission).toBe(false);
expect(account.primary_admin_user_id).toBe(tier4Accounts[0].users[0].id);
expect(account.secondary_admin_user_id).toBe(adduser.id);
});
it('アカウント情報の更新に失敗する(ディーラー未存在)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const adduser = await makeTestUser(source, {
account_id: tier4Accounts[0].account.id,
external_id: 'typist-user-external-id',
role: 'typist',
});
await expect(
service.updateAccountInfo(
makeContext('trackingId'),
tier4Accounts[0].users[0].external_id,
tier4Accounts[0].account.tier,
false,
tier4Accounts[0].users[0].id,
123,
adduser.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010502'), HttpStatus.BAD_REQUEST),
);
});
it('アカウント情報の更新に失敗する(プライマリ管理者ユーザ未存在)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
await expect(
service.updateAccountInfo(
makeContext('trackingId'),
tier5Accounts.admin.external_id,
tier5Accounts.account.tier,
true,
999,
tier4Accounts[0].account.id,
tier4Accounts[1].users[0].id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010502'), HttpStatus.BAD_REQUEST),
);
});
it('アカウント情報の更新に失敗する(セカンダリ管理者ユーザ未存在)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
await expect(
service.updateAccountInfo(
makeContext('trackingId'),
tier5Accounts.admin.external_id,
tier5Accounts.account.tier,
true,
tier4Accounts[0].users[0].id,
tier4Accounts[0].account.id,
999,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010502'), HttpStatus.BAD_REQUEST),
);
});
});
describe('getAccountInfo', () => {
let source: DataSource = null;
beforeEach(async () => {
@ -4775,12 +5164,14 @@ describe('getAccountInfo', () => {
await source.destroy();
source = null;
});
it('パラメータのユーザに対応するアカウント情報を取得できる', async () => {
const module = await makeTestingModule(source);
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
parent_account_id: 123,
parent_account_id: tier4Accounts[0].account.id,
});
const service = module.get<AccountsService>(AccountsService);
@ -4807,6 +5198,9 @@ describe('getAccountInfo', () => {
);
expect(accountResponse.account.secondryAdminUserId).toBe(undefined);
expect(accountResponse.account.tier).toBe(account.tier);
expect(accountResponse.account.parentAccountName).toBe(
tier4Accounts[0].account.company_name,
);
}
});
});

View File

@ -37,12 +37,15 @@ import {
ExpirationThresholdDate,
} from '../licenses/types/types';
import { GetLicenseSummaryResponse, Typist } from './types/types';
import { AccessToken } from '../../common/token';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { makePassword } from '../../common/password';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import {
AccountNotFoundError,
AdminUserNotFoundError,
DealerAccountNotFoundError,
} from '../../repositories/accounts/errors/types';
import { Context } from '../../common/log';
import {
LicensesShortageError,
@ -385,6 +388,13 @@ export class AccountsService {
userInfo.account_id,
);
let parentInfo: Account;
if (accountInfo.parent_account_id) {
parentInfo = await this.accountRepository.findAccountById(
accountInfo.parent_account_id,
);
}
return {
account: {
accountId: userInfo.account_id,
@ -395,6 +405,7 @@ export class AccountsService {
delegationPermission: accountInfo.delegation_permission,
primaryAdminUserId: accountInfo.primary_admin_user_id ?? undefined,
secondryAdminUserId: accountInfo.secondary_admin_user_id ?? undefined,
parentAccountName: parentInfo ? parentInfo.company_name : undefined,
},
};
} catch (e) {
@ -1473,6 +1484,58 @@ export class AccountsService {
}
}
/**
* ActiveWorktypeの更新
* @param context
* @param externalId
* @param id ActiveWorktypeの内部ID
* @returns active worktype
*/
async updateActiveWorktype(
context: Context,
externalId: string,
id: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateActiveWorktype.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// ActiveWorktypeを更新する
await this.accountRepository.updateActiveWorktypeId(accountId, id);
} 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.updateActiveWorktype.name}`,
);
}
}
/**
*
* @param context
@ -1553,4 +1616,72 @@ export class AccountsService {
this.logger.log(`[OUT] [${context.trackingId}] ${this.getPartners.name}`);
}
}
/**
*
* @param context
* @param externalId
* @param tier
* @param delegationPermission
* @param primaryAdminUserId
* @param parentAccountId
* @param secondryAdminUserId
* @returns UpdateAccountInfoResponse
*/
async updateAccountInfo(
context: Context,
externalId: string,
tier: number,
delegationPermission: boolean,
primaryAdminUserId: number,
parentAccountId?: number,
secondryAdminUserId?: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateAccountInfo.name} | params: { ` +
`externalId: ${externalId}, ` +
`delegationPermission: ${delegationPermission}, ` +
`primaryAdminUserId: ${primaryAdminUserId}, ` +
`parentAccountId: ${parentAccountId}, ` +
`secondryAdminUserId: ${secondryAdminUserId}, };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
await this.accountRepository.updateAccountInfo(
accountId,
tier,
delegationPermission,
primaryAdminUserId,
parentAccountId,
secondryAdminUserId,
);
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case DealerAccountNotFoundError:
case AdminUserNotFoundError:
throw new HttpException(
makeErrorResponse('E010502'),
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.updateAccountInfo.name}`,
);
}
}
}

View File

@ -124,6 +124,9 @@ export class Account {
@ApiProperty({ required: false })
secondryAdminUserId?: number | undefined;
@ApiProperty({ required: false })
parentAccountName?: string | undefined;
}
export class GetMyAccountResponse {
@ -547,12 +550,18 @@ export class UpdateAccountInfoRequest {
parentAccountId?: number | undefined;
@ApiProperty({ description: '代行操作許可' })
delegationPermission: boolean;
@ApiProperty({ description: 'プライマリ管理者ID', required: false })
@IsOptional()
primaryAdminUserId?: number | undefined;
@ApiProperty({ description: 'プライマリ管理者ID' })
primaryAdminUserId: number;
@ApiProperty({ description: 'セカンダリ管理者ID', required: false })
@IsOptional()
secondryAdminUserId?: number | undefined;
}
export class UpdateAccountInfoResponse {}
export class DeleteAccountRequest {
@ApiProperty({ description: 'アカウントID' })
accountId: number;
}
export class DeleteAccountResponse {}

View File

@ -27,10 +27,13 @@ import {
AudioUploadLocationResponse,
TemplateDownloadLocationRequest,
TemplateDownloadLocationResponse,
TemplateUploadFinishedReqponse,
TemplateUploadFinishedRequest,
TemplateUploadLocationResponse,
} from './types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { USER_ROLES } from '../../constants';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
@ -258,4 +261,84 @@ export class FilesController {
return { url };
}
@Get('template/upload-location')
@ApiResponse({
status: HttpStatus.OK,
type: TemplateUploadLocationResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'uploadTemplateLocation',
description:
'ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async uploadTemplateLocation(
@Req() req: Request,
): Promise<TemplateUploadLocationResponse> {
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(accessToken.userId);
console.log(context.trackingId);
return { url: '' };
}
@ApiResponse({
status: HttpStatus.OK,
type: TemplateUploadFinishedReqponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '不正なパラメータ',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'uploadTemplateFinished',
description: 'アップロードが完了したテンプレートファイルの情報を登録します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Post('template/upload-finished')
async templateUploadFinished(
@Req() req: Request,
@Body() body: TemplateUploadFinishedRequest,
): Promise<TemplateUploadFinishedReqponse> {
const { templateFile } = body;
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(accessToken.userId);
console.log(context.trackingId);
console.log(templateFile);
return {};
}
}

View File

@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { TemplateFile } from '../../templates/types/types';
export class AudioUploadLocationRequest {}
@ -31,6 +32,11 @@ export class TemplateDownloadLocationResponse {
url: string;
}
export class TemplateUploadLocationResponse {
@ApiProperty()
url: string;
}
export class AudioOptionItem {
@ApiProperty({ minLength: 1, maxLength: 16 })
optionItemLabel: string;
@ -87,3 +93,10 @@ export class AudioUploadFinishedResponse {
@ApiProperty({ description: '8桁固定の数字' })
jobNumber: string;
}
export class TemplateUploadFinishedRequest {
@ApiProperty({ description: 'テンプレートファイルのファイル情報' })
templateFile: TemplateFile;
}
export class TemplateUploadFinishedReqponse {}

View File

@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { TemplatesController } from './templates.controller';
import { TemplatesService } from './templates.service';
describe('TemplatesController', () => {
let controller: TemplatesController;
const mockTemplatesService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'],
isGlobal: true,
}),
],
controllers: [TemplatesController],
providers: [TemplatesService],
})
.overrideProvider(TemplatesService)
.useValue(mockTemplatesService)
.compile();
controller = module.get<TemplatesController>(TemplatesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,57 @@
import { Controller, Get, HttpStatus, Req, UseGuards } from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import jwt from 'jsonwebtoken';
import { AccessToken } from '../../common/token';
import { ErrorResponse } from '../../common/error/types/types';
import { GetTemplatesResponse } from './types/types';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES } from '../../constants';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { TemplatesService } from './templates.service';
@ApiTags('templates')
@Controller('templates')
export class TemplatesController {
constructor(private readonly templatesService: TemplatesService) {}
@ApiResponse({
status: HttpStatus.OK,
type: GetTemplatesResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({
operationId: 'getTemplates',
description: 'アカウント内のテンプレートファイルの一覧を取得します',
})
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getTemplates(@Req() req: Request): Promise<GetTemplatesResponse> {
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(accessToken.userId);
console.log(context.trackingId);
return { templates: [] };
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TemplatesController } from './templates.controller';
import { TemplatesService } from './templates.service';
@Module({
imports: [],
providers: [TemplatesService],
controllers: [TemplatesController],
})
export class TemplatesModule {}

View File

@ -0,0 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class TemplatesService {
private readonly logger = new Logger(TemplatesService.name);
constructor() {}
}

View File

@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
export class TemplateFile {
@ApiProperty({ description: 'テンプレートファイルのファイル名' })
name: string;
@ApiProperty({ description: 'テンプレートファイルのURL' })
url: string;
}
export class GetTemplatesResponse {
@ApiProperty({
description: 'テンプレートファイルの一覧',
type: [TemplateFile],
})
templates: TemplateFile[];
}

View File

@ -29,13 +29,19 @@ import {
PartnerLicenseInfoForRepository,
PartnerInfoFromDb,
} from '../../features/accounts/types/types';
import { AccountNotFoundError } from './errors/types';
import {
AccountNotFoundError,
AdminUserNotFoundError,
DealerAccountNotFoundError,
} from './errors/types';
import {
AlreadyLicenseAllocatedError,
AlreadyLicenseStatusChangedError,
CancellationPeriodExpiredError,
} from '../licenses/errors/types';
import { DateWithZeroTime } from '../../features/licenses/types/types';
import { Worktype } from '../worktypes/entity/worktype.entity';
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
@Injectable()
export class AccountsRepositoryService {
@ -765,4 +771,135 @@ export class AccountsRepositoryService {
};
});
}
/**
*
* @param accountId
* @param tier
* @returns account: 一階層上のアカウント
*/
async getOneUpperTierAccount(
accountId: number,
tier: number,
): Promise<Account | undefined> {
return await this.dataSource.transaction(async (entityManager) => {
const accountRepo = entityManager.getRepository(Account);
return await accountRepo.findOne({
where: {
id: accountId,
tier: tier - 1,
},
});
});
}
/**
*
* @param myAccountId
* @param tier
* @param delegationPermission
* @param primaryAdminUserId
* @param parentAccountId
* @param secondryAdminUserId
*/
async updateAccountInfo(
myAccountId: number,
tier: number,
delegationPermission: boolean,
primaryAdminUserId: number,
parentAccountId?: number,
secondryAdminUserId?: number,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
// ディーラーアカウントが指定されている場合、存在チェックを行う
if (parentAccountId) {
const dealerAccount = await this.getOneUpperTierAccount(
parentAccountId,
tier,
);
// 取得できない場合、エラー
if (!dealerAccount) {
throw new DealerAccountNotFoundError(
`Dealer account is not found. id: ${parentAccountId}}`,
);
}
}
const userRepo = entityManager.getRepository(User);
// プライマリ管理者ユーザーの存在チェック
if (primaryAdminUserId) {
const primaryAdminUser = await userRepo.findOne({
where: {
id: primaryAdminUserId,
account_id: myAccountId,
},
});
if (!primaryAdminUser) {
throw new AdminUserNotFoundError(
`Primary admin user is not found. id: ${primaryAdminUserId}, account_id: ${myAccountId}`,
);
}
}
// セカンダリ管理者ユーザーの存在チェック
if (secondryAdminUserId) {
const secondryAdminUser = await userRepo.findOne({
where: {
id: secondryAdminUserId,
account_id: myAccountId,
},
});
if (!secondryAdminUser) {
throw new AdminUserNotFoundError(
`Secondry admin user is not found. id: ${secondryAdminUserId}, account_id: ${myAccountId}`,
);
}
}
const accountRepo = entityManager.getRepository(Account);
// アカウント情報を更新
await accountRepo.update(
{ id: myAccountId },
{
parent_account_id: parentAccountId || null,
delegation_permission: delegationPermission,
primary_admin_user_id: primaryAdminUserId,
secondary_admin_user_id: secondryAdminUserId || null,
},
);
});
}
/*
* ActiveWorktypeIdを更新する
* @param accountId
* @param [id] ActiveWorktypeIdの内部ID
* @returns active worktype id
*/
async updateActiveWorktypeId(
accountId: number,
id?: number | undefined,
): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => {
const worktypeRepo = entityManager.getRepository(Worktype);
const accountRepo = entityManager.getRepository(Account);
if (id) {
// 自アカウント内に指定IDのワークタイプが存在するか確認
const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: id },
});
// ワークタイプが存在しない場合はエラー
if (!worktype) {
throw new WorktypeIdNotFoundError('Worktype is not found. id: ${id}');
}
}
// アカウントのActiveWorktypeIDを更新
await accountRepo.update(
{ id: accountId },
{ active_worktype_id: id ?? null },
);
});
}
}

View File

@ -1,2 +1,6 @@
// アカウント未発見エラー
export class AccountNotFoundError extends Error {}
// ディーラーアカウント未存在エラー
export class DealerAccountNotFoundError extends Error {}
// 管理者ユーザ未存在エラー
export class AdminUserNotFoundError extends Error {}