Adding
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
describe("Fake Test", () => {
|
||||
it("Does Not Work", () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,293 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { BrowserRouter, Route, Switch } from "react-router-dom";
|
||||
import NotFound404Page from "./components/status-pages/NotFound404Page";
|
||||
import TestPage from "./components/status-pages/TestPage";
|
||||
import Configuration from "./shared/configuration/Configuration";
|
||||
import AppContainer from "./components/layout/AppContainer";
|
||||
import ActivityPage from "./containers/activity-page/ActivityPage";
|
||||
import ActivityDetailPage from "./containers/activity-detail-page/ActivityDetailPage";
|
||||
import DepartmentGoalsPage from "./containers/goals/department/DepartmentGoalsPage";
|
||||
import PersonnelGoalsPage from "./containers/goals/personnel/PersonnelGoalsPage";
|
||||
import OrganizationView from "./containers/organization-view/OrganizationView";
|
||||
import { useDispatch } from "react-redux";
|
||||
import RoleManagement from "./containers/role-management/RoleMangement";
|
||||
import PayoutProfilePage from "./containers/payout-profile/PayoutProfilePage";
|
||||
import CommissionsPage from "./containers/commissions-page/CommissionsPage";
|
||||
import ProfileSearchPage from "./containers/profile/ProfileSearchPage";
|
||||
import ProfileSelector from "./containers/profile/ProfileSelector";
|
||||
import { refreshProfile } from "./actions/ProfileActions";
|
||||
import ExportPayoutPage from "./containers/payouts/ExportPayoutPage";
|
||||
import DashboardPage from "./containers/dashboard/DashboardPage";
|
||||
import ActivityFormPage from "./containers/activity-form-page/ActivityFormPage";
|
||||
import { SettingsProvider } from "./shared/providers/SettingsProvider";
|
||||
import DepartmentDetailPage from "./containers/department-detail-page/DepartmentDetailPage";
|
||||
import PermissionGuard from "./containers/shared/PermissionGuard";
|
||||
import EndOfEmploymentPage from "./containers/eoe-list/EndOfEmploymentPage";
|
||||
import DeadActivityPage from "./containers/dead-activity-page/DeadActivityPage";
|
||||
import RoleMappingPage from "./containers/role-mapping-page/RoleMappingPage";
|
||||
import ReportPage from "./containers/report-page/ReportPage";
|
||||
|
||||
const App: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(refreshProfile());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
//Normally we use Connected Router which requires has. React-Dom router does not require a hash.
|
||||
<AppContainer>
|
||||
<SettingsProvider>
|
||||
<BrowserRouter basename={Configuration.Get.appUrlBaseName}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.root}
|
||||
render={() => <DashboardPage />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.dashboard}
|
||||
render={() => <DashboardPage />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.activityForm}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"CREATE_ACTIVITY"
|
||||
]}
|
||||
>
|
||||
<ActivityFormPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.activity}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"VIEW_ACTIVITY_AUDIT"
|
||||
]}
|
||||
>
|
||||
<ActivityPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${Configuration.Get.appUrls.activity}/:id`}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"VIEW_ACTIVITY_AUDIT"
|
||||
]}
|
||||
>
|
||||
<ActivityDetailPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.deadActivity}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"VIEW_DEAD_ACTIVITY_QUEUE"
|
||||
]}
|
||||
>
|
||||
<DeadActivityPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${Configuration.Get.appUrls.department}`}
|
||||
render={() => <DepartmentDetailPage />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.exportPayout}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"GENERATE_PAYOUT_FILES"
|
||||
]}
|
||||
>
|
||||
<ExportPayoutPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.eoeList}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"GENERATE_PAYOUT_FILES"
|
||||
]}
|
||||
>
|
||||
<EndOfEmploymentPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.departmentGoals}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"MANAGE_DEPARTMENT_GOALS"
|
||||
]}
|
||||
>
|
||||
<DepartmentGoalsPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.personnelGoals}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"MANAGE_PERSONNEL_GOALS"
|
||||
]}
|
||||
>
|
||||
<PersonnelGoalsPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.commissions}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"MANAGE_DEPARTMENT_GOALS"
|
||||
]}
|
||||
>
|
||||
<CommissionsPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.profile}
|
||||
render={() => <ProfileSelector />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.payoutProfiles}
|
||||
render={() => <PayoutProfilePage />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.profileSearch}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"MANAGE_DEPARTMENT_GOALS",
|
||||
"MANAGE_PERSONNEL_GOALS",
|
||||
"ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
]}
|
||||
>
|
||||
<ProfileSearchPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.testRoute}
|
||||
render={(routerProps) => (
|
||||
<TestPage {...routerProps} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.organization}
|
||||
render={(routerProps) => (
|
||||
<OrganizationView {...routerProps} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.roles}
|
||||
render={(routerProps) => (
|
||||
<RoleManagement {...routerProps} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.roleMappings}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={[
|
||||
"MANAGE_ROLE_MAPPINGS"
|
||||
]}
|
||||
>
|
||||
<RoleMappingPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={Configuration.Get.appUrls.reports}
|
||||
render={() => {
|
||||
return (
|
||||
<PermissionGuard
|
||||
showWarning
|
||||
requiredPermissions={["VIEW_REPORTS"]}
|
||||
>
|
||||
<ReportPage />
|
||||
</PermissionGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
render={(routerProps) => (
|
||||
<NotFound404Page {...routerProps} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</SettingsProvider>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,172 @@
|
||||
import { ActionCreator, Dispatch } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import {
|
||||
IActivitySearchForm,
|
||||
IAuditableActivity,
|
||||
IDepartmentProgress,
|
||||
IEmployeeProgress
|
||||
} from "../shared/models/Activity";
|
||||
import ActivityService from "./../shared/services/ActivityService";
|
||||
import { IAppState } from "../store/Store";
|
||||
import IPagedResponse from "./../shared/types/IPagedResponse";
|
||||
import { IMonthRange } from "../shared/types/IMonthRange";
|
||||
|
||||
export enum ActivityActionTypes {
|
||||
GET_ACTIVITIES = "ACTIVITY_GET_ACTIVITIES",
|
||||
GET_NEXT_ACTIVITY_PAGE = "GET_NEXT_ACTIVITY_PAGE",
|
||||
GET_LAST_ACTIVITY_PAGE = "GET_LAST_ACTIVITY_PAGE",
|
||||
GET_MY_ACTIVITIES = "ACTIVITY_GET_MY_ACTIVITIES",
|
||||
GET_MY_DEPARTMENT_ACTIVITIES = "ACTIVITY_GET_MY_DEPARTMENT_ACTIVITIES",
|
||||
SET_ACTIVITY_STATES_FILTER = "SET_ACTIVITY_STATES_FILTER"
|
||||
}
|
||||
|
||||
export interface IActivityGetAllAction {
|
||||
type: ActivityActionTypes.GET_ACTIVITIES;
|
||||
activities: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
|
||||
}
|
||||
export interface IActivityGetNextPageAction {
|
||||
type: ActivityActionTypes.GET_NEXT_ACTIVITY_PAGE;
|
||||
activities: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
|
||||
}
|
||||
|
||||
export interface IActivityGetLastPageAction {
|
||||
type: ActivityActionTypes.GET_LAST_ACTIVITY_PAGE;
|
||||
activities: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
|
||||
}
|
||||
|
||||
export interface IActivitySetStateFilter {
|
||||
type: ActivityActionTypes.SET_ACTIVITY_STATES_FILTER;
|
||||
allowedStates: string[];
|
||||
}
|
||||
|
||||
export interface IActivityGetMyAction {
|
||||
type: ActivityActionTypes.GET_MY_ACTIVITIES;
|
||||
progress: IAsyncStateContainer<IEmployeeProgress>;
|
||||
}
|
||||
|
||||
export interface IActivityGetMyDepartmentAction {
|
||||
type: ActivityActionTypes.GET_MY_DEPARTMENT_ACTIVITIES;
|
||||
departmentProgress: IAsyncStateContainer<IDepartmentProgress[]>;
|
||||
}
|
||||
|
||||
export type ActivityActions =
|
||||
| IActivityGetAllAction
|
||||
| IActivityGetMyAction
|
||||
| IActivityGetMyDepartmentAction
|
||||
| IActivityGetNextPageAction
|
||||
| IActivityGetLastPageAction
|
||||
| IActivitySetStateFilter;
|
||||
|
||||
export const getAllActivities: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IActivityGetAllAction>
|
||||
> = (page: number, limit: number, filters: IActivitySearchForm) => {
|
||||
return async (dispatch: Dispatch<ActivityActions>) => {
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_ACTIVITIES,
|
||||
activities: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newActivities = await ActivityService.getPaginatedActivities(
|
||||
page,
|
||||
limit,
|
||||
filters
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_ACTIVITIES,
|
||||
activities: newActivities
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getNextPageOfActivities: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IActivityGetNextPageAction>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<ActivityActions>, getState) => {
|
||||
const activityPageResponse = getState().activityState
|
||||
.filteredActivities;
|
||||
|
||||
if (activityPageResponse?.data?.nextPage) {
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_NEXT_ACTIVITY_PAGE,
|
||||
activities: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newActivities = await ActivityService.getPaginatedActivitiesFromUrl(
|
||||
activityPageResponse.data.nextPage
|
||||
// you don't need filters...
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_NEXT_ACTIVITY_PAGE,
|
||||
activities: newActivities
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getLastPageOfActivities: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IActivityGetLastPageAction>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<ActivityActions>, getState) => {
|
||||
const activityPageResponse = getState().activityState
|
||||
.filteredActivities;
|
||||
|
||||
if (activityPageResponse?.data?.previousPage) {
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_LAST_ACTIVITY_PAGE,
|
||||
activities: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newActivities = await ActivityService.getPaginatedActivitiesFromUrl(
|
||||
activityPageResponse.data.previousPage
|
||||
// you don't need the filters, they're in the page url...
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_LAST_ACTIVITY_PAGE,
|
||||
activities: newActivities
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getMyProgress: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IActivityGetMyAction>
|
||||
> = (monthRange: IMonthRange) => {
|
||||
return async (dispatch: Dispatch<IActivityGetMyAction>) => {
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_MY_ACTIVITIES,
|
||||
progress: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newActivities = await ActivityService.getMyProgress(monthRange);
|
||||
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_MY_ACTIVITIES,
|
||||
progress: newActivities
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getMyDepartmentProgress: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IActivityGetMyDepartmentAction>
|
||||
> = (monthRange: IMonthRange) => {
|
||||
return async (dispatch: Dispatch<IActivityGetMyDepartmentAction>) => {
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_MY_DEPARTMENT_ACTIVITIES,
|
||||
departmentProgress: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const departmentActivities = await ActivityService.getMyDepartmentProgress(
|
||||
monthRange
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: ActivityActionTypes.GET_MY_DEPARTMENT_ACTIVITIES,
|
||||
departmentProgress: departmentActivities
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Dispatch } from "react";
|
||||
import { ActionCreator } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import { IAuditableCommission } from "../shared/models/Commission";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import { IAppState } from "../store/Store";
|
||||
import CommissionService from "../shared/services/CommissionService";
|
||||
|
||||
export enum CommissionActionTypes {
|
||||
GET_ALL_COMMISSIONS = "GET_ALL_COMMISSIONS",
|
||||
SET_COMMISSIONS = "SET_COMMISSIONS"
|
||||
}
|
||||
|
||||
export interface ICommissionsGetAllAction {
|
||||
type: CommissionActionTypes.GET_ALL_COMMISSIONS;
|
||||
commissions: IAsyncStateContainer<IAuditableCommission[]>;
|
||||
}
|
||||
|
||||
export interface ICommissionsSetAction {
|
||||
type: CommissionActionTypes.SET_COMMISSIONS;
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
}
|
||||
|
||||
export type CommissionActions = ICommissionsGetAllAction;
|
||||
|
||||
export const getAllCommissions: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, ICommissionsGetAllAction>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<ICommissionsGetAllAction>) => {
|
||||
dispatch({
|
||||
type: CommissionActionTypes.GET_ALL_COMMISSIONS,
|
||||
commissions: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const allCommissions = await CommissionService.getAllCommissions();
|
||||
|
||||
dispatch({
|
||||
type: CommissionActionTypes.GET_ALL_COMMISSIONS,
|
||||
commissions: allCommissions
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Dispatch } from "react";
|
||||
import { ActionCreator } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import {
|
||||
IAuditableDeadActivity,
|
||||
IDeadActivitySearchForm
|
||||
} from "../shared/models/DeadActivity";
|
||||
import DeadActivityService from "../shared/services/DeadActivityService";
|
||||
import DeadDeadActivityService from "../shared/services/DeadActivityService";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import IPagedResponse from "../shared/types/IPagedResponse";
|
||||
import { IAppState } from "../store/Store";
|
||||
|
||||
export enum DeadActivityActionTypes {
|
||||
GET_DEAD_ACTIVITIES = "DEAD_ACTIVITY_GET_DEAD_ACTIVITIES",
|
||||
GET_NEXT_DEAD_ACTIVITY_PAGE = "GET_NEXT_DEAD_ACTIVITY_PAGE",
|
||||
GET_LAST_DEAD_ACTIVITY_PAGE = "GET_LAST_DEAD_ACTIVITY_PAGE",
|
||||
SET_DEAD_ACTIVTY_STATE_FILTER = "SET_DEAD_ACTIVITY_STATE_FILTER"
|
||||
}
|
||||
|
||||
export interface IDeadActivityGetAllAction {
|
||||
type: DeadActivityActionTypes.GET_DEAD_ACTIVITIES;
|
||||
deadActivities: IAsyncStateContainer<
|
||||
IPagedResponse<IAuditableDeadActivity>
|
||||
>;
|
||||
}
|
||||
export interface IDeadActivityGetNextPageAction {
|
||||
type: DeadActivityActionTypes.GET_NEXT_DEAD_ACTIVITY_PAGE;
|
||||
deadActivities: IAsyncStateContainer<
|
||||
IPagedResponse<IAuditableDeadActivity>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface IDeadActivityGetLastPageAction {
|
||||
type: DeadActivityActionTypes.GET_LAST_DEAD_ACTIVITY_PAGE;
|
||||
deadActivities: IAsyncStateContainer<
|
||||
IPagedResponse<IAuditableDeadActivity>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface IDeadActivityStateFilter {
|
||||
type: DeadActivityActionTypes.SET_DEAD_ACTIVTY_STATE_FILTER;
|
||||
allowedStates: string[];
|
||||
}
|
||||
|
||||
export type DeadActivityActions =
|
||||
| IDeadActivityGetAllAction
|
||||
| IDeadActivityGetNextPageAction
|
||||
| IDeadActivityGetLastPageAction
|
||||
| IDeadActivityStateFilter;
|
||||
|
||||
export const getAllDeadActivities: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IDeadActivityGetAllAction>
|
||||
> = (page: number, limit: number, filters: IDeadActivitySearchForm) => {
|
||||
return async (dispatch: Dispatch<DeadActivityActions>) => {
|
||||
dispatch({
|
||||
type: DeadActivityActionTypes.GET_DEAD_ACTIVITIES,
|
||||
deadActivities: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newDeadActivities = await DeadDeadActivityService.getPaginatedDeadActivities(
|
||||
page,
|
||||
limit,
|
||||
filters
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: DeadActivityActionTypes.GET_DEAD_ACTIVITIES,
|
||||
deadActivities: newDeadActivities
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getNextPageOfDeadActivities: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IDeadActivityGetNextPageAction>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<DeadActivityActions>, getState) => {
|
||||
const deadActivityPageResponse = getState().deadActivityState
|
||||
.filteredDeadActivities;
|
||||
|
||||
if (deadActivityPageResponse?.data?.nextPage) {
|
||||
dispatch({
|
||||
type: DeadActivityActionTypes.GET_NEXT_DEAD_ACTIVITY_PAGE,
|
||||
deadActivities: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newDeadActivities = await DeadActivityService.getPaginatedDeadActivitiesFromUrl(
|
||||
deadActivityPageResponse.data.nextPage
|
||||
// you don't need filters...
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: DeadActivityActionTypes.GET_NEXT_DEAD_ACTIVITY_PAGE,
|
||||
deadActivities: newDeadActivities
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getLastPageOfDeadActivities: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IDeadActivityGetLastPageAction>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<DeadActivityActions>, getState) => {
|
||||
const deadActivityPageResponse = getState().deadActivityState
|
||||
.filteredDeadActivities;
|
||||
|
||||
if (deadActivityPageResponse?.data?.previousPage) {
|
||||
dispatch({
|
||||
type: DeadActivityActionTypes.GET_LAST_DEAD_ACTIVITY_PAGE,
|
||||
deadActivities: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newDeadActivities = await DeadActivityService.getPaginatedDeadActivitiesFromUrl(
|
||||
deadActivityPageResponse.data.previousPage
|
||||
// you don't need the filters, they're in the page url...
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: DeadActivityActionTypes.GET_LAST_DEAD_ACTIVITY_PAGE,
|
||||
deadActivities: newDeadActivities
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import { IDepartment, IDepartmentDto } from "../shared/models/Department";
|
||||
import { ActionCreator, Dispatch } from "redux";
|
||||
import { IAppState } from "../store/Store";
|
||||
import DepartmentService from "./../shared/services/DepartmentService";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
export enum DepartmentActionTypes {
|
||||
GET_ALL_DEPARTMENTS = "GET_ALL_DEPARTMENTS",
|
||||
ADD_NEW_DEPARTMENT = "ADD_NEW_DEPARTNEMTN",
|
||||
UPDATE_EXISTING_DEPARTNEMTN = "UPDATE_EXISTING_DEPARTMENT"
|
||||
}
|
||||
|
||||
export interface IDepartmentGetAllAction {
|
||||
type: DepartmentActionTypes.GET_ALL_DEPARTMENTS;
|
||||
departments: IAsyncStateContainer<IDepartment[]>;
|
||||
}
|
||||
|
||||
export interface IDepartmentsAddNew {
|
||||
type: DepartmentActionTypes.ADD_NEW_DEPARTMENT;
|
||||
newDepartment: IAsyncStateContainer<IDepartment>;
|
||||
}
|
||||
|
||||
export interface IDepartmentUpdateExisting {
|
||||
type: DepartmentActionTypes.UPDATE_EXISTING_DEPARTNEMTN;
|
||||
updatedDepartment: IAsyncStateContainer<IDepartment>;
|
||||
}
|
||||
|
||||
export type DepartmentActions =
|
||||
| IDepartmentGetAllAction
|
||||
| IDepartmentsAddNew
|
||||
| IDepartmentUpdateExisting;
|
||||
|
||||
export const getAllDepartments: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IDepartmentGetAllAction>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<IDepartmentGetAllAction>) => {
|
||||
dispatch({
|
||||
type: DepartmentActionTypes.GET_ALL_DEPARTMENTS,
|
||||
departments: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const newDepartments = await DepartmentService.getAllDepartments();
|
||||
|
||||
dispatch({
|
||||
type: DepartmentActionTypes.GET_ALL_DEPARTMENTS,
|
||||
departments: newDepartments
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const submitNewDepartment: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IDepartmentsAddNew>
|
||||
> = (newDepartment: IDepartmentDto) => {
|
||||
return async (dispatch: Dispatch<IDepartmentsAddNew>) => {
|
||||
dispatch({
|
||||
type: DepartmentActionTypes.ADD_NEW_DEPARTMENT,
|
||||
newDepartment: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const submittedDepartment = await DepartmentService.submitNewDepartment(
|
||||
newDepartment
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: DepartmentActionTypes.ADD_NEW_DEPARTMENT,
|
||||
newDepartment: submittedDepartment
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const updateExistingDepartment: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IDepartmentGetAllAction>
|
||||
> = (id: number, newDepartment: IDepartmentDto) => {
|
||||
return async (dispatch: Dispatch<IDepartmentUpdateExisting>) => {
|
||||
dispatch({
|
||||
type: DepartmentActionTypes.UPDATE_EXISTING_DEPARTNEMTN,
|
||||
updatedDepartment: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const submittedDepartment = await DepartmentService.updateExistingDepartment(
|
||||
id,
|
||||
newDepartment
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: DepartmentActionTypes.UPDATE_EXISTING_DEPARTNEMTN,
|
||||
updatedDepartment: submittedDepartment
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import { IEmployee } from "../shared/models/Employee";
|
||||
import { ActionCreator, Dispatch } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import { IAppState } from "../store/Store";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import EmployeeService from "../shared/services/EmployeeService";
|
||||
import {
|
||||
IDepartmentAssignmentDto,
|
||||
IDepartmentAssignmentUpdateDto
|
||||
} from "../shared/models/DepartmentAssignment";
|
||||
|
||||
export enum EmployeeActionTypes {
|
||||
SELECT_EMPLOYEE = "SELECT_EMPLOYEE",
|
||||
SUBMIT_DEPT_ASSIGNMENT = "SUBMIT_DEPARTMENT_ASSIGNMENT",
|
||||
SUBMIT_END_DEPARTMENT_ASSIGNMENT = "SUBMIT_END_DEPARTMENT_ASSIGNMENT"
|
||||
}
|
||||
|
||||
export interface IEmployeeSelectAction {
|
||||
type: EmployeeActionTypes.SELECT_EMPLOYEE;
|
||||
selectedEmployee: IAsyncStateContainer<IEmployee>;
|
||||
}
|
||||
|
||||
export interface IEmployeeSubmitDeptAssignmentAction {
|
||||
type: EmployeeActionTypes.SUBMIT_DEPT_ASSIGNMENT;
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
}
|
||||
|
||||
export interface IEmployeeSubmitEndDeptAssignmentAction {
|
||||
type: EmployeeActionTypes.SUBMIT_END_DEPARTMENT_ASSIGNMENT;
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
}
|
||||
|
||||
export type EmployeeActions =
|
||||
| IEmployeeSelectAction
|
||||
| IEmployeeSubmitDeptAssignmentAction
|
||||
| IEmployeeSubmitEndDeptAssignmentAction;
|
||||
|
||||
export const trySelectEmployee: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IEmployeeSelectAction>
|
||||
> = (employeeId: string) => {
|
||||
return async (dispatch: Dispatch<IEmployeeSelectAction>) => {
|
||||
dispatch({
|
||||
type: EmployeeActionTypes.SELECT_EMPLOYEE,
|
||||
selectedEmployee: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retreivedEmployee = await EmployeeService.getByEmployeeId(
|
||||
employeeId
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: EmployeeActionTypes.SELECT_EMPLOYEE,
|
||||
selectedEmployee: retreivedEmployee
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const submitDepartmentAssignment: ActionCreator<
|
||||
ThunkAction<
|
||||
Promise<any>,
|
||||
IAppState,
|
||||
null,
|
||||
IEmployeeSubmitDeptAssignmentAction
|
||||
>
|
||||
> = (
|
||||
employeeId: string,
|
||||
deptAssignmentData: Partial<IDepartmentAssignmentDto>
|
||||
) => {
|
||||
return async (dispatch: Dispatch<IEmployeeSubmitDeptAssignmentAction>) => {
|
||||
dispatch({
|
||||
type: EmployeeActionTypes.SUBMIT_DEPT_ASSIGNMENT,
|
||||
submitState: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const postState = await EmployeeService.assignNewDepartment(
|
||||
deptAssignmentData,
|
||||
employeeId
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: EmployeeActionTypes.SUBMIT_DEPT_ASSIGNMENT,
|
||||
submitState: postState
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const submitDepartmentAssignmentUpdate: ActionCreator<
|
||||
ThunkAction<
|
||||
Promise<any>,
|
||||
IAppState,
|
||||
null,
|
||||
IEmployeeSubmitEndDeptAssignmentAction
|
||||
>
|
||||
> = (assignment: IDepartmentAssignmentUpdateDto) => {
|
||||
return async (
|
||||
dispatch: Dispatch<IEmployeeSubmitEndDeptAssignmentAction>
|
||||
) => {
|
||||
dispatch({
|
||||
type: EmployeeActionTypes.SUBMIT_END_DEPARTMENT_ASSIGNMENT,
|
||||
submitState: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const postState = await EmployeeService.updateDepartmentAssignment(
|
||||
assignment
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: EmployeeActionTypes.SUBMIT_END_DEPARTMENT_ASSIGNMENT,
|
||||
submitState: postState
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { ActionCreator, Dispatch } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import { IMonthRange } from "../shared/types/IMonthRange";
|
||||
import { IGoalState } from "../reducers/GoalReducer";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IDepartmentGoalsForRange,
|
||||
IDepartmentGoalsInput,
|
||||
IEmployeeGoals,
|
||||
IGoal,
|
||||
IPersonnelGoals,
|
||||
IPersonnelGoalsInput
|
||||
} from "../shared/models/Goal";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import GoalService from "./../shared/services/GoalService";
|
||||
|
||||
export enum GoalActionTypes {
|
||||
GET_ALL_GOALS = "GET_ALL_GOALS",
|
||||
GET_DEPT_GOALS = "GET_DEPT_GOALS",
|
||||
GET_PERS_GOALS = "GET_PERS_GOALS",
|
||||
GET_MY_GOALS = "GET_MY_GOALS",
|
||||
GET_MY_DEPTS_GOALS = "GET_MY_DEPTS_GOALS"
|
||||
}
|
||||
|
||||
export interface IGoalGetAllAction {
|
||||
type: GoalActionTypes.GET_ALL_GOALS;
|
||||
retrievedGoals: IAsyncStateContainer<IGoal[]>;
|
||||
}
|
||||
|
||||
export interface IGoalGetMyAction {
|
||||
type: GoalActionTypes.GET_MY_GOALS;
|
||||
currentUserGoals: IAsyncStateContainer<IEmployeeGoals>;
|
||||
}
|
||||
|
||||
export interface IGoalGetMyDeptsAction {
|
||||
type: GoalActionTypes.GET_MY_DEPTS_GOALS;
|
||||
currentUsersDepartmentsGoals: IAsyncStateContainer<
|
||||
IDepartmentGoalsForRange[]
|
||||
>;
|
||||
}
|
||||
|
||||
export interface IGoalGetDeptAction {
|
||||
type: GoalActionTypes.GET_DEPT_GOALS;
|
||||
retrievedGoals: IAsyncStateContainer<IDepartmentGoals>;
|
||||
}
|
||||
|
||||
export interface IGoalGetPersAction {
|
||||
type: GoalActionTypes.GET_PERS_GOALS;
|
||||
retrievedGoals: IAsyncStateContainer<IPersonnelGoals>;
|
||||
}
|
||||
|
||||
export type GoalActions =
|
||||
| IGoalGetMyAction
|
||||
| IGoalGetMyDeptsAction
|
||||
| IGoalGetAllAction
|
||||
| IGoalGetDeptAction
|
||||
| IGoalGetPersAction;
|
||||
|
||||
export const queryGoals: ActionCreator<
|
||||
ThunkAction<Promise<any>, IGoalState, null, IGoalGetAllAction>
|
||||
> = (range: IMonthRange, includePersonnel: boolean, departmentId?: number) => {
|
||||
return async (dispatch: Dispatch<IGoalGetAllAction>) => {
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_ALL_GOALS,
|
||||
retrievedGoals: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedGoals = await GoalService.getAllGoals(
|
||||
range,
|
||||
includePersonnel,
|
||||
departmentId
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_ALL_GOALS,
|
||||
retrievedGoals: retrievedGoals
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getMyGoals: ActionCreator<
|
||||
ThunkAction<Promise<any>, IGoalState, null, IGoalGetMyAction>
|
||||
> = (monthRange: IMonthRange) => {
|
||||
return async (dispatch: Dispatch<IGoalGetMyAction>) => {
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_MY_GOALS,
|
||||
currentUserGoals: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedGoals = await GoalService.getMyGoals(monthRange);
|
||||
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_MY_GOALS,
|
||||
currentUserGoals: retrievedGoals
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getMyDepartmentsGoals: ActionCreator<
|
||||
ThunkAction<Promise<any>, IGoalState, null, IGoalGetMyDeptsAction>
|
||||
> = (monthRange: IMonthRange) => {
|
||||
return async (dispatch: Dispatch<IGoalGetMyDeptsAction>) => {
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_MY_DEPTS_GOALS,
|
||||
currentUsersDepartmentsGoals: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedGoals = await GoalService.getMyDepartmentsGoals(
|
||||
monthRange
|
||||
);
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_MY_DEPTS_GOALS,
|
||||
currentUsersDepartmentsGoals: retrievedGoals
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getDepartmentGoals: ActionCreator<
|
||||
ThunkAction<Promise<any>, IGoalState, null, IGoalGetDeptAction>
|
||||
> = (input: IDepartmentGoalsInput) => {
|
||||
return async (dispatch: Dispatch<IGoalGetDeptAction>) => {
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_DEPT_GOALS,
|
||||
retrievedGoals: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedGoals = await GoalService.getDepartmentGoals(input);
|
||||
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_DEPT_GOALS,
|
||||
retrievedGoals: retrievedGoals
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getPersonnelGoals: ActionCreator<
|
||||
ThunkAction<Promise<any>, IGoalState, null, IGoalGetPersAction>
|
||||
> = (input: IPersonnelGoalsInput) => {
|
||||
return async (dispatch: Dispatch<IGoalGetPersAction>) => {
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_PERS_GOALS,
|
||||
retrievedGoals: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedGoals = await GoalService.getPersonnelGoals(input);
|
||||
|
||||
dispatch({
|
||||
type: GoalActionTypes.GET_PERS_GOALS,
|
||||
retrievedGoals: retrievedGoals
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ActionCreator, Dispatch } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import { IGoalState } from "../reducers/GoalReducer";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import { IPayoutState } from "../reducers/PayoutReducer";
|
||||
import PayoutService from "../shared/services/PayoutService";
|
||||
import {
|
||||
IEmployeePayoutsInput,
|
||||
IPayoutLineItem
|
||||
} from "../shared/models/Payout";
|
||||
import { IMonthRange } from "../shared/types/IMonthRange";
|
||||
|
||||
export enum PayoutActionTypes {
|
||||
GET_MY_PAYOUTS = "GET_MY_PAYOUTS",
|
||||
GET_EMP_PAYOUTS = "GET_EMP_PAYOUTS"
|
||||
}
|
||||
|
||||
export interface IPayoutsGetMyAction {
|
||||
type: PayoutActionTypes.GET_MY_PAYOUTS;
|
||||
currentUserPayouts: IAsyncStateContainer<IPayoutLineItem[]>;
|
||||
}
|
||||
|
||||
export interface IPayoutsGetEmpAction {
|
||||
type: PayoutActionTypes.GET_EMP_PAYOUTS;
|
||||
retrievedPayouts: IAsyncStateContainer<IPayoutLineItem[]>;
|
||||
}
|
||||
|
||||
export type PayoutActions = IPayoutsGetMyAction | IPayoutsGetEmpAction;
|
||||
|
||||
export const getMyPayouts: ActionCreator<
|
||||
ThunkAction<Promise<any>, IPayoutState, null, IPayoutsGetMyAction>
|
||||
> = (month: number, year: number) => {
|
||||
return async (dispatch: Dispatch<IPayoutsGetMyAction>) => {
|
||||
dispatch({
|
||||
type: PayoutActionTypes.GET_MY_PAYOUTS,
|
||||
currentUserPayouts: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedPayouts = await PayoutService.getMyPayouts(month, year);
|
||||
|
||||
dispatch({
|
||||
type: PayoutActionTypes.GET_MY_PAYOUTS,
|
||||
currentUserPayouts: retrievedPayouts
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getEmployeePayouts: ActionCreator<
|
||||
ThunkAction<Promise<any>, IGoalState, null, IPayoutsGetEmpAction>
|
||||
> = (input: IEmployeePayoutsInput) => {
|
||||
return async (dispatch: Dispatch<IPayoutsGetEmpAction>) => {
|
||||
dispatch({
|
||||
type: PayoutActionTypes.GET_EMP_PAYOUTS,
|
||||
retrievedPayouts: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedPayouts = await PayoutService.getEmployeePayouts(input);
|
||||
|
||||
dispatch({
|
||||
type: PayoutActionTypes.GET_EMP_PAYOUTS,
|
||||
retrievedPayouts: retrievedPayouts
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ActionCreator, Dispatch } from "redux";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import { IAppState } from "./../store/Store";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import { IEmployee, IEmployeeSummary } from "../shared/models/Employee";
|
||||
import ProfileDao from "../shared/dao/ProfileDao";
|
||||
|
||||
export enum ProfileActionTypes {
|
||||
REFRESH_PROFILE = "REFRESH_PROFILE",
|
||||
SEARCH_PROFILE = "SEARCH_PROFILE"
|
||||
}
|
||||
|
||||
export interface IProfileRefreshAction {
|
||||
type: ProfileActionTypes.REFRESH_PROFILE;
|
||||
profile: IAsyncStateContainer<IEmployee>;
|
||||
}
|
||||
|
||||
export interface IProfileSearchAction {
|
||||
type: ProfileActionTypes.SEARCH_PROFILE;
|
||||
searchResults: IAsyncStateContainer<IEmployeeSummary[]>;
|
||||
}
|
||||
|
||||
export type ProfileActions = IProfileRefreshAction | IProfileSearchAction;
|
||||
|
||||
export const refreshProfile: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IProfileRefreshAction>
|
||||
> = (employeeId?: string) => {
|
||||
return async (dispatch: Dispatch<IProfileRefreshAction>) => {
|
||||
dispatch({
|
||||
type: ProfileActionTypes.REFRESH_PROFILE,
|
||||
profile: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedProfile = await ProfileDao.getProfile(employeeId);
|
||||
|
||||
dispatch({
|
||||
type: ProfileActionTypes.REFRESH_PROFILE,
|
||||
profile: retrievedProfile
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const profileSearch: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IProfileSearchAction>
|
||||
> = (searchQuery: string, role?: string) => {
|
||||
return async (dispatch: Dispatch<IProfileSearchAction>) => {
|
||||
dispatch({
|
||||
type: ProfileActionTypes.SEARCH_PROFILE,
|
||||
searchResults: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedProfile = await ProfileDao.profileSearch(
|
||||
searchQuery,
|
||||
role
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: ProfileActionTypes.SEARCH_PROFILE,
|
||||
searchResults: retrievedProfile
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Dispatch } from "react";
|
||||
import { ActionCreator } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import {
|
||||
IAuditableRoleMapping,
|
||||
IRoleMappingChange
|
||||
} from "../shared/models/RoleMapping";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import { IAppState } from "../store/Store";
|
||||
import RoleMappingService from "../shared/services/RoleMappingService";
|
||||
|
||||
export enum RoleMappingActionTypes {
|
||||
GET_ALL_ROLE_MAPPINGS = "GET_ALL_ROLE_MAPPINGS",
|
||||
SET_ROLE_MAPPINGS = "SET_ROLE_MAPPINGS",
|
||||
SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE = "SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE"
|
||||
}
|
||||
|
||||
export interface IRoleMappingsGetAllAction {
|
||||
type: RoleMappingActionTypes.GET_ALL_ROLE_MAPPINGS;
|
||||
roleMappings: IAsyncStateContainer<IAuditableRoleMapping[]>;
|
||||
}
|
||||
|
||||
export interface IRoleMappingsSetAction {
|
||||
type: RoleMappingActionTypes.SET_ROLE_MAPPINGS;
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
}
|
||||
|
||||
export interface IRoleMappingSubmitUpdateAction {
|
||||
type: RoleMappingActionTypes.SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE;
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
}
|
||||
|
||||
export type RoleMappingActions = IRoleMappingsGetAllAction;
|
||||
|
||||
export const getAllRoleMappings: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IRoleMappingsGetAllAction>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<IRoleMappingsGetAllAction>) => {
|
||||
dispatch({
|
||||
type: RoleMappingActionTypes.GET_ALL_ROLE_MAPPINGS,
|
||||
roleMappings: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const allRoleMappings = await RoleMappingService.getAllRoleMappings();
|
||||
|
||||
dispatch({
|
||||
type: RoleMappingActionTypes.GET_ALL_ROLE_MAPPINGS,
|
||||
roleMappings: allRoleMappings
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const submitRoleMappingUpdate: ActionCreator<
|
||||
ThunkAction<Promise<any>, IAppState, null, IRoleMappingSubmitUpdateAction>
|
||||
> = (roleMapping: IRoleMappingChange) => {
|
||||
return async (dispatch: Dispatch<IRoleMappingSubmitUpdateAction>) => {
|
||||
dispatch({
|
||||
type: RoleMappingActionTypes.SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE,
|
||||
submitState: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const postState = await RoleMappingService.assignNewRoleMapping(
|
||||
roleMapping
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: RoleMappingActionTypes.SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE,
|
||||
submitState: postState
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Dispatch } from "react";
|
||||
import { ActionCreator } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import { IUserAssignmentState } from "../reducers/UserAssignmentReducer";
|
||||
import AsyncState from "../shared/enumerations/AsyncState";
|
||||
import { IUserAssignment } from "../shared/models/UserAssignment";
|
||||
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
|
||||
import UserAssignmentService from "../shared/services/UserAssignmentService";
|
||||
|
||||
export enum UserAssignmentActionTypes {
|
||||
GET_ALL_USER_ASSIGNMENTS = "GET_ALL_USER_ASSIGNMENTS",
|
||||
SET_REGIONAL_MANAGER = "SET_REGIONAL_MANAGER"
|
||||
}
|
||||
|
||||
export interface IUserAssignmentGetAllAction {
|
||||
type: UserAssignmentActionTypes.GET_ALL_USER_ASSIGNMENTS;
|
||||
userAssignments: IAsyncStateContainer<IUserAssignment[]>;
|
||||
}
|
||||
|
||||
export type UserAssignmentActions = IUserAssignmentGetAllAction;
|
||||
|
||||
export const getAllUserAssignments: ActionCreator<
|
||||
ThunkAction<
|
||||
Promise<any>,
|
||||
IUserAssignmentState,
|
||||
null,
|
||||
IUserAssignmentGetAllAction
|
||||
>
|
||||
> = () => {
|
||||
return async (dispatch: Dispatch<IUserAssignmentGetAllAction>) => {
|
||||
dispatch({
|
||||
type: UserAssignmentActionTypes.GET_ALL_USER_ASSIGNMENTS,
|
||||
userAssignments: { state: AsyncState.Loading }
|
||||
});
|
||||
|
||||
const retrievedAssignments = await UserAssignmentService.getAllUserAssignments();
|
||||
|
||||
dispatch({
|
||||
type: UserAssignmentActionTypes.GET_ALL_USER_ASSIGNMENTS,
|
||||
userAssignments: retrievedAssignments
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { RedirectRequest } from "@azure/msal-browser";
|
||||
|
||||
export const loginConfig: RedirectRequest = {
|
||||
scopes: ["User.Read"]
|
||||
};
|
||||
|
||||
export const tokenConfig: RedirectRequest = {
|
||||
scopes: ["e9cb601e-e98d-4881-90fc-d5758dac5ec8/.default"]
|
||||
};
|
||||
|
||||
export const graphConfig = {
|
||||
graphMeEndpoint: "https://graph.microsoft-ppe.com/v1.0/me"
|
||||
};
|
||||
|
||||
export const securityConfig = {
|
||||
auth: {
|
||||
clientId: "e9cb601e-e98d-4881-90fc-d5758dac5ec8",
|
||||
authority:
|
||||
"https://login.microsoftonline.com/63cd1554-2783-4a86-ae50-b9e6c21c9499",
|
||||
redirectUri: "/incentive-manager",
|
||||
postLogoutRedirectUri: "/"
|
||||
}
|
||||
};
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
IAuditableActivity,
|
||||
SystemType
|
||||
} from "../../shared/models/Activity";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
import ActivityApproval from "./ActivityApproval";
|
||||
|
||||
describe("ActivityApproval", () => {
|
||||
describe("User has permission to approve activity", () => {
|
||||
// you don't need all of the old tests, because the front end doesn't do this logic anymore.
|
||||
it("should display buttons when permissions present", () => {
|
||||
const activity: IAuditableActivity = mockActivity();
|
||||
activity.type = ActivityType.Referral;
|
||||
activity.state = ActivityState.ManualReview;
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: [ActivityState.Accepted, ActivityState.Rejected]
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ActivityApproval
|
||||
authorization={mockAuth}
|
||||
activity={activity}
|
||||
onSetActivityState={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='Accepted-activity-button']").length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find("[data-test='Rejected-activity-button']").length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("should not display buttons when not present", () => {
|
||||
const activity: IAuditableActivity = mockActivity();
|
||||
activity.type = ActivityType.Referral;
|
||||
activity.state = ActivityState.ManualReview;
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: [ActivityState.Accepted]
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ActivityApproval
|
||||
authorization={mockAuth}
|
||||
activity={activity}
|
||||
onSetActivityState={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='Accepted-activity-button']").length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find("[data-test='Rejected-activity-button']").length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockActivity = (): IAuditableActivity => {
|
||||
return {
|
||||
id: 1,
|
||||
product: Product2.Mortgage,
|
||||
type: ActivityType.Referral,
|
||||
sourceSystem: SystemType.Dna,
|
||||
sourceId: "11",
|
||||
sourceCreated: moment.utc(),
|
||||
departmentId: "25",
|
||||
employeeId: "11771",
|
||||
employeeEmail: "[email protected]",
|
||||
accepted: moment.utc(),
|
||||
state: ActivityState.Accepted,
|
||||
created: moment.utc(),
|
||||
createdBy: "Created Doe",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment.utc(),
|
||||
lastUpdatedBy: "Updated Doe",
|
||||
lastUpdatedByType: UserType.Associate,
|
||||
detail: {},
|
||||
activityPayouts: []
|
||||
};
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import { Button } from "reactstrap";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityStateTitles,
|
||||
IAuditableActivity,
|
||||
IAuthorizedActivityCommands,
|
||||
ManualActivityState
|
||||
} from "../../shared/models/Activity";
|
||||
import moment, { Moment } from "moment";
|
||||
import PermissionGuard from "../../containers/shared/PermissionGuard";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
|
||||
interface IActivityApprovalProps {
|
||||
activity: IAuditableActivity;
|
||||
onSetActivityState: (
|
||||
newState: ActivityState,
|
||||
newDate: Moment | undefined
|
||||
) => void;
|
||||
authorization: IAuthorizedActivityCommands;
|
||||
}
|
||||
|
||||
const ActivityApproval: React.FC<IActivityApprovalProps> = (
|
||||
props: IActivityApprovalProps
|
||||
) => {
|
||||
if (
|
||||
props.activity.activityPayouts.length > 0 ||
|
||||
props.authorization.allowedChanges?.length === 0
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const createActionButton = (
|
||||
state: ActivityState,
|
||||
newDate: Moment | undefined
|
||||
) => {
|
||||
return (
|
||||
<tr>
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
PermissionCode.SET_ACTIVITY_ACCEPTED_DATE
|
||||
]}
|
||||
>
|
||||
<td>
|
||||
<input
|
||||
type="date"
|
||||
onKeyDown={(e) => e.preventDefault()}
|
||||
data-test="accepted-date-input"
|
||||
min={moment(moment.now())
|
||||
.subtract(1, "months")
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}
|
||||
max={moment(moment.now())
|
||||
.add(1, "months")
|
||||
.format("YYYY-MM-DD")}
|
||||
onChange={(event) =>
|
||||
(newDate = moment(event.target.value) ?? "")
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</PermissionGuard>
|
||||
<td>
|
||||
<Button
|
||||
data-test={state + "-activity-button"}
|
||||
className="d-inline mr-2 mt-1"
|
||||
outline
|
||||
color="success"
|
||||
onClick={() => props.onSetActivityState(state, newDate)}
|
||||
>
|
||||
{ActivityStateTitles[state]}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const manualActions = Object.keys(ManualActivityState);
|
||||
const availableActions = props.authorization.allowedChanges.map(
|
||||
(allowed) => {
|
||||
return (
|
||||
manualActions.includes(allowed) &&
|
||||
createActionButton(allowed, undefined)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return <>{availableActions}</>;
|
||||
};
|
||||
|
||||
export default ActivityApproval;
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import ActivityDetailAlertBar from "./ActivityDetailAlertBar";
|
||||
|
||||
describe("Activity Detail Alert Bar", () => {
|
||||
it("should return activity failure alert if activity state is failure", () => {
|
||||
const activity: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Failure,
|
||||
data: "Activity Failed message"
|
||||
};
|
||||
|
||||
const activityChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const activityTransferChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityDetailAlertBar
|
||||
activity={activity}
|
||||
activityStateChange={activityChange}
|
||||
activityTransferChange={activityTransferChange}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.debug();
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-failure-alert']").length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='activity-failure-alert']")
|
||||
.childAt(0)
|
||||
.text()
|
||||
).toEqual("Activity Failed message");
|
||||
});
|
||||
|
||||
it("should return activity changes failure alert if activity change is failure", () => {
|
||||
const activity: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const activityChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Failure,
|
||||
data: "Activity Change Failed message"
|
||||
};
|
||||
|
||||
const activityTransferChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityDetailAlertBar
|
||||
activity={activity}
|
||||
activityStateChange={activityChange}
|
||||
activityTransferChange={activityTransferChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("[data-test='activity-change-alert']").length).toBe(
|
||||
1
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='activity-change-alert']")
|
||||
.childAt(0)
|
||||
.text()
|
||||
).toEqual("Activity Change Failed message");
|
||||
});
|
||||
|
||||
it("should return loading alert if activity details are loading", () => {
|
||||
const activity: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Loading
|
||||
};
|
||||
|
||||
const activityChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const activityTransferChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityDetailAlertBar
|
||||
activity={activity}
|
||||
activityStateChange={activityChange}
|
||||
activityTransferChange={activityTransferChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-loading-alert']").length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("should return loading alert if activity state change is loading", () => {
|
||||
const activity: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const activityChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Loading,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const activityTransferChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityDetailAlertBar
|
||||
activity={activity}
|
||||
activityStateChange={activityChange}
|
||||
activityTransferChange={activityTransferChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-loading-alert']").length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("should return an empty fragment if no async states are failure", () => {
|
||||
const activity: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const activityChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const activityTransferChange: IAsyncStateContainer<any> = {
|
||||
state: AsyncState.Success,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityDetailAlertBar
|
||||
activity={activity}
|
||||
activityStateChange={activityChange}
|
||||
activityTransferChange={activityTransferChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
});
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import { Alert } from "reactstrap";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {
|
||||
IAuditableActivity,
|
||||
IActivityTransferAction,
|
||||
IViewableActivityDetail
|
||||
} from "../../shared/models/Activity";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import Translate from "../../translate/Translate";
|
||||
|
||||
interface IActivityDetailAlertBarProps {
|
||||
activity: IAsyncStateContainer<IViewableActivityDetail>;
|
||||
activityStateChange: IAsyncStateContainer<IAuditableActivity>;
|
||||
activityTransferChange: IAsyncStateContainer<IActivityTransferAction>;
|
||||
}
|
||||
|
||||
const alertTranlate =
|
||||
Translate.Get.components.activityDetails.activityDetailAlertBar;
|
||||
|
||||
const ActivityDetailAlertBar: React.FC<IActivityDetailAlertBarProps> = (
|
||||
props: IActivityDetailAlertBarProps
|
||||
) => {
|
||||
if (props.activityStateChange.state === AsyncState.Failure) {
|
||||
return (
|
||||
<Alert color="danger" data-test="activity-change-alert">
|
||||
{props.activityStateChange.data}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.activity.state === AsyncState.Failure) {
|
||||
return (
|
||||
<Alert color="danger" data-test="activity-failure-alert">
|
||||
{props.activity.data}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.activityTransferChange.state === AsyncState.Failure) {
|
||||
return (
|
||||
<Alert color="danger" data-test="activity-transfer-change-alert">
|
||||
{props.activityTransferChange.data}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
props.activity.state === AsyncState.Loading ||
|
||||
props.activityStateChange.state === AsyncState.Loading ||
|
||||
props.activityTransferChange.state === AsyncState.Loading
|
||||
) {
|
||||
return (
|
||||
<Alert color="warning" data-test="activity-loading-alert">
|
||||
{alertTranlate.loadingMessage}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default ActivityDetailAlertBar;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IActivityDetail } from "../../shared/models/Activity";
|
||||
import ActivityDetailDisplay from "./ActivityDetailDisplay";
|
||||
|
||||
describe("ActivityDetailDisplay", () => {
|
||||
it("should display activity detail table", () => {
|
||||
const detail = {
|
||||
id: 1,
|
||||
accountNumber: "11",
|
||||
memberNumber: "12",
|
||||
memberName: "Member Doe",
|
||||
accountStatus: "Open",
|
||||
accountBalance: 21,
|
||||
accountNewBalance: 11
|
||||
} as IActivityDetail;
|
||||
|
||||
const wrapper = shallow(<ActivityDetailDisplay detail={detail} />);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-detail-display']").length
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IActivityDetail } from "../../shared/models/Activity";
|
||||
import Translate from "../../translate/Translate";
|
||||
import GoalService from "../../shared/services/GoalService";
|
||||
import { IDeadActivityDetail } from "../../shared/models/DeadActivity";
|
||||
|
||||
interface IActivityDetailDisplayProps {
|
||||
detail: IActivityDetail | IDeadActivityDetail;
|
||||
}
|
||||
|
||||
const detailTranslate =
|
||||
Translate.Get.components.activityDetails.activityDetailDisplay;
|
||||
|
||||
const ActivityDetailDisplay: React.FC<IActivityDetailDisplayProps> = (
|
||||
props: IActivityDetailDisplayProps
|
||||
) => {
|
||||
return (
|
||||
<div data-test="activity-detail-display">
|
||||
<h5>{detailTranslate.title}</h5>
|
||||
<table className="table table-responsive">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.id}</td>
|
||||
<td>{props.detail.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.dnaPersonNumber}</td>
|
||||
<td>{props.detail.dnaPersonNbr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.accountNumber}</td>
|
||||
<td>{props.detail.accountNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.subAccountNumber}</td>
|
||||
<td>{props.detail.subAccountNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.subjectPersonNbr}</td>
|
||||
<td>{props.detail.subjectPersonNbr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.subjectOrgNbr}</td>
|
||||
<td>{props.detail.subjectOrgNbr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.memberName}</td>
|
||||
<td>{props.detail.memberName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.stationName}</td>
|
||||
<td>{props.detail.stationName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.branchOrgNbr}</td>
|
||||
<td>{props.detail.branchOrgNbr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.branchOrgName}</td>
|
||||
<td>{props.detail.branchOrgName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.accountStatus}</td>
|
||||
<td>{props.detail.accountStatus}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.accountBalance}</td>
|
||||
<td>
|
||||
{GoalService.formatMonetary(
|
||||
props.detail.accountBalance
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.debtProtectionAddDate}</td>
|
||||
<td>
|
||||
{props.detail.debtProtectionAddDate
|
||||
? moment(
|
||||
props.detail.debtProtectionAddDate
|
||||
).format("MM-DD-YYYY")
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{detailTranslate.headers.debtProtectionRemoveDate}
|
||||
</td>
|
||||
<td>
|
||||
{props.detail.debtProtectionRemoveDate
|
||||
? moment(
|
||||
props.detail.debtProtectionRemoveDate
|
||||
).format("MM-DD-YYYY")
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{detailTranslate.headers.isDebtProtectionEligible}
|
||||
</td>
|
||||
<td>
|
||||
{props.detail.isDebtProtectionEligible
|
||||
? "True"
|
||||
: "False"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{detailTranslate.headers.gapInsuranceAddDate}</td>
|
||||
<td>
|
||||
{props.detail.gapInsuranceAddDate
|
||||
? moment(
|
||||
props.detail.gapInsuranceAddDate
|
||||
).format("MM-DD-YYYY")
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{detailTranslate.headers.gapInsuranceRemoveDate}
|
||||
</td>
|
||||
<td>
|
||||
{props.detail.gapInsuranceRemoveDate
|
||||
? moment(
|
||||
props.detail.gapInsuranceRemoveDate
|
||||
).format("MM-DD-YYYY")
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{detailTranslate.headers.isGapInsuranceEligible}
|
||||
</td>
|
||||
<td>
|
||||
{props.detail.isGapInsuranceEligible
|
||||
? "True"
|
||||
: "False"}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityDetailDisplay;
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IActivityDetailUpdate } from "../../shared/models/Activity";
|
||||
import ActivityDetailUpdatesTable from "./ActivityDetailUpdatesTable";
|
||||
|
||||
describe("ActivityDetailUpdatesTable", () => {
|
||||
it("should display the changes in a table", () => {
|
||||
const changes = [
|
||||
{
|
||||
id: 1,
|
||||
note: "Account Number change",
|
||||
created: moment.utc()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
note: "Account Number change",
|
||||
created: moment.utc()
|
||||
}
|
||||
] as IActivityDetailUpdate[];
|
||||
const wrapper = shallow(
|
||||
<ActivityDetailUpdatesTable activityDetailUpdates={changes} />
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-detail-updates-table']").length
|
||||
).toBe(1);
|
||||
// table rows
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-detail-updates-table'] tbody tr")
|
||||
.length
|
||||
).toBe(2);
|
||||
// alert component
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-detail-updates-table-alert']")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should display an alert if there are no changes", () => {
|
||||
const wrapper = shallow(
|
||||
<ActivityDetailUpdatesTable activityDetailUpdates={[]} />
|
||||
);
|
||||
|
||||
// alert component
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-detail-updates-table-alert']")
|
||||
.length
|
||||
).toBe(1);
|
||||
//table
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-detail-updates-table'] table")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { Alert } from "reactstrap";
|
||||
import { IActivityDetailUpdate } from "../../shared/models/Activity";
|
||||
import Translate from "../../translate/Translate";
|
||||
|
||||
interface IActivityStateChangesTableProps {
|
||||
activityDetailUpdates: IActivityDetailUpdate[];
|
||||
}
|
||||
|
||||
const tableTranslate =
|
||||
Translate.Get.components.activityDetails.activityDetailUpdatesTable;
|
||||
|
||||
const ActivityStateChangesTable: React.FC<IActivityStateChangesTableProps> = (
|
||||
props: IActivityStateChangesTableProps
|
||||
) => {
|
||||
return (
|
||||
<div data-test="activity-detail-updates-table">
|
||||
<h5>{tableTranslate.title}</h5>
|
||||
{props.activityDetailUpdates.length ? (
|
||||
<table className="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{tableTranslate.headers.note}</th>
|
||||
<th>{tableTranslate.headers.created}</th>
|
||||
<th>{tableTranslate.headers.createdBy}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.activityDetailUpdates.map((change) => (
|
||||
<tr key={change.id}>
|
||||
<td>{change.note}</td>
|
||||
<td>
|
||||
{moment
|
||||
.utc(change.created)
|
||||
.local()
|
||||
.format("lll")}
|
||||
</td>
|
||||
<td>{change.createdBy}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<Alert
|
||||
color="info"
|
||||
data-test="activity-detail-updates-table-alert"
|
||||
>
|
||||
{tableTranslate.noChangesMessage}
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityStateChangesTable;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
IAuditableActivity,
|
||||
SystemType
|
||||
} from "../../shared/models/Activity";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
|
||||
import ActivityDisplay from "./ActivityDisplay";
|
||||
import { mockProfileState } from "../../shared/testing/mocks/MockProfileState";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
|
||||
describe("ActivityDisplay", () => {
|
||||
it("should display the activity table", () => {
|
||||
const activity: IAuditableActivity = {
|
||||
employeeEmail: "",
|
||||
employeeId: "",
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
type: ActivityType.Referral,
|
||||
sourceSystem: SystemType.Dna,
|
||||
sourceId: "11",
|
||||
sourceCreated: moment.utc(),
|
||||
departmentId: "25",
|
||||
accepted: moment.utc(),
|
||||
state: ActivityState.Accepted,
|
||||
created: moment.utc(),
|
||||
createdBy: "Created Doe",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment.utc(),
|
||||
lastUpdatedBy: "Updated Doe",
|
||||
lastUpdatedByType: UserType.Associate,
|
||||
detail: {},
|
||||
activityPayouts: []
|
||||
};
|
||||
|
||||
let callCount = 0;
|
||||
const onSetState = () => {
|
||||
callCount++;
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ActivityDisplay
|
||||
activity={activity}
|
||||
profile={mockProfileState.profile.data!}
|
||||
onSetActivityState={onSetState}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("[data-test='activity-display']").length).toBe(1);
|
||||
expect(wrapper.find("[data-test='activity-display'] tr").length).toBe(
|
||||
12
|
||||
);
|
||||
});
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import moment, { Moment } from "moment";
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityState,
|
||||
IAuditableActivity,
|
||||
IAuthorizedActivityCommands
|
||||
} from "../../shared/models/Activity";
|
||||
import Translate from "../../translate/Translate";
|
||||
import ActivityApproval from "./ActivityApproval";
|
||||
import { IEmployee } from "../../shared/models/Employee";
|
||||
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
|
||||
import UserDisplay from "../../containers/shared/UserDisplay";
|
||||
import DateTimeService from "../../shared/services/DateTimeService";
|
||||
|
||||
interface IActivityDisplayProps {
|
||||
activity: IAuditableActivity;
|
||||
onSetActivityState: (
|
||||
newState: ActivityState,
|
||||
newDate: Moment | undefined
|
||||
) => void;
|
||||
profile: IEmployee;
|
||||
authorization: IAuthorizedActivityCommands;
|
||||
}
|
||||
|
||||
const activityTranslate =
|
||||
Translate.Get.components.activityDetails.activityDisplay;
|
||||
|
||||
const ActivityDisplay: React.FC<IActivityDisplayProps> = (
|
||||
props: IActivityDisplayProps
|
||||
) => {
|
||||
return (
|
||||
<div data-test="activity-display">
|
||||
<h5>{activityTranslate.title}</h5>
|
||||
<table className="table table-responsive">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.id}</td>
|
||||
<td>{props.activity.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.product}</td>
|
||||
<td>{props.activity.product}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.type}</td>
|
||||
<td>{props.activity.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.sourceSystem}</td>
|
||||
<td>{props.activity.sourceSystem}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.sourceId}</td>
|
||||
<td>{props.activity.sourceId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.sourceCreated}</td>
|
||||
<td>
|
||||
{DateTimeService.CastServerTimeToClientTimeString(
|
||||
props.activity.sourceCreated
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.departmentId}</td>
|
||||
<td>
|
||||
<DepartmentDisplay
|
||||
departmentId={parseInt(
|
||||
props.activity.departmentId,
|
||||
10
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.employeeId}</td>
|
||||
<td>
|
||||
<UserDisplay
|
||||
employeeId={props.activity.employeeId}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.accepted}</td>
|
||||
<td>
|
||||
{DateTimeService.CastServerTimeToClientTimeString(
|
||||
props.activity.accepted
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.state}</td>
|
||||
<td>{props.activity.state}</td>
|
||||
</tr>
|
||||
{props.activity.activityPayouts.length === 0 && (
|
||||
<ActivityApproval
|
||||
activity={props.activity}
|
||||
onSetActivityState={(newState, newDate) =>
|
||||
props.onSetActivityState(newState, newDate)
|
||||
}
|
||||
authorization={props.authorization}
|
||||
/>
|
||||
)}
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.createdBy}</td>
|
||||
<td title={props.activity.createdByType}>
|
||||
{props.activity.createdBy} on{" "}
|
||||
{DateTimeService.CastServerTimeToClientTimeString(
|
||||
props.activity.created
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{activityTranslate.headers.lastUpdatedBy}</td>
|
||||
<td title={props.activity.lastUpdatedByType}>
|
||||
{props.activity.lastUpdatedBy} on{" "}
|
||||
{DateTimeService.CastServerTimeToClientTimeString(
|
||||
props.activity.lastUpdated
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityDisplay;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IMetaDataLineItem } from "../../shared/models/Activity";
|
||||
import { Alert } from "reactstrap";
|
||||
|
||||
interface IActivityMetaDataDisplayProps {
|
||||
activityMetaData: IMetaDataLineItem[];
|
||||
}
|
||||
|
||||
const metaDataTranslate =
|
||||
Translate.Get.components.activityDetails.activityMetaDataDisplay;
|
||||
|
||||
const ActivityMetaDataDisplay: React.FC<IActivityMetaDataDisplayProps> = (
|
||||
props: IActivityMetaDataDisplayProps
|
||||
) => {
|
||||
return (
|
||||
<div data-test="activity-metadata-display">
|
||||
<h5>{metaDataTranslate.title}</h5>
|
||||
{props.activityMetaData.length ? (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{metaDataTranslate.headers.metaData}</th>
|
||||
<th>{metaDataTranslate.headers.metaDataType}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.activityMetaData.map((data) => (
|
||||
<tr key={data.id}>
|
||||
<td>{data.metaData}</td>
|
||||
<td>{data.metaDataType}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<Alert color="info" data-test="no-metaData-alert">
|
||||
{metaDataTranslate.noMetaDataMessage}
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityMetaDataDisplay;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IPayoutLineItem } from "../../shared/models/Payout";
|
||||
import ActivityPayoutsDisplay from "./ActivityPayoutsDisplay";
|
||||
|
||||
describe("ActivityPayoutsDisplay", () => {
|
||||
it("should display the payouts in a table", () => {
|
||||
const payouts: IPayoutLineItem[] = [
|
||||
{
|
||||
activityPeriod: moment.utc(),
|
||||
departmentId: 25,
|
||||
role: {
|
||||
description: "Teller",
|
||||
name: "Teller Doe",
|
||||
permissions: [],
|
||||
hasCommissions: true
|
||||
},
|
||||
employeeId: "123",
|
||||
activityTotal: 100,
|
||||
payoutTotal: 200
|
||||
},
|
||||
{
|
||||
activityPeriod: moment.utc(),
|
||||
departmentId: 25,
|
||||
role: {
|
||||
description: "Branch Manager",
|
||||
name: "B.M. Doe",
|
||||
permissions: [],
|
||||
hasCommissions: true
|
||||
},
|
||||
employeeId: "456",
|
||||
activityTotal: 100,
|
||||
payoutTotal: 200
|
||||
}
|
||||
] as IPayoutLineItem[];
|
||||
|
||||
const wrapper = shallow(<ActivityPayoutsDisplay payouts={payouts} />);
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-payouts-display']").length
|
||||
).toBe(1);
|
||||
// table rows
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-payouts-display'] tbody tr")
|
||||
.length
|
||||
).toBe(2);
|
||||
// alert component
|
||||
expect(wrapper.find("[data-test='no-payouts-alert']").length).toBe(0);
|
||||
});
|
||||
|
||||
it("should display an alert if there are no payouts", () => {
|
||||
const wrapper = shallow(<ActivityPayoutsDisplay payouts={[]} />);
|
||||
// alert component
|
||||
expect(wrapper.find("[data-test='no-payouts-alert']").length).toBe(1);
|
||||
//table
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-payouts-display'] table").length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IPayoutLineItem } from "../../shared/models/Payout";
|
||||
import moment from "moment";
|
||||
import { Alert } from "reactstrap";
|
||||
|
||||
interface IActivityPayoutsDisplayProps {
|
||||
payouts: IPayoutLineItem[];
|
||||
}
|
||||
|
||||
const payoutsTranslate =
|
||||
Translate.Get.components.activityDetails.activityPayoutsDisplay;
|
||||
|
||||
const ActivityPayoutsDisplay: React.FC<IActivityPayoutsDisplayProps> = (
|
||||
props: IActivityPayoutsDisplayProps
|
||||
) => {
|
||||
return (
|
||||
<div data-test="activity-payouts-display">
|
||||
<h5>{payoutsTranslate.title}</h5>
|
||||
{props.payouts.length ? (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{payoutsTranslate.headers.period}</th>
|
||||
<th>{payoutsTranslate.headers.departmentId}</th>
|
||||
<th>{payoutsTranslate.headers.role}</th>
|
||||
<th>{payoutsTranslate.headers.employeeId}</th>
|
||||
<th>{payoutsTranslate.headers.activityTotal}</th>
|
||||
<th>{payoutsTranslate.headers.payoutTotal}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.payouts.map((payout) => (
|
||||
<tr key={payout.id}>
|
||||
<td>
|
||||
{moment
|
||||
.utc(payout.activityPeriod)
|
||||
.format("ll")}
|
||||
</td>
|
||||
<td>{payout.departmentId}</td>
|
||||
<td>{payout.role?.description}</td>
|
||||
<td>{payout.employeeId}</td>
|
||||
<td>{payout.activityTotal}</td>
|
||||
<td>{payout.payoutTotal}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<Alert color="info" data-test="no-payouts-alert">
|
||||
{payoutsTranslate.noPayoutsMessage}
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityPayoutsDisplay;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IActivityStateChange } from "../../shared/models/Activity";
|
||||
import ActivityStateChangesTable from "./ActivityStateChangesTable";
|
||||
|
||||
describe("ActivityStateChangesTable", () => {
|
||||
it("should display the changes in a table", () => {
|
||||
const changes = [
|
||||
{
|
||||
id: 1,
|
||||
state: "Created",
|
||||
note: "Auto created",
|
||||
created: moment.utc()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
state: "Created",
|
||||
note: "Auto created",
|
||||
created: moment.utc()
|
||||
}
|
||||
] as IActivityStateChange[];
|
||||
const wrapper = shallow(
|
||||
<ActivityStateChangesTable activityStateChanges={changes} />
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-state-changes-table']").length
|
||||
).toBe(1);
|
||||
// table rows
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-state-changes-table'] tbody tr")
|
||||
.length
|
||||
).toBe(2);
|
||||
// alert component
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-state-changes-table-alert']")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should display an alert if there are no changes", () => {
|
||||
const wrapper = shallow(
|
||||
<ActivityStateChangesTable activityStateChanges={[]} />
|
||||
);
|
||||
|
||||
// alert component
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-state-changes-table-alert']")
|
||||
.length
|
||||
).toBe(1);
|
||||
//table
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-state-changes-table'] table")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { Alert } from "reactstrap";
|
||||
import { IActivityStateChange } from "../../shared/models/Activity";
|
||||
import Translate from "../../translate/Translate";
|
||||
import Configuration from "../../shared/configuration/Configuration";
|
||||
|
||||
interface IActivityStateChangesTableProps {
|
||||
activityStateChanges: IActivityStateChange[];
|
||||
}
|
||||
|
||||
const tableTranslate =
|
||||
Translate.Get.components.activityDetails.activityStateChangesTable;
|
||||
|
||||
const urlPrefix = Configuration.PrefixAppUrlWithBasename(
|
||||
Configuration.Get.appUrls.activity
|
||||
);
|
||||
|
||||
const ActivityStateChangesTable: React.FC<IActivityStateChangesTableProps> = (
|
||||
props: IActivityStateChangesTableProps
|
||||
) => {
|
||||
// If there is a matching activity, make it a link.
|
||||
const getNote = (change: IActivityStateChange) => {
|
||||
if (change.activityMatchId) {
|
||||
return (
|
||||
<a href={urlPrefix + "/" + change.activityMatchId}>
|
||||
{change.note}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return change.note;
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test="activity-state-changes-table">
|
||||
<h5>{tableTranslate.title}</h5>
|
||||
{props.activityStateChanges.length ? (
|
||||
<table className="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{tableTranslate.headers.state}</th>
|
||||
<th>{tableTranslate.headers.accepted}</th>
|
||||
<th>{tableTranslate.headers.note}</th>
|
||||
<th>{tableTranslate.headers.created}</th>
|
||||
<th>{tableTranslate.headers.createdBy}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.activityStateChanges.map((change) => (
|
||||
<tr key={change.id}>
|
||||
<td>{change.state}</td>
|
||||
<td>
|
||||
{change.acceptedDate == null
|
||||
? ""
|
||||
: moment(change.acceptedDate).format(
|
||||
"ll"
|
||||
)}
|
||||
</td>
|
||||
<td>{getNote(change)}</td>
|
||||
<td>
|
||||
{moment
|
||||
.utc(change.created)
|
||||
.local()
|
||||
.format("lll")}
|
||||
</td>
|
||||
<td>{change.createdBy}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<Alert
|
||||
color="info"
|
||||
data-test="activity-state-changes-table-alert"
|
||||
>
|
||||
{tableTranslate.noChangesMessage}
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityStateChangesTable;
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityTransferState,
|
||||
ActivityType,
|
||||
IActivityTransferAction,
|
||||
IAuditableActivity,
|
||||
SystemType
|
||||
} from "../../shared/models/Activity";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
import { IEmployee } from "../../shared/models/Employee";
|
||||
import ActivityTransferApproval from "./ActivityTransferApproval";
|
||||
|
||||
describe("ActivityTransferApproval", () => {
|
||||
it("should not display if the transfer action is undefined", () => {
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: []
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferApproval
|
||||
transferAction={undefined}
|
||||
authorization={mockAuth}
|
||||
activity={mockActivity()}
|
||||
approveTransfer={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-approval-component']").length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should display if the transfer action exists", () => {
|
||||
const userProfile = mockProfile();
|
||||
userProfile.departments = [];
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: false,
|
||||
canApproveTransferFrom: false,
|
||||
allowedChanges: []
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferApproval
|
||||
transferAction={mockActivityTransferAction()}
|
||||
authorization={mockAuth}
|
||||
activity={mockActivity()}
|
||||
approveTransfer={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-approval-component']").length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find("[data-test='approve-transfer-button']").length
|
||||
).toBe(0);
|
||||
expect(
|
||||
wrapper.find("[data-test='reject-transfer-button']").length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should display approval buttons if profile has a department manager department assignment", () => {
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: []
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferApproval
|
||||
transferAction={mockActivityTransferAction()}
|
||||
authorization={mockAuth}
|
||||
activity={mockActivity()}
|
||||
approveTransfer={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='approve-transfer-button']").length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find("[data-test='reject-transfer-button']").length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("should display approval buttons if profile has a department manager department assignment", () => {
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: []
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferApproval
|
||||
transferAction={mockActivityTransferAction()}
|
||||
authorization={mockAuth}
|
||||
activity={mockActivity()}
|
||||
approveTransfer={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='approve-transfer-button']").length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find("[data-test='reject-transfer-button']").length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("should not display approval buttons if profile has been approved for the department", () => {
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: false,
|
||||
canApproveTransferFrom: false,
|
||||
allowedChanges: []
|
||||
};
|
||||
const activityFromDept25 = mockActivity();
|
||||
// set up transfer approved by the appUser's department
|
||||
const transferAction = mockActivityTransferAction();
|
||||
transferAction.newActivityDepartmentId = 1;
|
||||
transferAction.requestedDepartmentApprover = "Aaron Manager";
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferApproval
|
||||
transferAction={transferAction}
|
||||
authorization={mockAuth}
|
||||
activity={activityFromDept25}
|
||||
approveTransfer={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='approve-transfer-button']").length
|
||||
).toBe(0);
|
||||
expect(
|
||||
wrapper.find("[data-test='reject-transfer-button']").length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should call the approveTransfer function when buttons are clicked", () => {
|
||||
const mockAuth = {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: []
|
||||
};
|
||||
const approveTransferSpy = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferApproval
|
||||
transferAction={mockActivityTransferAction()}
|
||||
authorization={mockAuth}
|
||||
activity={mockActivity()}
|
||||
approveTransfer={approveTransferSpy}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find("[data-test='approve-transfer-button']").simulate("click");
|
||||
expect(approveTransferSpy).toHaveBeenCalledWith(
|
||||
11,
|
||||
ActivityTransferState.Accepted
|
||||
);
|
||||
expect(approveTransferSpy).toHaveBeenCalledTimes(1);
|
||||
wrapper.find("[data-test='reject-transfer-button']").simulate("click");
|
||||
expect(approveTransferSpy).toHaveBeenCalledWith(
|
||||
11,
|
||||
ActivityTransferState.Rejected
|
||||
);
|
||||
expect(approveTransferSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
const mockProfile = (): IEmployee => {
|
||||
return {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code: PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT,
|
||||
description: "ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
}
|
||||
],
|
||||
hasCommissions: false,
|
||||
rank: 99
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [
|
||||
{
|
||||
employeeId: "",
|
||||
departmentId: 25,
|
||||
startDate: moment(),
|
||||
note: "",
|
||||
isHome: true,
|
||||
slidingPayout: 100,
|
||||
role: {
|
||||
name: "Manager",
|
||||
description: "Manager",
|
||||
hasCommissions: true,
|
||||
permissions: [],
|
||||
rank: 99
|
||||
},
|
||||
created: moment(),
|
||||
createdBy: "",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedBy: "",
|
||||
lastUpdatedByType: UserType.Associate
|
||||
}
|
||||
],
|
||||
assignments: []
|
||||
};
|
||||
};
|
||||
|
||||
const mockActivity = (): IAuditableActivity => {
|
||||
return {
|
||||
id: 1,
|
||||
product: ("ATMCard" as unknown) as Product2,
|
||||
type: ActivityType.Referral,
|
||||
sourceSystem: SystemType.Dna,
|
||||
sourceId: "11",
|
||||
sourceCreated: moment.utc(),
|
||||
departmentId: "25",
|
||||
employeeId: "2",
|
||||
employeeEmail: "[email protected]",
|
||||
accepted: moment.utc(),
|
||||
state: ActivityState.Accepted,
|
||||
created: moment.utc(),
|
||||
createdBy: "Created Doe",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment.utc(),
|
||||
lastUpdatedBy: "Updated Doe",
|
||||
lastUpdatedByType: UserType.Associate,
|
||||
detail: {
|
||||
id: 1,
|
||||
accountNumber: "11",
|
||||
dnaPersonNbr: "12",
|
||||
memberName: "Member Doe",
|
||||
accountStatus: "Open",
|
||||
accountBalance: 21,
|
||||
accountNewBalance: 11
|
||||
},
|
||||
metaData: [],
|
||||
activityPayouts: []
|
||||
};
|
||||
};
|
||||
|
||||
const mockActivityTransferAction = (): IActivityTransferAction => {
|
||||
return {
|
||||
id: 11,
|
||||
activityStateBeforeTransfer: ActivityState.Accepted,
|
||||
newActivityDepartmentId: 25,
|
||||
newActivityEmployeeId: "C0002",
|
||||
requestedActivityId: 1,
|
||||
state: ActivityTransferState.Requested
|
||||
};
|
||||
};
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import React from "react";
|
||||
import { Button } from "reactstrap";
|
||||
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
|
||||
import PermissionGuard from "../../containers/shared/PermissionGuard";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
import {
|
||||
ActivityTransferState,
|
||||
IActivity,
|
||||
IActivityTransferAction,
|
||||
IAuthorizedActivityCommands
|
||||
} from "../../shared/models/Activity";
|
||||
import Translate from "../../translate/Translate";
|
||||
|
||||
interface IActivityTransferApproval {
|
||||
transferAction?: IActivityTransferAction;
|
||||
activity: IActivity;
|
||||
authorization: IAuthorizedActivityCommands;
|
||||
approveTransfer: (
|
||||
requestId: number,
|
||||
decision: ActivityTransferState
|
||||
) => void;
|
||||
}
|
||||
|
||||
const approvalTranslate =
|
||||
Translate.Get.components.activityDetails.activityTransferApproval;
|
||||
|
||||
const ActivityTransferApproval: React.FC<IActivityTransferApproval> = (
|
||||
props: IActivityTransferApproval
|
||||
) => {
|
||||
const canApproveForCurrentDepartment =
|
||||
props.authorization.canApproveTransferFrom;
|
||||
const canApproveForNewDepartment = props.authorization.canApproveTransferTo;
|
||||
|
||||
return props.transferAction ? (
|
||||
<div data-test="activity-approval-component">
|
||||
<h5>{approvalTranslate.title}</h5>
|
||||
<table className="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{approvalTranslate.headers.department}</th>
|
||||
<th>{approvalTranslate.headers.employeeId}</th>
|
||||
<th>
|
||||
{approvalTranslate.headers.approvalFrom}
|
||||
<DepartmentDisplay
|
||||
departmentId={+props.activity.departmentId}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
{approvalTranslate.headers.approvalFrom}
|
||||
<DepartmentDisplay
|
||||
departmentId={
|
||||
props.transferAction.newActivityDepartmentId
|
||||
}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<DepartmentDisplay
|
||||
departmentId={
|
||||
props.transferAction.newActivityDepartmentId
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>{props.transferAction.newActivityEmployeeId}</td>
|
||||
<td>
|
||||
{props.transferAction.requestedDepartmentApprover ||
|
||||
approvalTranslate.pending}
|
||||
</td>
|
||||
<td>
|
||||
{props.transferAction.newDepartmentApprover ||
|
||||
approvalTranslate.pending}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{(canApproveForCurrentDepartment || canApproveForNewDepartment) && (
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_ASSOCIATES,
|
||||
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_DEPARTMENTS
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
data-test="approve-transfer-button"
|
||||
className="d-inline mr-2 mt-1"
|
||||
outline
|
||||
color="success"
|
||||
onClick={() =>
|
||||
props.approveTransfer(
|
||||
props.transferAction!.id,
|
||||
ActivityTransferState.Accepted
|
||||
)
|
||||
}
|
||||
>
|
||||
{approvalTranslate.buttons.approve}
|
||||
</Button>
|
||||
<Button
|
||||
data-test="reject-transfer-button"
|
||||
className="d-inline mt-1"
|
||||
outline
|
||||
color="danger"
|
||||
onClick={() =>
|
||||
props.approveTransfer(
|
||||
props.transferAction!.id,
|
||||
ActivityTransferState.Rejected
|
||||
)
|
||||
}
|
||||
>
|
||||
{approvalTranslate.buttons.reject}
|
||||
</Button>
|
||||
</div>
|
||||
</PermissionGuard>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTransferApproval;
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
IActivity,
|
||||
SystemType
|
||||
} from "../../shared/models/Activity";
|
||||
import ActivityTransferRequest from "./ActivityTransferRequest";
|
||||
|
||||
describe("ActivityTransferRequest", () => {
|
||||
it("should call props onFormSubmit with selected department and employee", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferRequest
|
||||
departments={[]}
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='activity-transfer-request-button']")
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
|
||||
wrapper
|
||||
.find("UserSearchSelect")
|
||||
.prop<(field: string, newValue: any) => void>("onUserSelected")(
|
||||
"25",
|
||||
"C002"
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='activity-transfer-request-button']")
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
|
||||
wrapper.find("[data-test='dept-input']").prop<any>("onChange")({
|
||||
target: { value: "25" }
|
||||
});
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='activity-transfer-request-button']")
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
|
||||
wrapper
|
||||
.find("[data-test='activity-transfer-request-button']")
|
||||
.simulate("click");
|
||||
|
||||
expect(mockFormSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockFormSubmit).toHaveBeenLastCalledWith("25", "C002");
|
||||
});
|
||||
|
||||
it("should not display if the activity has payouts", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [
|
||||
{
|
||||
payoutLineItem: {
|
||||
id: 1,
|
||||
activityPeriod: moment.utc(),
|
||||
departmentId: 25,
|
||||
role: {
|
||||
description: "Teller",
|
||||
name: "Teller Doe",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 1
|
||||
},
|
||||
product: "ATMCard",
|
||||
goalId: 2,
|
||||
goalValue: 3,
|
||||
commissionId: 4,
|
||||
commissionValue: 5,
|
||||
employeeId: "123",
|
||||
activityTotal: 100,
|
||||
payoutTotal: 200,
|
||||
slidingPayout: 12,
|
||||
activityPayouts: undefined,
|
||||
payoutProfileId: undefined
|
||||
}
|
||||
}
|
||||
],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferRequest
|
||||
departments={[]}
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("[data-test='transfer-request-form']").length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it("should not display if the activity is in PendingTransfer state", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.PendingTransfer
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferRequest
|
||||
departments={[]}
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("[data-test='transfer-request-form']").length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
});
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, FormGroup, Input, Label } from "reactstrap";
|
||||
import PermissionGuard from "../../containers/shared/PermissionGuard";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
import { IActivity } from "../../shared/models/Activity";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import Translate from "../../translate/Translate";
|
||||
import UserSearchSelect from "../activity/manual-activity-wizard/UserSearchSelect";
|
||||
|
||||
interface IActivityTransferRequestForm {
|
||||
departmentId: string;
|
||||
employeeId: string;
|
||||
}
|
||||
|
||||
interface IActivityTransferRequestProps {
|
||||
departments: IDepartment[];
|
||||
activity: IActivity;
|
||||
onFormSubmit: (departmentId: string, employeeId: string) => void;
|
||||
}
|
||||
|
||||
const formTranslate =
|
||||
Translate.Get.components.activityDetails.activityTransferRequest;
|
||||
|
||||
const ActivityTransferRequest: React.FC<IActivityTransferRequestProps> = (
|
||||
props: IActivityTransferRequestProps
|
||||
) => {
|
||||
const [formState, setFormState] = useState<IActivityTransferRequestForm>({
|
||||
departmentId: "",
|
||||
employeeId: ""
|
||||
});
|
||||
|
||||
const submitRequest = () => {
|
||||
props.onFormSubmit(formState.departmentId, formState.employeeId);
|
||||
};
|
||||
const departments = props.departments;
|
||||
|
||||
// BUG? Did they tell you this is wrong and that it should work like approvals?
|
||||
// OK. Just inject the activity.data.authorized from the parent and check if it can go to "PendingTransfer".
|
||||
// For now they haven't asked for this, so still use the permission.
|
||||
return ActivityService.isEligbleForTransfer(props.activity) ? (
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_ASSOCIATES,
|
||||
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_DEPARTMENTS
|
||||
]}
|
||||
>
|
||||
<div data-test="transfer-request-form">
|
||||
<h5>{formTranslate.title}</h5>
|
||||
<FormGroup>
|
||||
<Label>{formTranslate.userSearch.label}</Label>
|
||||
<UserSearchSelect
|
||||
data-test="appUser-search-select"
|
||||
employeeId=""
|
||||
onUserSelected={(_, newValue) => {
|
||||
setFormState((prevState) => ({
|
||||
departmentId: prevState.departmentId,
|
||||
employeeId: newValue
|
||||
}));
|
||||
}}
|
||||
showValidation={false}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{formTranslate.department.label}</Label>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="dept-input"
|
||||
value={formState.departmentId}
|
||||
onChange={(event) => {
|
||||
const departmentId = event.target.value;
|
||||
setFormState((prevState) => ({
|
||||
departmentId,
|
||||
employeeId: prevState.employeeId
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-option"
|
||||
hidden={formState.departmentId !== ""}
|
||||
key=""
|
||||
value=""
|
||||
>
|
||||
{formTranslate.department.placeholder}
|
||||
</option>
|
||||
{departments.map((department) => {
|
||||
return (
|
||||
<option
|
||||
key={department.id}
|
||||
value={department.id.toString()}
|
||||
>
|
||||
{department.departmentName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
<Button
|
||||
data-test="activity-transfer-request-button"
|
||||
color="primary"
|
||||
onClick={submitRequest}
|
||||
disabled={
|
||||
formState.employeeId.length === 0 ||
|
||||
isNaN(parseInt(formState.departmentId))
|
||||
}
|
||||
>
|
||||
{formTranslate.submitButton}
|
||||
</Button>
|
||||
</div>
|
||||
</PermissionGuard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTransferRequest;
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
IActivity,
|
||||
SystemType
|
||||
} from "../../shared/models/Activity";
|
||||
import ActivityTransferToOwnDepartmentRequest from "./ActivityTransferToOwnDepartmentRequest";
|
||||
import { IDepartmentAssignment } from "../../shared/models/DepartmentAssignment";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
|
||||
describe("ActivityTransferToOwnDepartmentRequest", () => {
|
||||
it("should call props onFormSubmit with selected department and current activity employeeId", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
|
||||
const mockOwnedDepartments: IDepartmentAssignment[] = [
|
||||
{
|
||||
id: 3,
|
||||
employeeId: "C0000",
|
||||
departmentId: 12,
|
||||
startDate: moment(),
|
||||
endDate: undefined,
|
||||
note: "Test",
|
||||
isHome: true,
|
||||
slidingPayout: 1,
|
||||
created: moment(),
|
||||
createdBy: "George Frick",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdatedBy: "Incentive Manager",
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedByType: UserType.System,
|
||||
role: {
|
||||
name: "test role",
|
||||
rank: 1,
|
||||
description: "test test",
|
||||
hasCommissions: false,
|
||||
permissions: [
|
||||
{
|
||||
code: "Secret",
|
||||
description: "Secret Test Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
employeeId: "C0000",
|
||||
departmentId: 14,
|
||||
startDate: moment(),
|
||||
endDate: undefined,
|
||||
note: "Test",
|
||||
isHome: true,
|
||||
slidingPayout: 1,
|
||||
created: moment(),
|
||||
createdBy: "George Frick",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdatedBy: "Incentive Manager",
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedByType: UserType.System,
|
||||
role: {
|
||||
name: "test role",
|
||||
rank: 1,
|
||||
description: "test test",
|
||||
hasCommissions: false,
|
||||
permissions: [
|
||||
{
|
||||
code: "Secret",
|
||||
description: "Secret Test Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferToOwnDepartmentRequest
|
||||
allDepartments={[]}
|
||||
ownedDepartments={mockOwnedDepartments}
|
||||
currentEmployeeId="C0000"
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(
|
||||
"[data-test='activity-transfer-request-own-department-button']"
|
||||
)
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
|
||||
wrapper.find("[data-test='dept-input']").prop<any>("onChange")({
|
||||
target: { value: "25" }
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(
|
||||
"[data-test='activity-transfer-request-own-department-button']"
|
||||
)
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
|
||||
wrapper
|
||||
.find(
|
||||
"[data-test='activity-transfer-request-own-department-button']"
|
||||
)
|
||||
.simulate("click");
|
||||
|
||||
expect(mockFormSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockFormSubmit).toHaveBeenLastCalledWith("25", "C0000");
|
||||
});
|
||||
|
||||
it("it should not display if the activity is not owned by the current appUser", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
|
||||
const mockOwnedDepartments: IDepartmentAssignment[] = [
|
||||
{
|
||||
id: 3,
|
||||
employeeId: "C0000",
|
||||
departmentId: 12,
|
||||
startDate: moment(),
|
||||
endDate: undefined,
|
||||
note: "Test",
|
||||
isHome: true,
|
||||
slidingPayout: 1,
|
||||
created: moment(),
|
||||
createdBy: "George Frick",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdatedBy: "Incentive Manager",
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedByType: UserType.System,
|
||||
role: {
|
||||
name: "test role",
|
||||
rank: 1,
|
||||
description: "test test",
|
||||
hasCommissions: false,
|
||||
permissions: [
|
||||
{
|
||||
code: "Secret",
|
||||
description: "Secret Test Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
employeeId: "C0000",
|
||||
departmentId: 14,
|
||||
startDate: moment(),
|
||||
endDate: undefined,
|
||||
note: "Test",
|
||||
isHome: true,
|
||||
slidingPayout: 1,
|
||||
created: moment(),
|
||||
createdBy: "George Frick",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdatedBy: "Incentive Manager",
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedByType: UserType.System,
|
||||
role: {
|
||||
name: "test role",
|
||||
rank: 1,
|
||||
description: "test test",
|
||||
hasCommissions: false,
|
||||
permissions: [
|
||||
{
|
||||
code: "Secret",
|
||||
description: "Secret Test Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferToOwnDepartmentRequest
|
||||
allDepartments={[]}
|
||||
ownedDepartments={mockOwnedDepartments}
|
||||
currentEmployeeId="C0001"
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='transfer-request-own-department-form']")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("it should not display if the appUser only has one current department", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
|
||||
const mockOwnedDepartments: IDepartmentAssignment[] = [
|
||||
{
|
||||
id: 3,
|
||||
employeeId: "C0000",
|
||||
departmentId: 12,
|
||||
startDate: moment(),
|
||||
endDate: undefined,
|
||||
note: "Test",
|
||||
isHome: true,
|
||||
slidingPayout: 1,
|
||||
created: moment(),
|
||||
createdBy: "George Frick",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdatedBy: "Incentive Manager",
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedByType: UserType.System,
|
||||
role: {
|
||||
name: "test role",
|
||||
rank: 1,
|
||||
description: "test test",
|
||||
hasCommissions: false,
|
||||
permissions: [
|
||||
{
|
||||
code: "Secret",
|
||||
description: "Secret Test Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferToOwnDepartmentRequest
|
||||
allDepartments={[]}
|
||||
ownedDepartments={mockOwnedDepartments}
|
||||
currentEmployeeId="C0000"
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='transfer-request-own-department-form']")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should not display if the activity has payouts", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [
|
||||
{
|
||||
payoutLineItem: {
|
||||
id: 1,
|
||||
activityPeriod: moment.utc(),
|
||||
departmentId: 25,
|
||||
role: {
|
||||
description: "Teller",
|
||||
name: "Teller Doe",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 1
|
||||
},
|
||||
product: "ATMCard",
|
||||
goalId: 2,
|
||||
goalValue: 3,
|
||||
commissionId: 4,
|
||||
commissionValue: 5,
|
||||
employeeId: "123",
|
||||
activityTotal: 100,
|
||||
payoutTotal: 200,
|
||||
slidingPayout: 12,
|
||||
activityPayouts: undefined,
|
||||
payoutProfileId: undefined
|
||||
}
|
||||
}
|
||||
],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferToOwnDepartmentRequest
|
||||
allDepartments={[]}
|
||||
ownedDepartments={[]}
|
||||
currentEmployeeId="C0000"
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='transfer-request-own-department-form']")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should not display if the activity is in PendingTransfer state", () => {
|
||||
const mockFormSubmit = jest.fn();
|
||||
const mockActivity: IActivity = {
|
||||
id: 1,
|
||||
product: Product2.ATMCard,
|
||||
sourceSystem: SystemType.Dna,
|
||||
activityPayouts: [],
|
||||
departmentId: "1",
|
||||
detail: {},
|
||||
employeeEmail: "[email protected]",
|
||||
employeeId: "C0000",
|
||||
type: ActivityType.Sale,
|
||||
state: ActivityState.PendingTransfer
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ActivityTransferToOwnDepartmentRequest
|
||||
allDepartments={[]}
|
||||
ownedDepartments={[]}
|
||||
currentEmployeeId="C0000"
|
||||
activity={mockActivity}
|
||||
onFormSubmit={mockFormSubmit}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='transfer-request-own-department-form']")
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, FormGroup, Input, Label } from "reactstrap";
|
||||
import { IActivity } from "../../shared/models/Activity";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IDepartmentAssignment } from "../../shared/models/DepartmentAssignment";
|
||||
|
||||
interface IActivityTransferRequestForm {
|
||||
departmentId: string;
|
||||
employeeId: string;
|
||||
}
|
||||
|
||||
interface IActivityTransferToOwnDepartmentRequestProps {
|
||||
ownedDepartments: IDepartmentAssignment[] | undefined;
|
||||
allDepartments: IDepartment[];
|
||||
activity: IActivity;
|
||||
currentEmployeeId: string;
|
||||
onFormSubmit: (departmentId: string, employeeId: string) => void;
|
||||
}
|
||||
|
||||
const formTranslate =
|
||||
Translate.Get.components.activityDetails
|
||||
.activityTransferToOwnDepartmentRequest;
|
||||
|
||||
const ActivityTransferToOwnDepartmentRequest: React.FC<IActivityTransferToOwnDepartmentRequestProps> = (
|
||||
props: IActivityTransferToOwnDepartmentRequestProps
|
||||
) => {
|
||||
const [formState, setFormState] = useState<IActivityTransferRequestForm>({
|
||||
departmentId: "",
|
||||
employeeId: props.activity.employeeId
|
||||
});
|
||||
|
||||
const submitRequest = () => {
|
||||
props.onFormSubmit(formState.departmentId, formState.employeeId);
|
||||
};
|
||||
|
||||
function isActivityOwnedByUserWithMultipleDepartments(): boolean {
|
||||
return (
|
||||
props.ownedDepartments != null &&
|
||||
props.ownedDepartments.length >= 2 &&
|
||||
props.currentEmployeeId == props.activity.employeeId
|
||||
);
|
||||
}
|
||||
|
||||
// BUG? Did they tell you this is wrong and that it should work like approvals?
|
||||
// OK. Just inject the activity.data.authorized from the parent and check if it can go to "PendingTransfer".
|
||||
// For now they haven't asked for this, so still use the permission.
|
||||
return ActivityService.isEligbleForTransfer(props.activity) &&
|
||||
isActivityOwnedByUserWithMultipleDepartments() ? (
|
||||
<div data-test="transfer-request-own-department-form">
|
||||
<h5>{formTranslate.title}</h5>
|
||||
<FormGroup>
|
||||
<Label>{formTranslate.department.label}</Label>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="dept-input"
|
||||
value={formState.departmentId}
|
||||
onChange={(event) => {
|
||||
const departmentId = event.target.value;
|
||||
setFormState((prevState) => ({
|
||||
departmentId,
|
||||
employeeId: prevState.employeeId
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-option"
|
||||
hidden={formState.departmentId !== ""}
|
||||
key=""
|
||||
value=""
|
||||
>
|
||||
{formTranslate.department.placeholder}
|
||||
</option>
|
||||
{props.allDepartments.map((department) => {
|
||||
if (
|
||||
props.ownedDepartments?.some(
|
||||
(ownedDepartment) =>
|
||||
ownedDepartment.departmentId ===
|
||||
department.id &&
|
||||
department.id.toString() !=
|
||||
props.activity.departmentId
|
||||
)
|
||||
)
|
||||
return (
|
||||
<option
|
||||
key={department.id}
|
||||
value={department.id.toString()}
|
||||
>
|
||||
{department.departmentName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
<Button
|
||||
data-test="activity-transfer-request-own-department-button"
|
||||
color="primary"
|
||||
onClick={submitRequest}
|
||||
disabled={
|
||||
formState.employeeId.length === 0 ||
|
||||
isNaN(parseInt(formState.departmentId))
|
||||
}
|
||||
>
|
||||
{formTranslate.submitButton}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTransferToOwnDepartmentRequest;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import ActivityAuditLog from "./ActivityAuditLog";
|
||||
import AsyncState from "./../../../shared/enumerations/AsyncState";
|
||||
import { IActivitySearchForm } from "../../../shared/models/Activity";
|
||||
|
||||
describe("An associate opens the Activity Audit Log", () => {
|
||||
let prevCalled = 0;
|
||||
let nextCalled = 0;
|
||||
const getPrev = () => {
|
||||
prevCalled++;
|
||||
};
|
||||
const getNext = () => {
|
||||
nextCalled++;
|
||||
};
|
||||
|
||||
it("displays loading message when loading", () => {
|
||||
let testCalls = 0;
|
||||
let lastFilter: IActivitySearchForm = {
|
||||
query: "",
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
state: "",
|
||||
product: "",
|
||||
employeeId: "",
|
||||
departmentId: 0
|
||||
};
|
||||
const testCall = (sort: string) => {
|
||||
testCalls++;
|
||||
};
|
||||
|
||||
let wrapper = shallow(
|
||||
<ActivityAuditLog
|
||||
activityContainer={{
|
||||
state: AsyncState.Loading,
|
||||
data: {
|
||||
currentPage: 1,
|
||||
items: [],
|
||||
totalItems: 1,
|
||||
totalPages: 1,
|
||||
nextPage: "",
|
||||
previousPage: ""
|
||||
}
|
||||
}}
|
||||
getNextPage={getPrev}
|
||||
getPrevPage={getNext}
|
||||
form={lastFilter}
|
||||
changeSort={testCall}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
wrapper.find("Alert").find('[color="warning"]').prop("hidden")
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find("Alert").find('[color="danger"]').prop("hidden")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("displays loading message when error", () => {
|
||||
let testCalls = 0;
|
||||
let lastFilter: IActivitySearchForm = {
|
||||
query: "",
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
state: "",
|
||||
product: "",
|
||||
employeeId: "",
|
||||
departmentId: 0
|
||||
};
|
||||
const testCall = (sort: string) => {
|
||||
testCalls++;
|
||||
};
|
||||
let wrapper = shallow(
|
||||
<ActivityAuditLog
|
||||
activityContainer={{
|
||||
state: AsyncState.Failure,
|
||||
data: {
|
||||
currentPage: 1,
|
||||
items: [],
|
||||
totalItems: 1,
|
||||
totalPages: 1,
|
||||
nextPage: "",
|
||||
previousPage: ""
|
||||
}
|
||||
}}
|
||||
getNextPage={getPrev}
|
||||
getPrevPage={getNext}
|
||||
form={lastFilter}
|
||||
changeSort={testCall}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
wrapper.find("Alert").find('[color="warning"]').prop("hidden")
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find("Alert").find('[color="danger"]').prop("hidden")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("displays no message when success", () => {
|
||||
let testCalls = 0;
|
||||
let lastFilter: IActivitySearchForm = {
|
||||
query: "",
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
state: "",
|
||||
product: "",
|
||||
employeeId: "",
|
||||
departmentId: 0
|
||||
};
|
||||
const testCall = (sort: string) => {
|
||||
testCalls++;
|
||||
};
|
||||
let wrapper = shallow(
|
||||
<ActivityAuditLog
|
||||
activityContainer={{
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
currentPage: 1,
|
||||
items: [],
|
||||
totalItems: 1,
|
||||
totalPages: 1,
|
||||
nextPage: "",
|
||||
previousPage: ""
|
||||
}
|
||||
}}
|
||||
getNextPage={getPrev}
|
||||
getPrevPage={getNext}
|
||||
form={lastFilter}
|
||||
changeSort={testCall}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
wrapper.find("Alert").find('[color="warning"]').prop("hidden")
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find("Alert").find('[color="danger"]').prop("hidden")
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import { Alert, Col, Row } from "reactstrap";
|
||||
import {
|
||||
IActivitySearchForm,
|
||||
IAuditableActivity
|
||||
} from "../../../shared/models/Activity";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import ActivityTable from "../activity-table/ActivityTable";
|
||||
import IPagedResponse from "../../../shared/types/IPagedResponse";
|
||||
import PagedContentControls from "../../paged-content-controls/PagedContentControls";
|
||||
|
||||
export interface IActivityAuditLogProps {
|
||||
activityContainer: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
|
||||
getNextPage: () => void;
|
||||
getPrevPage: () => void;
|
||||
changeSort: (sort: string) => void;
|
||||
form: IActivitySearchForm;
|
||||
}
|
||||
|
||||
const ActivityAuditLogTranslate =
|
||||
Translate.Get.components.activity.activityAuditLog;
|
||||
|
||||
const ActivityAuditLog: React.FC<IActivityAuditLogProps> = (props) => {
|
||||
const changeSort = (headerName: string) => {
|
||||
if (props.changeSort) {
|
||||
props.changeSort(headerName);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-row justify-content-center alight-items-center">
|
||||
<div>
|
||||
<Alert
|
||||
color="warning"
|
||||
hidden={
|
||||
props.activityContainer.state !== AsyncState.Loading
|
||||
}
|
||||
>
|
||||
<p className="m-0">
|
||||
{ActivityAuditLogTranslate.loadingMessage}
|
||||
</p>
|
||||
</Alert>
|
||||
<Alert
|
||||
color="danger"
|
||||
hidden={
|
||||
props.activityContainer.state !== AsyncState.Failure
|
||||
}
|
||||
>
|
||||
<p className="m-0">
|
||||
{ActivityAuditLogTranslate.errorMessage}
|
||||
</p>
|
||||
</Alert>
|
||||
<div className="d-flex flex-row justify-content-center align-items-center">
|
||||
<PagedContentControls
|
||||
nextPage={() => props.getNextPage()}
|
||||
prevPage={() => props.getPrevPage()}
|
||||
pagedResponse={props.activityContainer.data}
|
||||
/>
|
||||
</div>
|
||||
<ActivityTable
|
||||
activities={props.activityContainer.data?.items ?? []}
|
||||
changeSort={changeSort}
|
||||
sortBy={props.form.sortBy}
|
||||
sortDescending={props.form.sortDescending}
|
||||
/>
|
||||
<div className="d-flex flex-row justify-content-center align-items-center">
|
||||
<PagedContentControls
|
||||
nextPage={() => props.getNextPage()}
|
||||
prevPage={() => props.getPrevPage()}
|
||||
pagedResponse={props.activityContainer.data}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityAuditLog;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import "./ActivityTable.scss";
|
||||
import React from "react";
|
||||
import { IAuditableActivity } from "./../../../shared/models/Activity";
|
||||
import moment from "moment";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
import { AutoTable } from "../../auto-table/AutoTable";
|
||||
import Configuration from "../../../shared/configuration/Configuration";
|
||||
import DepartmentDisplay from "../../../containers/shared/DepartmentDisplay";
|
||||
import UserDisplay from "../../../containers/shared/UserDisplay";
|
||||
import DateTimeService from "../../../shared/services/DateTimeService";
|
||||
|
||||
interface IActivityTable {
|
||||
activities?: IAuditableActivity[];
|
||||
}
|
||||
|
||||
const ActivityTableTranslate = Translate.Get.components.activity.activityTable;
|
||||
|
||||
const urlPrefix = Configuration.PrefixAppUrlWithBasename(
|
||||
Configuration.Get.appUrls.activity
|
||||
);
|
||||
|
||||
const ActivitySummaryTable: React.FC<IActivityTable> = (props) => {
|
||||
const sortByUpdatedDate = (
|
||||
data: IAuditableActivity[]
|
||||
): IAuditableActivity[] => {
|
||||
return data.sort((a, b) => moment(b.lastUpdated).diff(a.lastUpdated));
|
||||
};
|
||||
|
||||
const headers = [
|
||||
ActivityTableTranslate.headers.lastUpdated,
|
||||
ActivityTableTranslate.headers.id,
|
||||
ActivityTableTranslate.headers.createdBy,
|
||||
ActivityTableTranslate.headers.sourceSystem,
|
||||
ActivityTableTranslate.headers.detailAccountNumber,
|
||||
ActivityTableTranslate.headers.detailSubjectPersonNbr,
|
||||
ActivityTableTranslate.headers.detailMemberName
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IAuditableActivity) =>
|
||||
DateTimeService.CastServerTimeToClientTimeString(x.lastUpdated),
|
||||
(x: IAuditableActivity) => (
|
||||
<a
|
||||
data-test="activity-detail-link"
|
||||
href={urlPrefix + "/" + x.id}
|
||||
className="alert-link"
|
||||
>
|
||||
{x.id}
|
||||
</a>
|
||||
),
|
||||
(x: IAuditableActivity) => x.createdBy,
|
||||
(x: IAuditableActivity) => x.sourceSystem,
|
||||
(x: IAuditableActivity) => x.detail?.accountNumber ?? "-",
|
||||
(x: IAuditableActivity) => x.detail?.subjectPersonNbr ?? "-",
|
||||
(x: IAuditableActivity) => x.detail?.memberName ?? "-"
|
||||
];
|
||||
|
||||
if (props.activities && props.activities.length > 0) {
|
||||
return (
|
||||
<AutoTable
|
||||
elements={sortByUpdatedDate(props.activities)}
|
||||
headers={headers}
|
||||
cells={cells}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AutoTable elements={[]} headers={headers} cells={[]} />;
|
||||
};
|
||||
|
||||
export default ActivitySummaryTable;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.scrollable-top-parent {
|
||||
width: 95vw;
|
||||
overflow-x: auto;
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
.scrollable-top-child {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import ActivityTable from "./ActivityTable";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
IAuditableActivity,
|
||||
SystemType
|
||||
} from "../../../shared/models/Activity";
|
||||
import { UserType } from "../../../shared/models/Auditable";
|
||||
import Product2 from "../../../shared/enumerations/Product";
|
||||
import moment from "moment";
|
||||
|
||||
describe("An associate views the activity table", () => {
|
||||
it("Displays a message when there is no data to load", () => {
|
||||
let wrapper = shallow(<ActivityTable activities={undefined} />);
|
||||
expect(wrapper.find('[data-test="no-data-message"]').length).toBe(1);
|
||||
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays a message when there is no data to load", () => {
|
||||
let wrapper = shallow(<ActivityTable activities={[]} />);
|
||||
expect(wrapper.find('[data-test="no-data-message"]').length).toBe(1);
|
||||
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it("Does not display message when there is data", () => {
|
||||
const activities: IAuditableActivity[] = [
|
||||
{
|
||||
id: 100,
|
||||
lastUpdatedByType: UserType.System,
|
||||
lastUpdatedBy: "Tester",
|
||||
lastUpdated: moment().utc(),
|
||||
createdByType: UserType.System,
|
||||
createdBy: "Tester1",
|
||||
created: moment().utc(),
|
||||
employeeId: "11771",
|
||||
departmentId: "56",
|
||||
accepted: moment().utc(),
|
||||
detail: {},
|
||||
product: Product2.Vehicle,
|
||||
sourceCreated: moment().utc(),
|
||||
sourceId: "11566",
|
||||
sourceSystem: SystemType.Dna,
|
||||
state: ActivityState.Accepted,
|
||||
type: ActivityType.Sale,
|
||||
activityPayouts: [],
|
||||
employeeEmail: ""
|
||||
}
|
||||
];
|
||||
let wrapper = shallow(<ActivityTable activities={activities} />);
|
||||
expect(wrapper.find("[data-test='no-data-message']").length).toBe(0);
|
||||
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import "./ActivityTable.scss";
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
IAuditableActivity
|
||||
} from "../../../shared/models/Activity";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
import {
|
||||
AutoTable,
|
||||
IAutoTableHeader,
|
||||
IColumnSort
|
||||
} from "../../auto-table/AutoTable";
|
||||
import Configuration from "../../../shared/configuration/Configuration";
|
||||
import DepartmentDisplay from "../../../containers/shared/DepartmentDisplay";
|
||||
import UserDisplay from "../../../containers/shared/UserDisplay";
|
||||
import DateTimeService from "../../../shared/services/DateTimeService";
|
||||
|
||||
export interface IActivityTable {
|
||||
activities?: IAuditableActivity[];
|
||||
changeSort?: (sort: string) => void;
|
||||
sortBy?: string;
|
||||
sortDescending?: boolean;
|
||||
activityTypeFilter?: ActivityType;
|
||||
activityStateFilter?: ActivityState[];
|
||||
}
|
||||
|
||||
const ActivityTableTranslate = Translate.Get.components.activity.activityTable;
|
||||
|
||||
const urlPrefix = Configuration.PrefixAppUrlWithBasename(
|
||||
Configuration.Get.appUrls.activity
|
||||
);
|
||||
|
||||
const ActivityTable: React.FC<IActivityTable> = (props) => {
|
||||
const headers = [
|
||||
ActivityTableTranslate.headers.accepted,
|
||||
ActivityTableTranslate.headers.id,
|
||||
ActivityTableTranslate.headers.product,
|
||||
ActivityTableTranslate.headers.type,
|
||||
ActivityTableTranslate.headers.createdBy,
|
||||
ActivityTableTranslate.headers.sourceSystem,
|
||||
ActivityTableTranslate.headers.departmentId,
|
||||
ActivityTableTranslate.headers.personId,
|
||||
ActivityTableTranslate.headers.state,
|
||||
ActivityTableTranslate.headers.detailAccountNumber,
|
||||
ActivityTableTranslate.headers.detailSubjectPersonNbr,
|
||||
ActivityTableTranslate.headers.detailSubjectOrgNbr,
|
||||
ActivityTableTranslate.headers.detailMemberName
|
||||
];
|
||||
|
||||
const fieldNames = [
|
||||
"Accepted",
|
||||
"Id",
|
||||
"Product",
|
||||
"Type",
|
||||
"CreatedBy",
|
||||
"SourceSystem",
|
||||
"",
|
||||
"",
|
||||
"State",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
];
|
||||
|
||||
const changeSort = (headerName: string) => {
|
||||
if (props.changeSort) {
|
||||
props.changeSort(headerName);
|
||||
}
|
||||
};
|
||||
|
||||
const sortHeaders: IAutoTableHeader[] = headers.map((headerName, idx) => {
|
||||
return {
|
||||
display: headerName,
|
||||
onClick: () => {
|
||||
if (fieldNames[idx] !== "") {
|
||||
changeSort(fieldNames[idx]);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const cells = [
|
||||
(x: IAuditableActivity) =>
|
||||
DateTimeService.CastServerTimeToClientTimeString(x.accepted),
|
||||
(x: IAuditableActivity) => (
|
||||
<a
|
||||
data-test="activity-detail-link"
|
||||
href={urlPrefix + "/" + x.id}
|
||||
className="alert-link"
|
||||
>
|
||||
{x.id}
|
||||
</a>
|
||||
),
|
||||
(x: IAuditableActivity) => x.product,
|
||||
(x: IAuditableActivity) => x.type,
|
||||
(x: IAuditableActivity) => x.createdBy,
|
||||
(x: IAuditableActivity) => x.sourceSystem,
|
||||
(x: IAuditableActivity) => (
|
||||
<DepartmentDisplay departmentId={parseInt(x.departmentId, 10)} />
|
||||
),
|
||||
(x: IAuditableActivity) => <UserDisplay employeeId={x.employeeId} />,
|
||||
(x: IAuditableActivity) => x.state,
|
||||
(x: IAuditableActivity) => x.detail?.accountNumber ?? "-",
|
||||
(x: IAuditableActivity) => x.detail?.subjectPersonNbr ?? "-",
|
||||
(x: IAuditableActivity) => x.detail?.subjectOrgNbr ?? "-",
|
||||
(x: IAuditableActivity) => x.detail?.memberName ?? "-"
|
||||
];
|
||||
|
||||
let activities = props.activities || [];
|
||||
if (props.activityTypeFilter) {
|
||||
activities = activities.filter(
|
||||
(activity) => activity.type === props.activityTypeFilter
|
||||
);
|
||||
}
|
||||
|
||||
if (props.activityStateFilter && props.activityStateFilter.length > 0) {
|
||||
activities = activities.filter((activity) =>
|
||||
props.activityStateFilter?.includes(activity.state)
|
||||
);
|
||||
}
|
||||
|
||||
const findIndexForColumn = (column?: string) => {
|
||||
return fieldNames.indexOf(column || "LastUpdated");
|
||||
};
|
||||
|
||||
const x: IColumnSort = {
|
||||
index: findIndexForColumn(props.sortBy),
|
||||
isReversed: props.sortDescending ?? false
|
||||
};
|
||||
|
||||
if (activities.length > 0) {
|
||||
return (
|
||||
<div className="pt-2" data-test="autotable-container">
|
||||
<AutoTable
|
||||
className="border"
|
||||
elements={activities}
|
||||
headers={sortHeaders}
|
||||
cells={cells}
|
||||
sortedColumn={x}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="scrollable-top-parent">
|
||||
<div className="scrollable-top-child d-flex flex-row justify-content-center align-items-center bg-light">
|
||||
<p data-test="no-data-message" className=" mt-2">
|
||||
<i>{ActivityTableTranslate.noData}</i>
|
||||
</p>
|
||||
</div>
|
||||
<AutoTable
|
||||
className="scrollable-top-child"
|
||||
elements={[]}
|
||||
headers={sortHeaders}
|
||||
cells={[]}
|
||||
sortedColumn={props.changeSort ? x : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTable;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ActivityState,
|
||||
IActivitySearchForm
|
||||
} from "../../../shared/models/Activity";
|
||||
import ActivityTable, {
|
||||
IActivityTable
|
||||
} from "../../activity/activity-table/ActivityTable";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import { FormGroup, Label } from "reactstrap";
|
||||
import Select from "react-select";
|
||||
|
||||
const ActivityTableTranslate = Translate.Get.components.activity.activityTable;
|
||||
|
||||
const ActivityTableWithStateFilter: React.FC<IActivityTable> = (
|
||||
props: IActivityTable
|
||||
) => {
|
||||
const [form, setForm] = useState<IActivitySearchForm>({
|
||||
state: ["Accepted"],
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
query: "",
|
||||
product: "",
|
||||
type: "",
|
||||
departmentId: 0,
|
||||
employeeId: ""
|
||||
});
|
||||
|
||||
const activityStateOptions = Object.values(ActivityState).map(
|
||||
(state) => new Option(state, state)
|
||||
);
|
||||
|
||||
const acceptedActivityStateOption = activityStateOptions.find(
|
||||
(state) => state.value === "Accepted"
|
||||
);
|
||||
|
||||
const updateState = (newState: string[]) => {
|
||||
setForm({
|
||||
...form,
|
||||
state: newState
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<FormGroup className="form-inline">
|
||||
<Label style={{ marginRight: "5px" }}>
|
||||
{ActivityTableTranslate.headers.state}
|
||||
</Label>
|
||||
<Select
|
||||
isMulti
|
||||
name="ActivityState"
|
||||
className="basic-multi-select"
|
||||
placeholder="All States"
|
||||
options={activityStateOptions}
|
||||
defaultValue={acceptedActivityStateOption}
|
||||
onChange={(event) => {
|
||||
updateState(Array.from(event.map((e) => e.value)));
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActivityTable
|
||||
activities={props.activities}
|
||||
activityStateFilter={form.state as ActivityState[]}
|
||||
activityTypeFilter={props.activityTypeFilter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTableWithStateFilter;
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import ActivityDetailsForm from "./ActivityDetailsForm";
|
||||
import { IManualActivityFormState } from "./../../../shared/models/Activity";
|
||||
import moment from "moment";
|
||||
import { Product2 } from "./../../../shared/enumerations/Product";
|
||||
import { wrap } from "module";
|
||||
|
||||
describe("A appUser inputing data into the Activity Details Form", () => {
|
||||
it("Builds State From Input Changes", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
detailAccountBalance: undefined,
|
||||
detailAccountNumber: undefined,
|
||||
detailAccountStatus: undefined,
|
||||
detailMemberName: undefined,
|
||||
detailSubjectPersonNbr: undefined,
|
||||
detailSubjectOrgNbr: undefined,
|
||||
detailDebtProtectionAddDate: undefined,
|
||||
detailIsDebtProtectionEligible: undefined,
|
||||
sourceId: undefined
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityDetailsForm
|
||||
formData={formData}
|
||||
//Overwrite form data with what the simulated change returns
|
||||
handleChange={(newstate) => {
|
||||
formData = newstate;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
//Have to manually update the formData using setProps to mock that things are hooked up to wizard
|
||||
|
||||
wrapper
|
||||
.find('[name="AccountNumber"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "1001" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="MemberNumber"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "8321010" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="MemberName"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "Batman" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="AccountStatus"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "Open" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="AccountBalance"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: 10000 } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="detailDebtProtectionEligible"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { checked: true } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="detailDebtProtectionAddDate"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "2021-01-01T12:00" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="orgNumber"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "10" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="MemberNumber"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "1021" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
expect(formData).toEqual({
|
||||
detailAccountBalance: 10000,
|
||||
detailAccountNumber: "1001",
|
||||
detailAccountStatus: "Open",
|
||||
detailMemberName: "Batman",
|
||||
detailSubjectPersonNbr: "1021",
|
||||
detailSubjectOrgNbr: "10",
|
||||
detailDebtProtectionAddDate: moment("2021-01-01T12:00"),
|
||||
detailIsDebtProtectionEligible: true
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find('[name="AccountBalance"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: 10000000 } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="AccountStatus"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "Closed" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="MemberName"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "The Joker" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="detailDebtProtectionEligible"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { checked: false } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="detailDebtProtectionAddDate"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "2021-01-01T12:00" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="MemberNumber"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "1029" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="orgNumber"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "11" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
expect(formData).toEqual({
|
||||
detailAccountBalance: 10000000,
|
||||
detailAccountNumber: "1001",
|
||||
detailAccountStatus: "Closed",
|
||||
detailMemberName: "The Joker",
|
||||
detailSubjectPersonNbr: "1029",
|
||||
detailSubjectOrgNbr: "11",
|
||||
detailDebtProtectionAddDate: moment("2021-01-01T12:00"),
|
||||
detailIsDebtProtectionEligible: false
|
||||
});
|
||||
});
|
||||
|
||||
describe("LIC Referrals have a special sourceId input", () => {
|
||||
it("Only shows SourceSystemId when required", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
detailAccountBalance: undefined,
|
||||
detailAccountNewBalance: undefined,
|
||||
detailAccountNumber: undefined,
|
||||
detailAccountStatus: undefined,
|
||||
detailMemberName: undefined,
|
||||
detailSubjectPersonNbr: undefined,
|
||||
detailSubjectOrgNbr: undefined,
|
||||
detailDebtProtectionAddDate: undefined,
|
||||
detailIsDebtProtectionEligible: undefined,
|
||||
sourceId: undefined
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityDetailsForm
|
||||
requireSourceIdField={false}
|
||||
formData={formData}
|
||||
//Overwrite form data with what the simulated change returns
|
||||
handleChange={(newstate) => {
|
||||
formData = newstate;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="source-id-col"]')
|
||||
.last()
|
||||
.prop("hidden")
|
||||
).toBe(true);
|
||||
|
||||
wrapper.setProps({ requireSourceIdField: true });
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="source-id-col"]')
|
||||
.last()
|
||||
.prop("hidden")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("Clears SourceSystemId when it is no longer required", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
detailAccountBalance: undefined,
|
||||
detailAccountNewBalance: undefined,
|
||||
detailAccountNumber: undefined,
|
||||
detailAccountStatus: undefined,
|
||||
detailMemberName: undefined,
|
||||
detailSubjectPersonNbr: undefined,
|
||||
detailSubjectOrgNbr: undefined,
|
||||
detailDebtProtectionAddDate: undefined,
|
||||
detailIsDebtProtectionEligible: undefined,
|
||||
sourceId: undefined
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityDetailsForm
|
||||
requireSourceIdField={false}
|
||||
formData={formData}
|
||||
//Overwrite form data with what the simulated change returns
|
||||
handleChange={(newstate) => {
|
||||
formData = newstate;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="source-id-col"]')
|
||||
.last()
|
||||
.prop("hidden")
|
||||
).toBe(true);
|
||||
|
||||
wrapper.setProps({ requireSourceIdField: true });
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="source-id-col"]')
|
||||
.last()
|
||||
.prop("hidden")
|
||||
).toBe(false);
|
||||
|
||||
wrapper
|
||||
.find('[name="SourceSystemId"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "100" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
expect(formData.sourceId).toEqual("100");
|
||||
|
||||
wrapper.setProps({ requireSourceIdField: false });
|
||||
wrapper.update();
|
||||
|
||||
expect(formData.sourceId).toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
import moment from "moment";
|
||||
import React, { useEffect } from "react";
|
||||
import { Row, Col, FormGroup, Input, Label, FormFeedback } from "reactstrap";
|
||||
import { IManualActivityFormState } from "../../../shared/models/Activity";
|
||||
import { tryUpdateStateWithInput } from "../../../shared/services/InputService";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
|
||||
type ActivityDetailsFormData = Pick<
|
||||
IManualActivityFormState,
|
||||
| "detailAccountBalance"
|
||||
| "detailAccountNumber"
|
||||
| "detailSubAccountNumber"
|
||||
| "detailSubjectOrgNbr"
|
||||
| "detailAccountStatus"
|
||||
| "detailMemberName"
|
||||
| "detailSubjectPersonNbr"
|
||||
| "showValidation"
|
||||
| "detailDebtProtectionAddDate"
|
||||
| "detailDebtProtectionRemoveDate"
|
||||
| "detailIsDebtProtectionEligible"
|
||||
| "sourceId"
|
||||
>;
|
||||
|
||||
export interface IActivitySourceFormProps {
|
||||
/**
|
||||
* The slice of state ActivitySourceForm is responsible for, to update the fields update the
|
||||
* ActivityDetailsFormData type in the component see example for shape.
|
||||
* @example type ActivityDetailsFormData = Pick<IManualActivityFormState, "field1"| "field2">
|
||||
*/
|
||||
formData: ActivityDetailsFormData;
|
||||
requireAccountStatusField: boolean;
|
||||
requireAccountBalanceField: boolean;
|
||||
requireAccountNumberField: boolean;
|
||||
requireSourceIdField: boolean;
|
||||
/**Dispatches the entire slice of state every time it changes.
|
||||
* Use the spread operator to apply this to the parent state
|
||||
* @param newData the entire slice of state handled by the form, re-built with latest change
|
||||
*/
|
||||
handleChange: (newData: ActivityDetailsFormData) => void;
|
||||
}
|
||||
|
||||
const activityDetailsFormTranslate =
|
||||
Translate.Get.components.activity.manualActivityWizard.activityDetailsForm;
|
||||
|
||||
const ActivityDetailsForm: React.FC<IActivitySourceFormProps> = (
|
||||
props: IActivitySourceFormProps
|
||||
) => {
|
||||
const regex = new RegExp("^([0]|[1-9][0-9]{0,9}([.][0-9]{0,2})?)$");
|
||||
|
||||
const onFormDataChange = (field: string, newValue: any) => {
|
||||
const updatedFormData = tryUpdateStateWithInput(
|
||||
props.formData,
|
||||
field,
|
||||
newValue
|
||||
);
|
||||
props.handleChange(updatedFormData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onFormDataChange("sourceId", "");
|
||||
}, [props.requireSourceIdField]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
<b>{activityDetailsFormTranslate.formLabel}</b>
|
||||
</span>
|
||||
<Row className="mb-2">
|
||||
<Col md="3">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityDetailsFormTranslate.accountNumber.label}
|
||||
<span
|
||||
hidden={!props.requireAccountNumberField}
|
||||
className="text-danger"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="AccountNumber"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailAccountNumber}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailAccountNumber",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required={props.requireAccountNumberField}
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.detailAccountNumber
|
||||
}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityDetailsFormTranslate.accountNumber
|
||||
.placeholder
|
||||
}
|
||||
/>
|
||||
<FormFeedback>
|
||||
Please enter an account number.
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="3">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activityDetailsFormTranslate
|
||||
.detailSubAccountNumber.label
|
||||
}{" "}
|
||||
</Label>
|
||||
<Input
|
||||
name="SubAccountNumber"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailSubAccountNumber}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailSubAccountNumber",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required={false}
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.detailSubAccountNumber
|
||||
}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityDetailsFormTranslate
|
||||
.detailSubAccountNumber.placeholder
|
||||
}
|
||||
/>
|
||||
<FormFeedback>
|
||||
Please enter sub account number
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="3">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityDetailsFormTranslate.memberNumber.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="MemberNumber"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailSubjectPersonNbr}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailSubjectPersonNbr",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required={props.formData.detailSubjectOrgNbr === ""}
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.detailSubjectOrgNbr &&
|
||||
!props.formData.detailSubjectPersonNbr
|
||||
}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityDetailsFormTranslate.memberNumber
|
||||
.placeholder
|
||||
}
|
||||
/>
|
||||
<FormFeedback>
|
||||
Please enter a member number or organization number.
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="3">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityDetailsFormTranslate.orgNumber.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="orgNumber"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailSubjectOrgNbr}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailSubjectOrgNbr",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required={
|
||||
props.formData.detailSubjectPersonNbr === ""
|
||||
}
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.detailSubjectOrgNbr &&
|
||||
!props.formData.detailSubjectPersonNbr
|
||||
}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityDetailsFormTranslate.orgNumber
|
||||
.placeholder
|
||||
}
|
||||
/>
|
||||
<FormFeedback>
|
||||
Please enter a member number or organization number.
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col
|
||||
data-test="source-id-col"
|
||||
md="4"
|
||||
hidden={!props.requireSourceIdField}
|
||||
>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityDetailsFormTranslate.sourceSystemId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="SourceSystemId"
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
required={props.requireSourceIdField}
|
||||
invalid={
|
||||
props.requireSourceIdField &&
|
||||
props.formData.sourceId == ""
|
||||
}
|
||||
placeholder={
|
||||
activityDetailsFormTranslate.sourceSystemId
|
||||
.placeholder
|
||||
}
|
||||
value={props.formData.sourceId}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"sourceId",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
Please enter the Id from the source system
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityDetailsFormTranslate.memberName.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="MemberName"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailMemberName}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailMemberName",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.detailMemberName
|
||||
}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityDetailsFormTranslate.memberName
|
||||
.placeholder
|
||||
}
|
||||
/>
|
||||
<FormFeedback>Please enter member name.</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activityDetailsFormTranslate
|
||||
.debtProtectionAddDate.label
|
||||
}
|
||||
</Label>
|
||||
<Input
|
||||
name="detailDebtProtectionAddDate"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailDebtProtectionAddDate
|
||||
?.format("YYYY-MM-DDTHH:mm")
|
||||
.toString()}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailDebtProtectionAddDate",
|
||||
moment(event.target.value)
|
||||
);
|
||||
}}
|
||||
type="datetime-local"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activityDetailsFormTranslate
|
||||
.debtProtectionRemoveDate.label
|
||||
}
|
||||
</Label>
|
||||
<Input
|
||||
name="detailDebtProtectionRemoveDate"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailDebtProtectionRemoveDate
|
||||
?.format("YYYY-MM-DDTHH:mm")
|
||||
.toString()}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailDebtProtectionRemoveDate",
|
||||
moment(event.target.value)
|
||||
);
|
||||
}}
|
||||
type="datetime-local"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col className="my-auto">
|
||||
<FormGroup check inline>
|
||||
<Label check inline>
|
||||
{
|
||||
activityDetailsFormTranslate
|
||||
.debtProtectionEligible.label
|
||||
}
|
||||
</Label>
|
||||
<Input
|
||||
style={{ width: "1.3em" }}
|
||||
className="ml-3"
|
||||
name="detailDebtProtectionEligible"
|
||||
bsSize="lg"
|
||||
checked={
|
||||
props.formData.detailIsDebtProtectionEligible
|
||||
}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailIsDebtProtectionEligible",
|
||||
event.target.checked
|
||||
);
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityDetailsFormTranslate.accountStatus.label}{" "}
|
||||
<span
|
||||
hidden={!props.requireAccountStatusField}
|
||||
className="text-danger"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="AccountStatus"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailAccountStatus}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailAccountStatus",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required={props.requireAccountStatusField}
|
||||
invalid={
|
||||
props.requireAccountStatusField &&
|
||||
props.formData.showValidation &&
|
||||
!props.formData.detailAccountStatus
|
||||
}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityDetailsFormTranslate.accountStatus
|
||||
.placeholder
|
||||
}
|
||||
/>
|
||||
<FormFeedback>
|
||||
Account Status is required for this Product/Activity
|
||||
Type combination.
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityDetailsFormTranslate.accountBalance.label}
|
||||
<span
|
||||
hidden={!props.requireAccountBalanceField}
|
||||
className="text-danger"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="AccountBalance"
|
||||
bsSize="sm"
|
||||
value={props.formData.detailAccountBalance}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"detailAccountBalance",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
required={props.requireAccountBalanceField}
|
||||
invalid={
|
||||
props.requireAccountBalanceField &&
|
||||
((props.formData.showValidation &&
|
||||
!props.formData.detailAccountBalance) ||
|
||||
!regex.test(
|
||||
props.formData.detailAccountBalance + ""
|
||||
))
|
||||
}
|
||||
placeholder={
|
||||
activityDetailsFormTranslate.accountBalance
|
||||
.placeholder
|
||||
}
|
||||
/>
|
||||
<FormFeedback>
|
||||
{regex.test(
|
||||
props.formData.detailAccountBalance + ""
|
||||
)
|
||||
? "Account Balance is required for this Product/Activity Type combination."
|
||||
: "Valid Form: #########.##"}
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityDetailsForm;
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
/* eslint-disable @typescript-eslint/quotes */
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import ActivityProductForm from "./ActivityProductForm";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
generateDefaultActivityForm,
|
||||
IManualActivityFormState,
|
||||
ManualActivityState,
|
||||
SystemType
|
||||
} from "./../../../shared/models/Activity";
|
||||
import { IProductDetail, Product2 } from "../../../shared/enumerations/Product";
|
||||
import moment from "moment";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import UserSearchSelect from "./UserSearchSelect";
|
||||
|
||||
const activityProductFormTranslate =
|
||||
Translate.Get.components.activity.manualActivityWizard.activityProductForm;
|
||||
|
||||
describe("A appUser inputs data into the Activity Product Form", () => {
|
||||
it("Defaults Drop Downs When First Accessing the Page", () => {
|
||||
const formData: IManualActivityFormState = generateDefaultActivityForm();
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityProductForm
|
||||
formData={formData}
|
||||
handleChange={() => {
|
||||
console.log("🎈");
|
||||
}}
|
||||
departments={[]}
|
||||
products={productDetails()}
|
||||
/>
|
||||
);
|
||||
|
||||
const activityTypeInput = wrapper
|
||||
.find('[name="ActivityType"]')
|
||||
.find("Input");
|
||||
expect(activityTypeInput.length).toBe(Object.keys(ActivityType).length);
|
||||
|
||||
let productOptions = wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("option")
|
||||
.map((x) => x.text());
|
||||
expect(productOptions).toEqual([
|
||||
"Product Type...",
|
||||
"New Checking",
|
||||
"Gap Insurance"
|
||||
]);
|
||||
|
||||
const activityProduct = wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("Input");
|
||||
expect(activityProduct.prop("value")).toBe("");
|
||||
|
||||
const activityState = wrapper
|
||||
.find('[name="ActivityState"]')
|
||||
.find("Input");
|
||||
expect(activityState.find("option").length).toBe(
|
||||
Object.keys(ManualActivityState).length
|
||||
);
|
||||
expect(activityState.prop("value")).toBe("Accepted");
|
||||
});
|
||||
|
||||
it("Builds State From Input Changes", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
product: "",
|
||||
type: ActivityType.Referral,
|
||||
departmentId: "",
|
||||
employeeId: undefined,
|
||||
accepted: undefined,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityProductForm
|
||||
formData={formData}
|
||||
//Overwrite form data with what the simulated change returns
|
||||
handleChange={(newstate) => {
|
||||
formData = newstate;
|
||||
}}
|
||||
departments={[]}
|
||||
products={productDetails()}
|
||||
/>
|
||||
);
|
||||
|
||||
//Have to manually update the formData using setProps to mock that things are hooked up to wizard
|
||||
wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: Product2.GAPInsurance } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="ActivityState"]')
|
||||
.find("Input")
|
||||
.simulate("change", {
|
||||
target: { value: ActivityState.PendingReferral }
|
||||
});
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find(UserSearchSelect).prop("onUserSelected")(
|
||||
"employeeId",
|
||||
"Marty McFly"
|
||||
);
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="CreditToDepartment"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: 119 } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="AcceptedStateDateTime"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: moment("Oct 21 1995") } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
expect(formData).toEqual({
|
||||
type: ActivityType.Referral,
|
||||
product: Product2.GAPInsurance,
|
||||
departmentId: 119,
|
||||
employeeId: "Marty McFly",
|
||||
accepted: moment("Oct 21 1995"),
|
||||
state: ActivityState.PendingReferral
|
||||
});
|
||||
});
|
||||
|
||||
describe("Valid product and activity type selection", () => {
|
||||
it("displays valid products for sale activity type", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
product: "",
|
||||
type: ActivityType.Sale,
|
||||
departmentId: "",
|
||||
employeeId: undefined,
|
||||
accepted: undefined,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityProductForm
|
||||
formData={formData}
|
||||
handleChange={(newstate) => {
|
||||
formData = newstate;
|
||||
}}
|
||||
departments={[]}
|
||||
products={productDetails()}
|
||||
/>
|
||||
);
|
||||
|
||||
let productOptions = wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("option")
|
||||
.map((x) => x.text());
|
||||
expect(productOptions).toEqual([
|
||||
"Product Type...",
|
||||
"New Checking",
|
||||
"Gap Insurance"
|
||||
]);
|
||||
});
|
||||
|
||||
it("displays valid products for referral activity type", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
product: "",
|
||||
type: ActivityType.Referral,
|
||||
departmentId: "",
|
||||
employeeId: undefined,
|
||||
accepted: undefined,
|
||||
state: ActivityState.Accepted
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityProductForm
|
||||
formData={formData}
|
||||
handleChange={(newstate) => {
|
||||
formData = newstate;
|
||||
}}
|
||||
departments={[]}
|
||||
products={productDetails()}
|
||||
/>
|
||||
);
|
||||
|
||||
let productOptions = wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("option")
|
||||
.map((x) => x.text());
|
||||
expect(productOptions).toEqual([
|
||||
"Product Type...",
|
||||
"ATM Card",
|
||||
"CCBT",
|
||||
"New Checking",
|
||||
"Credit Card"
|
||||
]);
|
||||
});
|
||||
|
||||
it("displays valid activity types for selected product", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
product: "",
|
||||
type: ActivityType.Sale,
|
||||
departmentId: "",
|
||||
employeeId: undefined,
|
||||
accepted: undefined,
|
||||
state: ActivityState.Accepted,
|
||||
sourceSystem: SystemType.Manual,
|
||||
sourceCreated: moment(),
|
||||
showValidation: false
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityProductForm
|
||||
formData={formData}
|
||||
handleChange={(newstate) => {
|
||||
formData = newstate;
|
||||
}}
|
||||
departments={[]}
|
||||
products={productDetails()}
|
||||
/>
|
||||
);
|
||||
|
||||
// undefined selected
|
||||
let activityTypeOptions = wrapper
|
||||
.find('[name="ActivityType"]')
|
||||
.find("Input")
|
||||
.map((x) => x.prop("value"));
|
||||
expect(activityTypeOptions).toEqual(["Other", "Referral", "Sale"]);
|
||||
|
||||
// sale and referral product
|
||||
wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("Input")
|
||||
.simulate("change", {
|
||||
target: { value: "NewChecking" }
|
||||
});
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
activityTypeOptions = wrapper
|
||||
.find('[name="ActivityType"]')
|
||||
.find("Input")
|
||||
.map((x) => x.prop("value"));
|
||||
expect(activityTypeOptions).toEqual(["Referral", "Sale"]);
|
||||
|
||||
//Referral, other product
|
||||
wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("Input")
|
||||
.simulate("change", {
|
||||
target: { value: "ATMCard" }
|
||||
});
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
activityTypeOptions = wrapper
|
||||
.find('[name="ActivityType"]')
|
||||
.find("Input")
|
||||
.map((x) => x.prop("value"));
|
||||
expect(activityTypeOptions).toEqual(["Other", "Referral"]);
|
||||
|
||||
// sale only product
|
||||
wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("Input")
|
||||
.simulate("change", {
|
||||
target: { value: "GAPInsurance" }
|
||||
});
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
activityTypeOptions = wrapper
|
||||
.find('[name="ActivityType"]')
|
||||
.find("Input")
|
||||
.map((x) => x.prop("value"));
|
||||
expect(activityTypeOptions).toEqual(["Sale"]);
|
||||
|
||||
// referral only product
|
||||
wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("Input")
|
||||
.simulate("change", {
|
||||
target: { value: "CreditCardBalanceTransfer" }
|
||||
});
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
activityTypeOptions = wrapper
|
||||
.find('[name="ActivityType"]')
|
||||
.find("Input")
|
||||
.map((x) => x.prop("value"));
|
||||
expect(activityTypeOptions).toEqual(["Referral"]);
|
||||
|
||||
// referral only product
|
||||
wrapper
|
||||
.find('[name="ActivityProduct"]')
|
||||
.find("Input")
|
||||
.simulate("change", {
|
||||
target: { value: "CreditCardBalanceTransfer" }
|
||||
});
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
activityTypeOptions = wrapper
|
||||
.find('[name="ActivityType"]')
|
||||
.find("Input")
|
||||
.map((x) => x.prop("value"));
|
||||
expect(activityTypeOptions).toEqual(["Referral"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const productDetails = () => {
|
||||
return [
|
||||
{
|
||||
product: "ATMCard",
|
||||
title: "ATM Card",
|
||||
isSellable: false,
|
||||
isReferrable: true,
|
||||
isNonPaying: true
|
||||
},
|
||||
{
|
||||
product: "CreditCardBalanceTransfer",
|
||||
title: "CCBT",
|
||||
isSellable: false,
|
||||
isReferrable: true,
|
||||
isNonPaying: false
|
||||
},
|
||||
{
|
||||
product: "NewChecking",
|
||||
title: "New Checking",
|
||||
isSellable: true,
|
||||
isReferrable: true,
|
||||
isNonPaying: false
|
||||
},
|
||||
{
|
||||
product: "GAPInsurance",
|
||||
title: "Gap Insurance",
|
||||
isSellable: true,
|
||||
isReferrable: false,
|
||||
isNonPaying: false
|
||||
},
|
||||
{
|
||||
product: "CreditCard",
|
||||
title: "Credit Card",
|
||||
isSellable: false,
|
||||
isReferrable: true,
|
||||
isNonPaying: false
|
||||
}
|
||||
] as IProductDetail[];
|
||||
};
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { Col, FormFeedback, FormGroup, Input, Label, Row } from "reactstrap";
|
||||
import { IProductDetail } from "../../../shared/enumerations/Product";
|
||||
import { ManualActivityState } from "../../../shared/models/Activity";
|
||||
import { IManualActivityFormState } from "../../../shared/models/Activity";
|
||||
import { tryUpdateStateWithInput } from "../../../shared/services/InputService";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import ActivityService from "../../../shared/services/ActivityService";
|
||||
import ProductService from "../../../shared/services/ProductService";
|
||||
import UserSearchSelect from "./UserSearchSelect";
|
||||
|
||||
type ActivityProductFormData = Pick<
|
||||
IManualActivityFormState,
|
||||
| "type"
|
||||
| "product"
|
||||
| "departmentId"
|
||||
| "employeeId"
|
||||
| "state"
|
||||
| "accepted"
|
||||
| "showValidation"
|
||||
>;
|
||||
|
||||
export interface IActivityProductFormProps {
|
||||
/**
|
||||
* The slice of state ActivityProductform is responsible for, to update the fields update the
|
||||
* ActivityProductsFromData type in the component see example for shape.
|
||||
* @example type ActivtyProductFormData = Pick<IManualActivityFormState, "field1" | "field2" | "feild3">;
|
||||
*/
|
||||
formData: ActivityProductFormData;
|
||||
/**Dispatches the entire slice of state every time it changes.
|
||||
* Use the spread operator to apply this to the parent state
|
||||
* @param newData the entire slice of state handled by the form, re-built with latest change
|
||||
*/
|
||||
handleChange: (newData: ActivityProductFormData) => void;
|
||||
departments: IDepartment[];
|
||||
products: IProductDetail[];
|
||||
}
|
||||
|
||||
const activityProductFormTranslate =
|
||||
Translate.Get.components.activity.manualActivityWizard.activityProductForm;
|
||||
|
||||
const ActivityProductForm: React.FC<IActivityProductFormProps> = (
|
||||
props: IActivityProductFormProps
|
||||
) => {
|
||||
const onFormDataChange = (field: string, newValue: any) => {
|
||||
const updatedFormData = tryUpdateStateWithInput(
|
||||
props.formData,
|
||||
field,
|
||||
newValue
|
||||
);
|
||||
props.handleChange(updatedFormData);
|
||||
};
|
||||
const allowedProducts = ProductService.getValidProductsForActivityType(
|
||||
props.products,
|
||||
props.formData.type
|
||||
);
|
||||
const selectedProduct = props.products.find(
|
||||
(product) => product.product === props.formData.product
|
||||
);
|
||||
const allowedActivityTypes = ActivityService.getValidActivityTypesForProduct(
|
||||
selectedProduct
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
<b>{activityProductFormTranslate.formLabel}</b>
|
||||
</span>
|
||||
<Row className="mb-2" form>
|
||||
<Col md="4">
|
||||
<label>
|
||||
{activityProductFormTranslate.activityType.label}
|
||||
</label>
|
||||
<FormGroup tag="fieldset" check>
|
||||
{allowedActivityTypes.map((activityType) => {
|
||||
return (
|
||||
<>
|
||||
<Label check>
|
||||
<Input
|
||||
name="ActivityType"
|
||||
type="radio"
|
||||
checked={
|
||||
props.formData.type ==
|
||||
activityType
|
||||
}
|
||||
onChange={() => {
|
||||
onFormDataChange(
|
||||
"type",
|
||||
activityType
|
||||
);
|
||||
}}
|
||||
value={activityType}
|
||||
/>{" "}
|
||||
{activityType}
|
||||
</Label>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<label>
|
||||
{activityProductFormTranslate.productType.label}
|
||||
<span className="text-danger">*</span>
|
||||
</label>
|
||||
<Input
|
||||
name="ActivityProduct"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
required
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.product
|
||||
}
|
||||
value={props.formData.product}
|
||||
onChange={(event) => {
|
||||
onFormDataChange("product", event.target.value);
|
||||
}}
|
||||
>
|
||||
<option key={undefined} value="">
|
||||
{
|
||||
activityProductFormTranslate.productType
|
||||
.placeholder
|
||||
}
|
||||
</option>
|
||||
{allowedProducts.map((product) => {
|
||||
return (
|
||||
<option
|
||||
key={product.product}
|
||||
value={product.product}
|
||||
>
|
||||
{product.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
<FormFeedback>Please select a value.</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityProductFormTranslate.departmentId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="CreditToDepartment"
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
required
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.departmentId
|
||||
}
|
||||
data-test="dept-input"
|
||||
value={props.formData.departmentId}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"departmentId",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-option"
|
||||
key={undefined}
|
||||
value=""
|
||||
>
|
||||
{
|
||||
activityProductFormTranslate.departmentId
|
||||
.placeholder
|
||||
}
|
||||
</option>
|
||||
{props.departments.map((department) => {
|
||||
return (
|
||||
<option
|
||||
key={department.id}
|
||||
value={department.id}
|
||||
>
|
||||
{department.departmentName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
<FormFeedback>Please select a value.</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityProductFormTranslate.personId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<UserSearchSelect
|
||||
data-test="CreditToAssociate"
|
||||
employeeId={props.formData.employeeId ?? ""}
|
||||
onUserSelected={onFormDataChange}
|
||||
showValidation={props.formData.showValidation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityProductFormTranslate.activityState.label}
|
||||
</Label>
|
||||
<Input
|
||||
name="ActivityState"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
value={props.formData.state}
|
||||
onChange={(event) => {
|
||||
onFormDataChange("state", event.target.value);
|
||||
}}
|
||||
>
|
||||
{Object.values(ManualActivityState).map(
|
||||
(activityState) => {
|
||||
return (
|
||||
<option key={activityState}>
|
||||
{activityState}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activityProductFormTranslate.acceptedDateTime
|
||||
.label
|
||||
}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="AcceptedStateDateTime"
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
required
|
||||
invalid={
|
||||
props.formData.showValidation &&
|
||||
!props.formData.accepted
|
||||
}
|
||||
value={
|
||||
props.formData.accepted
|
||||
? props.formData.accepted
|
||||
.format("YYYY-MM-DD")
|
||||
.toString()
|
||||
: ""
|
||||
}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"accepted",
|
||||
event.target.value
|
||||
? moment(event.target.value)
|
||||
: ""
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>Accepted Date is required.</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="3" />
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityProductForm;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import ActivitySourceForm from "./ActivitySourceForm";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityType,
|
||||
IManualActivityFormState,
|
||||
SystemType
|
||||
} from "./../../../shared/models/Activity";
|
||||
import moment from "moment";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
|
||||
describe("A appUser inputing data into the Activity Source Form", () => {
|
||||
it("Defaults Drop Downs When First Accessing the Page", () => {
|
||||
const formData: IManualActivityFormState = {
|
||||
sourceCreated: undefined,
|
||||
sourceId: undefined,
|
||||
sourceSystem: undefined
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivitySourceForm
|
||||
formData={formData}
|
||||
handleChange={() => {
|
||||
console.log("🎈");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const activitySourceType = wrapper
|
||||
.find('[name="SourceSystemType"]')
|
||||
.find("Input");
|
||||
expect(activitySourceType.find("option").length).toBe(
|
||||
Object.keys(SystemType).length + 1
|
||||
);
|
||||
expect(activitySourceType.prop("value")).toBe(
|
||||
Translate.Get.components.activity.manualActivityWizard
|
||||
.activitySourceForm.sourceSystemType.placeholder
|
||||
);
|
||||
});
|
||||
|
||||
it("Does not allow default after selection made", () => {
|
||||
const formData: IManualActivityFormState = {
|
||||
sourceCreated: undefined,
|
||||
sourceId: undefined,
|
||||
sourceSystem: SystemType.Dna
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivitySourceForm
|
||||
formData={formData}
|
||||
handleChange={() => {
|
||||
console.log("🎈");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const activityTypeInput = wrapper
|
||||
.find('[name="SourceSystemType"]')
|
||||
.find("Input");
|
||||
|
||||
expect(
|
||||
activityTypeInput
|
||||
.find("option")
|
||||
.findWhere((x) => x.prop("hidden") === true).length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("Builds State From Input Changes", () => {
|
||||
let formData: IManualActivityFormState = {
|
||||
sourceCreated: undefined,
|
||||
sourceId: undefined,
|
||||
sourceSystem: undefined
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivitySourceForm
|
||||
formData={formData}
|
||||
handleChange={(newData) => {
|
||||
formData = newData;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
//Have to manually update the formData using setProps to mock that things are hooked up to wizard
|
||||
|
||||
wrapper
|
||||
.find('[name="SourceSystemType"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: SystemType.Allied } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="SourceSystemId"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: "101" } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[name="SourceCreatedDateTime"]')
|
||||
.find("Input")
|
||||
.simulate("change", { target: { value: moment("May 12 2020") } });
|
||||
wrapper.setProps({ formData: formData });
|
||||
wrapper.update();
|
||||
|
||||
expect(formData).toEqual({
|
||||
sourceSystem: SystemType.Allied,
|
||||
sourceId: "101",
|
||||
sourceCreated: moment("May 12 2020")
|
||||
});
|
||||
});
|
||||
});
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { Row, Col, Input, Label, FormGroup } from "reactstrap";
|
||||
import {
|
||||
IManualActivityFormState,
|
||||
SystemType
|
||||
} from "../../../shared/models/Activity";
|
||||
import { tryUpdateStateWithInput } from "../../../shared/services/InputService";
|
||||
import Translate from "../../../translate/Translate";
|
||||
|
||||
type ActivitySourceFormData = Pick<
|
||||
IManualActivityFormState,
|
||||
"sourceCreated" | "sourceId" | "sourceSystem"
|
||||
>;
|
||||
|
||||
export interface IActivitySourceFormProps {
|
||||
/**
|
||||
* The slice of state ActivitySourceForm is responsible for, to update the fields update the
|
||||
* ActivitySourceFormData type in the component see example for shape.
|
||||
* @example type ActivtyProductFormData = Pick<IManualActivityFormState, "field1"| "field2">
|
||||
*/
|
||||
formData: ActivitySourceFormData;
|
||||
/**Dispatches the entire slice of state every time it changes.
|
||||
* Use the spread operator to apply this to the parent state
|
||||
* @param newData the entire slice of state handled by the form, re-built with latest change
|
||||
*/
|
||||
handleChange: (newData: ActivitySourceFormData) => void;
|
||||
}
|
||||
|
||||
const activitySourceFormTranslate =
|
||||
Translate.Get.components.activity.manualActivityWizard.activitySourceForm;
|
||||
|
||||
const ActivitySourceForm: React.FC<IActivitySourceFormProps> = (
|
||||
props: IActivitySourceFormProps
|
||||
) => {
|
||||
const onFormDataChange = (field: string, newValue: any) => {
|
||||
const updatedFormData = tryUpdateStateWithInput(
|
||||
props.formData,
|
||||
field,
|
||||
newValue
|
||||
);
|
||||
props.handleChange(updatedFormData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>
|
||||
<b>{activitySourceFormTranslate.formLabel}</b>
|
||||
</span>
|
||||
<Row className="mb-3">
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activitySourceFormTranslate.sourceSystemType.label}
|
||||
</Label>
|
||||
<Input
|
||||
name="SourceSystemType"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
value={
|
||||
props.formData.sourceSystem === undefined
|
||||
? activitySourceFormTranslate
|
||||
.sourceSystemType.placeholder
|
||||
: props.formData.sourceSystem
|
||||
}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"sourceSystem",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
hidden={
|
||||
props.formData.sourceSystem !== undefined
|
||||
}
|
||||
key={undefined}
|
||||
>
|
||||
{
|
||||
activitySourceFormTranslate.sourceSystemType
|
||||
.placeholder
|
||||
}
|
||||
</option>
|
||||
{Object.values(SystemType).map((systemType) => {
|
||||
return (
|
||||
<option key={systemType}>
|
||||
{systemType}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activitySourceFormTranslate.sourceSystemId.label}
|
||||
</Label>
|
||||
<Input
|
||||
name="SourceSystemId"
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
placeholder={
|
||||
activitySourceFormTranslate.sourceSystemId
|
||||
.placeholder
|
||||
}
|
||||
value={props.formData.sourceId}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"sourceId",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activitySourceFormTranslate
|
||||
.sourceCreatedDateTime.label
|
||||
}
|
||||
</Label>
|
||||
<Input
|
||||
name="SourceCreatedDateTime"
|
||||
bsSize="sm"
|
||||
value={props.formData.sourceCreated
|
||||
?.format("YYYY-MM-DDTHH:mm")
|
||||
.toString()}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"sourceCreated",
|
||||
moment(event.target.value)
|
||||
);
|
||||
}}
|
||||
type="datetime-local"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivitySourceForm;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import ActivityWizardAlertBar from "./ActivityWizardAlertBar";
|
||||
import { shallow } from "enzyme";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
|
||||
describe("After a appUser submits, Activity Wizard Alert Bar Displays the proper message", () => {
|
||||
let state: AsyncState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = AsyncState.Inactive;
|
||||
});
|
||||
|
||||
it("Displays nothing at start", () => {
|
||||
let wrapper = shallow(<ActivityWizardAlertBar submitStatus={state} />);
|
||||
expect(wrapper.find('[data-test="Success"]').prop("hidden")).toBe(true);
|
||||
expect(wrapper.find('[data-test="Failure"]').prop("hidden")).toBe(true);
|
||||
expect(wrapper.find('[data-test="Loading"]').prop("hidden")).toBe(true);
|
||||
});
|
||||
|
||||
it("Displays loading message when submit loading", () => {
|
||||
state = AsyncState.Loading;
|
||||
let wrapper = shallow(<ActivityWizardAlertBar submitStatus={state} />);
|
||||
expect(wrapper.find('[data-test="Success"]').prop("hidden")).toBe(true);
|
||||
expect(wrapper.find('[data-test="Failure"]').prop("hidden")).toBe(true);
|
||||
expect(wrapper.find('[data-test="Loading"]').prop("hidden")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays success message when submit success", () => {
|
||||
state = AsyncState.Success;
|
||||
let wrapper = shallow(<ActivityWizardAlertBar submitStatus={state} />);
|
||||
expect(wrapper.find('[data-test="Success"]').prop("hidden")).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test="Failure"]').prop("hidden")).toBe(true);
|
||||
expect(wrapper.find('[data-test="Loading"]').prop("hidden")).toBe(true);
|
||||
});
|
||||
|
||||
it("Displays error message when submit Fails", () => {
|
||||
state = AsyncState.Failure;
|
||||
let wrapper = shallow(<ActivityWizardAlertBar submitStatus={state} />);
|
||||
expect(wrapper.find('[data-test="Success"]').prop("hidden")).toBe(true);
|
||||
expect(wrapper.find('[data-test="Failure"]').prop("hidden")).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test="Loading"]').prop("hidden")).toBe(true);
|
||||
// expect(
|
||||
// wrapper
|
||||
// .find('[data-test="Failure"]')
|
||||
// .find('[data-test="ErrorMessageFromStatus"]')
|
||||
// .text()
|
||||
// ).toContain("oof");
|
||||
});
|
||||
|
||||
it("Displays error message when submit Invalid", () => {
|
||||
state = AsyncState.Invalid;
|
||||
let wrapper = shallow(<ActivityWizardAlertBar submitStatus={state} />);
|
||||
expect(wrapper.find('[data-test="Success"]').prop("hidden")).toBe(true);
|
||||
expect(wrapper.find('[data-test="Failure"]').prop("hidden")).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test="Loading"]').prop("hidden")).toBe(true);
|
||||
// expect(
|
||||
// wrapper
|
||||
// .find('[data-test="Failure"]')
|
||||
// .find('[data-test="ErrorMessageFromStatus"]')
|
||||
// .text()
|
||||
// ).toContain("oof");
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { Alert, Spinner } from "reactstrap";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
|
||||
export interface IActivityWizardAlertBarProps {
|
||||
submitStatus: AsyncState;
|
||||
}
|
||||
|
||||
const ActivityWizardAlertBar: React.FC<IActivityWizardAlertBarProps> = (
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<div>
|
||||
<Alert
|
||||
className="py-1 mx-3"
|
||||
hidden={props.submitStatus != AsyncState.Loading}
|
||||
color="warning"
|
||||
data-test="Loading"
|
||||
>
|
||||
<div className="d-flex flex-row justify-content-left align-items-center">
|
||||
<Spinner color="warning" className="mr-4" />
|
||||
{
|
||||
Translate.Get.components.activity.manualActivityWizard
|
||||
.activityWizardAlertBar.submitLoadingMessage
|
||||
}
|
||||
</div>
|
||||
</Alert>
|
||||
<Alert
|
||||
className="py-1 mx-3"
|
||||
hidden={
|
||||
props.submitStatus !== AsyncState.Failure &&
|
||||
props.submitStatus !== AsyncState.Invalid
|
||||
}
|
||||
color="danger"
|
||||
data-test="Failure"
|
||||
>
|
||||
<b>
|
||||
{
|
||||
Translate.Get.components.activity.manualActivityWizard
|
||||
.activityWizardAlertBar.submitErrorHeader
|
||||
}
|
||||
</b>
|
||||
<p>
|
||||
{
|
||||
Translate.Get.components.activity.manualActivityWizard
|
||||
.activityWizardAlertBar.submitErrorMessage
|
||||
}
|
||||
</p>
|
||||
</Alert>
|
||||
<Alert
|
||||
className="py-1 mx-3"
|
||||
hidden={props.submitStatus != AsyncState.Success}
|
||||
color="success"
|
||||
data-test="Success"
|
||||
>
|
||||
{
|
||||
Translate.Get.components.activity.manualActivityWizard
|
||||
.activityWizardAlertBar.submitSuccessMessage
|
||||
}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityWizardAlertBar;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import "./ManualActivityWizard.scss";
|
||||
import React from "react";
|
||||
import ActivityProductForm from "./ActivityProductForm";
|
||||
import ActivityDetailsForm from "./ActivityDetailsForm";
|
||||
import { IManualActivityFormState } from "./../../../shared/models/Activity";
|
||||
import AsyncState from "./../../../shared/enumerations/AsyncState";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import ActivityWizardAlertBar from "./ActivityWizardAlertBar";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import { IProductDetail } from "../../../shared/enumerations/Product";
|
||||
import { Form } from "reactstrap";
|
||||
import { tryUpdateStateWithInput } from "../../../shared/services/InputService";
|
||||
import ActivityService from "../../../shared/services/ActivityService";
|
||||
|
||||
export interface IManualActivityWizardProps {
|
||||
formState: IManualActivityFormState;
|
||||
submitStatus: AsyncState;
|
||||
handleChange: (formState: IManualActivityFormState) => void;
|
||||
submitManualActivity: () => void;
|
||||
clearForm: () => void;
|
||||
departments: IDepartment[];
|
||||
products: IProductDetail[];
|
||||
}
|
||||
|
||||
export const ManualActivityWizard: React.FC<IManualActivityWizardProps> = (
|
||||
props: IManualActivityWizardProps
|
||||
) => {
|
||||
/**
|
||||
* Apply changes from form state slice to parent state, then update redux store
|
||||
* @param formData - Updated state slice from form
|
||||
*/
|
||||
const handlePartialChangeFromForm = (
|
||||
formData: Partial<IManualActivityFormState>
|
||||
): void => {
|
||||
const newState: IManualActivityFormState = {
|
||||
...props.formState,
|
||||
...formData
|
||||
};
|
||||
props.handleChange(newState);
|
||||
};
|
||||
|
||||
const onFormDataChange = (field: string, newValue: any) => {
|
||||
const updatedFormData = tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
field,
|
||||
newValue
|
||||
);
|
||||
handlePartialChangeFromForm(updatedFormData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5 className="d-flex flex-row justify-content-between align-content-center">
|
||||
{
|
||||
Translate.Get.components.activity.manualActivityWizard
|
||||
.wizardTitle
|
||||
}
|
||||
</h5>
|
||||
<ActivityWizardAlertBar submitStatus={props.submitStatus} />
|
||||
<Form className="px-5" noValidate>
|
||||
<ActivityProductForm
|
||||
formData={props.formState}
|
||||
handleChange={(newFormData) =>
|
||||
handlePartialChangeFromForm(newFormData)
|
||||
}
|
||||
departments={props.departments}
|
||||
products={props.products}
|
||||
/>
|
||||
<ActivityDetailsForm
|
||||
formData={props.formState}
|
||||
requireAccountStatusField={ActivityService.isAccountStatusFieldRequired(
|
||||
props.formState
|
||||
)}
|
||||
requireAccountBalanceField={ActivityService.isAccountBalanceFieldRequired(
|
||||
props.formState
|
||||
)}
|
||||
requireAccountNumberField={ActivityService.isAccountNumberFieldRequired(
|
||||
props.formState
|
||||
)}
|
||||
requireSourceIdField={ActivityService.isSourceIdFieldVisible(
|
||||
props.formState
|
||||
)}
|
||||
handleChange={(newFormData) =>
|
||||
handlePartialChangeFromForm(newFormData)
|
||||
}
|
||||
/>
|
||||
<div className="d-flex justify-content-end">
|
||||
<button
|
||||
className="btn btn-danger m-1"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.clearForm();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
disabled={props.submitStatus === AsyncState.Loading}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (
|
||||
ActivityService.isValidManualActivity(
|
||||
props.formState
|
||||
)
|
||||
) {
|
||||
props.submitManualActivity();
|
||||
} else {
|
||||
onFormDataChange("showValidation", true);
|
||||
}
|
||||
}}
|
||||
className="btn btn-primary m-1"
|
||||
>
|
||||
{
|
||||
Translate.Get.components.activity
|
||||
.manualActivityWizard.submitButton
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import UserSearchSelect from "./UserSearchSelect";
|
||||
import { IEmployeeSummary } from "../../../shared/models/Employee";
|
||||
import ProfileDao from "../../../shared/dao/ProfileDao";
|
||||
|
||||
jest.useFakeTimers("modern");
|
||||
const profileSearchMock = jest.spyOn(ProfileDao, "profileSearch");
|
||||
const formTranslate =
|
||||
Translate.Get.components.activity.manualActivityWizard.activityProductForm
|
||||
.personId;
|
||||
|
||||
describe("A component to search for users", () => {
|
||||
beforeEach(() => {
|
||||
profileSearchMock.mockReset();
|
||||
});
|
||||
|
||||
it("should not search if query is not less than > 1", () => {
|
||||
const mockOnUserSelected = jest.fn();
|
||||
|
||||
mount(
|
||||
<UserSearchSelect
|
||||
onUserSelected={mockOnUserSelected}
|
||||
employeeId={""}
|
||||
showValidation={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(profileSearchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should search if query is not less than > 1", async () => {
|
||||
const mockOnUserSelected = jest.fn();
|
||||
profileSearchMock.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: [
|
||||
{
|
||||
adEmail: "[email protected]",
|
||||
employeeId: "j1",
|
||||
fullName: "Joe One",
|
||||
personId: 1
|
||||
},
|
||||
{
|
||||
adEmail: "[email protected]",
|
||||
employeeId: "j2",
|
||||
fullName: "Joe Two",
|
||||
personId: 2
|
||||
}
|
||||
]
|
||||
} as IAsyncStateContainer<IEmployeeSummary[]>);
|
||||
const wrapper = mount(
|
||||
<UserSearchSelect
|
||||
onUserSelected={mockOnUserSelected}
|
||||
employeeId={""}
|
||||
showValidation={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const searchInput = () =>
|
||||
wrapper.find("[data-test='appUser-search-input']").find("input");
|
||||
|
||||
// simulate search to get mock search results
|
||||
await act(async () => {
|
||||
searchInput().simulate("change", { target: { value: "joe" } });
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(profileSearchMock).toHaveBeenCalledTimes(1);
|
||||
expect(profileSearchMock).toHaveBeenCalledWith("joe");
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(searchInput().prop("value")).toEqual("joe");
|
||||
|
||||
// verify dropdown is populated with search results
|
||||
const searchSelect = () =>
|
||||
wrapper.find("[data-test='appUser-search-select']").find("select");
|
||||
const options = searchSelect()
|
||||
.children()
|
||||
.map((option) => option.prop("children"));
|
||||
expect(options).toEqual([
|
||||
formTranslate.select,
|
||||
"Joe One (j1) [email protected]",
|
||||
"Joe Two (j2) [email protected]"
|
||||
]);
|
||||
|
||||
searchSelect().simulate("change", { target: { value: "j1" } });
|
||||
expect(mockOnUserSelected).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnUserSelected).toHaveBeenLastCalledWith("employeeId", "j1");
|
||||
});
|
||||
});
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import debounce from "debounce";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
FormFeedback,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupText
|
||||
} from "reactstrap";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import { IEmployeeSummary } from "../../../shared/models/Employee";
|
||||
import ProfileDao from "../../../shared/dao/ProfileDao";
|
||||
|
||||
export interface IUserSearchSelectProps {
|
||||
employeeId: string;
|
||||
showValidation: boolean;
|
||||
onUserSelected: (field: string, newValue: any) => void;
|
||||
placeHolder?: string;
|
||||
}
|
||||
|
||||
const formTranslate =
|
||||
Translate.Get.components.activity.manualActivityWizard.activityProductForm;
|
||||
|
||||
const UserSearchSelect: React.FC<IUserSearchSelectProps> = (props) => {
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [suggestions, setSuggestions] = useState<IEmployeeSummary[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.employeeId === "") {
|
||||
setSearchQuery("");
|
||||
}
|
||||
}, [props.employeeId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery && searchQuery.length > 1) {
|
||||
setSuggestions([]);
|
||||
search(searchQuery);
|
||||
}
|
||||
}, [searchQuery]);
|
||||
|
||||
const search = useCallback(
|
||||
debounce((query: string) => {
|
||||
ProfileDao.profileSearch(query).then((result) => {
|
||||
setSuggestions(() => result.data ?? []);
|
||||
});
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
|
||||
const selectUser = (employeeId: string) => {
|
||||
props.onUserSelected("employeeId", employeeId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputGroup size="sm">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>
|
||||
<i className="fa fa-search"></i>
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input
|
||||
data-test="appUser-search-input"
|
||||
placeholder={formTranslate.personId.placeholder}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
data-test="appUser-search-select"
|
||||
type="select"
|
||||
required
|
||||
invalid={props.showValidation && !props.employeeId}
|
||||
onChange={(event) => {
|
||||
selectUser(event.target.value);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-option"
|
||||
key="unselected"
|
||||
value={""}
|
||||
>
|
||||
{props.placeHolder
|
||||
? props.placeHolder
|
||||
: formTranslate.personId.select}
|
||||
</option>
|
||||
{suggestions.map((suggestion) => (
|
||||
<option
|
||||
key={suggestion.employeeId}
|
||||
value={suggestion.employeeId}
|
||||
>
|
||||
{`${suggestion.fullName} (${suggestion.employeeId}) ${suggestion.adEmail}`}
|
||||
</option>
|
||||
))}
|
||||
</Input>
|
||||
<FormFeedback>Please select an employee.</FormFeedback>
|
||||
</InputGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSearchSelect;
|
||||
@@ -0,0 +1,93 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { AutoTable } from "./AutoTable";
|
||||
|
||||
describe("AutoTable Component", () => {
|
||||
it("Renders headers and rows", () => {
|
||||
const wrapper = shallow(
|
||||
<AutoTable
|
||||
headers={["Header #1", "Header #2"]}
|
||||
elements={[{}, {}, {}]}
|
||||
cells={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
const headers = wrapper.find("[data-test='auto-table-header']");
|
||||
expect(headers.length).toBe(2);
|
||||
expect(headers.at(0).text()).toEqual("Header #1");
|
||||
expect(headers.at(1).text()).toEqual("Header #2");
|
||||
|
||||
let rows = wrapper.find("[data-test='auto-table-row_1']");
|
||||
expect(rows.length).toBe(1);
|
||||
rows = wrapper.find("[data-test='auto-table-row_2']");
|
||||
expect(rows.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Calls Header Callbacks", () => {
|
||||
let wasTriggered = false;
|
||||
const wrapper = shallow(
|
||||
<AutoTable
|
||||
headers={[
|
||||
"Header #1",
|
||||
{
|
||||
display: "Header #2",
|
||||
onClick: () => (wasTriggered = true)
|
||||
}
|
||||
]}
|
||||
elements={[{}, {}, {}]}
|
||||
cells={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
const headers = wrapper.find("[data-test='auto-table-header']");
|
||||
expect(headers.at(1).text()).toEqual("Header #2");
|
||||
|
||||
expect(wasTriggered).toBe(false);
|
||||
headers.at(1).simulate("click");
|
||||
expect(wasTriggered).toBe(true);
|
||||
});
|
||||
|
||||
it("Render Ascending Sorted Icon", () => {
|
||||
const wrapper = shallow(
|
||||
<AutoTable
|
||||
headers={["Header #1", "Header #2"]}
|
||||
elements={[{}, {}, {}]}
|
||||
cells={[]}
|
||||
sortedColumn={{
|
||||
index: 1,
|
||||
isReversed: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const headers = wrapper.find("[data-test='auto-table-header']");
|
||||
expect(headers.length).toBe(2);
|
||||
expect(headers.at(0).text()).toEqual("Header #1");
|
||||
expect(headers.at(1).text()).not.toEqual("Header #2");
|
||||
|
||||
const sortedAscIcon = wrapper.find("[data-test='asc-sort-icon-1']");
|
||||
expect(sortedAscIcon.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Render Descending Sorted Icon", () => {
|
||||
const wrapper = shallow(
|
||||
<AutoTable
|
||||
headers={["Header #1", "Header #2"]}
|
||||
elements={[{}, {}, {}]}
|
||||
cells={[]}
|
||||
sortedColumn={{
|
||||
index: 0,
|
||||
isReversed: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const headers = wrapper.find("[data-test='auto-table-header']");
|
||||
expect(headers.length).toBe(2);
|
||||
expect(headers.at(0).text()).not.toEqual("Header #1");
|
||||
expect(headers.at(1).text()).toEqual("Header #2");
|
||||
|
||||
const sortedDescIcon = wrapper.find("[data-test='desc-sort-icon-0']");
|
||||
expect(sortedDescIcon.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import React from "react";
|
||||
import { AutoTableRow } from "./auto-table-row/AutoTableRow";
|
||||
|
||||
export interface IColumnSort {
|
||||
index: number;
|
||||
isReversed: boolean;
|
||||
}
|
||||
|
||||
export interface IAutoTableHeader {
|
||||
display: string;
|
||||
onClick?: () => any;
|
||||
}
|
||||
|
||||
const instanceOfAutoTableHeader = (object: any): object is IAutoTableHeader => {
|
||||
if (typeof object === "object" && object !== null) {
|
||||
return "display" in object;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export interface IAutoTableProps<T> {
|
||||
/**
|
||||
* The headers of the table
|
||||
*/
|
||||
headers: Array<IAutoTableHeader | string | number>;
|
||||
|
||||
/**
|
||||
* Array of functions that describe how each cell of a row ought to be displayed.
|
||||
*/
|
||||
cells: Array<(element: T) => any>;
|
||||
|
||||
/**
|
||||
* The array of elements to display (each element being its own row).
|
||||
*/
|
||||
elements: T[];
|
||||
|
||||
/**
|
||||
* Optional callback method that is triggered when a appUser clicks on a row.
|
||||
*/
|
||||
onRowClick?: (elementClicked: T) => any;
|
||||
|
||||
/**
|
||||
* Optionally show a desired column as being sorted.
|
||||
* (This DOES NOT sort. Merely displays the header as such.)
|
||||
*/
|
||||
sortedColumn?: IColumnSort;
|
||||
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* So you want to show something else instead of this row.. what is it and which 0 index row to replace.
|
||||
*/
|
||||
overrideRow?: (element: T) => JSX.Element;
|
||||
overrideRowFn?: (element: T) => boolean;
|
||||
}
|
||||
|
||||
export class AutoTable<T> extends React.Component<IAutoTableProps<T>> {
|
||||
getHeaderText(header: IAutoTableHeader | string | number): string | number {
|
||||
if (instanceOfAutoTableHeader(header)) {
|
||||
return header.display;
|
||||
} else {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
getHeaderOnClick(
|
||||
header: IAutoTableHeader | string | number
|
||||
): (() => any) | undefined {
|
||||
if (instanceOfAutoTableHeader(header)) {
|
||||
return header.onClick;
|
||||
}
|
||||
}
|
||||
|
||||
isHeaderClickable(header: IAutoTableHeader | string | number): boolean {
|
||||
return instanceOfAutoTableHeader(header);
|
||||
}
|
||||
|
||||
shouldDisplayAscendingSort(index: number): boolean {
|
||||
return (
|
||||
!!this.props.sortedColumn &&
|
||||
this.props.sortedColumn.index === index &&
|
||||
!this.props.sortedColumn.isReversed
|
||||
);
|
||||
}
|
||||
|
||||
shouldDisplayDescendingSort(index: number): boolean {
|
||||
return (
|
||||
!!this.props.sortedColumn &&
|
||||
this.props.sortedColumn.index === index &&
|
||||
this.props.sortedColumn.isReversed
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table
|
||||
className={
|
||||
"table table-striped tablecenterCSS tablecenterheadCSS " +
|
||||
(this.props.onRowClick ? "table-hover " : "") +
|
||||
this.props.className
|
||||
}
|
||||
data-test="auto-table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{this.props.headers.map((header, index) => (
|
||||
<th
|
||||
key={"auto_table_header_" + index}
|
||||
onClick={this.getHeaderOnClick(header)}
|
||||
className={
|
||||
"auto-table-row " +
|
||||
(this.isHeaderClickable(header)
|
||||
? "auto-table-hover"
|
||||
: "")
|
||||
}
|
||||
data-test="auto-table-header"
|
||||
>
|
||||
{this.shouldDisplayAscendingSort(index) ? (
|
||||
<i
|
||||
className="fa fa-long-arrow-down"
|
||||
aria-hidden="true"
|
||||
data-test={"asc-sort-icon-" + index}
|
||||
>
|
||||
|
||||
</i>
|
||||
) : null}
|
||||
{this.shouldDisplayDescendingSort(index) ? (
|
||||
<i
|
||||
className="fa fa-long-arrow-up"
|
||||
aria-hidden="true"
|
||||
data-test={"desc-sort-icon-" + index}
|
||||
>
|
||||
|
||||
</i>
|
||||
) : null}
|
||||
{this.getHeaderText(header)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.elements.map((el, index) =>
|
||||
this.props.overrideRow !== undefined &&
|
||||
this.props.overrideRowFn !== undefined &&
|
||||
this.props.overrideRowFn(el) ? (
|
||||
this.props.overrideRow(el)
|
||||
) : (
|
||||
<AutoTableRow
|
||||
key={index}
|
||||
element={el}
|
||||
cells={this.props.cells}
|
||||
onRowClick={this.props.onRowClick}
|
||||
data-test={"auto-table-row_" + index}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.auto-table-hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { AutoTableRow } from "./AutoTableRow";
|
||||
|
||||
describe("AutoTableRow Component", () => {
|
||||
it("Renders Expected Cells", () => {
|
||||
const wrapper = shallow(
|
||||
<AutoTableRow
|
||||
element={{
|
||||
val1: "Test",
|
||||
val2: 55,
|
||||
val3: {
|
||||
child: "Child Val"
|
||||
}
|
||||
}}
|
||||
cells={[(x) => x.val1, (x) => x.val2, (x) => x.val3.child]}
|
||||
/>
|
||||
);
|
||||
|
||||
const cells = wrapper.find("[data-test='auto-table-cell']");
|
||||
expect(cells.length).toBe(3);
|
||||
expect(cells.at(0).text()).toEqual("Test");
|
||||
expect(cells.at(1).text()).toEqual("55");
|
||||
expect(cells.at(2).text()).toEqual("Child Val");
|
||||
});
|
||||
|
||||
it("OnClick() calls callback", () => {
|
||||
let wasTriggered = false;
|
||||
const wrapper = shallow(
|
||||
<AutoTableRow
|
||||
element={{}}
|
||||
cells={[]}
|
||||
onRowClick={() => (wasTriggered = true)}
|
||||
/>
|
||||
);
|
||||
|
||||
const row = wrapper.find("[data-test='auto-table-row']");
|
||||
expect(wasTriggered).toBe(false);
|
||||
|
||||
row.simulate("click");
|
||||
expect(wasTriggered).toBe(true);
|
||||
});
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import "./AutoTableRow.css";
|
||||
|
||||
export interface IAutoTableRowProps<T> {
|
||||
element: T;
|
||||
cells: Array<(element: T) => string | number | undefined>;
|
||||
onRowClick?: (elementClicked: T) => any;
|
||||
}
|
||||
|
||||
export class AutoTableRow<T> extends React.Component<IAutoTableRowProps<T>> {
|
||||
private rowClicked() {
|
||||
if (this.props.onRowClick) {
|
||||
this.props.onRowClick(this.props.element);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<tr
|
||||
className={
|
||||
"auto-table-row " +
|
||||
(this.props.onRowClick ? "auto-table-hover" : "")
|
||||
}
|
||||
onClick={() => this.rowClicked()}
|
||||
data-test="auto-table-row"
|
||||
>
|
||||
{this.props.cells.map((cell, index) => (
|
||||
<td
|
||||
key={"auto_table_row_" + index}
|
||||
data-test="auto-table-cell"
|
||||
className={index === 0 ? "tdLabel" : ""}
|
||||
>
|
||||
{cell(this.props.element)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import CommissionRatesTable from "./CommissionRatesTable";
|
||||
|
||||
describe("A table to set commission rates", () => {
|
||||
it("Displays the table", () => {
|
||||
const props = {
|
||||
commissions: [],
|
||||
roles: [],
|
||||
products: [],
|
||||
onRateChange: () => {}
|
||||
};
|
||||
|
||||
const wrapper = shallow(<CommissionRatesTable {...props} />);
|
||||
|
||||
const table = wrapper.find("[data-test='commission-rates-table']");
|
||||
|
||||
expect(table.length).toBe(1);
|
||||
});
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import { IProductDetail } from "../../shared/enumerations/Product";
|
||||
import { ICommissionRateChange } from "../../shared/models/Commission";
|
||||
import { IAppRole } from "../../shared/models/Settings";
|
||||
import CommissionRatesTableRow from "../../components/commissions/CommissionRatesTableRow";
|
||||
|
||||
interface ICommissionRatesTableProps {
|
||||
commissions: ICommissionRateChange[];
|
||||
roles: IAppRole[];
|
||||
products: IProductDetail[];
|
||||
onRateChange: (commission: ICommissionRateChange) => void;
|
||||
}
|
||||
|
||||
const CommissionRatesTable: React.FC<ICommissionRatesTableProps> = (
|
||||
props: ICommissionRatesTableProps
|
||||
) => {
|
||||
const commissionsForProduct = (product: IProductDetail) =>
|
||||
props.commissions.filter(
|
||||
(commission) => commission.product === product.product
|
||||
);
|
||||
|
||||
const changeRate = (commissionChange: ICommissionRateChange) => {
|
||||
props.onRateChange(commissionChange);
|
||||
};
|
||||
|
||||
return (
|
||||
<table
|
||||
className="table table-striped table-responsive tablecenterCSS tablecenterheadCSS"
|
||||
data-test="commission-rates-table"
|
||||
>
|
||||
<thead className="thead-light">
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
{props.roles.map((role) => (
|
||||
<th key={role.name} title={role.name} scope="col">
|
||||
{role.description}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="form-group">
|
||||
{props.products.map((product, index) => (
|
||||
<CommissionRatesTableRow
|
||||
key={`${product.product}_row`}
|
||||
product={product}
|
||||
roles={props.roles}
|
||||
commissions={commissionsForProduct(product)}
|
||||
onRateChange={changeRate}
|
||||
rowIndex={index * 100}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommissionRatesTable;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import CommissionRatesTableCell from "./CommissionRatesTableCell";
|
||||
|
||||
describe("Cell for setting commission rate", () => {
|
||||
it("Displays a commssion rates cell", () => {
|
||||
const props = {
|
||||
id: 0,
|
||||
value: 1,
|
||||
onRateChange: () => jest.fn(),
|
||||
tabIndex: 1
|
||||
};
|
||||
|
||||
const wrapper = shallow(<CommissionRatesTableCell {...props} />);
|
||||
|
||||
expect(wrapper.find('[data-test="commission-rate-cell"]').length).toBe(
|
||||
1
|
||||
);
|
||||
expect(wrapper.find('[data-test="valid-feedback"]').length).toBe(1);
|
||||
expect(wrapper.find('[data-test="invalid-feedback"]').length).toBe(1);
|
||||
});
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface ICommissionRatesTableCellProps {
|
||||
id: number;
|
||||
value: number;
|
||||
onRateChange: (newRate: number, id: number) => void;
|
||||
tabIndex: number;
|
||||
}
|
||||
|
||||
const cssValidClass = (originalValue: string, newValue: string) => {
|
||||
if (originalValue == newValue || (originalValue === "_" && !newValue))
|
||||
return "form-control";
|
||||
if (!newValue) return "form-control is-invalid";
|
||||
return "form-control is-valid";
|
||||
};
|
||||
|
||||
const CommissionRatesTableCell: React.FC<ICommissionRatesTableCellProps> = (
|
||||
props: ICommissionRatesTableCellProps
|
||||
) => {
|
||||
const originalValue = props.value + "";
|
||||
const [newValue, setNewValue] = useState<string>(originalValue);
|
||||
|
||||
const changeRate = (value: string) => {
|
||||
const filtered = value.replace(/\D/g, "").trim();
|
||||
props.onRateChange(parseInt(filtered, 10), props.id);
|
||||
setNewValue(filtered);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="input-group input-group-sm"
|
||||
data-test="commission-rate-cell"
|
||||
>
|
||||
<input
|
||||
className={cssValidClass(originalValue, newValue)}
|
||||
type="text"
|
||||
value={newValue}
|
||||
onChange={(event) => changeRate(event.target.value)}
|
||||
tabIndex={props.tabIndex}
|
||||
/>
|
||||
<div className="valid-feedback" data-test="valid-feedback">
|
||||
Unsaved: {originalValue} {"->"} {newValue}
|
||||
</div>
|
||||
<div className="invalid-feedback" data-test="invalid-feedback">
|
||||
Required
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommissionRatesTableCell;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IProductDetail } from "../../shared/enumerations/Product";
|
||||
import CommissionRatesTableRow from "./CommissionRatesTableRow";
|
||||
|
||||
describe("A row in the commission rates table", () => {
|
||||
it("Displays a row", () => {
|
||||
const props = {
|
||||
product: { title: "ATMCard" } as IProductDetail,
|
||||
roles: [],
|
||||
commissions: [],
|
||||
onRateChange: () => {},
|
||||
rowIndex: 1
|
||||
};
|
||||
|
||||
const wrapper = shallow(<CommissionRatesTableRow {...props} />);
|
||||
|
||||
const tableRow = wrapper.find('[data-test="commission-rate-row"]');
|
||||
|
||||
expect(tableRow.length).toBe(1);
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { IProductDetail } from "../../shared/enumerations/Product";
|
||||
import { ICommissionRateChange } from "../../shared/models/Commission";
|
||||
import { IAppRole } from "../../shared/models/Settings";
|
||||
import CommissionRatesTableCell from "./CommissionRatesTableCell";
|
||||
|
||||
interface ICommissionRatesTableRowProps {
|
||||
product: IProductDetail;
|
||||
roles: IAppRole[];
|
||||
commissions: ICommissionRateChange[];
|
||||
onRateChange: (commission: ICommissionRateChange) => void;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
const CommissionRatesTableRow: React.FC<ICommissionRatesTableRowProps> = (
|
||||
props: ICommissionRatesTableRowProps
|
||||
) => {
|
||||
const rateChange = (rate: number, role: IAppRole, id: number) => {
|
||||
const commission: ICommissionRateChange = {
|
||||
id: id,
|
||||
product: props.product.product,
|
||||
ratePerProduct: rate,
|
||||
role: role
|
||||
};
|
||||
props.onRateChange(commission);
|
||||
};
|
||||
|
||||
const existingCommission = (role: IAppRole) =>
|
||||
props.commissions.find(
|
||||
(com) =>
|
||||
com.product === props.product.product &&
|
||||
com.role.name === role.name
|
||||
);
|
||||
|
||||
return (
|
||||
<tr data-test="commission-rate-row">
|
||||
<td>{props.product.title}</td>
|
||||
{props.roles.map((role, index) => (
|
||||
<td key={`${props.product.title}_${role.name}`}>
|
||||
<CommissionRatesTableCell
|
||||
id={existingCommission(role)?.id ?? 0}
|
||||
value={existingCommission(role)?.ratePerProduct ?? 0}
|
||||
onRateChange={(newRate: number, id: number) => {
|
||||
rateChange(newRate, role, id);
|
||||
}}
|
||||
tabIndex={props.rowIndex + index + 1}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommissionRatesTableRow;
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import CommissionTable from "./CommissionTable";
|
||||
import { IAuditableCommission } from "../../shared/models/Commission";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
import moment from "moment";
|
||||
|
||||
describe("A table for displaying commission audit data", () => {
|
||||
it("Displays table", () => {
|
||||
const mockCommissions: IAuditableCommission[] = [
|
||||
{
|
||||
id: 1,
|
||||
product: "ATM Card",
|
||||
role: {
|
||||
name: "Teller",
|
||||
description: "",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 99
|
||||
},
|
||||
ratePerProduct: 10,
|
||||
created: moment("2021-07-04T10:00:00Z"),
|
||||
createdBy: "tester",
|
||||
createdByType: UserType.System,
|
||||
lastUpdated: moment("2021-07-04T10:00:00Z"),
|
||||
lastUpdatedBy: "tester",
|
||||
lastUpdatedByType: UserType.System
|
||||
}
|
||||
];
|
||||
|
||||
const wrapper = mount(
|
||||
<CommissionTable commissions={mockCommissions} />
|
||||
);
|
||||
|
||||
const tableData = wrapper.find("[data-test='auto-table-cell']");
|
||||
expect(tableData.length).toBe(8);
|
||||
});
|
||||
|
||||
it("Sorts table lastUpdated desc", () => {
|
||||
const mockCommissions: IAuditableCommission[] = [
|
||||
{
|
||||
id: 1,
|
||||
product: "ATM Card",
|
||||
role: {
|
||||
name: "Teller",
|
||||
description: "",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 99
|
||||
},
|
||||
ratePerProduct: 10,
|
||||
created: moment("2021-07-04T10:00:00Z"),
|
||||
createdBy: "tester",
|
||||
createdByType: UserType.System,
|
||||
lastUpdated: moment("2021-07-04T10:00:00Z"),
|
||||
lastUpdatedBy: "tester",
|
||||
lastUpdatedByType: UserType.System
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
product: "ATM Card",
|
||||
role: {
|
||||
name: "Teller",
|
||||
description: "",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 99
|
||||
},
|
||||
ratePerProduct: 10,
|
||||
created: moment("2021-07-04T10:00:00Z"),
|
||||
createdBy: "tester",
|
||||
createdByType: UserType.System,
|
||||
lastUpdated: moment("2021-07-04T10:45:00Z"),
|
||||
lastUpdatedBy: "tester",
|
||||
lastUpdatedByType: UserType.System
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
product: "ATM Card",
|
||||
role: {
|
||||
name: "Teller",
|
||||
description: "",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 99
|
||||
},
|
||||
ratePerProduct: 10,
|
||||
created: moment("2021-07-04T10:00:00Z"),
|
||||
createdBy: "tester",
|
||||
createdByType: UserType.System,
|
||||
lastUpdated: moment("2021-07-04T10:30:00Z"),
|
||||
lastUpdatedBy: "tester",
|
||||
lastUpdatedByType: UserType.System
|
||||
}
|
||||
];
|
||||
|
||||
const wrapper = shallow(
|
||||
<CommissionTable commissions={mockCommissions} />
|
||||
);
|
||||
|
||||
const sortedCommissions: IAuditableCommission[] = wrapper
|
||||
.find("AutoTable")
|
||||
.prop("elements");
|
||||
|
||||
const sortedIds = sortedCommissions.map((commission) => commission.id);
|
||||
expect(sortedIds).toEqual([2, 3, 1]);
|
||||
});
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IAuditableCommission } from "../../shared/models/Commission";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { AutoTable } from "../auto-table/AutoTable";
|
||||
|
||||
interface ICommissionTable {
|
||||
commissions: IAuditableCommission[];
|
||||
}
|
||||
|
||||
const CommissionTableTranslate =
|
||||
Translate.Get.components.commissions.commissionTable;
|
||||
|
||||
const sortByUpdatedDate = (
|
||||
data: IAuditableCommission[]
|
||||
): IAuditableCommission[] => {
|
||||
return data.sort((a, b) => moment(b.lastUpdated).diff(a.lastUpdated));
|
||||
};
|
||||
|
||||
const CommissionTable: React.FC<ICommissionTable> = (
|
||||
props: ICommissionTable
|
||||
) => {
|
||||
const headers = [
|
||||
CommissionTableTranslate.headers.id,
|
||||
CommissionTableTranslate.headers.role,
|
||||
CommissionTableTranslate.headers.ratePerProduct,
|
||||
CommissionTableTranslate.headers.product,
|
||||
CommissionTableTranslate.headers.lastUpdated,
|
||||
CommissionTableTranslate.headers.lastUpdatedBy,
|
||||
CommissionTableTranslate.headers.created,
|
||||
CommissionTableTranslate.headers.createdBy
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IAuditableCommission) => x.id,
|
||||
(x: IAuditableCommission) => x.role.name,
|
||||
(x: IAuditableCommission) => x.ratePerProduct,
|
||||
(x: IAuditableCommission) => x.product,
|
||||
(x: IAuditableCommission) =>
|
||||
moment.utc(x.lastUpdated).local().format("lll"),
|
||||
(x: IAuditableCommission) => x.lastUpdatedBy,
|
||||
(x: IAuditableCommission) =>
|
||||
moment.utc(x.created).local().format("lll"),
|
||||
(x: IAuditableCommission) => x.createdBy
|
||||
];
|
||||
|
||||
if (props.commissions && props.commissions.length) {
|
||||
return (
|
||||
<AutoTable
|
||||
elements={sortByUpdatedDate(props.commissions)}
|
||||
headers={headers}
|
||||
cells={cells}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AutoTable elements={[]} headers={headers} cells={[]} />;
|
||||
};
|
||||
|
||||
export default CommissionTable;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import TabbedCommissionView from "./TabbedCommissionView";
|
||||
|
||||
describe("Basic commission rates editor and table", () => {
|
||||
it("should render tabs", () => {
|
||||
const mockCommissions = [];
|
||||
const wrapper = shallow(
|
||||
<TabbedCommissionView commissions={mockCommissions} />
|
||||
);
|
||||
const tabbedView = wrapper.find("[data-test='tabbed-commission-view']");
|
||||
expect(tabbedView.length).toBe(1);
|
||||
expect(tabbedView.props()["pages"].length).toBe(2);
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import TabView from "../tab-view/TabView";
|
||||
import { IAuditableCommission } from "../../shared/models/Commission";
|
||||
import CommissionRatesEditor from "../../containers/commissions-page/CommissionRatesEditor";
|
||||
import CommissionTable from "./CommissionTable";
|
||||
|
||||
interface ITabbedCommissionViewProps {
|
||||
commissions: IAuditableCommission[];
|
||||
}
|
||||
|
||||
const viewTranslate = Translate.Get.components.commissions.tabbedComissionView;
|
||||
|
||||
const TabbedCommissionView: React.FC<ITabbedCommissionViewProps> = (
|
||||
props: ITabbedCommissionViewProps
|
||||
) => {
|
||||
return (
|
||||
<TabView
|
||||
data-test="tabbed-commission-view"
|
||||
pages={[
|
||||
{
|
||||
header: viewTranslate.ratesTab,
|
||||
element: <CommissionRatesEditor />
|
||||
},
|
||||
{
|
||||
header: viewTranslate.tableTab,
|
||||
element: <CommissionTable commissions={props.commissions} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabbedCommissionView;
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import React, { useState } from "react";
|
||||
import { Alert } from "reactstrap";
|
||||
import Translate from "../../translate/Translate";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import DeadActivityTable from "./DeadActivityTable";
|
||||
import IPagedResponse from "../../shared/types/IPagedResponse";
|
||||
import PagedContentControls from "../paged-content-controls/PagedContentControls";
|
||||
import {
|
||||
generateDeadActivityForm,
|
||||
IAuditableDeadActivity,
|
||||
IDeadActivitySearchForm
|
||||
} from "../../shared/models/DeadActivity";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
import { IManualActivityFormState } from "../../shared/models/Activity";
|
||||
import { DeadActivityWizard } from "./DeadActivityForm";
|
||||
import DeadActivityDisplay from "./DeadActivityDisplay";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import AsyncGuard from "../../containers/shared/AsyncGuard";
|
||||
|
||||
export interface IDeadActivityAuditLogProps {
|
||||
deadActivityContainer: IAsyncStateContainer<
|
||||
IPagedResponse<IAuditableDeadActivity>
|
||||
>;
|
||||
getNextPage: () => void;
|
||||
getPrevPage: () => void;
|
||||
changeSort: (sort: string) => void;
|
||||
form: IDeadActivitySearchForm;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const DeadActivityAuditLog: React.FC<IDeadActivityAuditLogProps> = (props) => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
let departments: IDepartment[] = [];
|
||||
if (settings?.departments) {
|
||||
departments = settings.departments.filter((x) => x.state == "Active");
|
||||
}
|
||||
const [
|
||||
activityForm,
|
||||
setActivityForm
|
||||
] = useState<IManualActivityFormState>();
|
||||
const [submitState, setSubmitState] = useState<AsyncState>(
|
||||
AsyncState.Inactive
|
||||
);
|
||||
const deadActivities = props.deadActivityContainer.data?.items ?? [];
|
||||
|
||||
const changeSort = (headerName: string) => {
|
||||
if (props.changeSort) {
|
||||
props.changeSort(headerName);
|
||||
}
|
||||
};
|
||||
|
||||
const submitActivity = async () => {
|
||||
if (activityForm && activityForm.deadActivityId) {
|
||||
setSubmitState(AsyncState.Loading);
|
||||
let result = await ActivityService.submitManualActivityForm(
|
||||
activityForm
|
||||
);
|
||||
await ActivityService.submitDeleteDeadActivity(
|
||||
activityForm.deadActivityId,
|
||||
result.data
|
||||
);
|
||||
setSubmitState(AsyncState.Success);
|
||||
setActivityForm(undefined);
|
||||
props.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteActivity = async () => {
|
||||
if (activityForm && activityForm.deadActivityId) {
|
||||
setSubmitState(AsyncState.Loading);
|
||||
await ActivityService.submitDeleteDeadActivity(
|
||||
activityForm.deadActivityId
|
||||
);
|
||||
setSubmitState(AsyncState.Success);
|
||||
setActivityForm(undefined);
|
||||
props.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const onFormChange = (newValue: IManualActivityFormState) => {
|
||||
setActivityForm(newValue);
|
||||
};
|
||||
|
||||
const onCancelClick = () => {
|
||||
setActivityForm(undefined);
|
||||
};
|
||||
|
||||
const onRowClick = (deadActivity: IAuditableDeadActivity) => {
|
||||
setSubmitState(AsyncState.Inactive);
|
||||
setActivityForm(generateDeadActivityForm(deadActivity));
|
||||
};
|
||||
|
||||
const getActivityById = (
|
||||
deadActivityId: number
|
||||
): IAuditableDeadActivity | undefined => {
|
||||
if (deadActivities) {
|
||||
const index = deadActivities.findIndex(
|
||||
(da) => da.id === deadActivityId
|
||||
);
|
||||
return deadActivities[index];
|
||||
}
|
||||
};
|
||||
|
||||
const deadActivityForm = () => {
|
||||
if (activityForm && activityForm.deadActivityId) {
|
||||
return (
|
||||
<div>
|
||||
<DeadActivityWizard
|
||||
formState={activityForm}
|
||||
submitStatus={submitState}
|
||||
handleChange={onFormChange}
|
||||
submitManualActivity={submitActivity}
|
||||
deleteManualActivity={deleteActivity}
|
||||
clearForm={onCancelClick}
|
||||
departments={departments}
|
||||
/>
|
||||
<DeadActivityDisplay
|
||||
activity={getActivityById(activityForm.deadActivityId)!}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-row justify-content-center alight-items-center">
|
||||
<AsyncGuard container={props.deadActivityContainer}>
|
||||
<div>
|
||||
<div className="d-flex flex-row justify-content-center align-items-center">
|
||||
<PagedContentControls
|
||||
nextPage={() => props.getNextPage()}
|
||||
prevPage={() => props.getPrevPage()}
|
||||
pagedResponse={props.deadActivityContainer.data}
|
||||
/>
|
||||
</div>
|
||||
<DeadActivityTable
|
||||
deadActivities={deadActivities}
|
||||
changeSort={changeSort}
|
||||
sortBy={props.form.sortBy}
|
||||
sortDescending={props.form.sortDescending}
|
||||
deadActivityForm={deadActivityForm()}
|
||||
onRowClick={onRowClick}
|
||||
editDeadActivityId={activityForm?.deadActivityId}
|
||||
/>
|
||||
<div className="d-flex flex-row justify-content-center align-items-center">
|
||||
<PagedContentControls
|
||||
nextPage={() => props.getNextPage()}
|
||||
prevPage={() => props.getPrevPage()}
|
||||
pagedResponse={props.deadActivityContainer.data}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AsyncGuard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeadActivityAuditLog;
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IAuditableDeadActivity } from "../../shared/models/DeadActivity";
|
||||
import GoalService from "../../shared/services/GoalService";
|
||||
import { Col, Row } from "reactstrap";
|
||||
|
||||
interface IActivityDisplayProps {
|
||||
activity: IAuditableDeadActivity;
|
||||
}
|
||||
|
||||
const activityTranslate =
|
||||
Translate.Get.components.activityDetails.activityDisplay;
|
||||
const detailTranslate =
|
||||
Translate.Get.components.activityDetails.activityDetailDisplay;
|
||||
|
||||
const DeadActivityDisplay: React.FC<IActivityDisplayProps> = (
|
||||
props: IActivityDisplayProps
|
||||
) => {
|
||||
const details = props.activity.deadDetail || {};
|
||||
|
||||
return (
|
||||
<Row className="mt-2">
|
||||
<Col md={4} className="col-md-4 offset-md-2">
|
||||
<table className="table table-responsive">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{activityTranslate.headers.type}
|
||||
</td>
|
||||
<td>{props.activity.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{activityTranslate.headers.sourceSystem}
|
||||
</td>
|
||||
<td>{props.activity.sourceSystem}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{activityTranslate.headers.sourceId}
|
||||
</td>
|
||||
<td>{props.activity.sourceId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{activityTranslate.headers.sourceCreated}
|
||||
</td>
|
||||
<td>
|
||||
{moment
|
||||
.utc(props.activity.sourceCreated)
|
||||
.local()
|
||||
.format("lll")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.dnaPersonNumber}
|
||||
</td>
|
||||
<td>{details.dnaPersonNbr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.accountNumber}
|
||||
</td>
|
||||
<td>{details.accountNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.subjectPersonNbr}
|
||||
</td>
|
||||
<td>{details.subjectPersonNbr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.subjectOrgNbr}
|
||||
</td>
|
||||
<td>{details.subjectOrgNbr}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<table className="table table-responsive">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.memberName}
|
||||
</td>
|
||||
<td>{details.memberName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.stationName}
|
||||
</td>
|
||||
<td>{details.stationName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.branchOrgNbr}
|
||||
</td>
|
||||
<td>{details.branchOrgNbr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.branchOrgName}
|
||||
</td>
|
||||
<td>{details.branchOrgName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.accountStatus}
|
||||
</td>
|
||||
<td>{details.accountStatus}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.accountBalance}
|
||||
</td>
|
||||
<td>
|
||||
{GoalService.formatMonetary(
|
||||
details.accountBalance
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{detailTranslate.headers.debtProtectionAddDate}
|
||||
</td>
|
||||
<td>
|
||||
{details.debtProtectionAddDate
|
||||
? moment(
|
||||
details.debtProtectionAddDate
|
||||
).format("MM-DD-YYYY")
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{
|
||||
detailTranslate.headers
|
||||
.debtProtectionRemoveDate
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{details.debtProtectionRemoveDate
|
||||
? moment(
|
||||
details.debtProtectionRemoveDate
|
||||
).format("MM-DD-YYYY")
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="tdLabel">
|
||||
{
|
||||
detailTranslate.headers
|
||||
.isDebtProtectionEligible
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{details.isDebtProtectionEligible
|
||||
? "True"
|
||||
: "False"}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeadActivityDisplay;
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
import {
|
||||
IManualActivityFormState,
|
||||
ManualActivityState
|
||||
} from "../../shared/models/Activity";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import { tryUpdateStateWithInput } from "../../shared/services/InputService";
|
||||
import Translate from "../../translate/Translate";
|
||||
import ActivityWizardAlertBar from "../activity/manual-activity-wizard/ActivityWizardAlertBar";
|
||||
import {
|
||||
Col,
|
||||
Form,
|
||||
FormFeedback,
|
||||
FormGroup,
|
||||
Input,
|
||||
Label,
|
||||
Row
|
||||
} from "reactstrap";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import UserSearchSelect from "../activity/manual-activity-wizard/UserSearchSelect";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
|
||||
export interface IManualActivityWizardProps {
|
||||
formState: IManualActivityFormState;
|
||||
submitStatus: AsyncState;
|
||||
handleChange: (formState: IManualActivityFormState) => void;
|
||||
submitManualActivity: () => void;
|
||||
deleteManualActivity: () => void;
|
||||
clearForm: () => void;
|
||||
departments: IDepartment[];
|
||||
}
|
||||
|
||||
const activityProductFormTranslate =
|
||||
Translate.Get.components.activity.manualActivityWizard.activityProductForm;
|
||||
|
||||
export const DeadActivityWizard: React.FC<IManualActivityWizardProps> = (
|
||||
props: IManualActivityWizardProps
|
||||
) => {
|
||||
const handlePartialChangeFromForm = (
|
||||
formData: Partial<IManualActivityFormState>
|
||||
): void => {
|
||||
const newState: IManualActivityFormState = {
|
||||
...props.formState,
|
||||
...formData
|
||||
};
|
||||
props.handleChange(newState);
|
||||
};
|
||||
|
||||
const onFormDataChange = (field: string, newValue: any) => {
|
||||
const updatedFormData = tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
field,
|
||||
newValue
|
||||
);
|
||||
handlePartialChangeFromForm(updatedFormData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ActivityWizardAlertBar submitStatus={props.submitStatus} />
|
||||
<Form className="px-5" noValidate>
|
||||
<Row>
|
||||
<Col md="2">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activityProductFormTranslate.departmentId
|
||||
.label
|
||||
}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="CreditToDepartment"
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
required
|
||||
invalid={
|
||||
props.formState.showValidation &&
|
||||
!props.formState.departmentId
|
||||
}
|
||||
data-test="dept-input"
|
||||
value={props.formState.departmentId}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"departmentId",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-option"
|
||||
key={undefined}
|
||||
value=""
|
||||
>
|
||||
{
|
||||
activityProductFormTranslate
|
||||
.departmentId.placeholder
|
||||
}
|
||||
</option>
|
||||
{props.departments.map((department) => {
|
||||
return (
|
||||
<option
|
||||
key={department.id}
|
||||
value={department.id}
|
||||
>
|
||||
{department.departmentName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
<FormFeedback>Please select a value.</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{activityProductFormTranslate.personId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<UserSearchSelect
|
||||
data-test="CreditToAssociate"
|
||||
employeeId={props.formState.employeeId ?? ""}
|
||||
onUserSelected={onFormDataChange}
|
||||
showValidation={props.formState.showValidation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="2">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activityProductFormTranslate.activityState
|
||||
.label
|
||||
}
|
||||
</Label>
|
||||
<Input
|
||||
name="ActivityState"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
value={props.formState.state}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"state",
|
||||
event.target.value
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Object.values(ManualActivityState).map(
|
||||
(activityState) => {
|
||||
return (
|
||||
<option key={activityState}>
|
||||
{activityState}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<option key="Created">Created</option>
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md="2">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{
|
||||
activityProductFormTranslate
|
||||
.acceptedDateTime.label
|
||||
}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="AcceptedStateDateTime"
|
||||
type="datetime-local"
|
||||
bsSize="sm"
|
||||
required
|
||||
invalid={
|
||||
props.formState.showValidation &&
|
||||
!props.formState.accepted
|
||||
}
|
||||
value={
|
||||
props.formState.accepted
|
||||
? props.formState.accepted
|
||||
.format("YYYY-MM-DDTHH:mm")
|
||||
.toString()
|
||||
: ""
|
||||
}
|
||||
onChange={(event) => {
|
||||
onFormDataChange(
|
||||
"accepted",
|
||||
event.target.value
|
||||
? moment(event.target.value)
|
||||
: ""
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
Accepted Date is required.
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="d-flex justify-content-end">
|
||||
<button
|
||||
className="btn btn-danger m-1"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.clearForm();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger m-1"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.deleteManualActivity();
|
||||
}}
|
||||
>
|
||||
Delete This Activity
|
||||
</button>
|
||||
<button
|
||||
disabled={props.submitStatus === AsyncState.Loading}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (
|
||||
ActivityService.isValidManualActivity(
|
||||
props.formState
|
||||
)
|
||||
) {
|
||||
props.submitManualActivity();
|
||||
} else {
|
||||
onFormDataChange("showValidation", true);
|
||||
}
|
||||
}}
|
||||
className="btn btn-primary m-1"
|
||||
>
|
||||
{
|
||||
Translate.Get.components.activity
|
||||
.manualActivityWizard.submitButton
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import React from "react";
|
||||
import { ActivityType } from "./../../shared/models/Activity";
|
||||
import moment from "moment";
|
||||
import Translate from "../../translate/Translate";
|
||||
import {
|
||||
AutoTable,
|
||||
IAutoTableHeader,
|
||||
IColumnSort
|
||||
} from "../auto-table/AutoTable";
|
||||
import UserDisplay from "../../containers/shared/UserDisplay";
|
||||
import { IAuditableDeadActivity } from "../../shared/models/DeadActivity";
|
||||
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
|
||||
|
||||
interface IDeadActivityTable {
|
||||
deadActivities?: IAuditableDeadActivity[];
|
||||
changeSort?: (sort: string) => void;
|
||||
sortBy?: string;
|
||||
sortDescending?: boolean;
|
||||
activityTypeFilter?: ActivityType;
|
||||
deadActivityForm?: JSX.Element;
|
||||
editDeadActivityId?: number;
|
||||
onRowClick: (activity: IAuditableDeadActivity) => void;
|
||||
}
|
||||
|
||||
const _Translate = Translate.Get.components.deadActivity.deadActivityTable;
|
||||
|
||||
const DeadActivityTable: React.FC<IDeadActivityTable> = (props) => {
|
||||
const headers = [
|
||||
_Translate.headers.accepted,
|
||||
_Translate.headers.id,
|
||||
_Translate.headers.product,
|
||||
_Translate.headers.activityType,
|
||||
_Translate.headers.source,
|
||||
_Translate.headers.sourceId,
|
||||
_Translate.headers.reasonNote,
|
||||
_Translate.headers.associate,
|
||||
_Translate.headers.department,
|
||||
_Translate.headers.accountNbr,
|
||||
_Translate.headers.memberName,
|
||||
_Translate.headers.dpEligible
|
||||
];
|
||||
|
||||
const fieldNames = [
|
||||
"Accepted",
|
||||
"Id",
|
||||
"Product",
|
||||
"Type",
|
||||
"SourceSystem",
|
||||
"SourceId",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
];
|
||||
|
||||
const changeSort = (headerName: string) => {
|
||||
if (props.changeSort) {
|
||||
props.changeSort(headerName);
|
||||
}
|
||||
};
|
||||
|
||||
const sortHeaders: IAutoTableHeader[] = headers.map((headerName, idx) => {
|
||||
return {
|
||||
display: headerName,
|
||||
onClick: () => {
|
||||
if (fieldNames[idx] !== "") {
|
||||
changeSort(fieldNames[idx]);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const cells = [
|
||||
(x: IAuditableDeadActivity) =>
|
||||
moment.utc(x.accepted).local().format("lll"),
|
||||
(x: IAuditableDeadActivity) => x.id,
|
||||
(x: IAuditableDeadActivity) => x.product,
|
||||
(x: IAuditableDeadActivity) => x.type,
|
||||
(x: IAuditableDeadActivity) => x.sourceSystem,
|
||||
(x: IAuditableDeadActivity) => x.sourceId,
|
||||
(x: IAuditableDeadActivity) => x.reason + ": " + x.reasonNote,
|
||||
(x: IAuditableDeadActivity) => (
|
||||
<UserDisplay employeeId={x.employeeId ?? ""} />
|
||||
),
|
||||
(x: IAuditableDeadActivity) => (
|
||||
<DepartmentDisplay
|
||||
departmentId={x.departmentId ? parseInt(x.departmentId) : -1}
|
||||
/>
|
||||
),
|
||||
(x: IAuditableDeadActivity) => x.deadDetail?.accountNumber ?? "-",
|
||||
(x: IAuditableDeadActivity) => x.deadDetail?.memberName ?? "-",
|
||||
(x: IAuditableDeadActivity) =>
|
||||
(x.deadDetail?.isDebtProtectionEligible ? "Yes - " : "No - ") +
|
||||
(x.deadDetail?.debtProtectionAddDate
|
||||
? moment
|
||||
.utc(x.deadDetail?.debtProtectionAddDate)
|
||||
.local()
|
||||
.format("ll")
|
||||
: "")
|
||||
];
|
||||
|
||||
let deadActivities = props.deadActivities || [];
|
||||
if (props.activityTypeFilter) {
|
||||
deadActivities = deadActivities.filter(
|
||||
(activity) => activity.type === props.activityTypeFilter
|
||||
);
|
||||
}
|
||||
|
||||
const findIndexForColumn = (column?: string) => {
|
||||
return fieldNames.indexOf(column || "Accepted");
|
||||
};
|
||||
|
||||
const changeSortedColumn: IColumnSort = {
|
||||
index: findIndexForColumn(props.sortBy),
|
||||
isReversed: props.sortDescending ?? false
|
||||
};
|
||||
|
||||
if (deadActivities.length > 0) {
|
||||
return (
|
||||
<div className="mt-auto" data-test="autotable-container">
|
||||
<AutoTable
|
||||
className="border scale-down"
|
||||
elements={deadActivities}
|
||||
headers={sortHeaders}
|
||||
cells={cells}
|
||||
sortedColumn={changeSortedColumn}
|
||||
onRowClick={props.onRowClick}
|
||||
overrideRowFn={(activity: IAuditableDeadActivity) => {
|
||||
return (
|
||||
props.deadActivityForm != undefined &&
|
||||
activity.id === props.editDeadActivityId
|
||||
);
|
||||
}}
|
||||
overrideRow={(activity: IAuditableDeadActivity) => {
|
||||
return props.deadActivityForm ? (
|
||||
<tr key="dead_activity_override_row">
|
||||
<td colSpan={99}>{props.deadActivityForm}</td>
|
||||
</tr>
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="scrollable-top-parent">
|
||||
<div className="scrollable-top-child d-flex flex-row justify-content-center align-items-center bg-light">
|
||||
<p data-test="no-data-message" className=" mt-2">
|
||||
<i>{_Translate.noData}</i>
|
||||
</p>
|
||||
</div>
|
||||
<AutoTable
|
||||
className="scrollable-top-child"
|
||||
elements={[]}
|
||||
headers={sortHeaders}
|
||||
cells={[]}
|
||||
sortedColumn={props.changeSort ? changeSortedColumn : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeadActivityTable;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import { IDepartmentAssignmentWithEmployee } from "../../shared/models/DepartmentAssignment";
|
||||
import DepartmentDetailAssignments from "./DepartmentDetailAssignments";
|
||||
import { mockDeptAssignments } from "../../shared/testing/mocks/MockDepartmentAssignments";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
|
||||
describe("DepartmentDetailAssignments", () => {
|
||||
it("should display the table if there are assignments", () => {
|
||||
const assignments: IDepartmentAssignmentWithEmployee[] = [
|
||||
{
|
||||
...mockDeptAssignments[0],
|
||||
employee: {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code:
|
||||
PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT,
|
||||
description: "ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
}
|
||||
],
|
||||
hasCommissions: false
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [],
|
||||
assignments: []
|
||||
}
|
||||
}
|
||||
];
|
||||
const wrapper = mount(
|
||||
<DepartmentDetailAssignments assignments={assignments} />
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='no-department-detail-assignments']")
|
||||
.length
|
||||
).toBe(0);
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-assignments']").length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("should display no assignments if assignments are undefined", () => {
|
||||
const wrapper = mount(<DepartmentDetailAssignments />);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='no-department-detail-assignments']")
|
||||
.length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-assignments']").length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should display no assignments if assignments length is 0", () => {
|
||||
const assignments: IDepartmentAssignmentWithEmployee[] = [];
|
||||
const wrapper = mount(
|
||||
<DepartmentDetailAssignments assignments={assignments} />
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='no-department-detail-assignments']")
|
||||
.length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-assignments']").length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IDepartmentAssignmentWithEmployee } from "../../shared/models/DepartmentAssignment";
|
||||
import { AutoTable } from "../auto-table/AutoTable";
|
||||
import Translate from "../../translate/Translate";
|
||||
import UserDisplay, {
|
||||
UserDisplayTargets
|
||||
} from "../../containers/shared/UserDisplay";
|
||||
|
||||
interface IDepartmentDetailAssignments {
|
||||
assignments?: IDepartmentAssignmentWithEmployee[];
|
||||
}
|
||||
|
||||
const assignmentTranslate =
|
||||
Translate.Get.components.departmentDetail.assignments;
|
||||
|
||||
const DepartmentDetailAssignments: React.FC<IDepartmentDetailAssignments> = (
|
||||
props
|
||||
) => {
|
||||
const headers = [
|
||||
assignmentTranslate.headers.home,
|
||||
assignmentTranslate.headers.name,
|
||||
assignmentTranslate.headers.startDate,
|
||||
assignmentTranslate.headers.endDate,
|
||||
assignmentTranslate.headers.role,
|
||||
assignmentTranslate.headers.payout,
|
||||
assignmentTranslate.headers.note
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IDepartmentAssignmentWithEmployee) =>
|
||||
x.isHome ? assignmentTranslate.home : assignmentTranslate.float,
|
||||
(x: IDepartmentAssignmentWithEmployee) => (
|
||||
<UserDisplay
|
||||
employeeId={x.employee.employeeId}
|
||||
target={UserDisplayTargets.Dashboard}
|
||||
/>
|
||||
),
|
||||
(x: IDepartmentAssignmentWithEmployee) =>
|
||||
moment(x.startDate).format("ll"),
|
||||
(x: IDepartmentAssignmentWithEmployee) =>
|
||||
x.endDate
|
||||
? moment(x.endDate).format("ll")
|
||||
: assignmentTranslate.current,
|
||||
(x: IDepartmentAssignmentWithEmployee) =>
|
||||
x.role ? x.role.name : assignmentTranslate.missingRole,
|
||||
(x: IDepartmentAssignmentWithEmployee) => x.slidingPayout + "%",
|
||||
(x: IDepartmentAssignmentWithEmployee) => x.note
|
||||
];
|
||||
|
||||
if (!props.assignments?.length) {
|
||||
return (
|
||||
<div data-test="no-department-detail-assignments">
|
||||
<em className="mb-2">{assignmentTranslate.noAssignments}</em>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AutoTable
|
||||
data-test="department-detail-assignments"
|
||||
className="border rounded"
|
||||
headers={headers}
|
||||
cells={cells}
|
||||
elements={props.assignments}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentDetailAssignments;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
import { IDepartmentDetails } from "../../shared/models/Department";
|
||||
import DepartmentDetailDisplay from "./DepartmentDetailDisplay";
|
||||
import { getMockDepartments } from "../../shared/testing/mocks/MockDepartments";
|
||||
import { mockDeptAssignments } from "../../shared/testing/mocks/MockDepartmentAssignments";
|
||||
import { IDepartmentAssignmentWithEmployee } from "../../shared/models/DepartmentAssignment";
|
||||
|
||||
describe("DepartmentDetailDisplay", () => {
|
||||
it("should display department details", () => {
|
||||
const mockEmployee = {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code: PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT,
|
||||
description: "ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
}
|
||||
],
|
||||
hasCommissions: false
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [],
|
||||
assignments: []
|
||||
};
|
||||
const mockDepartmentAssignments: IDepartmentAssignmentWithEmployee[] = [
|
||||
{ ...mockDeptAssignments[0], employee: mockEmployee },
|
||||
{ ...mockDeptAssignments[1], employee: mockEmployee }
|
||||
];
|
||||
|
||||
const mockDepartment = getMockDepartments()[0];
|
||||
mockDepartment.regionCode = "NORTH";
|
||||
const details: IDepartmentDetails = {
|
||||
assignments: mockDepartmentAssignments,
|
||||
regionalManager: mockEmployee
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DepartmentDetailDisplay
|
||||
department={mockDepartment}
|
||||
details={details}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("[data-test='department-detail-id']").text()).toBe(
|
||||
"55"
|
||||
);
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-region']").text()
|
||||
).toBe("NORTH");
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='department-detail-regionalManager']")
|
||||
.text()
|
||||
).toBe("John Tester");
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-status']").text()
|
||||
).toBe("Active");
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='department-detail-activeAssignments']")
|
||||
.text()
|
||||
).toBe("2");
|
||||
});
|
||||
|
||||
it("should display department details", () => {
|
||||
const mockEmployee = {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code: PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT,
|
||||
description: "ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
}
|
||||
],
|
||||
hasCommissions: false
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [],
|
||||
assignments: []
|
||||
};
|
||||
const mockDepartmentAssignments: IDepartmentAssignmentWithEmployee[] = [
|
||||
{ ...mockDeptAssignments[0], employee: mockEmployee },
|
||||
{ ...mockDeptAssignments[1], employee: mockEmployee }
|
||||
];
|
||||
|
||||
const mockDepartment = getMockDepartments()[0];
|
||||
const details: IDepartmentDetails = {
|
||||
assignments: mockDepartmentAssignments
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DepartmentDetailDisplay
|
||||
department={mockDepartment}
|
||||
details={details}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-region']").text()
|
||||
).toBe("No region assigned");
|
||||
expect(
|
||||
wrapper
|
||||
.find("[data-test='department-detail-regionalManager']")
|
||||
.text()
|
||||
).toBe("No regional manager assigned");
|
||||
});
|
||||
});
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IDepartment,
|
||||
IDepartmentDetails
|
||||
} from "../../shared/models/Department";
|
||||
import Translate from "../../translate/Translate";
|
||||
import BusinessLineDisplay from "../../containers/shared/BusinessLineDisplay";
|
||||
|
||||
interface IDepartmentDetailDisplay {
|
||||
department: IDepartment;
|
||||
details: IDepartmentDetails;
|
||||
}
|
||||
|
||||
const detailTranslate = Translate.Get.components.departmentDetail.detailDisplay;
|
||||
|
||||
const DepartmentDetailDisplay: React.FC<IDepartmentDetailDisplay> = (props) => {
|
||||
return (
|
||||
<table
|
||||
data-test="department-detail-display"
|
||||
className="table table-responsive tablecenterCSS tablecenterheadCSS"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{detailTranslate.headers.id}</th>
|
||||
<td data-test="department-detail-id">
|
||||
{props.department.id}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{detailTranslate.headers.region}</th>
|
||||
<td data-test="department-detail-region">
|
||||
{props.department.regionCode ?? (
|
||||
<em>{detailTranslate.noRegion}</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{detailTranslate.headers.regionalManager}</th>
|
||||
<td data-test="department-detail-regionalManager">
|
||||
{props.details?.regionalManager?.fullName ?? (
|
||||
<em>{detailTranslate.noRegionalManager}</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{detailTranslate.headers.status}</th>
|
||||
<td data-test="department-detail-status">
|
||||
{props.department.state}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{detailTranslate.headers.businessLine}</th>
|
||||
<td data-test="department-detail-businessLine">
|
||||
<BusinessLineDisplay
|
||||
businessLineCode={props.department.businessLineCode}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{detailTranslate.headers.activeAssignments}</th>
|
||||
|
||||
<td data-test="department-detail-activeAssignments">
|
||||
{props.details.assignments.length ?? 0}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentDetailDisplay;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
|
||||
import { IDepartmentProgress } from "../../shared/models/Activity";
|
||||
import UserDisplay, {
|
||||
UserDisplayTargets
|
||||
} from "../../containers/shared/UserDisplay";
|
||||
import ProductTableHeader from "../shared/ProductTableHeader";
|
||||
import ProgressTableRow from "../shared/ProgressTableRow";
|
||||
import ProgressPercentageTableRow from "../shared/ProgressPercentageTableRow";
|
||||
|
||||
export interface IDepartmentProgressProps {
|
||||
departmentProgress: IDepartmentProgress;
|
||||
}
|
||||
|
||||
const DepartmentProgressDisplay: React.FC<IDepartmentProgressProps> = (
|
||||
props: IDepartmentProgressProps
|
||||
) => {
|
||||
const departmentId = props.departmentProgress.departmentId;
|
||||
const goals = props.departmentProgress.goals;
|
||||
const progress = props.departmentProgress.progress;
|
||||
const employeeGoals = props.departmentProgress.employeeGoals;
|
||||
const employeeActivity = props.departmentProgress.employeeProgress;
|
||||
const employees = props.departmentProgress.employeeList;
|
||||
|
||||
const employeeRows = employees.map((employeeId) => {
|
||||
const goal = employeeGoals[employeeId];
|
||||
const activity = employeeActivity[employeeId];
|
||||
if (!goal || !activity) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
<ProgressTableRow
|
||||
label={
|
||||
<UserDisplay
|
||||
employeeId={employeeId}
|
||||
target={UserDisplayTargets.Dashboard}
|
||||
/>
|
||||
}
|
||||
progress={activity}
|
||||
goals={goal}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<table
|
||||
className="table table-striped tablecenterCSS tablecenterheadCSS"
|
||||
data-test="department-goal-progress-table-view"
|
||||
>
|
||||
<ProductTableHeader />
|
||||
<tbody>
|
||||
<ProgressTableRow
|
||||
label={<DepartmentDisplay departmentId={departmentId} />}
|
||||
progress={progress}
|
||||
goals={goals}
|
||||
/>
|
||||
<ProgressPercentageTableRow
|
||||
label="Goal Progress"
|
||||
progress={progress}
|
||||
goals={goals}
|
||||
/>
|
||||
{employeeRows}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentProgressDisplay;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import DepartmentDisplay, {
|
||||
DepartmentDisplayTargets
|
||||
} from "../../containers/shared/DepartmentDisplay";
|
||||
import { IDepartmentProgress } from "../../shared/models/Activity";
|
||||
import ProductTableHeader from "../shared/ProductTableHeader";
|
||||
import ProgressTableRow from "../shared/ProgressTableRow";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import { IMonthRange } from "../../shared/types/IMonthRange";
|
||||
|
||||
export interface IDepartmentProgressProps {
|
||||
showDetails?: boolean;
|
||||
label: string;
|
||||
monthRange: IMonthRange;
|
||||
departments: IDepartmentProgress[];
|
||||
}
|
||||
|
||||
const MultiDepartmentProgressDisplay: React.FC<IDepartmentProgressProps> = (
|
||||
props: IDepartmentProgressProps
|
||||
) => {
|
||||
const totals = ActivityService.calculateTotalProgress(props.departments);
|
||||
|
||||
const detailRows = props.showDetails
|
||||
? props.departments.map((progress, index) => {
|
||||
return (
|
||||
<ProgressTableRow
|
||||
label={
|
||||
<DepartmentDisplay
|
||||
departmentId={progress.departmentId}
|
||||
target={DepartmentDisplayTargets.Dashboard}
|
||||
/>
|
||||
}
|
||||
progress={progress.progress}
|
||||
goals={progress.goals}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<table
|
||||
className="table table-striped tablecenterCSS tablecenterheadCSS"
|
||||
data-test="multi-department-goal-progress-table-view"
|
||||
>
|
||||
<ProductTableHeader />
|
||||
<tbody>
|
||||
<ProgressTableRow
|
||||
label={props.label + " Totals"}
|
||||
progress={totals.activityTotal}
|
||||
goals={totals.goalTotal}
|
||||
/>
|
||||
{detailRows}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiDepartmentProgressDisplay;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import TabbedDepartmentProgressView from "./TabbedDepartmentProgressView";
|
||||
import { IDepartmentGoalsForRange } from "../../shared/models/Goal";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
import moment from "moment";
|
||||
import {
|
||||
ActivityType,
|
||||
IDepartmentProgress,
|
||||
SystemType
|
||||
} from "../../shared/models/Activity";
|
||||
import * as reactRedux from "react-redux";
|
||||
import { IDepartmentState } from "../../reducers/DepartmentReducer";
|
||||
|
||||
describe("Tabbed department progress view", () => {
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
it("should render the tab view", () => {
|
||||
const state = {
|
||||
departmentState: {
|
||||
departments: {
|
||||
data: [{ id: 1 }]
|
||||
}
|
||||
} as IDepartmentState
|
||||
};
|
||||
useDispatchMock.mockReturnValue(jest.fn());
|
||||
useSelectorMock.mockImplementation((x) => x(state));
|
||||
const goals: IDepartmentGoalsForRange = {
|
||||
departmentId: 1,
|
||||
monthRange: {
|
||||
fromMonth: 1,
|
||||
fromYear: 2021,
|
||||
toMonth: 2,
|
||||
toYear: 2021
|
||||
},
|
||||
goals: {
|
||||
productGoals: {
|
||||
"product one": 10,
|
||||
"product two": 20
|
||||
}
|
||||
},
|
||||
goalAudit: [
|
||||
{
|
||||
id: 1,
|
||||
departmentId: 1,
|
||||
employeeId: "C1",
|
||||
product: Product2.Mortgage,
|
||||
startDate: moment(),
|
||||
endDate: moment(),
|
||||
value: 10,
|
||||
created: moment(),
|
||||
createdBy: "manager",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedBy: "manager",
|
||||
lastUpdatedByType: UserType.Associate
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
departmentId: 1,
|
||||
employeeId: "C1",
|
||||
product: Product2.Vehicle,
|
||||
startDate: moment(),
|
||||
endDate: moment(),
|
||||
value: 20,
|
||||
created: moment(),
|
||||
createdBy: "manager",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedBy: "manager",
|
||||
lastUpdatedByType: UserType.Associate
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const progress: IDepartmentProgress = {
|
||||
departmentId: 1,
|
||||
monthRange: {
|
||||
fromMonth: 1,
|
||||
fromYear: 2021,
|
||||
toMonth: 2,
|
||||
toYear: 2021
|
||||
},
|
||||
progress: {
|
||||
productActivity: {
|
||||
"product one": 1
|
||||
}
|
||||
},
|
||||
activityAudit: [
|
||||
{
|
||||
id: 88,
|
||||
product: Product2.Mortgage,
|
||||
type: ActivityType.Sale,
|
||||
sourceSystem: SystemType.Dna,
|
||||
detail: {},
|
||||
departmentId: "1",
|
||||
employeeId: "C1",
|
||||
created: moment(),
|
||||
createdBy: "manager",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedBy: "manager",
|
||||
lastUpdatedByType: UserType.Associate,
|
||||
activityPayouts: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<TabbedDepartmentProgressView
|
||||
departmentGoals={goals}
|
||||
departmentProgress={progress}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='department-progress-tabbed-view']").length
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { IDepartmentProgress } from "../../shared/models/Activity";
|
||||
import TabView from "../tab-view/TabView";
|
||||
import GoalTable from "../goal/GoalTable";
|
||||
import Translate from "../../translate/Translate";
|
||||
import DepartmentProgressDisplay from "./DepartmentProgressDisplay";
|
||||
import ActivityTableWithStateFilter from "../activity/activity-table/ActivityTableWithStateFilter";
|
||||
|
||||
export interface IDepartmentProgressProps {
|
||||
departmentProgress: IDepartmentProgress;
|
||||
}
|
||||
|
||||
const TabViewTranslate = Translate.Get.containers.progressView;
|
||||
|
||||
const TabbedDepartmentProgressView: React.FC<IDepartmentProgressProps> = (
|
||||
props: IDepartmentProgressProps
|
||||
) => {
|
||||
return (
|
||||
<TabView
|
||||
data-test="department-progress-tabbed-view"
|
||||
pages={[
|
||||
{
|
||||
header:
|
||||
TabViewTranslate.progressTabGroup.departmentProgress,
|
||||
element: (
|
||||
<DepartmentProgressDisplay
|
||||
departmentProgress={props.departmentProgress}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: TabViewTranslate.progressTabGroup.goalAudit,
|
||||
element: (
|
||||
<GoalTable goals={props.departmentProgress.goalAudit} />
|
||||
)
|
||||
},
|
||||
{
|
||||
header: TabViewTranslate.progressTabGroup.activityAudit,
|
||||
element: (
|
||||
<ActivityTableWithStateFilter
|
||||
activities={props.departmentProgress.activityAudit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabbedDepartmentProgressView;
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
/* eslint-disable @typescript-eslint/quotes */
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import AddNewDepartmentRow from "./AddNewDepartmentRow";
|
||||
import Translate from "../../translate/Translate";
|
||||
|
||||
let wrapper: any;
|
||||
|
||||
const mockUniqueFunction = jest.fn();
|
||||
const mockSubmitFunction = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockUniqueFunction.mockReturnValue(true);
|
||||
wrapper = mount(
|
||||
<table>
|
||||
<tbody>
|
||||
<AddNewDepartmentRow
|
||||
submitNewDepartment={(department) =>
|
||||
mockSubmitFunction(department)
|
||||
}
|
||||
isDepartmentNameUnique={(deptName) => {
|
||||
return mockUniqueFunction();
|
||||
}}
|
||||
businessLines={[
|
||||
{ name: "businessLine1", code: "bl1", orgCode: "ORG" },
|
||||
{ name: "businessLine2", code: "bl2", orgCode: "ORG" }
|
||||
]}
|
||||
departmentStateSet={{
|
||||
Active: "Active",
|
||||
Inactive: "Inactive"
|
||||
}}
|
||||
regions={[
|
||||
{
|
||||
name: "North",
|
||||
businessLineCode: "BCHOPS",
|
||||
code: "NORTH"
|
||||
},
|
||||
{
|
||||
name: "South",
|
||||
businessLineCode: "BCHOPS",
|
||||
code: "South"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
});
|
||||
|
||||
describe("Associate views initial form", () => {
|
||||
it("Has proper placeholders in initial state", () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.businessLinePlaceholder
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.regionPlaceholder
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.statePlaceholder
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate Can Make Changes", () => {
|
||||
test("Updating Values Updates State", () => {
|
||||
expect(wrapper.find("td").length).toBe(5);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("NewName");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("bl1");
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe("NORTH");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("Inactive");
|
||||
});
|
||||
|
||||
test("Only - allowed as special charcter", () => {
|
||||
expect(wrapper.find("td").length).toBe(5);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", {
|
||||
target: { value: "New-Name Name.!@#$!#@$" }
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("New-NameName");
|
||||
});
|
||||
|
||||
test("Clear is hidden until Changes are Made - Name", () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("Clear is hidden until Changes are Made - BusinessLine", () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("Clear is hidden until Changes are Made - State", () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("Clear is hidden until Changes are Made - Region", () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.prop("hidden")
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate Inputs Are Validated", () => {
|
||||
test("Name Must Be at least 3 Characters", () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Ne" } });
|
||||
wrapper.update();
|
||||
///Input disabled because too short
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
expect(wrapper.find("FormFeedback").text()).toContain(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.validation.departmentNameLength
|
||||
);
|
||||
|
||||
//Input Enabled once name is > 2 characters
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "New" } });
|
||||
wrapper.update();
|
||||
expect(wrapper.find("FormFeedback").hostNodes().length).toBe(0);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("Name Must Be Unique", () => {
|
||||
mockUniqueFunction.mockReturnValue(false);
|
||||
wrapper.update();
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewNewName" } });
|
||||
wrapper.update();
|
||||
|
||||
///Input disabled because unique check fails
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
expect(wrapper.find("FormFeedback").text()).toContain(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.validation.departmentNameUnique
|
||||
);
|
||||
|
||||
mockUniqueFunction.mockReturnValue(true);
|
||||
wrapper.update();
|
||||
|
||||
//Input Enabled once name is unique
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewNewName" } });
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate can Abandon Changes", () => {
|
||||
test("Cancel Button Clears Inputs and Returns to Display State", () => {
|
||||
expect(wrapper.find("td").length).toBe(5);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("NewName");
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe("NORTH");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("Inactive");
|
||||
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.simulate("click");
|
||||
expect(wrapper.find("td").length).toBe(5);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.businessLinePlaceholder
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.regionPlaceholder
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.statePlaceholder
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate can Submit Changes", () => {
|
||||
test("Submit Button Sends Correct Form Data", () => {
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.simulate("click");
|
||||
expect(mockSubmitFunction.mock.calls[0][0]).toEqual({
|
||||
departmentName: "NewName",
|
||||
businessLineCode: "bl1",
|
||||
regionCode: "NORTH",
|
||||
state: "Inactive"
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
//Assert Values have cleared since submit
|
||||
expect(wrapper.find("td").length).toBe(5);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.businessLinePlaceholder
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.regionPlaceholder
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe(
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow
|
||||
.statePlaceholder
|
||||
);
|
||||
});
|
||||
});
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { Button, ButtonGroup, FormFeedback, Input } from "reactstrap";
|
||||
import { IDepartmentDto } from "../../shared/models/Department";
|
||||
import { IBusinessLine, IRegion } from "../../shared/models/Settings";
|
||||
import { tryUpdateStateWithInput } from "../../shared/services/InputService";
|
||||
import Translate from "./../../translate/Translate";
|
||||
|
||||
export interface IAddNewDepartmentRow {
|
||||
departmentStateSet: { [key: string]: string };
|
||||
businessLines: IBusinessLine[];
|
||||
regions: IRegion[];
|
||||
submitNewDepartment: (newDepartment: IDepartmentDto) => void;
|
||||
isDepartmentNameUnique: (deptName?: string) => boolean;
|
||||
}
|
||||
|
||||
const newDepartmentTranslate =
|
||||
Translate.Get.components.departmentTable.addNewDepartmentRow;
|
||||
|
||||
const AddNewDepartmentRow: React.FC<IAddNewDepartmentRow> = (props) => {
|
||||
const emptyFormState = {
|
||||
departmentName: undefined,
|
||||
orgCode: undefined,
|
||||
businessLineCode: undefined,
|
||||
regionCode: undefined,
|
||||
state: undefined
|
||||
};
|
||||
|
||||
const [departmentFormState, setFormState] = useState<
|
||||
Partial<IDepartmentDto>
|
||||
>(emptyFormState);
|
||||
|
||||
const clearFormState = () => {
|
||||
setFormState(emptyFormState);
|
||||
};
|
||||
|
||||
const nameLengthIsInvalid = () => {
|
||||
return (
|
||||
(departmentFormState.departmentName ?? "UNDEFINED_IS_VALID")
|
||||
.length < 3
|
||||
);
|
||||
};
|
||||
|
||||
const nameIsNotUnique = () => {
|
||||
return !props.isDepartmentNameUnique(
|
||||
departmentFormState.departmentName
|
||||
);
|
||||
};
|
||||
|
||||
const shouldDisableSubmit = () => {
|
||||
return (
|
||||
isFormMissingValue() || nameIsNotUnique() || nameLengthIsInvalid()
|
||||
);
|
||||
};
|
||||
|
||||
const isFormMissingValue = () => {
|
||||
return (
|
||||
departmentFormState.businessLineCode == undefined ||
|
||||
departmentFormState.departmentName == undefined ||
|
||||
departmentFormState.state == undefined ||
|
||||
departmentFormState.regionCode === undefined
|
||||
);
|
||||
};
|
||||
|
||||
const shouldHideClear = () => {
|
||||
if (
|
||||
departmentFormState.businessLineCode == undefined &&
|
||||
departmentFormState.departmentName == undefined &&
|
||||
departmentFormState.state == undefined &&
|
||||
departmentFormState.regionCode === undefined
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
value={departmentFormState?.departmentName ?? ""}
|
||||
placeholder={newDepartmentTranslate.namePlaceholder}
|
||||
data-test="dept-name-input"
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"departmentName",
|
||||
event.target.value.replace(/[^a-zA-Z0-9-]/g, "")
|
||||
)
|
||||
);
|
||||
}}
|
||||
invalid={nameLengthIsInvalid() || nameIsNotUnique()}
|
||||
/>
|
||||
{nameLengthIsInvalid() ? (
|
||||
<FormFeedback>
|
||||
{newDepartmentTranslate.validation.departmentNameLength}
|
||||
</FormFeedback>
|
||||
) : nameIsNotUnique() ? (
|
||||
<FormFeedback>
|
||||
{newDepartmentTranslate.validation.departmentNameUnique}
|
||||
</FormFeedback>
|
||||
) : undefined}
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="business-line-input"
|
||||
value={
|
||||
departmentFormState.businessLineCode === undefined
|
||||
? newDepartmentTranslate.businessLinePlaceholder
|
||||
: departmentFormState.businessLineCode
|
||||
}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"businessLineCode",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
hidden={
|
||||
departmentFormState.businessLineCode !== undefined
|
||||
}
|
||||
key={undefined}
|
||||
>
|
||||
{newDepartmentTranslate.businessLinePlaceholder}
|
||||
</option>
|
||||
{props.businessLines.map((bl) => {
|
||||
return (
|
||||
<option key={bl.code} value={bl.code}>
|
||||
{bl.code}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
value={
|
||||
departmentFormState.regionCode === undefined
|
||||
? newDepartmentTranslate.regionPlaceholder
|
||||
: departmentFormState.regionCode || ""
|
||||
}
|
||||
data-test="region-input"
|
||||
onChange={(event) =>
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"regionCode",
|
||||
event.target.value || null
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option
|
||||
hidden={departmentFormState.regionCode !== undefined}
|
||||
key={undefined}
|
||||
>
|
||||
{newDepartmentTranslate.regionPlaceholder}
|
||||
</option>
|
||||
<option key="none" value="">
|
||||
<{newDepartmentTranslate.noneRegion}>
|
||||
</option>
|
||||
{props.regions.map((region) => (
|
||||
<option key={region.code} value={region.code}>
|
||||
{region.code}
|
||||
</option>
|
||||
))}
|
||||
</Input>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
value={
|
||||
departmentFormState.state === undefined
|
||||
? newDepartmentTranslate.statePlaceholder
|
||||
: departmentFormState.state
|
||||
}
|
||||
data-test="dept-state-input"
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"state",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
hidden={departmentFormState.state !== undefined}
|
||||
key={undefined}
|
||||
>
|
||||
{newDepartmentTranslate.statePlaceholder}
|
||||
</option>
|
||||
{Object.entries(props.departmentStateSet).map(
|
||||
([, value]) => {
|
||||
return (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Input>
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
data-test="submit-button"
|
||||
onClick={() => {
|
||||
props.submitNewDepartment(
|
||||
departmentFormState as IDepartmentDto
|
||||
);
|
||||
clearFormState();
|
||||
}}
|
||||
disabled={shouldDisableSubmit()}
|
||||
>
|
||||
{newDepartmentTranslate.addButton}
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
data-test="clear-button"
|
||||
onClick={() => {
|
||||
clearFormState();
|
||||
}}
|
||||
hidden={shouldHideClear()}
|
||||
>
|
||||
<span className="fa fa-times" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNewDepartmentRow;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Input {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import DepartmentTable from "./DepartmentTable";
|
||||
import AsyncState from "./../../shared/enumerations/AsyncState";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
|
||||
describe("Associate Views the DepartmentTable", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentTable
|
||||
businessLines={[
|
||||
{ name: "businessLine1", code: "bl1", orgCode: "ORG" },
|
||||
{ name: "businessLine2", code: "bl2", orgCode: "ORG" }
|
||||
]}
|
||||
regions={[
|
||||
{ name: "North", code: "NORTH", businessLineCode: "BCHOPS" }
|
||||
]}
|
||||
departmentStateSet={{ Avctive: "Active", Inactive: "Inactive" }}
|
||||
organizationSet={{}}
|
||||
submitNewDepartment={() => jest.fn()}
|
||||
updateDepartment={() => jest.fn}
|
||||
departments={
|
||||
[
|
||||
{
|
||||
id: 5,
|
||||
departmentName: "Test",
|
||||
businessLineCode: "bl1",
|
||||
state: "Active"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
departmentName: "Test2",
|
||||
businessLineCode: "bl2",
|
||||
state: "Active"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
departmentName: "Test3",
|
||||
businessLineCode: "bl1",
|
||||
state: "Active"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
departmentName: "Test4",
|
||||
businessLineCode: "bl2",
|
||||
state: "Active"
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
departmentName: "Test5",
|
||||
businessLineCode: "bl1",
|
||||
state: "InActive"
|
||||
}
|
||||
] as IDepartment[]
|
||||
}
|
||||
submitState={{ state: AsyncState.Success }}
|
||||
/>
|
||||
);
|
||||
it("Displays all departments", () => {
|
||||
// Users cannot make departments.
|
||||
expect(wrapper.find("AddNewDepartmentRow").length).toBe(0);
|
||||
expect(wrapper.find("DepartmentTableRow").length).toBe(4);
|
||||
});
|
||||
});
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from "react";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IDepartment, IDepartmentDto } from "../../shared/models/Department";
|
||||
import DepartmentTableRow from "./DepartmentTableRow";
|
||||
import { IBusinessLine, IRegion } from "../../shared/models/Settings";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import DepartmentTableAlertBar from "./DepartmentTableAlertBar";
|
||||
import { CustomInput, Form } from "reactstrap";
|
||||
import DepartmentService from "../../shared/services/DepartmentService";
|
||||
|
||||
export interface DepartmentTableProps {
|
||||
departments: IDepartment[];
|
||||
organizationSet: { [key: string]: string };
|
||||
departmentStateSet: { [key: string]: string };
|
||||
businessLines: IBusinessLine[];
|
||||
regions: IRegion[];
|
||||
submitNewDepartment: (newDepartment: IDepartmentDto) => void;
|
||||
updateDepartment: (updatedData: IDepartmentDto, id: number) => void;
|
||||
submitState: IAsyncStateContainer<IDepartment>;
|
||||
}
|
||||
|
||||
const DepartmentTable: React.FC<DepartmentTableProps> = (props) => {
|
||||
const [showDetails, setShowDetails] = useState<boolean>(false);
|
||||
let departments = props.departments;
|
||||
if (!showDetails) {
|
||||
departments = departments.filter(
|
||||
(department) => department.state === "Active"
|
||||
);
|
||||
}
|
||||
|
||||
const departmentRows = departments.map((department) => {
|
||||
return (
|
||||
<DepartmentTableRow
|
||||
key={department.id}
|
||||
department={department}
|
||||
departmentStateSet={props.departmentStateSet}
|
||||
businessLines={props.businessLines}
|
||||
regions={props.regions}
|
||||
updateDepartment={(updatedData) => {
|
||||
props.updateDepartment(updatedData, department.id);
|
||||
}}
|
||||
isDepartmentNameUnique={(deptName) =>
|
||||
DepartmentService.isDepartmentNameUnique(
|
||||
departments,
|
||||
deptName
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const departmentHeaders = Translate.Get.components.departmentTable.headers.map(
|
||||
(header: string) => {
|
||||
return <th>{header}</th>;
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h5>{Translate.Get.components.departmentTable.title}</h5>
|
||||
<DepartmentTableAlertBar submitState={props.submitState} />
|
||||
<div className="d-flex justify-content-end">
|
||||
<Form inline noValidate>
|
||||
<CustomInput
|
||||
type="checkbox"
|
||||
id="toggle-all-activity"
|
||||
checked={showDetails}
|
||||
onChange={(event) => {
|
||||
setShowDetails(event.target.checked);
|
||||
}}
|
||||
label="Show All Departments"
|
||||
className="ml-4 mr-1"
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
<table className="table table-striped border rounded">
|
||||
<thead>
|
||||
<tr>
|
||||
{departmentHeaders}
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{departmentRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentTable;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import DepartmentTableAlertBar from "./DepartmentTableAlertBar";
|
||||
import AsyncState from "./../../shared/enumerations/AsyncState";
|
||||
import Translate from "./../../translate/Translate";
|
||||
|
||||
describe("DepartmentTableAlertBar", () => {
|
||||
it("Displays Nothing If Inactive", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentTableAlertBar
|
||||
submitState={{ state: AsyncState.Inactive }}
|
||||
></DepartmentTableAlertBar>
|
||||
);
|
||||
expect(wrapper.find("Alert").length).toBe(0);
|
||||
expect(wrapper.find("div").length).toBe(1);
|
||||
});
|
||||
|
||||
it("Displays Loading Message if Loading", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentTableAlertBar
|
||||
submitState={{ state: AsyncState.Loading }}
|
||||
></DepartmentTableAlertBar>
|
||||
);
|
||||
expect(wrapper.find("Alert").length).toBe(1);
|
||||
expect(wrapper.find("p").text()).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableAlertBar
|
||||
.loading
|
||||
);
|
||||
expect(wrapper.find("Alert").prop("color")).toEqual("warning");
|
||||
});
|
||||
|
||||
it("Displays Success Message if Success", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentTableAlertBar
|
||||
submitState={{
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
departmentName: "Test",
|
||||
id: 5,
|
||||
businessLineCode: "3",
|
||||
state: "Active"
|
||||
}
|
||||
}}
|
||||
></DepartmentTableAlertBar>
|
||||
);
|
||||
expect(wrapper.find("Alert").length).toBe(1);
|
||||
expect(wrapper.find("Alert").prop("color")).toEqual("success");
|
||||
expect(wrapper.find("p").text()).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableAlertBar
|
||||
.success
|
||||
);
|
||||
expect(wrapper.find("p").text()).toContain("Test");
|
||||
});
|
||||
|
||||
it("Displays Fail Message if Failure", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentTableAlertBar
|
||||
submitState={{ state: AsyncState.Failure }}
|
||||
></DepartmentTableAlertBar>
|
||||
);
|
||||
expect(wrapper.find("Alert").length).toBe(1);
|
||||
expect(wrapper.find("Alert").prop("color")).toEqual("danger");
|
||||
expect(wrapper.find("p").text()).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableAlertBar
|
||||
.failure
|
||||
);
|
||||
});
|
||||
});
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { Alert, Spinner } from "reactstrap";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import AsyncState from "./../../shared/enumerations/AsyncState";
|
||||
import Translate from "./../../translate/Translate";
|
||||
|
||||
export interface IDepartmentTableAlertBarProps {
|
||||
submitState: IAsyncStateContainer<IDepartment>;
|
||||
}
|
||||
|
||||
const alertBarTranslate =
|
||||
Translate.Get.components.departmentTable.departmentTableAlertBar;
|
||||
|
||||
const DepartmentTableAlertBar: React.FC<IDepartmentTableAlertBarProps> = (
|
||||
props
|
||||
) => {
|
||||
if (props.submitState.state == AsyncState.Success) {
|
||||
return (
|
||||
<Alert className="py-1 mx-3" color="success">
|
||||
<p className="mb-0">
|
||||
{alertBarTranslate.success +
|
||||
props.submitState.data?.departmentName ?? "-"}
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.submitState.state == AsyncState.Loading) {
|
||||
return (
|
||||
<Alert
|
||||
className="py-1 mx-3 d-flex flex-row align-items-center"
|
||||
color="warning"
|
||||
>
|
||||
<Spinner className="mr-2" size="sm" />
|
||||
<p className="mb-0">{alertBarTranslate.loading}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.submitState.state == AsyncState.Failure) {
|
||||
return (
|
||||
<Alert
|
||||
className="d-flex flex-row align-items-center py-1 mx-3"
|
||||
color="danger"
|
||||
>
|
||||
<span className="fa fa-times-circle mr-2" />
|
||||
<p className="mb-0">{alertBarTranslate.failure}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default DepartmentTableAlertBar;
|
||||
+586
@@ -0,0 +1,586 @@
|
||||
/* eslint-disable @typescript-eslint/quotes */
|
||||
import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import DepartmentTableRow from "./DepartmentTableRow";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
import ViewDepartmentTableRow from "./ViewDepartmentTableRow";
|
||||
|
||||
let wrapper: any;
|
||||
|
||||
const mockUniqueFunction = jest.fn();
|
||||
const mockSubmitFunction = jest.fn();
|
||||
|
||||
export interface IFakePermissionProps {
|
||||
requiredPermissions: string[];
|
||||
showWarning?: boolean;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const FakePermissionGuard: React.FC<IFakePermissionProps> = (props) => {
|
||||
return props.children;
|
||||
};
|
||||
|
||||
jest.mock("../../containers/shared/PermissionGuard", () => FakePermissionGuard);
|
||||
|
||||
beforeEach(() => {
|
||||
mockUniqueFunction.mockReturnValue(true);
|
||||
wrapper = mount(
|
||||
<table>
|
||||
<tbody>
|
||||
<DepartmentTableRow
|
||||
department={
|
||||
{
|
||||
id: 4,
|
||||
departmentName: "testName",
|
||||
businessLineCode: "bl2",
|
||||
regionCode: "",
|
||||
state: "Active"
|
||||
} as IDepartment
|
||||
}
|
||||
updateDepartment={(department) =>
|
||||
mockSubmitFunction(department)
|
||||
}
|
||||
isDepartmentNameUnique={(deptName) => {
|
||||
return mockUniqueFunction();
|
||||
}}
|
||||
businessLines={[
|
||||
{ name: "businessLine1", code: "bl1", orgCode: "ORG" },
|
||||
{ name: "businessLine2", code: "bl2", orgCode: "ORG" }
|
||||
]}
|
||||
departmentStateSet={{
|
||||
Avctive: "Active",
|
||||
Inactive: "Inactive"
|
||||
}}
|
||||
regions={[
|
||||
{
|
||||
name: "North",
|
||||
businessLineCode: "BCHOPS",
|
||||
code: "NORTH"
|
||||
},
|
||||
{
|
||||
name: "South",
|
||||
businessLineCode: "BCHOPS",
|
||||
code: "SOUTH"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
});
|
||||
|
||||
describe("User Views a DepartmentTableRow", () => {
|
||||
it("Displays Proper Department Data", () => {
|
||||
let x = 0;
|
||||
const temp = () => {
|
||||
x++;
|
||||
};
|
||||
const tempWrapper = shallow(
|
||||
<ViewDepartmentTableRow
|
||||
department={
|
||||
{
|
||||
id: 4,
|
||||
departmentName: "testName",
|
||||
businessLineCode: "123",
|
||||
state: "Active"
|
||||
} as IDepartment
|
||||
}
|
||||
toggleEdit={temp}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(tempWrapper.find('[data-test="department_tr_4"]').length).toBe(
|
||||
1
|
||||
);
|
||||
expect(tempWrapper.find('[data-test="edit-row"]').length).toBe(0);
|
||||
expect(tempWrapper.find("td").length).toBe(6);
|
||||
expect(tempWrapper.find('[data-test="dept-name"]').length).toBe(1);
|
||||
expect(tempWrapper.find('[data-test="business-line"]').length).toBe(1);
|
||||
expect(tempWrapper.find('[data-test="dept-state"]').length).toBe(1);
|
||||
expect(
|
||||
tempWrapper.find("td").find("Button").children().text()
|
||||
).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableRow
|
||||
.editButton
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate Can Toggle and Make Edits", () => {
|
||||
test("Clicking Edit Button Opens Edit", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test="display-row"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(wrapper.find("td").length).toBe(6);
|
||||
expect(
|
||||
wrapper.find('[data-test="dept-name-input"]').hostNodes().length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('[data-test="business-line-input"]').hostNodes().length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('[data-test="dept-state-input"]').hostNodes().length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('[data-test="submit-button"]').hostNodes().text()
|
||||
).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableRow
|
||||
.saveUpdateButton
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="clear-button"]').hostNodes().length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test("Initial Values are Set Properly", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test="display-row"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(wrapper.find("td").length).toBe(6);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("testName");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("bl2");
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe("");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("Active");
|
||||
expect(
|
||||
wrapper.find('[data-test="submit-button"]').hostNodes().text()
|
||||
).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableRow
|
||||
.saveUpdateButton
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="clear-button"]').hostNodes().length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test("Updating Values Updates State", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test="display-row"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(wrapper.find("td").length).toBe(6);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("NewName");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("bl1");
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe("NORTH");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("Inactive");
|
||||
});
|
||||
|
||||
test("Only - allowed as special charcter", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find("td").length).toBe(6);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", {
|
||||
target: { value: "New-Name Name.!@#$!#@$" }
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("New-NameName");
|
||||
});
|
||||
|
||||
test("Submit is Disabled until Changes are Made - Name", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "testName" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("Submit is Disabled until Changes are Made - BusinessLine", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl2" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("Submit is Disabled until Changes are Made - State", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Active" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate Inputs Are Validated", () => {
|
||||
test("Name Must Be at least 3 Characters", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Ne" } });
|
||||
wrapper.update();
|
||||
///Input disabled because too short
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
expect(wrapper.find("FormFeedback").text()).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableRow
|
||||
.validation.departmentNameLength
|
||||
);
|
||||
|
||||
//Input Enabled once name is > 2 characters
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "New" } });
|
||||
wrapper.update();
|
||||
expect(wrapper.find("FormFeedback").hostNodes().length).toBe(0);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("Name Must Be Unique If Name is Not Original Name", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
mockUniqueFunction.mockReturnValue(false);
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewNewName" } });
|
||||
wrapper.update();
|
||||
|
||||
///Input disabled because unique check fails
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
expect(wrapper.find("FormFeedback").text()).toContain(
|
||||
Translate.Get.components.departmentTable.departmentTableRow
|
||||
.validation.departmentNameUnique
|
||||
);
|
||||
|
||||
//Input Enabled once name is unique
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewNewName" } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate can Abandon Changes", () => {
|
||||
test("Cancel Button Clears Inputs and Returns to Display State", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test="display-row"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(wrapper.find("td").length).toBe(6);
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "SOUTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test="department_tr_4"]').length).toBe(1);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(0);
|
||||
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
//Assert Values did Not Update After Cancel and are Reset to Prop Values
|
||||
expect(wrapper.find('[data-test="display-row"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(wrapper.find("td").length).toBe(6);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("testName");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("bl2");
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe("");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("Active");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Associate can Submit Changes", () => {
|
||||
test("Submit Button Sends Correct Form Data", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NewName" } });
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "bl1" } });
|
||||
wrapper
|
||||
.find('[data-test="region-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "NORTH" } });
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: "Inactive" } });
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.simulate("click");
|
||||
expect(mockSubmitFunction.mock.calls[0][0]).toEqual({
|
||||
departmentName: "NewName",
|
||||
businessLineCode: "bl1",
|
||||
state: "Inactive",
|
||||
id: 4,
|
||||
regionCode: "NORTH"
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test="department_tr_4"]').length).toBe(1);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(0);
|
||||
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
|
||||
//Assert Values have updated since submit
|
||||
expect(wrapper.find('[data-test="display-row"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(wrapper.find("td").length).toBe(6);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-name-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("testName");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="business-line-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("bl2");
|
||||
expect(
|
||||
wrapper.find('[data-test="region-input"]').hostNodes().prop("value")
|
||||
).toBe("");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="dept-state-input"]')
|
||||
.hostNodes()
|
||||
.prop("value")
|
||||
).toBe("Active");
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import React, { useState } from "react";
|
||||
import { IDepartment, IDepartmentDto } from "../../shared/models/Department";
|
||||
import { IBusinessLine, IRegion } from "../../shared/models/Settings";
|
||||
import ViewDepartmentTableRow from "./ViewDepartmentTableRow";
|
||||
import EditDepartmentTableRow from "./EditDepartmentTableRow";
|
||||
|
||||
export interface IDepartmentTableRowProps {
|
||||
department: IDepartment;
|
||||
departmentStateSet: { [key: string]: string };
|
||||
businessLines: IBusinessLine[];
|
||||
regions: IRegion[];
|
||||
updateDepartment: (updatedData: IDepartmentDto) => void;
|
||||
isDepartmentNameUnique: (deptName: string) => boolean;
|
||||
}
|
||||
|
||||
const DepartmentTableRow: React.FC<IDepartmentTableRowProps> = (props) => {
|
||||
const [isEditing, setEditing] = useState<boolean>(false);
|
||||
|
||||
return isEditing ? (
|
||||
<EditDepartmentTableRow
|
||||
department={props.department}
|
||||
updateDepartment={props.updateDepartment}
|
||||
businessLines={props.businessLines}
|
||||
regions={props.regions}
|
||||
isDepartmentNameUnique={props.isDepartmentNameUnique}
|
||||
departmentStateSet={props.departmentStateSet}
|
||||
disableEdit={() => setEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<ViewDepartmentTableRow
|
||||
department={props.department}
|
||||
toggleEdit={() => setEditing(true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentTableRow;
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import React, { useState } from "react";
|
||||
import { IDepartment, IDepartmentDto } from "../../shared/models/Department";
|
||||
import { tryUpdateStateWithInput } from "../../shared/services/InputService";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
FormFeedback,
|
||||
Input,
|
||||
InputGroup
|
||||
} from "reactstrap";
|
||||
import { IBusinessLine, IRegion } from "../../shared/models/Settings";
|
||||
import Translate from "../../translate/Translate";
|
||||
|
||||
export interface IEditDepartmentTableRowProps {
|
||||
department: IDepartment;
|
||||
departmentStateSet: { [key: string]: string };
|
||||
businessLines: IBusinessLine[];
|
||||
regions: IRegion[];
|
||||
updateDepartment: (updatedData: IDepartmentDto) => void;
|
||||
isDepartmentNameUnique: (deptName: string) => boolean;
|
||||
disableEdit: () => void;
|
||||
}
|
||||
|
||||
const tableRowTranslate =
|
||||
Translate.Get.components.departmentTable.departmentTableRow;
|
||||
|
||||
const EditDepartmentTableRow: React.FC<IEditDepartmentTableRowProps> = (
|
||||
props
|
||||
) => {
|
||||
const [departmentFormState, setFormState] = useState<IDepartmentDto>(() => {
|
||||
return props.department;
|
||||
});
|
||||
|
||||
const shouldDisableSubmit = () => {
|
||||
if (
|
||||
departmentFormState.businessLineCode ==
|
||||
props.department.businessLineCode &&
|
||||
departmentFormState.departmentName ==
|
||||
props.department.departmentName &&
|
||||
departmentFormState.state == props.department.state &&
|
||||
departmentFormState.regionCode == props.department.regionCode
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return nameLengthIsInvalid() || nameIsNotUnique();
|
||||
};
|
||||
|
||||
const nameLengthIsInvalid = () => {
|
||||
return (
|
||||
(departmentFormState.departmentName ?? "UNDEFINED_IS_VALID")
|
||||
.length < 3
|
||||
);
|
||||
};
|
||||
|
||||
const nameIsNotUnique = () => {
|
||||
return (
|
||||
props.department.departmentName !==
|
||||
departmentFormState.departmentName &&
|
||||
!props.isDepartmentNameUnique(departmentFormState.departmentName)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<tr data-test="edit-row">
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
value={departmentFormState?.departmentName ?? ""}
|
||||
placeholder={tableRowTranslate.namePlaceholder}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"departmentName",
|
||||
event.target.value.replace(/[^a-zA-Z0-9-]/g, "")
|
||||
)
|
||||
);
|
||||
}}
|
||||
data-test="dept-name-input"
|
||||
invalid={nameLengthIsInvalid() || nameIsNotUnique()}
|
||||
/>
|
||||
{nameLengthIsInvalid() ? (
|
||||
<FormFeedback>
|
||||
{tableRowTranslate.validation.departmentNameLength}
|
||||
</FormFeedback>
|
||||
) : nameIsNotUnique() ? (
|
||||
<FormFeedback>
|
||||
{tableRowTranslate.validation.departmentNameUnique}
|
||||
</FormFeedback>
|
||||
) : undefined}
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
value={departmentFormState?.businessLineCode ?? ""}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"businessLineCode",
|
||||
event.target.value || null
|
||||
)
|
||||
);
|
||||
}}
|
||||
data-test="business-line-input"
|
||||
>
|
||||
<option key="none" value="">
|
||||
<{tableRowTranslate.noneBusinessLine}>
|
||||
</option>
|
||||
{props.businessLines.map((bl) => {
|
||||
return (
|
||||
<option key={bl.code} value={bl.code}>
|
||||
{bl.code}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
value={departmentFormState?.regionCode || ""}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"regionCode",
|
||||
event.target.value || null
|
||||
)
|
||||
);
|
||||
}}
|
||||
data-test="region-input"
|
||||
>
|
||||
<option key="none" value="">
|
||||
<{tableRowTranslate.noneRegion}>
|
||||
</option>
|
||||
{props.regions.map((region) => (
|
||||
<option key={region.code} value={region.code}>
|
||||
{region.code}
|
||||
</option>
|
||||
))}
|
||||
</Input>
|
||||
</td>
|
||||
<td data-test="dept-orgNbr">
|
||||
<InputGroup>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
value={departmentFormState?.dnaOrgNbr ?? ""}
|
||||
placeholder={tableRowTranslate.orgNbrPlaceholder}
|
||||
maxLength={5}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"dnaOrgNbr",
|
||||
event.target.value.replace(/[^0-9]/g, "")
|
||||
)
|
||||
);
|
||||
}}
|
||||
data-test="dept-orgNbr-input"
|
||||
/>
|
||||
</InputGroup>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
value={departmentFormState?.state ?? ""}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
departmentFormState,
|
||||
"state",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
data-test="dept-state-input"
|
||||
>
|
||||
{Object.entries(props.departmentStateSet).map(
|
||||
([, value]) => {
|
||||
return (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Input>
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
props.updateDepartment(departmentFormState);
|
||||
props.disableEdit();
|
||||
}}
|
||||
disabled={shouldDisableSubmit()}
|
||||
data-test="submit-button"
|
||||
>
|
||||
{tableRowTranslate.saveUpdateButton}
|
||||
</Button>
|
||||
<Button
|
||||
className="m-0"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
props.disableEdit();
|
||||
}}
|
||||
data-test="clear-button"
|
||||
>
|
||||
<span className="fa fa-times" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditDepartmentTableRow;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
import { Button } from "reactstrap";
|
||||
import Translate from "../../translate/Translate";
|
||||
import PermissionGuard from "../../containers/shared/PermissionGuard";
|
||||
|
||||
export interface IViewDepartmentTableRowProps {
|
||||
department: IDepartment;
|
||||
toggleEdit: () => void;
|
||||
}
|
||||
|
||||
const tableRowTranslate =
|
||||
Translate.Get.components.departmentTable.departmentTableRow;
|
||||
|
||||
const ViewDepartmentTableRow: React.FC<IViewDepartmentTableRowProps> = (
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<tr data-test={`department_tr_${props.department.id}`}>
|
||||
<td data-test="dept-name">{props.department.departmentName}</td>
|
||||
<td data-test="business-line">
|
||||
{props.department.businessLineCode}
|
||||
</td>
|
||||
<td data-test="region">
|
||||
{props.department.regionCode ?? (
|
||||
<em>{tableRowTranslate.noneRegion}</em>
|
||||
)}
|
||||
</td>
|
||||
<td data-test="dept-orgNbr">{props.department.dnaOrgNbr}</td>
|
||||
<td data-test="dept-state">{props.department.state}</td>
|
||||
<td data-test="edit-button">
|
||||
<PermissionGuard requiredPermissions={["MANAGE_DEPARTMENTS"]}>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
props.toggleEdit();
|
||||
}}
|
||||
>
|
||||
{tableRowTranslate.editButton}
|
||||
</Button>
|
||||
</PermissionGuard>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewDepartmentTableRow;
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import React from "react";
|
||||
import EmployeeAlertBar from "./EmployeeAlertBar";
|
||||
import AsyncState from "./../../shared/enumerations/AsyncState";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import { IEmployee } from "./../../shared/models/Employee";
|
||||
import { mount } from "enzyme";
|
||||
|
||||
describe("EmployeeAlertBar After Employee Navigates to Employee URL", () => {
|
||||
const data: IEmployee = {
|
||||
additionalPermissions: [],
|
||||
assignments: [],
|
||||
personId: 5,
|
||||
adEmail: "[email protected]",
|
||||
employeeId: "123",
|
||||
baseRole: "Developers",
|
||||
homeDepartment: "Development",
|
||||
fullName: "Test McTester",
|
||||
departments: [],
|
||||
role: {
|
||||
name: "role",
|
||||
description: "role",
|
||||
permissions: [],
|
||||
hasCommissions: true
|
||||
}
|
||||
};
|
||||
|
||||
it("Displays a not found message when state is success but data is undefined", () => {
|
||||
const wrapper = mount(
|
||||
<EmployeeAlertBar
|
||||
employeeContainer={{
|
||||
state: AsyncState.Success,
|
||||
data: undefined
|
||||
}}
|
||||
searchEmployeeId={"123"}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test="not-found"]').hostNodes().length).toBe(
|
||||
1
|
||||
);
|
||||
expect(wrapper.find('[data-test="error"]').hostNodes().length).toBe(0);
|
||||
expect(wrapper.find('[data-test="loading"]').hostNodes().length).toBe(
|
||||
0
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
Translate.Get.components.employee.employeeAlertBar.notFound.title
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
Translate.Get.components.employee.employeeAlertBar.notFound.message
|
||||
);
|
||||
expect(wrapper.text()).toContain("123");
|
||||
});
|
||||
|
||||
it("Displays a loading message when data is loading", () => {
|
||||
const wrapper = mount(
|
||||
<EmployeeAlertBar
|
||||
employeeContainer={{
|
||||
state: AsyncState.Loading,
|
||||
data: undefined
|
||||
}}
|
||||
searchEmployeeId={"123"}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test="not-found"]').hostNodes().length).toBe(
|
||||
0
|
||||
);
|
||||
expect(wrapper.find('[data-test="error"]').hostNodes().length).toBe(0);
|
||||
expect(wrapper.find('[data-test="loading"]').hostNodes().length).toBe(
|
||||
1
|
||||
);
|
||||
|
||||
expect(wrapper.find("p").hostNodes().text()).toContain(
|
||||
Translate.Get.components.employee.employeeAlertBar.loading.title
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays a error message when request failed", () => {
|
||||
const wrapper = mount(
|
||||
<EmployeeAlertBar
|
||||
employeeContainer={{
|
||||
state: AsyncState.Failure,
|
||||
data: undefined
|
||||
}}
|
||||
searchEmployeeId={"123"}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test="not-found"]').hostNodes().length).toBe(
|
||||
0
|
||||
);
|
||||
expect(wrapper.find('[data-test="error"]').hostNodes().length).toBe(1);
|
||||
expect(wrapper.find('[data-test="loading"]').hostNodes().length).toBe(
|
||||
0
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
Translate.Get.components.employee.employeeAlertBar.error.title
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
Translate.Get.components.employee.employeeAlertBar.error.message
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays nothing when successfully finds data", () => {
|
||||
const wrapper = mount(
|
||||
<EmployeeAlertBar
|
||||
employeeContainer={{ state: AsyncState.Success, data: data }}
|
||||
searchEmployeeId={"123"}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test="not-found"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="error"]').length).toBe(0);
|
||||
expect(wrapper.find('[data-test="loading"]').length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import IAsyncStateContainer from "./../../shared/types/IAsyncStateContainer";
|
||||
import { IEmployee } from "./../../shared/models/Employee";
|
||||
import AsyncState from "./../../shared/enumerations/AsyncState";
|
||||
import { Alert, Spinner } from "reactstrap";
|
||||
import Translate from "./../../translate/Translate";
|
||||
|
||||
interface IEmployeeAlertBarProps {
|
||||
employeeContainer: IAsyncStateContainer<IEmployee>;
|
||||
searchEmployeeId: string;
|
||||
}
|
||||
|
||||
const employeeAlertBarTranslate =
|
||||
Translate.Get.components.employee.employeeAlertBar;
|
||||
|
||||
const EmployeeAlertBar: React.FC<IEmployeeAlertBarProps> = (props) => {
|
||||
if (
|
||||
props.employeeContainer.state === AsyncState.Success &&
|
||||
!props.employeeContainer.data
|
||||
) {
|
||||
return (
|
||||
<Alert
|
||||
className="py-1 mx-3 my-1"
|
||||
color="warning"
|
||||
data-test="not-found"
|
||||
>
|
||||
<div className="d-flex flex-col justify-content-start align-items-center">
|
||||
<div className="mr-4">
|
||||
<p className="fa fa-exclamation-triangle m-0" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-0">
|
||||
<b>
|
||||
{employeeAlertBarTranslate.notFound.title}
|
||||
{props.searchEmployeeId}
|
||||
</b>
|
||||
</p>
|
||||
<p className="mb-0">
|
||||
{employeeAlertBarTranslate.notFound.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.employeeContainer.state === AsyncState.Failure) {
|
||||
return (
|
||||
<Alert className="py-1 mx-3 my-1" color="danger" data-test="error">
|
||||
<div className="d-flex flex-col justify-content-start align-items-center">
|
||||
<div className="mr-4">
|
||||
<span className="fa fa-times-circle m-0" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-0">
|
||||
<b>{employeeAlertBarTranslate.error.title}</b>
|
||||
</p>
|
||||
<p className="mb-0">
|
||||
{employeeAlertBarTranslate.error.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.employeeContainer.state === AsyncState.Loading) {
|
||||
return (
|
||||
<Alert
|
||||
className="py-1 mx-3 my-1"
|
||||
color="warning"
|
||||
data-test="loading"
|
||||
>
|
||||
<div className="d-flex flex-col justify-content-start align-items-center">
|
||||
<div className="mr-4">
|
||||
<Spinner className="m-0" color="warning" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-0">
|
||||
<b>{employeeAlertBarTranslate.loading.title}</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default EmployeeAlertBar;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import EmployeeDepartmentAssignments from "./EmployeeDepartmentAssignments";
|
||||
import { IDepartmentAssignment } from "../../shared/models/DepartmentAssignment";
|
||||
|
||||
describe("Display an employees department assignments", () => {
|
||||
it("Displays an employee with missing role", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
wrapper = shallow(
|
||||
<EmployeeDepartmentAssignments assignments={undefined} />
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toBe("<span>No Assignments</span>");
|
||||
});
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
|
||||
import { IDepartmentAssignment } from "../../shared/models/DepartmentAssignment";
|
||||
import moment from "moment";
|
||||
|
||||
interface IEmployeeDepartmentAssignments {
|
||||
assignments?: IDepartmentAssignment[];
|
||||
}
|
||||
|
||||
const EmployeeDepartmentAssignments: React.FC<IEmployeeDepartmentAssignments> = (
|
||||
props
|
||||
) => {
|
||||
if (!props.assignments) {
|
||||
return <span>No Assignments</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Department</th>
|
||||
<th>Home/Float</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Role</th>
|
||||
<th>Payout</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.assignments.map((assignment) => {
|
||||
return (
|
||||
<tr key={assignment.id}>
|
||||
<td>
|
||||
<DepartmentDisplay
|
||||
departmentId={assignment.departmentId}
|
||||
/>
|
||||
</td>
|
||||
<td>{assignment.isHome ? "Home" : "Float"}</td>
|
||||
<td>
|
||||
{moment(assignment.startDate).format("ll")}
|
||||
</td>
|
||||
<td>
|
||||
{assignment.endDate
|
||||
? moment(assignment.endDate).format(
|
||||
"ll"
|
||||
)
|
||||
: "Current"}
|
||||
</td>
|
||||
<td
|
||||
title={
|
||||
"Rank: " + assignment.role
|
||||
? assignment.role.rank + ""
|
||||
: "0"
|
||||
}
|
||||
>
|
||||
{assignment.role
|
||||
? assignment.role.name
|
||||
: "Missing Role!"}
|
||||
</td>
|
||||
<td>{assignment.slidingPayout}%</td>
|
||||
<td>{assignment.note}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeDepartmentAssignments;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { IAppRole } from "../../shared/models/Settings";
|
||||
|
||||
interface IEmployeeRoleProps {
|
||||
role?: IAppRole;
|
||||
}
|
||||
|
||||
const EmployeeRole: React.FC<IEmployeeRoleProps> = (props) => {
|
||||
if (!props.role) {
|
||||
return <span>No Role</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.role.name} - {props.role.description}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeRole;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import GoalProgressControls from "./GoalProgressControls";
|
||||
import moment from "moment";
|
||||
|
||||
describe("Component to select a month range", () => {
|
||||
it("should provide the current month and year for to and from values", () => {
|
||||
const stubOnChange = jest.fn();
|
||||
mount(<GoalProgressControls onChange={stubOnChange} />);
|
||||
|
||||
expect(stubOnChange).toHaveBeenCalledWith({
|
||||
fromMonth: moment().month() + 1,
|
||||
fromYear: moment().year(),
|
||||
toMonth: moment().month() + 1,
|
||||
toYear: moment().year()
|
||||
});
|
||||
});
|
||||
|
||||
it("should provide the month and year for to and from values when input is changed", () => {
|
||||
const stubOnChange = jest.fn();
|
||||
|
||||
const wrapper = mount(<GoalProgressControls onChange={stubOnChange} />);
|
||||
|
||||
stubOnChange.mockClear();
|
||||
|
||||
wrapper
|
||||
.find("input[name='dashboardStartMonth']")
|
||||
.simulate("change", { target: { value: "08/01/2021" } });
|
||||
|
||||
expect(stubOnChange).toHaveBeenCalledWith({
|
||||
fromMonth: 8,
|
||||
fromYear: 2021,
|
||||
toMonth: moment().month() + 1,
|
||||
toYear: moment().year()
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find("input[name='dashboardEndMonth']")
|
||||
.simulate("change", { target: { value: "9/30/2021" } });
|
||||
|
||||
expect(stubOnChange).toHaveBeenCalledWith({
|
||||
fromMonth: 8,
|
||||
fromYear: 2021,
|
||||
toMonth: 9,
|
||||
toYear: 2021
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import moment, { Moment } from "moment";
|
||||
import { IMonthRange } from "../../shared/types/IMonthRange";
|
||||
import { Button, Form, Input } from "reactstrap";
|
||||
|
||||
export interface IGoalProgressControlsProps {
|
||||
onChange: (range: IMonthRange) => void;
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
const GoalProgressControls: React.FC<IGoalProgressControlsProps> = (
|
||||
props: IGoalProgressControlsProps
|
||||
) => {
|
||||
const [fromDate, setFromDate] = useState<Moment>(moment().startOf("month"));
|
||||
const [toDate, setToDate] = useState<Moment>(moment().endOf("month"));
|
||||
|
||||
useEffect(() => {
|
||||
props.onChange({
|
||||
fromMonth: fromDate.month() + 1,
|
||||
fromYear: fromDate.year(),
|
||||
toMonth: toDate.month() + 1,
|
||||
toYear: toDate.year()
|
||||
});
|
||||
}, [fromDate, toDate]);
|
||||
|
||||
const previousMonth = () => {
|
||||
const newFromMonth = moment(fromDate.subtract(1, "months"));
|
||||
setFromDate(newFromMonth);
|
||||
const newToMonth = moment(toDate.subtract(1, "months"));
|
||||
setToDate(newToMonth);
|
||||
};
|
||||
|
||||
const nextMonth = () => {
|
||||
const newFromMonth = moment(fromDate.add(1, "months"));
|
||||
setFromDate(newFromMonth);
|
||||
const newToMonth = moment(toDate.add(1, "months"));
|
||||
setToDate(newToMonth);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-end">
|
||||
<Form inline noValidate>
|
||||
<Button size="sm" color="info" onClick={previousMonth}>
|
||||
Previous Month
|
||||
</Button>
|
||||
<Input
|
||||
name="dashboardStartMonth"
|
||||
id="dashboardStartMonth"
|
||||
type="month"
|
||||
bsSize="sm"
|
||||
required
|
||||
className="ml-2"
|
||||
value={fromDate.format("YYYY-MM")}
|
||||
onChange={(event) => {
|
||||
setFromDate(
|
||||
moment(event.target.value).startOf("month")
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
name="dashboardEndMonth"
|
||||
id="dashboardEndMonth"
|
||||
type="month"
|
||||
bsSize="sm"
|
||||
required
|
||||
className="ml-2"
|
||||
value={toDate.format("YYYY-MM").toString()}
|
||||
onChange={(event) => {
|
||||
setToDate(moment(event.target.value).endOf("month"));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
color="info"
|
||||
onClick={nextMonth}
|
||||
>
|
||||
Next Month
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoalProgressControls;
|
||||
@@ -0,0 +1,9 @@
|
||||
.scrollable-top-parent {
|
||||
width: 95vw;
|
||||
overflow-x: auto;
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
.scrollable-top-child {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import moment from "moment";
|
||||
import { IGoal } from "../../shared/models/Goal";
|
||||
import GoalTable from "./GoalTable";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
|
||||
describe("An associate views the goal audit table", () => {
|
||||
it("Displays a message when there is no data to load", () => {
|
||||
let wrapper = shallow(<GoalTable goals={undefined} />);
|
||||
expect(wrapper.find('[data-test="no-data-message"]').length).toBe(1);
|
||||
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays a message when there is no data to load", () => {
|
||||
let wrapper = shallow(<GoalTable goals={[]} />);
|
||||
expect(wrapper.find('[data-test="no-data-message"]').length).toBe(1);
|
||||
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it("Does not display message when there is data", () => {
|
||||
const goals: IGoal[] = [
|
||||
{
|
||||
id: 100,
|
||||
lastUpdatedByType: UserType.System,
|
||||
lastUpdatedBy: "Tester",
|
||||
lastUpdated: moment().utc(),
|
||||
createdByType: UserType.System,
|
||||
createdBy: "Tester1",
|
||||
created: moment().utc(),
|
||||
employeeId: "11771",
|
||||
departmentId: 56,
|
||||
product: Product2.Vehicle,
|
||||
startDate: moment().utc(),
|
||||
endDate: moment().utc(),
|
||||
value: 1
|
||||
}
|
||||
];
|
||||
let wrapper = shallow(<GoalTable goals={goals} />);
|
||||
expect(wrapper.find("[data-test='no-data-message']").length).toBe(0);
|
||||
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user