This commit is contained in:
Zeeshaun
2026-03-23 11:21:52 -05:00
parent de9ddd5001
commit a17bc4b4a9
426 changed files with 60574 additions and 1 deletions
@@ -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: "/"
}
};
@@ -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: []
};
};
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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
);
});
});
@@ -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;
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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
};
};
@@ -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;
@@ -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
);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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;
@@ -0,0 +1,9 @@
.scrollable-top-parent {
width: 95vw;
overflow-x: auto;
transform: rotateX(180deg);
}
.scrollable-top-child {
transform: rotateX(180deg);
}
@@ -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
);
});
});
@@ -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;
@@ -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;
@@ -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("");
});
});
});
@@ -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;
@@ -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[];
};
@@ -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;
@@ -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")
});
});
});
@@ -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;
@@ -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");
});
});
@@ -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;
@@ -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>
);
};
@@ -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");
});
});
@@ -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}
>
&nbsp;
</i>
) : null}
{this.shouldDisplayDescendingSort(index) ? (
<i
className="fa fa-long-arrow-up"
aria-hidden="true"
data-test={"desc-sort-icon-" + index}
>
&nbsp;
</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>
);
}
}
@@ -0,0 +1,3 @@
.auto-table-hover {
cursor: pointer;
}
@@ -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);
});
});
@@ -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>
);
}
}
@@ -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);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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]);
});
});
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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;
@@ -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;
@@ -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>
);
};
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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");
});
});
@@ -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;
@@ -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;
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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
);
});
});
@@ -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="">
&lt;{newDepartmentTranslate.noneRegion}&gt;
</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;
@@ -0,0 +1,3 @@
Input {
margin-bottom: 0px;
}
@@ -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);
});
});
@@ -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;
@@ -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
);
});
});
@@ -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;
@@ -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");
});
});
@@ -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;
@@ -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="">
&lt;{tableRowTranslate.noneBusinessLine}&gt;
</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="">
&lt;{tableRowTranslate.noneRegion}&gt;
</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;
@@ -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;
@@ -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;
@@ -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>");
});
});
@@ -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;
@@ -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